Commit fb238d7a authored by Nikola Milojevic's avatar Nikola Milojevic

Merge branch 'incubation-5mp-enable-cloud-run' into 'master'

Enable Cloud Run deploys

See merge request gitlab-org/gitlab!80158
parents 6214b6e6 ab22fc4c
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import { GlButton, GlTable } from '@gitlab/ui'; import { GlButton, GlTable } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
const cloudRun = 'cloudRun';
const cloudStorage = 'cloudStorage';
const i18n = { const i18n = {
cloudRun: __('Cloud Run'), cloudRun: __('Cloud Run'),
cloudRunDescription: __('Deploy container based web apps on Google managed clusters'), cloudRunDescription: __('Deploy container based web apps on Google managed clusters'),
...@@ -28,6 +31,13 @@ export default { ...@@ -28,6 +31,13 @@ export default {
required: true, required: true,
}, },
}, },
methods: {
actionUrl(key) {
if (key === cloudRun) return this.cloudRunUrl;
else if (key === cloudStorage) return this.cloudStorageUrl;
return '#';
},
},
fields: [ fields: [
{ key: 'title', label: i18n.service }, { key: 'title', label: i18n.service },
{ key: 'description', label: i18n.description }, { key: 'description', label: i18n.description },
...@@ -37,12 +47,19 @@ export default { ...@@ -37,12 +47,19 @@ export default {
{ {
title: i18n.cloudRun, title: i18n.cloudRun,
description: i18n.cloudRunDescription, description: i18n.cloudRunDescription,
action: { title: i18n.configureViaMergeRequest, disabled: true }, action: {
key: cloudRun,
title: i18n.configureViaMergeRequest,
},
}, },
{ {
title: i18n.cloudStorage, title: i18n.cloudStorage,
description: i18n.cloudStorageDescription, description: i18n.cloudStorageDescription,
action: { title: i18n.configureViaMergeRequest, disabled: true }, action: {
key: cloudStorage,
title: i18n.configureViaMergeRequest,
disabled: true,
},
}, },
], ],
i18n, i18n,
...@@ -54,7 +71,9 @@ export default { ...@@ -54,7 +71,9 @@ export default {
<p>{{ $options.i18n.deploymentsDescription }}</p> <p>{{ $options.i18n.deploymentsDescription }}</p>
<gl-table :fields="$options.fields" :items="$options.items"> <gl-table :fields="$options.fields" :items="$options.items">
<template #cell(action)="{ value }"> <template #cell(action)="{ value }">
<gl-button :disabled="value.disabled">{{ value.title }}</gl-button> <gl-button :disabled="value.disabled" :href="actionUrl(value.key)">
{{ value.title }}
</gl-button>
</template> </template>
</gl-table> </gl-table>
</div> </div>
......
...@@ -23,11 +23,11 @@ export default { ...@@ -23,11 +23,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
deploymentsCloudRunUrl: { enableCloudRunUrl: {
type: String, type: String,
required: true, required: true,
}, },
deploymentsCloudStorageUrl: { enableCloudStorageUrl: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -47,8 +47,8 @@ export default { ...@@ -47,8 +47,8 @@ export default {
</gl-tab> </gl-tab>
<gl-tab :title="__('Deployments')"> <gl-tab :title="__('Deployments')">
<deployments-service-table <deployments-service-table
:cloud-run-url="deploymentsCloudRunUrl" :cloud-run-url="enableCloudRunUrl"
:cloud-storage-url="deploymentsCloudStorageUrl" :cloud-storage-url="enableCloudStorageUrl"
/> />
</gl-tab> </gl-tab>
<gl-tab :title="__('Services')" disabled /> <gl-tab :title="__('Services')" disabled />
......
...@@ -4,10 +4,63 @@ class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::Base ...@@ -4,10 +4,63 @@ class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::Base
before_action :validate_gcp_token! before_action :validate_gcp_token!
def cloud_run def cloud_run
render json: "Placeholder" params = { token_in_session: token_in_session }
enable_cloud_run_response = GoogleCloud::EnableCloudRunService
.new(project, current_user, params).execute
if enable_cloud_run_response[:status] == :error
flash[:error] = enable_cloud_run_response[:message]
redirect_to project_google_cloud_index_path(project)
else
params = { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN }
generate_pipeline_response = GoogleCloud::GeneratePipelineService
.new(project, current_user, params).execute
if generate_pipeline_response[:status] == :error
flash[:error] = 'Failed to generate pipeline'
redirect_to project_google_cloud_index_path(project)
else
cloud_run_mr_params = cloud_run_mr_params(generate_pipeline_response[:branch_name])
redirect_to project_new_merge_request_path(project, merge_request: cloud_run_mr_params)
end
end
rescue Google::Apis::ClientError => error
handle_gcp_error(error, project)
end end
def cloud_storage def cloud_storage
render json: "Placeholder" render json: "Placeholder"
end end
private
def cloud_run_mr_params(branch_name)
{
title: cloud_run_mr_title,
description: cloud_run_mr_description(branch_name),
source_project_id: project.id,
target_project_id: project.id,
source_branch: branch_name,
target_branch: project.default_branch
}
end
def cloud_run_mr_title
'Enable deployments to Cloud Run'
end
def cloud_run_mr_description(branch_name)
<<-TEXT
This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).
The `deploy-to-cloud-run` job:
* Requires the following environment variables
* `GCP_PROJECT_ID`
* `GCP_SERVICE_ACCOUNT_KEY`
* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library
This pipeline definition has been committed to the branch `#{branch_name}`.
You may modify the pipeline definition further or accept the changes as-is if suitable.
TEXT
end
end end
...@@ -6,6 +6,8 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController ...@@ -6,6 +6,8 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
screen: 'home', screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project, serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project), createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg') emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg')
}.to_json }.to_json
end end
......
# frozen_string_literal: true
module GoogleCloud
class EnableCloudRunService < :: BaseService
def execute
gcp_project_ids = unique_gcp_project_ids
if gcp_project_ids.empty?
error("No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.")
else
google_api_client = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
gcp_project_ids.each do |gcp_project_id|
google_api_client.enable_cloud_run(gcp_project_id)
google_api_client.enable_artifacts_registry(gcp_project_id)
google_api_client.enable_cloud_build(gcp_project_id)
end
success({ gcp_project_ids: gcp_project_ids })
end
end
private
def unique_gcp_project_ids
all_gcp_project_ids = project.variables.filter { |var| var.key == 'GCP_PROJECT_ID' }.map { |var| var.value }
all_gcp_project_ids.uniq
end
def token_in_session
@params[:token_in_session]
end
end
end
...@@ -7,11 +7,12 @@ require 'google/apis/container_v1beta1' ...@@ -7,11 +7,12 @@ require 'google/apis/container_v1beta1'
require 'google/apis/cloudbilling_v1' require 'google/apis/cloudbilling_v1'
require 'google/apis/cloudresourcemanager_v1' require 'google/apis/cloudresourcemanager_v1'
require 'google/apis/iam_v1' require 'google/apis/iam_v1'
require 'google/apis/serviceusage_v1'
module GoogleApi module GoogleApi
module CloudPlatform module CloudPlatform
class Client < GoogleApi::Auth class Client < GoogleApi::Auth
SCOPE = 'https://www.googleapis.com/auth/cloud-platform' SCOPE = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/service.management'
LEAST_TOKEN_LIFE_TIME = 10.minutes LEAST_TOKEN_LIFE_TIME = 10.minutes
CLUSTER_MASTER_AUTH_USERNAME = 'admin' CLUSTER_MASTER_AUTH_USERNAME = 'admin'
CLUSTER_IPV4_CIDR_BLOCK = '/16' CLUSTER_IPV4_CIDR_BLOCK = '/16'
...@@ -133,8 +134,27 @@ module GoogleApi ...@@ -133,8 +134,27 @@ module GoogleApi
cloud_resource_manager_service.set_project_iam_policy(gcp_project_id, body) cloud_resource_manager_service.set_project_iam_policy(gcp_project_id, body)
end end
def enable_cloud_run(gcp_project_id)
enable_service(gcp_project_id, 'run.googleapis.com')
end
def enable_artifacts_registry(gcp_project_id)
enable_service(gcp_project_id, 'artifactregistry.googleapis.com')
end
def enable_cloud_build(gcp_project_id)
enable_service(gcp_project_id, 'cloudbuild.googleapis.com')
end
private private
def enable_service(gcp_project_id, service_name)
name = "projects/#{gcp_project_id}/services/#{service_name}"
service = Google::Apis::ServiceusageV1::ServiceUsageService.new
service.authorization = access_token
service.enable_service(name)
end
def make_cluster_options(cluster_name, cluster_size, machine_type, legacy_abac, enable_addons) def make_cluster_options(cluster_name, cluster_size, machine_type, legacy_abac, enable_addons)
{ {
cluster: { cluster: {
......
...@@ -24,8 +24,8 @@ const HOME_PROPS = { ...@@ -24,8 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}], serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account', createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration', emptyIllustrationUrl: '#url-empty-illustration',
deploymentsCloudRunUrl: '#url-deployments-cloud-run', enableCloudRunUrl: '#url-enable-cloud-run',
deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl', enableCloudStorageUrl: '#enableCloudStorageUrl',
}; };
describe('google_cloud App component', () => { describe('google_cloud App component', () => {
......
...@@ -12,8 +12,8 @@ describe('google_cloud DeploymentsServiceTable component', () => { ...@@ -12,8 +12,8 @@ describe('google_cloud DeploymentsServiceTable component', () => {
beforeEach(() => { beforeEach(() => {
const propsData = { const propsData = {
cloudRunUrl: '#url-deployments-cloud-run', cloudRunUrl: '#url-enable-cloud-run',
cloudStorageUrl: '#url-deployments-cloud-storage', cloudStorageUrl: '#url-enable-cloud-storage',
}; };
wrapper = mount(DeploymentsServiceTable, { propsData }); wrapper = mount(DeploymentsServiceTable, { propsData });
}); });
...@@ -29,12 +29,13 @@ describe('google_cloud DeploymentsServiceTable component', () => { ...@@ -29,12 +29,13 @@ describe('google_cloud DeploymentsServiceTable component', () => {
it('should contain configure cloud run button', () => { it('should contain configure cloud run button', () => {
const cloudRunButton = findCloudRunButton(); const cloudRunButton = findCloudRunButton();
expect(cloudRunButton.exists()).toBe(true); expect(cloudRunButton.exists()).toBe(true);
expect(cloudRunButton.props().disabled).toBe(true); expect(cloudRunButton.attributes('href')).toBe('#url-enable-cloud-run');
}); });
it('should contain configure cloud storage button', () => { it('should contain configure cloud storage button', () => {
const cloudStorageButton = findCloudStorageButton(); const cloudStorageButton = findCloudStorageButton();
expect(cloudStorageButton.exists()).toBe(true); expect(cloudStorageButton.exists()).toBe(true);
expect(cloudStorageButton.props().disabled).toBe(true); expect(cloudStorageButton.props().disabled).toBe(true);
expect(cloudStorageButton.attributes('href')).toBe('#url-enable-cloud-storage');
}); });
}); });
...@@ -20,8 +20,8 @@ describe('google_cloud Home component', () => { ...@@ -20,8 +20,8 @@ describe('google_cloud Home component', () => {
serviceAccounts: [{}, {}], serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account', createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration', emptyIllustrationUrl: '#url-empty-illustration',
deploymentsCloudRunUrl: '#url-deployments-cloud-run', enableCloudRunUrl: '#url-enable-cloud-run',
deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl', enableCloudStorageUrl: '#enableCloudStorageUrl',
}; };
beforeEach(() => { beforeEach(() => {
......
...@@ -6,6 +6,8 @@ RSpec.describe GoogleApi::CloudPlatform::Client do ...@@ -6,6 +6,8 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
let(:token) { 'token' } let(:token) { 'token' }
let(:client) { described_class.new(token, nil) } let(:client) { described_class.new(token, nil) }
let(:user_agent_options) { client.instance_eval { user_agent_header } } let(:user_agent_options) { client.instance_eval { user_agent_header } }
let(:gcp_project_id) { String('gcp_proj_id') }
let(:operation) { true }
describe '.session_key_for_redirect_uri' do describe '.session_key_for_redirect_uri' do
let(:state) { 'random_string' } let(:state) { 'random_string' }
...@@ -296,4 +298,40 @@ RSpec.describe GoogleApi::CloudPlatform::Client do ...@@ -296,4 +298,40 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
client.grant_service_account_roles(mock_gcp_id, mock_email) client.grant_service_account_roles(mock_gcp_id, mock_email)
end end
end end
describe '#enable_cloud_run' do
subject { client.enable_cloud_run(gcp_project_id) }
it 'calls Google Api IamService#create_service_account_key' do
expect_any_instance_of(Google::Apis::ServiceusageV1::ServiceUsageService)
.to receive(:enable_service)
.with("projects/#{gcp_project_id}/services/run.googleapis.com")
.and_return(operation)
is_expected.to eq(operation)
end
end
describe '#enable_artifacts_registry' do
subject { client.enable_artifacts_registry(gcp_project_id) }
it 'calls Google Api IamService#create_service_account_key' do
expect_any_instance_of(Google::Apis::ServiceusageV1::ServiceUsageService)
.to receive(:enable_service)
.with("projects/#{gcp_project_id}/services/artifactregistry.googleapis.com")
.and_return(operation)
is_expected.to eq(operation)
end
end
describe '#enable_cloud_build' do
subject { client.enable_cloud_build(gcp_project_id) }
it 'calls Google Api IamService#create_service_account_key' do
expect_any_instance_of(Google::Apis::ServiceusageV1::ServiceUsageService)
.to receive(:enable_service)
.with("projects/#{gcp_project_id}/services/cloudbuild.googleapis.com")
.and_return(operation)
is_expected.to eq(operation)
end
end
end end
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Projects::GoogleCloud::DeploymentsController do RSpec.describe Projects::GoogleCloud::DeploymentsController do
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
let_it_be(:user_guest) { create(:user) } let_it_be(:user_guest) { create(:user) }
let_it_be(:user_developer) { create(:user) } let_it_be(:user_developer) { create(:user) }
...@@ -36,8 +37,6 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do ...@@ -36,8 +37,6 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
it 'returns not found on GET request' do it 'returns not found on GET request' do
urls_list.each do |url| urls_list.each do |url|
unauthorized_members.each do |unauthorized_member| unauthorized_members.each do |unauthorized_member|
sign_in(unauthorized_member)
get url get url
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
...@@ -65,18 +64,63 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do ...@@ -65,18 +64,63 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" } let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
before do before do
sign_in(user_maintainer)
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true) allow(client).to receive(:validate_token).and_return(true)
end end
end end
it 'renders placeholder' do it 'redirects to google_cloud home on enable service error' do
authorized_members.each do |authorized_member| # since GPC_PROJECT_ID is not set, enable cloud run service should return an error
sign_in(authorized_member)
get url get url
expect(response).to have_gitlab_http_status(:ok) expect(response).to redirect_to(project_google_cloud_index_path(project))
end
it 'tracks error and redirects to gcp_error' do
mock_google_error = Google::Apis::ClientError.new('some_error')
allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
allow(service).to receive(:execute).and_raise(mock_google_error)
end
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(mock_google_error, { project_id: project.id })
get url
expect(response).to render_template(:gcp_error)
end
context 'GCP_PROJECT_IDs are defined' do
it 'redirects to google_cloud home on generate pipeline error' do
allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
end
allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service|
allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error })
end
get url
expect(response).to redirect_to(project_google_cloud_index_path(project))
end
it 'redirects to create merge request form' do
allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
allow(service).to receive(:execute).and_return({ status: :success })
end
allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |service|
allow(service).to receive(:execute).and_return({ status: :success })
end
get url
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to include(project_new_merge_request_path(project))
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GoogleCloud::EnableCloudRunService do
describe 'when a project does not have any gcp projects' do
let_it_be(:project) { create(:project) }
it 'returns error' do
result = described_class.new(project).execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.')
end
end
describe 'when a project has 3 gcp projects' do
let_it_be(:project) { create(:project) }
before do
project.variables.build(environment_scope: 'production', key: 'GCP_PROJECT_ID', value: 'prj-prod')
project.variables.build(environment_scope: 'staging', key: 'GCP_PROJECT_ID', value: 'prj-staging')
project.save!
end
it 'enables cloud run, artifacts registry and cloud build', :aggregate_failures do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
expect(instance).to receive(:enable_cloud_run).with('prj-prod')
expect(instance).to receive(:enable_artifacts_registry).with('prj-prod')
expect(instance).to receive(:enable_cloud_build).with('prj-prod')
expect(instance).to receive(:enable_cloud_run).with('prj-staging')
expect(instance).to receive(:enable_artifacts_registry).with('prj-staging')
expect(instance).to receive(:enable_cloud_build).with('prj-staging')
end
result = described_class.new(project).execute
expect(result[:status]).to eq(:success)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment