GraphQL: Add `group.runnerCloudProvisioning` field
-
Review changes -
-
Download -
Patches
-
Plain diff
What does this MR do and why?
This MR adds the runnerCloudProvisioning
field to GroupType
, similar to !145831 (merged) which introduced it at the ProjectType
level. This doesn't require a documentation review since it is autogenerated from an existing type.
It also temporarily removes the regions
/zones
/machineTypes
fields, since they currently only work at the project level due to JWT class limitations, and are not actually in use.
Patch to re-add support
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d2d6e23ba90a..3ebac12f6b6f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -9951,6 +9951,75 @@ The edge type for [`CiProjectVariable`](#ciprojectvariable).
| <a id="ciprojectvariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="ciprojectvariableedgenode"></a>`node` | [`CiProjectVariable`](#ciprojectvariable) | The item at the end of the edge. |
+#### `CiRunnerCloudProvisioningMachineTypeConnection`
+
+The connection type for [`CiRunnerCloudProvisioningMachineType`](#cirunnercloudprovisioningmachinetype).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningmachinetypeconnectionedges"></a>`edges` | [`[CiRunnerCloudProvisioningMachineTypeEdge]`](#cirunnercloudprovisioningmachinetypeedge) | A list of edges. |
+| <a id="cirunnercloudprovisioningmachinetypeconnectionnodes"></a>`nodes` | [`[CiRunnerCloudProvisioningMachineType]`](#cirunnercloudprovisioningmachinetype) | A list of nodes. |
+| <a id="cirunnercloudprovisioningmachinetypeconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiRunnerCloudProvisioningMachineTypeEdge`
+
+The edge type for [`CiRunnerCloudProvisioningMachineType`](#cirunnercloudprovisioningmachinetype).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningmachinetypeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cirunnercloudprovisioningmachinetypeedgenode"></a>`node` | [`CiRunnerCloudProvisioningMachineType`](#cirunnercloudprovisioningmachinetype) | The item at the end of the edge. |
+
+#### `CiRunnerCloudProvisioningRegionConnection`
+
+The connection type for [`CiRunnerCloudProvisioningRegion`](#cirunnercloudprovisioningregion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningregionconnectionedges"></a>`edges` | [`[CiRunnerCloudProvisioningRegionEdge]`](#cirunnercloudprovisioningregionedge) | A list of edges. |
+| <a id="cirunnercloudprovisioningregionconnectionnodes"></a>`nodes` | [`[CiRunnerCloudProvisioningRegion]`](#cirunnercloudprovisioningregion) | A list of nodes. |
+| <a id="cirunnercloudprovisioningregionconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiRunnerCloudProvisioningRegionEdge`
+
+The edge type for [`CiRunnerCloudProvisioningRegion`](#cirunnercloudprovisioningregion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningregionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cirunnercloudprovisioningregionedgenode"></a>`node` | [`CiRunnerCloudProvisioningRegion`](#cirunnercloudprovisioningregion) | The item at the end of the edge. |
+
+#### `CiRunnerCloudProvisioningZoneConnection`
+
+The connection type for [`CiRunnerCloudProvisioningZone`](#cirunnercloudprovisioningzone).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningzoneconnectionedges"></a>`edges` | [`[CiRunnerCloudProvisioningZoneEdge]`](#cirunnercloudprovisioningzoneedge) | A list of edges. |
+| <a id="cirunnercloudprovisioningzoneconnectionnodes"></a>`nodes` | [`[CiRunnerCloudProvisioningZone]`](#cirunnercloudprovisioningzone) | A list of nodes. |
+| <a id="cirunnercloudprovisioningzoneconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiRunnerCloudProvisioningZoneEdge`
+
+The edge type for [`CiRunnerCloudProvisioningZone`](#cirunnercloudprovisioningzone).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningzoneedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cirunnercloudprovisioningzoneedgenode"></a>`node` | [`CiRunnerCloudProvisioningZone`](#cirunnercloudprovisioningzone) | The item at the end of the edge. |
+
#### `CiRunnerConnection`
The connection type for [`CiRunner`](#cirunner).
@@ -16523,6 +16592,29 @@ Returns [`CiRunnerStatus!`](#cirunnerstatus).
| ---- | ---- | ----------- |
| <a id="cirunnerstatuslegacymode"></a>`legacyMode` **{warning-solid}** | [`String`](#string) | **Deprecated** in GitLab 15.0. Will be removed in 17.0. |
+### `CiRunnerCloudProvisioningMachineType`
+
+Machine type used for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningmachinetypedescription"></a>`description` | [`String`](#string) | Description of the machine type. |
+| <a id="cirunnercloudprovisioningmachinetypename"></a>`name` | [`GoogleCloudMachineType`](#googlecloudmachinetype) | Name of the machine type. |
+| <a id="cirunnercloudprovisioningmachinetypezone"></a>`zone` | [`GoogleCloudZone`](#googlecloudzone) | Zone of the machine type. |
+
+### `CiRunnerCloudProvisioningRegion`
+
+Region used for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningregiondescription"></a>`description` | [`String`](#string) | Description of the region. |
+| <a id="cirunnercloudprovisioningregionname"></a>`name` | [`GoogleCloudRegion`](#googlecloudregion) | Name of the region. |
+
### `CiRunnerCloudProvisioningStep`
Step used to provision the runner to Google Cloud.
@@ -16535,6 +16627,17 @@ Step used to provision the runner to Google Cloud.
| <a id="cirunnercloudprovisioningsteplanguageidentifier"></a>`languageIdentifier` | [`String`](#string) | Identifier of the language used for the instructions field. This identifier can be any of the identifiers specified in the [list of supported languages and lexers](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers). |
| <a id="cirunnercloudprovisioningsteptitle"></a>`title` | [`String`](#string) | Title of the step. |
+### `CiRunnerCloudProvisioningZone`
+
+Zone used for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningzonedescription"></a>`description` | [`String`](#string) | Description of the zone. |
+| <a id="cirunnercloudprovisioningzonename"></a>`name` | [`GoogleCloudZone`](#googlecloudzone) | Name of the zone. |
+
### `CiRunnerGoogleCloudProvisioning`
Information used for runner Google Cloud provisioning.
@@ -16544,9 +16647,26 @@ Information used for runner Google Cloud provisioning.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnergooglecloudprovisioningprojectsetupshellscript"></a>`projectSetupShellScript` | [`String`](#string) | Instructions for setting up a Google Cloud project. |
+| <a id="cirunnergooglecloudprovisioningregions"></a>`regions` | [`CiRunnerCloudProvisioningRegionConnection`](#cirunnercloudprovisioningregionconnection) | Regions available for provisioning a runner. Only available for projects. (see [Connections](#connections)) |
#### Fields with arguments
+##### `CiRunnerGoogleCloudProvisioning.machineTypes`
+
+Machine types available for provisioning a runner. Only available for projects.
+
+Returns [`CiRunnerCloudProvisioningMachineTypeConnection`](#cirunnercloudprovisioningmachinetypeconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnergooglecloudprovisioningmachinetypeszone"></a>`zone` | [`GoogleCloudZone!`](#googlecloudzone) | Zone to retrieve machine types for. |
+
##### `CiRunnerGoogleCloudProvisioning.provisioningSteps`
Steps used to provision a runner in the cloud.
@@ -16562,6 +16682,22 @@ Returns [`[CiRunnerCloudProvisioningStep!]`](#cirunnercloudprovisioningstep).
| <a id="cirunnergooglecloudprovisioningprovisioningstepsrunnertoken"></a>`runnerToken` | [`String`](#string) | Authentication token of the runner. |
| <a id="cirunnergooglecloudprovisioningprovisioningstepszone"></a>`zone` | [`GoogleCloudZone!`](#googlecloudzone) | Name of the zone to provision the runner in. |
+##### `CiRunnerGoogleCloudProvisioning.zones`
+
+Zones available for provisioning a runner. Only available for projects.
+
+Returns [`CiRunnerCloudProvisioningZoneConnection`](#cirunnercloudprovisioningzoneconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnergooglecloudprovisioningzonesregion"></a>`region` | [`GoogleCloudRegion`](#googlecloudregion) | Region to retrieve zones for. Returns all zones if not specified. |
+
### `CiRunnerManager`
#### Fields
diff --git a/ee/app/graphql/types/ci/runner_google_cloud_provisioning_type.rb b/ee/app/graphql/types/ci/runner_google_cloud_provisioning_type.rb
index 7f0ffc898365..49ae83e67ff4 100644
--- a/ee/app/graphql/types/ci/runner_google_cloud_provisioning_type.rb
+++ b/ee/app/graphql/types/ci/runner_google_cloud_provisioning_type.rb
@@ -12,6 +12,34 @@ class RunnerGoogleCloudProvisioningType < BaseObject
authorize :read_runner_cloud_provisioning_info
+ field :regions, Types::Ci::RunnerCloudProvisioningRegionType.connection_type,
+ description: 'Regions available for provisioning a runner. Only available for projects.',
+ null: true,
+ connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension,
+ max_page_size: GoogleCloudPlatform::Compute::ListRegionsService::MAX_RESULTS_LIMIT,
+ default_page_size: GoogleCloudPlatform::Compute::ListRegionsService::MAX_RESULTS_LIMIT
+
+ field :zones, Types::Ci::RunnerCloudProvisioningZoneType.connection_type,
+ description: 'Zones available for provisioning a runner. Only available for projects.',
+ null: true,
+ connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension,
+ max_page_size: GoogleCloudPlatform::Compute::ListZonesService::MAX_RESULTS_LIMIT,
+ default_page_size: GoogleCloudPlatform::Compute::ListZonesService::MAX_RESULTS_LIMIT do
+ argument :region, Types::GoogleCloud::RegionType, required: false,
+ description: 'Region to retrieve zones for. Returns all zones if not specified.'
+ end
+
+ field :machine_types,
+ Types::Ci::RunnerCloudProvisioningMachineTypeType.connection_type,
+ description: 'Machine types available for provisioning a runner. Only available for projects.',
+ null: true,
+ connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension,
+ max_page_size: GoogleCloudPlatform::Compute::ListMachineTypesService::MAX_RESULTS_LIMIT,
+ default_page_size: GoogleCloudPlatform::Compute::ListMachineTypesService::MAX_RESULTS_LIMIT do
+ argument :zone, Types::GoogleCloud::ZoneType, required: true,
+ description: 'Zone to retrieve machine types for.'
+ end
+
field :project_setup_shell_script, GraphQL::Types::String, null: true,
description: 'Instructions for setting up a Google Cloud project.'
@@ -23,6 +51,44 @@ def self.authorized?(object, context)
super(object[:container], context)
end
+ def regions(after: nil, first: nil)
+ check_wlif_availability!
+
+ response = GoogleCloudPlatform::Compute::ListRegionsService
+ .new(container: container, current_user: current_user,
+ params: default_params(after, first).merge(google_cloud_project_id: google_cloud_project_id))
+ .execute
+
+ externally_paginated_array(response, after)
+ end
+
+ def zones(region: nil, after: nil, first: nil)
+ check_wlif_availability!
+
+ params = default_params(after, first)
+ params[:filter] = "name=#{region}-*" if region
+ params[:google_cloud_project_id] = google_cloud_project_id if google_cloud_project_id
+
+ response = GoogleCloudPlatform::Compute::ListZonesService
+ .new(container: container, current_user: current_user, params: params)
+ .execute
+
+ externally_paginated_array(response, after)
+ end
+
+ def machine_types(zone:, after: nil, first: nil)
+ check_wlif_availability!
+
+ response = GoogleCloudPlatform::Compute::ListMachineTypesService
+ .new(
+ container: container, current_user: current_user, zone: zone,
+ params: default_params(after, first).merge(google_cloud_project_id: google_cloud_project_id)
+ )
+ .execute
+
+ externally_paginated_array(response, after)
+ end
+
def project_setup_shell_script
template = ERB.new(File.read(SHELL_SCRIPT_TEMPLATE_PATH))
@@ -42,6 +108,26 @@ def container
def google_cloud_project_id
object[:cloud_project_id]
end
+
+ def default_params(after, first)
+ { max_results: first, page_token: after }.compact
+ end
+
+ def externally_paginated_array(response, after)
+ raise_resource_not_available_error!(response.message) if response.error?
+
+ Gitlab::Graphql::ExternallyPaginatedArray.new(
+ after,
+ response.payload[:next_page_token],
+ *response.payload[:items]
+ )
+ end
+
+ def check_wlif_availability!
+ return if container.is_a?(Project)
+
+ raise_resource_not_available_error!('This field is currently only available for projects')
+ end
end
end
end
diff --git a/ee/spec/requests/api/graphql/project/runner_google_cloud_provisioning_spec.rb b/ee/spec/requests/api/graphql/project/runner_google_cloud_provisioning_spec.rb
index 9318ed14121e..1ec1eba044d2 100644
--- a/ee/spec/requests/api/graphql/project/runner_google_cloud_provisioning_spec.rb
+++ b/ee/spec/requests/api/graphql/project/runner_google_cloud_provisioning_spec.rb
@@ -7,11 +7,6 @@
using RSpec::Parameterized::TableSyntax
let_it_be_with_refind(:group) { create(:group) }
- let_it_be(:group_owner) { create(:user).tap { |user| group.add_owner(user) } }
- let_it_be_with_refind(:group_wlif_integration) do
- create(:google_cloud_platform_workload_identity_federation_integration, project: nil, group: group)
- end
-
let_it_be_with_refind(:project) { create(:project, group: group) }
let_it_be(:project_maintainer) { create(:user).tap { |user| group.add_maintainer(user) } }
let_it_be_with_refind(:project_wlif_integration) do
@@ -45,156 +40,375 @@
stub_saas_features(google_cloud_support: true)
end
- where(:parent_field, :container, :current_user) do
- :group | ref(:group) | ref(:group_owner)
- :project | ref(:project) | ref(:project_maintainer)
- end
+ describe 'collections' do
+ # the collection methods currently only work for projects, due to the fact that GoogleCloudPlatform::Jwt
+ # only accepts projects
+ let(:container) { project }
+ let(:parent_field) { :project }
+ let(:current_user) { project_maintainer }
+ let(:client_klass) { GoogleCloudPlatform::Compute::Client }
+ let(:expected_compute_client_args) do
+ {
+ wlif_integration: container.google_cloud_platform_workload_identity_federation_integration,
+ user: current_user,
+ params: { google_cloud_project_id: google_cloud_project_id }
+ }
+ end
- with_them do
- context 'when cloud_project_id is invalid' do
- let(:google_cloud_project_id) { 'project_id_override' }
+ let(:current_page_token) { nil }
+ let(:expected_next_page_token) { nil }
+ let(:base_item_query_args) { {} }
+ let(:item_query_args) { {} }
+ let(:node_name) { :regions }
+ let(:item_type) { 'CiRunnerCloudProvisioningRegion' }
+ let(:inner_fragment) do
+ query_nodes(
+ node_name,
+ args: base_item_query_args.merge(item_query_args),
+ of: item_type,
+ include_pagination_info: true)
+ end
- it 'returns an error' do
- request
+ shared_examples 'a query handling client errors' do
+ shared_examples 'returns error when client raises' do |error_klass, message|
+ it "returns error when client raises #{error_klass}" do
+ expect_next_instance_of(GoogleCloudPlatform::Compute::Client, expected_compute_client_args) do |client|
+ expect(client).to receive(client_method).and_raise(error_klass, message)
+ end
- expect_graphql_errors_to_include('"project_id_override" is not a valid project name')
+ post_graphql(query, current_user: current_user)
+ expect_graphql_errors_to_include(message)
+ end
end
+
+ it_behaves_like 'returns error when client raises', GoogleCloudPlatform::ApiError, 'api error'
+ it_behaves_like 'returns error when client raises', GoogleCloudPlatform::AuthenticationError,
+ 'Unable to authenticate against Google Cloud'
end
- describe 'projectSetupShellScript' do
- let(:inner_fragment) { 'projectSetupShellScript' }
- let(:options_response) do
+ shared_examples 'a query calling compute client' do
+ let(:page_size) { GoogleCloudPlatform::Compute::BaseService::MAX_RESULTS_LIMIT }
+ let(:actual_returned_nodes) { returned_nodes }
+ let(:expected_client_args) { {} }
+ let(:expected_pagination_client_args) do
+ { max_results: page_size, page_token: current_page_token, order_by: nil }
+ end
+
+ before do
+ allow_next_instance_of(client_klass, expected_compute_client_args) do |client|
+ allow(client).to receive(client_method)
+ .with(a_hash_including(**expected_pagination_client_args.merge(expected_client_args))) do
+ compute_type = client_method.to_s.camelize.singularize
+ google_cloud_object_list(compute_type, actual_returned_nodes, next_page_token: expected_next_page_token)
+ end
+ end
+
request
+ end
- graphql_data_at(
- GraphqlHelpers.fieldnamerize(parent_field), 'runnerCloudProvisioning', 'projectSetupShellScript')
+ shared_examples 'a client returning paginated response' do
+ it 'returns paginated response with items from client' do
+ graphql_field_name = GraphqlHelpers.fieldnamerize(client_method)
+
+ expect(options_response[graphql_field_name]).to match({
+ 'nodes' => expected_nodes.map { |node_props| a_graphql_entity_for(nil, **node_props) },
+ 'pageInfo' => a_hash_including(
+ 'hasPreviousPage' => !!current_page_token,
+ 'hasNextPage' => !!expected_next_page_token,
+ 'endCursor' => expected_next_page_token
+ )
+ })
+ end
end
- it 'returns a script' do
- request
- expect_graphql_errors_to_be_empty
+ it_behaves_like 'a working graphql query'
+ it_behaves_like 'a client returning paginated response'
+
+ context 'with arguments' do
+ let(:current_page_token) { 'prev_page_token' }
+ let(:page_size) { 10 }
+ let(:base_item_query_args) do
+ { after: current_page_token, first: page_size }
+ end
+
+ it_behaves_like 'a client returning paginated response'
+
+ context 'with pagination arguments requesting next page' do
+ let(:current_page_token) { 'next_page_token' }
+ let(:expected_next_page_token) { 'next_page_token2' }
+ let(:page_size) { 1 }
+ let(:expected_nodes) { returned_nodes[1..] }
+ let(:actual_returned_nodes) { returned_nodes[1..] }
+ let(:base_item_query_args) { { after: current_page_token, first: page_size } }
+
+ it_behaves_like 'a client returning paginated response'
+ end
+ end
+ end
+
+ describe 'regions' do
+ let(:item_type) { 'CiRunnerCloudProvisioningRegion' }
+ let(:client_method) { :regions }
+ let(:node_name) { :regions }
+ let(:regions) do
+ [
+ { name: 'us-east1', description: 'us-east1' },
+ { name: 'us-west1', description: 'us-west1' }
+ ]
+ end
+
+ let(:returned_nodes) { regions }
+ let(:expected_nodes) { returned_nodes }
+ let(:expected_client_args) { { filter: nil } }
+
+ it_behaves_like 'a query handling client errors'
+ it_behaves_like 'a query calling compute client'
+ end
+
+ describe 'zones' do
+ let(:item_type) { 'CiRunnerCloudProvisioningZone' }
+ let(:client_method) { :zones }
+ let(:node_name) { :zones }
+ let(:zones) do
+ [
+ { name: 'us-east1-a', description: 'us-east1-a' },
+ { name: 'us-west1-a', description: 'us-west1-a' }
+ ]
+ end
+
+ let(:returned_nodes) { zones }
+ let(:expected_nodes) { returned_nodes }
+ let(:expected_client_args) { { filter: nil } }
- expect(options_response).to be_a(String)
- expect(options_response).to include google_cloud_project_id
+ it_behaves_like 'a query handling client errors'
+ it_behaves_like 'a query calling compute client'
+
+ context 'with specified region' do
+ let(:region) { 'us-east1' }
+ let(:item_query_args) { { region: region } }
+ let(:returned_nodes) { zones.select { |z| z[:name].starts_with?(region) } }
+ let(:expected_next_page_token) { 'next_page_token' }
+
+ it_behaves_like 'a query calling compute client' do
+ let(:expected_client_args) { { filter: "name=#{region}-*" } }
+ end
end
end
- describe 'provisioningSteps' do
- let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
+ describe 'machineTypes' do
+ let(:item_type) { 'CiRunnerCloudProvisioningMachineType' }
+ let(:client_method) { :machine_types }
+ let(:node_name) { :machine_types }
+ let(:machine_types) do
+ [
+ { zone: zone, name: 'e2-highcpu-8', description: 'Efficient Instance, 8 vCPUs, 8 GB RAM' },
+ { zone: zone, name: 'e2-highcpu-16', description: 'Efficient Instance, 16 vCPUs, 16 GB RAM' }
+ ]
+ end
- let(:region) { 'us-central1' }
- let(:zone) { 'us-central1-a' }
- let(:machine_type) { 'n2d-standard-2' }
- let(:runner_token) { runner.token }
- let(:args) do
- {
- region: region,
- zone: zone,
- ephemeral_machine_type: machine_type,
- runner_token: runner_token
- }
+ let(:zone) { 'us-east1-a' }
+ let(:item_query_args) { { zone: zone } }
+ let(:returned_nodes) { machine_types }
+ let(:expected_nodes) { returned_nodes }
+ let(:expected_client_args) { { filter: "name=#{zone}-*" } }
+
+ it_behaves_like 'a query handling client errors'
+ it_behaves_like 'a query calling compute client'
+ end
+
+ context 'when integration is not present' do
+ before do
+ container.google_cloud_platform_workload_identity_federation_integration.destroy!
end
- let(:inner_fragment) do
- query_graphql_field(:provisioning_steps, args,
- all_graphql_fields_for('CiRunnerCloudProvisioningStep'), '[CiRunnerCloudProvisioningStep!]')
+ it 'returns error' do
+ post_graphql(query, current_user: current_user)
+ expect_graphql_errors_to_include(/integration not set/)
end
+ end
- let(:options_response) do
- request
- graphql_data_at(GraphqlHelpers.fieldnamerize(parent_field), 'runnerCloudProvisioning', 'provisioningSteps')
+ context 'when integration is inactive' do
+ before do
+ container.google_cloud_platform_workload_identity_federation_integration.update_column(:active, false)
end
- it 'returns provisioning steps', :aggregate_failures do
- request
- expect_graphql_errors_to_be_empty
+ it 'returns error' do
+ post_graphql(query, current_user: current_user)
+ expect_graphql_errors_to_include(/integration not active/)
+ end
+ end
- expect(options_response).to match([
- {
- 'instructions' => /google_project += "#{google_cloud_project_id}"/,
- 'languageIdentifier' => 'terraform',
- 'title' => 'Save the Terraform script to a file'
- },
+ private
+
+ def google_cloud_object_list(compute_type, returned_nodes, next_page_token:)
+ item_type = "Google::Cloud::Compute::V1::#{compute_type}"
+
+ # rubocop:disable RSpec/VerifiedDoubles -- these generated objects don't actually expose the methods
+ double("#{item_type}List",
+ items: returned_nodes.map { |props| double(item_type, **props) },
+ next_page_token: next_page_token
+ )
+ # rubocop:enable RSpec/VerifiedDoubles
+ end
+ end
+
+ describe 'common group/project fields' do
+ let_it_be(:group_owner) { create(:user).tap { |user| group.add_owner(user) } }
+ let_it_be_with_refind(:group_wlif_integration) do
+ create(:google_cloud_platform_workload_identity_federation_integration, project: nil, group: group)
+ end
+
+ where(:parent_field, :container, :current_user) do
+ :group | ref(:group) | ref(:group_owner)
+ :project | ref(:project) | ref(:project_maintainer)
+ end
+
+ with_them do
+ context 'when cloud_project_id is invalid' do
+ let(:google_cloud_project_id) { 'project_id_override' }
+
+ it 'returns an error' do
+ request
+
+ expect_graphql_errors_to_include('"project_id_override" is not a valid project name')
+ end
+ end
+
+ describe 'projectSetupShellScript' do
+ let(:inner_fragment) { 'projectSetupShellScript' }
+ let(:options_response) do
+ request
+
+ graphql_data_at(
+ GraphqlHelpers.fieldnamerize(parent_field), 'runnerCloudProvisioning', 'projectSetupShellScript')
+ end
+
+ it 'returns a script' do
+ request
+ expect_graphql_errors_to_be_empty
+
+ expect(options_response).to be_a(String)
+ expect(options_response).to include google_cloud_project_id
+ end
+ end
+
+ describe 'provisioningSteps' do
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ let(:region) { 'us-central1' }
+ let(:zone) { 'us-central1-a' }
+ let(:machine_type) { 'n2d-standard-2' }
+ let(:runner_token) { runner.token }
+ let(:args) do
{
- 'instructions' => /gitlab_runner="#{runner_token}"/,
- 'languageIdentifier' => 'shell',
- 'title' => 'Apply the Terraform script'
+ region: region,
+ zone: zone,
+ ephemeral_machine_type: machine_type,
+ runner_token: runner_token
}
- ])
- end
+ end
- context 'with nil runner token' do
- let(:runner_token) { nil }
+ let(:inner_fragment) do
+ query_graphql_field(:provisioning_steps, args,
+ all_graphql_fields_for('CiRunnerCloudProvisioningStep'), '[CiRunnerCloudProvisioningStep!]')
+ end
+
+ let(:options_response) do
+ request
+ graphql_data_at(GraphqlHelpers.fieldnamerize(parent_field), 'runnerCloudProvisioning', 'provisioningSteps')
+ end
- it 'is successful and generates a unique deployment id' do
+ it 'returns provisioning steps', :aggregate_failures do
request
expect_graphql_errors_to_be_empty
expect(options_response).to match([
- a_hash_including('instructions' => /name = "grit-[A-Za-z0-9_\-]{8}"/),
- an_instance_of(Hash)
+ {
+ 'instructions' => /google_project += "#{google_cloud_project_id}"/,
+ 'languageIdentifier' => 'terraform',
+ 'title' => 'Save the Terraform script to a file'
+ },
+ {
+ 'instructions' => /gitlab_runner="#{runner_token}"/,
+ 'languageIdentifier' => 'shell',
+ 'title' => 'Apply the Terraform script'
+ }
])
end
- context 'when user does not have permissions to create runner' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(current_user, :create_runner, anything).and_return(false)
- end
+ context 'with nil runner token' do
+ let(:runner_token) { nil }
- it 'returns an error' do
+ it 'is successful and generates a unique deployment id' do
request
+ expect_graphql_errors_to_be_empty
- expect_graphql_errors_to_include(s_('Runners|The user is not allowed to create a runner'))
+ expect(options_response).to match([
+ a_hash_including('instructions' => /name = "grit-[A-Za-z0-9_\-]{8}"/),
+ an_instance_of(Hash)
+ ])
end
- end
- end
- context 'with invalid runner token' do
- let(:runner_token) { 'invalid-token' }
+ context 'when user does not have permissions to create runner' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(current_user, :create_runner, anything).and_return(false)
+ end
- it 'returns an error' do
- request
+ it 'returns an error' do
+ request
- expect_graphql_errors_to_include(s_('Runners|The runner authentication token is invalid'))
+ expect_graphql_errors_to_include(s_('Runners|The user is not allowed to create a runner'))
+ end
+ end
end
- end
- context 'when user cannot provision runners' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(current_user, :provision_cloud_runner, container)
- .and_return(false)
+ context 'with invalid runner token' do
+ let(:runner_token) { 'invalid-token' }
+
+ it 'returns an error' do
+ request
+
+ expect_graphql_errors_to_include(s_('Runners|The runner authentication token is invalid'))
+ end
end
- it 'returns an error' do
- request
+ context 'when user cannot provision runners' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(current_user, :provision_cloud_runner, container)
+ .and_return(false)
+ end
+
+ it 'returns an error' do
+ request
- expect_graphql_errors_to_include("You don't have permissions to provision cloud runners")
+ expect_graphql_errors_to_include("You don't have permissions to provision cloud runners")
+ end
end
end
- end
- context 'when user is not a maintainer or higher' do
- let(:current_user) { create(:user).tap { |user| container.add_developer(user) } }
+ context 'when user is not a maintainer or higher' do
+ let(:current_user) { create(:user).tap { |user| container.add_developer(user) } }
- it { is_expected.to be nil }
- end
-
- context 'when SaaS feature is not enabled' do
- before do
- stub_saas_features(google_cloud_support: false)
+ it { is_expected.to be nil }
end
- it { is_expected.to be nil }
- end
+ context 'when SaaS feature is not enabled' do
+ before do
+ stub_saas_features(google_cloud_support: false)
+ end
- context 'when google_cloud_runner_provisioning FF is disabled' do
- before do
- stub_feature_flags(google_cloud_runner_provisioning: false)
+ it { is_expected.to be nil }
end
- it { is_expected.to be nil }
+ context 'when google_cloud_runner_provisioning FF is disabled' do
+ before do
+ stub_feature_flags(google_cloud_runner_provisioning: false)
+ end
+
+ it { is_expected.to be nil }
+ end
end
end
end
EE: true
Closes #438316 (closed)
Review status
Reviews | Reviewer | Status |
---|---|---|
backend (initial) | @10io | ![]() |
backend (maintainer) | @allison.browne | ![]() |
devopsverify | @allison.browne | ![]() |
authorization | @jarka | ![]() |
MR acceptance checklist
Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Screenshots or screen recordings
Screenshots are required for UI changes, and strongly recommended for all other merge requests.
How to set up and validate locally
Numbered steps to set up and validate the change are strongly suggested.
-
Run the following query in http://gdk.test:3000/-/graphql-explorer:
{ group(fullPath: "gitlab-org") { id runnerCloudProvisioning( provider: GOOGLE_CLOUD cloudProjectId: "dev-gcp-s3c-integrati-9abafed1" ) { ... on CiRunnerGoogleCloudProvisioning { provisioningSteps( region: "us-central1" zone: "us-central1-a" ephemeralMachineType: "n2d-standard-2" ) { title languageIdentifier instructions } } } } project(fullPath: "gitlab-org/playground") { id runnerCloudProvisioning( provider: GOOGLE_CLOUD cloudProjectId: "dev-gcp-s3c-integrati-9abafed1" ) { ... on CiRunnerGoogleCloudProvisioning { zones(first: 2) { nodes { description } } provisioningSteps( region: "us-central1" zone: "us-central1-a" ephemeralMachineType: "n2d-standard-2" ) { title languageIdentifier instructions } } } } }
Merge request reports
- latest version89e8b0694 commits,
- version 13979de0084 commits,
- version 128598a5bd4 commits,
- version 11e571425d4 commits,
- version 10e81c9e4d4 commits,
- version 9afb760b94 commits,
- version 8832c2e083 commits,
- version 7a8bded1e3 commits,
- version 64d6f67dd2 commits,
- version 51aa674ee1 commit,
- version 41ee037211 commit,
- version 3a06345e61 commit,
- version 265a959d711 commits,
- version 165a959d71 commit,
- Side-by-side
- Inline
Some changes are not shown
For a faster browsing experience, some files are collapsed by default.