diff --git a/ee/lib/gitlab/ci/google_cloud/generate_build_environment_variables_service.rb b/ee/lib/gitlab/ci/google_cloud/generate_build_environment_variables_service.rb index 67ed067a84e39cc7a2eee87852a42987571e9f92..ef41e1f58ce6ded4f465936ea1030f4e9d655518 100644 --- a/ee/lib/gitlab/ci/google_cloud/generate_build_environment_variables_service.rb +++ b/ee/lib/gitlab/ci/google_cloud/generate_build_environment_variables_service.rb @@ -14,11 +14,7 @@ def initialize(build) def execute return [] unless @integration&.active - config_json = ::GoogleCloudPlatform::BaseClient.credentials( - audience: @integration.wlif, - encoded_jwt: encoded_jwt - ).to_json - + config_json = ::GoogleCloudPlatform.credentials(audience: @integration.wlif, encoded_jwt: encoded_jwt).to_json var_attributes = { value: config_json, public: false, masked: true, file: true } [ @@ -30,7 +26,7 @@ def execute private def encoded_jwt - JwtV2.for_build(@build, aud: ::GoogleCloudPlatform::BaseClient::GLGO_BASE_URL, wlif: @integration.wlif) + JwtV2.for_build(@build, aud: ::GoogleCloudPlatform::GLGO_BASE_URL, wlif: @integration.wlif) end end end diff --git a/ee/lib/google_cloud_platform.rb b/ee/lib/google_cloud_platform.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2e0312118c01a08e9262bb46947e1b0b4c3e408 --- /dev/null +++ b/ee/lib/google_cloud_platform.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module GoogleCloudPlatform + GLGO_BASE_URL = if Gitlab.staging? + 'https://glgo.staging.runway.gitlab.net' + else + 'https://auth.gcp.gitlab.com' + end + + GLGO_TOKEN_ENDPOINT_URL = "#{GLGO_BASE_URL}/token".freeze + + CREDENTIALS_TYPE = 'external_account' + STS_URL = 'https://sts.googleapis.com/v1/token' + SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:jwt' + CREDENTIAL_SOURCE_FORMAT = { + 'type' => 'json', + 'subject_token_field_name' => 'token' + }.freeze + + ApiError = Class.new(StandardError) + AuthenticationError = Class.new(StandardError) + + def self.credentials(audience:, encoded_jwt:) + { + type: CREDENTIALS_TYPE, + audience: audience, + token_url: STS_URL, + subject_token_type: SUBJECT_TOKEN_TYPE, + credential_source: { + url: GLGO_TOKEN_ENDPOINT_URL, + headers: { 'Authorization' => "Bearer #{encoded_jwt}" }, + format: CREDENTIAL_SOURCE_FORMAT + } + } + end +end diff --git a/ee/lib/google_cloud_platform/artifact_registry/client.rb b/ee/lib/google_cloud_platform/artifact_registry/client.rb index 72fb24e7205d0e49b840b795560b81ddfb2fd3a3..76b85fe6d426d47c93d817750107916e16afcd05 100644 --- a/ee/lib/google_cloud_platform/artifact_registry/client.rb +++ b/ee/lib/google_cloud_platform/artifact_registry/client.rb @@ -9,18 +9,9 @@ class Client < ::GoogleCloudPlatform::BaseClient DEFAULT_PAGE_SIZE = 10 - GCP_SUBJECT_TOKEN_ERROR_MESSAGE = 'Unable to retrieve Identity Pool subject token' - GCP_TOKEN_EXCHANGE_ERROR_MESSAGE = 'Token exchange failed' - - AuthenticationError = Class.new(StandardError) - ApiError = Class.new(StandardError) - - BLANK_PARAMETERS_ERROR_MESSAGE = 'All GCP parameters are required' - SAAS_ONLY_ERROR_MESSAGE = "This is a saas only feature that can't run here" - # Initialize and build a new ArtifactRegistry client. # This will use glgo and a workload identity federation instance to exchange - # a JWT from GitLab for an access token to be used with the GCP API. + # a JWT from GitLab for an access token to be used with the Google Cloud API. # # +project+ The Project instance. # +user+ The User instance. @@ -38,18 +29,12 @@ class Client < ::GoogleCloudPlatform::BaseClient # +ArgumentError+ if one or more of the parameters is blank. # +RuntimeError+ if this is used outside the Saas instance. def initialize(project:, user:, gcp_project_id:, gcp_location:, gcp_repository:, gcp_wlif:) - raise SAAS_ONLY_ERROR_MESSAGE unless Gitlab::Saas.feature_available?(:google_artifact_registry) + super(project: project, user: user, gcp_project_id: gcp_project_id, gcp_wlif: gcp_wlif) - super(project: project, user: user) - - if gcp_project_id.blank? || gcp_location.blank? || gcp_repository.blank? || gcp_wlif.blank? - raise ArgumentError, BLANK_PARAMETERS_ERROR_MESSAGE - end + raise ArgumentError, BLANK_PARAMETERS_ERROR_MESSAGE if gcp_location.blank? || gcp_repository.blank? - @gcp_project_id = gcp_project_id @gcp_location = gcp_location @gcp_repository = gcp_repository - @gcp_wlif = gcp_wlif end # Get the Artifact Registry repository object and return it. @@ -61,9 +46,10 @@ def initialize(project:, user:, gcp_project_id:, gcp_location:, gcp_repository:, # # Possible exceptions: # - # +GoogleCloudPlatform::ArtifactRegistry::Client::AuthenticationError+ if an error occurs during the + # +GoogleCloudPlatform::AuthenticationError+ if an error occurs during the # authentication. - # +GoogleCloudPlatform::ArtifactRegistry::Client::ApiError+ if an error occurs when interacting with the GCP API. + # +GoogleCloudPlatform::ApiError+ if an error occurs when interacting with the + # Google Cloud API. def repository request = ::Google::Cloud::ArtifactRegistry::V1::GetRepositoryRequest.new(name: repository_full_name) @@ -94,9 +80,10 @@ def repository # # Possible exceptions: # - # +GoogleCloudPlatform::ArtifactRegistry::Client::AuthenticationError+ if an error occurs during the + # +GoogleCloudPlatform::AuthenticationError+ if an error occurs during the # authentication. - # +GoogleCloudPlatform::ArtifactRegistry::Client::ApiError+ if an error occurs when interacting with the GCP API. + # +GoogleCloudPlatform::ApiError+ if an error occurs when interacting with the + # Google Cloud API. def docker_images(page_size: nil, page_token: nil, order_by: nil) page_size = DEFAULT_PAGE_SIZE if page_size.blank? request = ::Google::Cloud::ArtifactRegistry::V1::ListDockerImagesRequest.new( @@ -115,15 +102,16 @@ def docker_images(page_size: nil, page_token: nil, order_by: nil) # It will call the gRPC version of # https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.dockerImages/get # - # +name+ Name of the docker image as returned by the GCP API when using +docker_images+ + # +name+ Name of the docker image as returned by the Google Cloud API when using +docker_images+ # # Return an instance of +Google::Cloud::ArtifactRegistry::V1::DockerImage+. # # Possible exceptions: # - # +GoogleCloudPlatform::ArtifactRegistry::Client::AuthenticationError+ if an error occurs during the + # +GoogleCloudPlatform::AuthenticationError+ if an error occurs during the # authentication. - # +GoogleCloudPlatform::ArtifactRegistry::Client::ApiError+ if an error occurs when interacting with the GCP API. + # +GoogleCloudPlatform::ApiError+ if an error occurs when interacting with the + # Google Cloud API. def docker_image(name:) request = ::Google::Cloud::ArtifactRegistry::V1::GetDockerImageRequest.new(name: name) @@ -136,7 +124,7 @@ def docker_image(name:) def gcp_client ::Google::Cloud::ArtifactRegistry::V1::ArtifactRegistry::Client.new do |config| - json_key_io = StringIO.new(::Gitlab::Json.dump(credentials(wlif: @gcp_wlif))) + json_key_io = StringIO.new(::Gitlab::Json.dump(credentials)) ext_credentials = Google::Auth::ExternalAccount::Credentials.make_creds( json_key_io: json_key_io, scope: CLOUD_PLATFORM_SCOPE @@ -146,20 +134,8 @@ def gcp_client end strong_memoize_attr :gcp_client - def handling_errors - yield - rescue RuntimeError => e - if e.message.include?(GCP_SUBJECT_TOKEN_ERROR_MESSAGE) || e.message.include?(GCP_TOKEN_EXCHANGE_ERROR_MESSAGE) - raise AuthenticationError, e.message - end - - raise - rescue ::Google::Cloud::Error => e - raise ApiError, e.message - end - def repository_full_name - "projects/#{@gcp_project_id}/locations/#{@gcp_location}/repositories/#{@gcp_repository}" + "projects/#{gcp_project_id}/locations/#{@gcp_location}/repositories/#{@gcp_repository}" end strong_memoize_attr :repository_full_name end diff --git a/ee/lib/google_cloud_platform/base_client.rb b/ee/lib/google_cloud_platform/base_client.rb index afc14afd54b4069b75b5038b18993bbd1614d6ef..75df56e7e0c34e923a17249f17d2d7b9a2e99e2e 100644 --- a/ee/lib/google_cloud_platform/base_client.rb +++ b/ee/lib/google_cloud_platform/base_client.rb @@ -2,66 +2,77 @@ module GoogleCloudPlatform class BaseClient - GLGO_BASE_URL = if Gitlab.staging? - 'https://glgo.staging.runway.gitlab.net' - else - 'https://auth.gcp.gitlab.com' - end - - GLGO_TOKEN_ENDPOINT_URL = "#{GLGO_BASE_URL}/token".freeze - - CREDENTIALS_TYPE = 'external_account' - STS_URL = 'https://sts.googleapis.com/v1/token' - SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:jwt' - CREDENTIAL_SOURCE_FORMAT = { - 'type' => 'json', - 'subject_token_field_name' => 'token' - }.freeze - CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform' - BLANK_PARAMETERS_ERROR_MESSAGE = 'Project and user parameters are required' + GCP_SUBJECT_TOKEN_ERROR_MESSAGE = 'Unable to retrieve glgo token' + GCP_TOKEN_EXCHANGE_ERROR_MESSAGE = 'Token exchange failed' - def self.credentials(audience:, encoded_jwt:) - { - type: CREDENTIALS_TYPE, - audience: audience, - token_url: STS_URL, - subject_token_type: SUBJECT_TOKEN_TYPE, - credential_source: { - url: GLGO_TOKEN_ENDPOINT_URL, - headers: { 'Authorization' => "Bearer #{encoded_jwt}" }, - format: CREDENTIAL_SOURCE_FORMAT - } - } - end + SAAS_ONLY_ERROR_MESSAGE = "This is a SaaS-only feature that can't run here" + BLANK_PARAMETERS_ERROR_MESSAGE = 'All Google Cloud parameters are required' - def initialize(project:, user:) - raise ArgumentError, BLANK_PARAMETERS_ERROR_MESSAGE if project.blank? || user.blank? + # Initialize and build a new Compute client. + # This will use glgo and a workload identity federation instance to exchange + # a JWT from GitLab for an access token to be used with the Google Cloud API. + # + # +project+ The Project instance. + # +user+ The User instance. + # +gcp_project_id+ The Google project_id as a string. Example: 'my-project'. + # +gcp_wlif+ The Google workload identity federation string. Similar to a URL but without the + # protocol. Example: + # '//iam.googleapis.com/projects/555/locations/global/workloadIdentityPools/pool/providers/sandbox'. + # + # All parameters are required. + # + # Possible exceptions: + # + # +ArgumentError+ if one or more of the parameters is blank. + # +RuntimeError+ if this is used outside the SaaS instance. + def initialize(project:, user:, gcp_project_id:, gcp_wlif:) + if project.blank? || user.blank? || gcp_project_id.blank? || gcp_wlif.blank? + raise ArgumentError, BLANK_PARAMETERS_ERROR_MESSAGE + end + + raise SAAS_ONLY_ERROR_MESSAGE unless Gitlab::Saas.feature_available?(:google_artifact_registry) @project = project @user = user + @gcp_project_id = gcp_project_id + @gcp_wlif = gcp_wlif end private - def credentials(wlif:) - self.class.credentials( - audience: wlif, - encoded_jwt: encoded_jwt(wlif: wlif) + attr_reader :project, :user, :gcp_project_id, :gcp_wlif + + def credentials + ::GoogleCloudPlatform.credentials( + audience: gcp_wlif, + encoded_jwt: encoded_jwt ) end - def encoded_jwt(wlif:) + def encoded_jwt jwt = ::GoogleCloudPlatform::Jwt.new( - project: @project, - user: @user, + project: project, + user: user, claims: { audience: GLGO_BASE_URL, - wlif: wlif + wlif: gcp_wlif } ) jwt.encoded end + + def handling_errors + yield + rescue RuntimeError => e + if e.message.include?(GCP_SUBJECT_TOKEN_ERROR_MESSAGE) || e.message.include?(GCP_TOKEN_EXCHANGE_ERROR_MESSAGE) + raise ::GoogleCloudPlatform::AuthenticationError, e.message + end + + raise + rescue ::Google::Cloud::Error => e + raise ::GoogleCloudPlatform::ApiError, e.message + end end end diff --git a/ee/lib/google_cloud_platform/compute/client.rb b/ee/lib/google_cloud_platform/compute/client.rb index 7a998b41d8857823bd422d4d9af0573990f323b6..55e6935b7057cc25ca03bde58c733d568ec42c5f 100644 --- a/ee/lib/google_cloud_platform/compute/client.rb +++ b/ee/lib/google_cloud_platform/compute/client.rb @@ -9,43 +9,6 @@ class Client < ::GoogleCloudPlatform::BaseClient COMPUTE_API_ENDPOINT = 'https://compute.googleapis.com' - GCP_SUBJECT_TOKEN_ERROR_MESSAGE = 'Unable to retrieve Identity Pool subject token' - GCP_TOKEN_EXCHANGE_ERROR_MESSAGE = 'Token exchange failed' - - AuthenticationError = Class.new(StandardError) - ApiError = Class.new(StandardError) - - BLANK_PARAMETERS_ERROR_MESSAGE = 'All GCP parameters are required' - SAAS_ONLY_ERROR_MESSAGE = "This is a saas only feature that can't run here" - - # Initialize and build a new Compute client. - # This will use glgo and a workload identity federation instance to exchange - # a JWT from GitLab for an access token to be used with the GCP API. - # - # +project+ The Project instance. - # +user+ The User instance. - # +gcp_project_id+ The Google project_id as a string. Example: 'my-project'. - # +gcp_wlif+ The Google workload identity federation string. Similar to a URL but without the - # protocol. Example: - # '//iam.googleapis.com/projects/555/locations/global/workloadIdentityPools/pool/providers/sandbox'. - # - # All parameters are required. - # - # Possible exceptions: - # - # +ArgumentError+ if one or more of the parameters is blank. - # +RuntimeError+ if this is used outside the Saas instance. - def initialize(project:, user:, gcp_project_id:, gcp_wlif:) - raise SAAS_ONLY_ERROR_MESSAGE unless Gitlab::Saas.feature_available?(:google_artifact_registry) - - super(project: project, user: user) - - raise ArgumentError, BLANK_PARAMETERS_ERROR_MESSAGE if gcp_project_id.blank? || gcp_wlif.blank? - - @gcp_project_id = gcp_project_id - @gcp_wlif = gcp_wlif - end - # Retrieves the list of region resources available to the specified project. # # It will call the REST version of https://cloud.google.com/compute/docs/reference/rest/v1/region/list. @@ -69,8 +32,9 @@ def initialize(project:, user:, gcp_project_id:, gcp_wlif:) # # Possible exceptions: # - # +GoogleCloudPlatform::Compute::Client::AuthenticationError+ if an error occurs during the authentication. - # +GoogleCloudPlatform::Compute::Client::ApiError+ if an error occurs when interacting with the GCP API. + # +GoogleCloudPlatform::Compute::BaseClient::AuthenticationError+ if an error occurs during the authentication. + # +GoogleCloudPlatform::Compute::BaseClient::ApiError+ if an error occurs when interacting with the + # Google Cloud API. def regions(filter: nil, max_results: 500, order_by: nil, page_token: nil) request = ::Google::Cloud::Compute::V1::ListRegionsRequest.new( project: @gcp_project_id, @@ -107,8 +71,9 @@ def regions(filter: nil, max_results: 500, order_by: nil, page_token: nil) # # Possible exceptions: # - # +GoogleCloudPlatform::Compute::Client::AuthenticationError+ if an error occurs during the authentication. - # +GoogleCloudPlatform::Compute::Client::ApiError+ if an error occurs when interacting with the GCP API. + # +GoogleCloudPlatform::Compute::BaseClient::AuthenticationError+ if an error occurs during the authentication. + # +GoogleCloudPlatform::Compute::BaseClient::ApiError+ if an error occurs when interacting with the + # Google Cloud API. def zones(filter: nil, max_results: 500, order_by: nil, page_token: nil) request = ::Google::Cloud::Compute::V1::ListZonesRequest.new( project: @gcp_project_id, @@ -146,8 +111,9 @@ def zones(filter: nil, max_results: 500, order_by: nil, page_token: nil) # # Possible exceptions: # - # +GoogleCloudPlatform::Compute::Client::AuthenticationError+ if an error occurs during the authentication. - # +GoogleCloudPlatform::Compute::Client::ApiError+ if an error occurs when interacting with the GCP API. + # +GoogleCloudPlatform::Compute::BaseClient::AuthenticationError+ if an error occurs during the authentication. + # +GoogleCloudPlatform::Compute::BaseClient::ApiError+ if an error occurs when interacting with the + # Google Cloud API. def machine_types(zone:, filter: nil, max_results: 500, order_by: nil, page_token: nil) request = ::Google::Cloud::Compute::V1::ListMachineTypesRequest.new( project: @gcp_project_id, @@ -187,7 +153,7 @@ def client_for(klass) end def external_credentials - json_key_io = StringIO.new(::Gitlab::Json.dump(credentials(wlif: @gcp_wlif))) + json_key_io = StringIO.new(::Gitlab::Json.dump(credentials)) ext_credentials = Google::Auth::ExternalAccount::Credentials.make_creds( json_key_io: json_key_io, scope: CLOUD_PLATFORM_SCOPE @@ -195,18 +161,6 @@ def external_credentials ::Google::Cloud::Compute::V1::Instances::Credentials.new(ext_credentials) end strong_memoize_attr :external_credentials - - def handling_errors - yield - rescue RuntimeError => e - if e.message.include?(GCP_SUBJECT_TOKEN_ERROR_MESSAGE) || e.message.include?(GCP_TOKEN_EXCHANGE_ERROR_MESSAGE) - raise AuthenticationError, e.message - end - - raise - rescue ::Google::Cloud::Error => e - raise ApiError, e.message - end end end end diff --git a/ee/spec/lib/google_cloud_platform/artifact_registry/client_spec.rb b/ee/spec/lib/google_cloud_platform/artifact_registry/client_spec.rb index 99b1e9cbd89cd6eacdab8c4a9e4d1936d3c87073..ebc6a156dcc250e54b31e84bded426b55d1d41c5 100644 --- a/ee/spec/lib/google_cloud_platform/artifact_registry/client_spec.rb +++ b/ee/spec/lib/google_cloud_platform/artifact_registry/client_spec.rb @@ -58,12 +58,12 @@ it_behaves_like 'transforming the error', message: "test #{described_class::GCP_SUBJECT_TOKEN_ERROR_MESSAGE} test", from_klass: RuntimeError, - to_klass: described_class::AuthenticationError + to_klass: ::GoogleCloudPlatform::AuthenticationError it_behaves_like 'transforming the error', message: "test #{described_class::GCP_TOKEN_EXCHANGE_ERROR_MESSAGE} test", from_klass: RuntimeError, - to_klass: described_class::AuthenticationError + to_klass: ::GoogleCloudPlatform::AuthenticationError it_behaves_like 'transforming the error', message: "test", @@ -73,7 +73,7 @@ it_behaves_like 'transforming the error', message: "test", from_klass: ::Google::Cloud::Error, - to_klass: described_class::ApiError + to_klass: ::GoogleCloudPlatform::ApiError end describe 'validations' do @@ -212,9 +212,9 @@ end def stub_authentication_requests - stub_request(:get, ::GoogleCloudPlatform::BaseClient::GLGO_TOKEN_ENDPOINT_URL) + stub_request(:get, ::GoogleCloudPlatform::GLGO_TOKEN_ENDPOINT_URL) .to_return(status: 200, body: ::Gitlab::Json.dump(token: 'token')) - stub_request(:post, ::GoogleCloudPlatform::BaseClient::STS_URL) + stub_request(:post, ::GoogleCloudPlatform::STS_URL) .to_return(status: 200, body: ::Gitlab::Json.dump(token: 'token')) end end diff --git a/ee/spec/lib/google_cloud_platform/compute/client_spec.rb b/ee/spec/lib/google_cloud_platform/compute/client_spec.rb index bd6ab50897c087839f1329bf30440c51e6d09873..641147a5f9a5bb1bf0e81dcf7b199b56fdd6b3ae 100644 --- a/ee/spec/lib/google_cloud_platform/compute/client_spec.rb +++ b/ee/spec/lib/google_cloud_platform/compute/client_spec.rb @@ -52,12 +52,12 @@ it_behaves_like 'transforming the error', message: "test #{described_class::GCP_SUBJECT_TOKEN_ERROR_MESSAGE} test", from_klass: RuntimeError, - to_klass: described_class::AuthenticationError + to_klass: ::GoogleCloudPlatform::AuthenticationError it_behaves_like 'transforming the error', message: "test #{described_class::GCP_TOKEN_EXCHANGE_ERROR_MESSAGE} test", from_klass: RuntimeError, - to_klass: described_class::AuthenticationError + to_klass: ::GoogleCloudPlatform::AuthenticationError it_behaves_like 'transforming the error', message: "test", @@ -67,7 +67,7 @@ it_behaves_like 'transforming the error', message: "test", from_klass: ::Google::Cloud::Error, - to_klass: described_class::ApiError + to_klass: ::GoogleCloudPlatform::ApiError end describe 'validations' do @@ -300,9 +300,9 @@ end def stub_authentication_requests - stub_request(:get, ::GoogleCloudPlatform::BaseClient::GLGO_TOKEN_ENDPOINT_URL) + stub_request(:get, ::GoogleCloudPlatform::GLGO_TOKEN_ENDPOINT_URL) .to_return(status: 200, body: ::Gitlab::Json.dump(token: 'token')) - stub_request(:post, ::GoogleCloudPlatform::BaseClient::STS_URL) + stub_request(:post, ::GoogleCloudPlatform::STS_URL) .to_return(status: 200, body: ::Gitlab::Json.dump(token: 'token')) end end