Commit 9778f115 authored by Sri's avatar Sri

Enable Cloud Run deploys

This commit enables the Cloud Run deployment method in
the `Project :: Infra :: Google Cloud` section.

Minimal frontend changes that rename props and flip the
disabled switch.

Backend changes introduces a service to enable the underlying
Google Cloud services via their API. Then of course there's a
controller method that responds to the user clicking the now
enabled button.

Detailed changelist:

- Frontend
    - Enable the relevant button in deployment service table
    - Rename the necessary props
    - Specs / tests
- Backend
    - Rename @js_data attributes
    - Enable Cloud Run service
        - Service that enables cloud run, artifacts registry
          and cloud build for the logged in user's
          Google Cloud account
    - Google API -> Client
        - Methods that make the underlying calls to enable
          cloud run, artifacts registry and cloud build
    - Controller method to handle user selecting the
      `Deploy Cloud Run` button
        - Calls the newly created `EnableCloudRunService`
        - Calls the `GeneratePipelineService`
        - Redirects the user appropriately based on
          service responses
    - Specs / tests
parent 9870ee7a
......@@ -2,6 +2,9 @@
import { GlButton, GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
const cloudRun = 'cloudRun';
const cloudStorage = 'cloudStorage';
const i18n = {
cloudRun: __('Cloud Run'),
cloudRunDescription: __('Deploy container based web apps on Google managed clusters'),
......@@ -28,6 +31,13 @@ export default {
required: true,
},
},
methods: {
actionUrl(key) {
if (key === cloudRun) return this.cloudRunUrl;
else if (key === cloudStorage) return this.cloudStorageUrl;
return '#';
},
},
fields: [
{ key: 'title', label: i18n.service },
{ key: 'description', label: i18n.description },
......@@ -37,12 +47,19 @@ export default {
{
title: i18n.cloudRun,
description: i18n.cloudRunDescription,
action: { title: i18n.configureViaMergeRequest, disabled: true },
action: {
key: cloudRun,
title: i18n.configureViaMergeRequest,
},
},
{
title: i18n.cloudStorage,
description: i18n.cloudStorageDescription,
action: { title: i18n.configureViaMergeRequest, disabled: true },
action: {
key: cloudStorage,
title: i18n.configureViaMergeRequest,
disabled: true,
},
},
],
i18n,
......@@ -54,7 +71,9 @@ export default {
<p>{{ $options.i18n.deploymentsDescription }}</p>
<gl-table :fields="$options.fields" :items="$options.items">
<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>
</gl-table>
</div>
......
......@@ -23,11 +23,11 @@ export default {
type: String,
required: true,
},
deploymentsCloudRunUrl: {
enableCloudRunUrl: {
type: String,
required: true,
},
deploymentsCloudStorageUrl: {
enableCloudStorageUrl: {
type: String,
required: true,
},
......@@ -47,8 +47,8 @@ export default {
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployments-service-table
:cloud-run-url="deploymentsCloudRunUrl"
:cloud-storage-url="deploymentsCloudStorageUrl"
:cloud-run-url="enableCloudRunUrl"
:cloud-storage-url="enableCloudStorageUrl"
/>
</gl-tab>
<gl-tab :title="__('Services')" disabled />
......
......@@ -4,10 +4,63 @@ class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::Base
before_action :validate_gcp_token!
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
def cloud_storage
render json: "Placeholder"
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
......@@ -6,6 +6,8 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_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')
}.to_json
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'
require 'google/apis/cloudbilling_v1'
require 'google/apis/cloudresourcemanager_v1'
require 'google/apis/iam_v1'
require 'google/apis/serviceusage_v1'
module GoogleApi
module CloudPlatform
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
CLUSTER_MASTER_AUTH_USERNAME = 'admin'
CLUSTER_IPV4_CIDR_BLOCK = '/16'
......@@ -133,8 +134,27 @@ module GoogleApi
cloud_resource_manager_service.set_project_iam_policy(gcp_project_id, body)
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
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)
{
cluster: {
......
......@@ -24,8 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
deploymentsCloudRunUrl: '#url-deployments-cloud-run',
deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
enableCloudRunUrl: '#url-enable-cloud-run',
enableCloudStorageUrl: '#enableCloudStorageUrl',
};
describe('google_cloud App component', () => {
......
......@@ -12,8 +12,8 @@ describe('google_cloud DeploymentsServiceTable component', () => {
beforeEach(() => {
const propsData = {
cloudRunUrl: '#url-deployments-cloud-run',
cloudStorageUrl: '#url-deployments-cloud-storage',
cloudRunUrl: '#url-enable-cloud-run',
cloudStorageUrl: '#url-enable-cloud-storage',
};
wrapper = mount(DeploymentsServiceTable, { propsData });
});
......@@ -29,12 +29,13 @@ describe('google_cloud DeploymentsServiceTable component', () => {
it('should contain configure cloud run button', () => {
const cloudRunButton = findCloudRunButton();
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', () => {
const cloudStorageButton = findCloudStorageButton();
expect(cloudStorageButton.exists()).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', () => {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
deploymentsCloudRunUrl: '#url-deployments-cloud-run',
deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
enableCloudRunUrl: '#url-enable-cloud-run',
enableCloudStorageUrl: '#enableCloudStorageUrl',
};
beforeEach(() => {
......
......@@ -6,6 +6,8 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
let(:token) { 'token' }
let(:client) { described_class.new(token, nil) }
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
let(:state) { 'random_string' }
......@@ -296,4 +298,40 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
client.grant_service_account_roles(mock_gcp_id, mock_email)
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
......@@ -3,7 +3,8 @@
require 'spec_helper'
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_developer) { create(:user) }
......@@ -36,8 +37,6 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
it 'returns not found on GET request' do
urls_list.each do |url|
unauthorized_members.each do |unauthorized_member|
sign_in(unauthorized_member)
get url
expect(response).to have_gitlab_http_status(:not_found)
......@@ -65,18 +64,63 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
before do
sign_in(user_maintainer)
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
end
end
it 'renders placeholder' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
it 'redirects to google_cloud home on enable service error' do
# since GPC_PROJECT_ID is not set, enable cloud run service should return an error
get url
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 have_gitlab_http_status(:ok)
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
......
# 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' 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