Skip to content
Snippets Groups Projects

GraphQL: Add `group.runnerCloudProvisioning` field

Merged Pedro Pombeiro requested to merge pedropombeiro/438316/add-group-level-query-field into master
All threads resolved!

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 :white_check_mark:
backend (maintainer) @allison.browne :white_check_mark:
devopsverify @allison.browne :white_check_mark:
authorization @jarka :white_check_mark:

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.

image

How to set up and validate locally

Numbered steps to set up and validate the change are strongly suggested.

  1. 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
            }
          }
        }
      }
    }
Edited by Pedro Pombeiro

Merge request reports

Loading
Loading

Activity

Filter activity
  • Approvals
  • Assignees & reviewers
  • Comments (from bots)
  • Comments (from users)
  • Commits & branches
  • Edits
  • Labels
  • Lock status
  • Mentions
  • Merge request status
  • Tracking
  • David Fernandez removed review request for @10io

    removed review request for @10io

  • Pedro Pombeiro added 121 commits

    added 121 commits

    Compare with previous version

  • Pedro Pombeiro reset approvals from @jarka by pushing to the branch

    reset approvals from @jarka by pushing to the branch

  • Pedro Pombeiro changed the description

    changed the description

  • Pedro Pombeiro added 1 commit

    added 1 commit

    Compare with previous version

  • Pedro Pombeiro requested review from @10io

    requested review from @10io

  • Vladimir Shushlin
  • Vladimir Shushlin
  • Thanks, @pedropombeiro!

    I left a few comments, otherwise looks great :thumbsup: :100:

  • Vladimir Shushlin removed review request for @vshushlin

    removed review request for @vshushlin

  • David Fernandez
  • David Fernandez removed review request for @10io

    removed review request for @10io

  • Pedro Pombeiro added 89 commits

    added 89 commits

    Compare with previous version

  • Pedro Pombeiro requested review from @10io and @vshushlin

    requested review from @10io and @vshushlin

  • Pedro Pombeiro requested review from @jarka

    requested review from @jarka

  • Pedro Pombeiro removed review request for @jarka

    removed review request for @jarka

  • Pedro Pombeiro changed the description

    changed the description

  • Pedro Pombeiro added 1 commit

    added 1 commit

    Compare with previous version

  • mentioned in issue #443855 (closed)

  • David Fernandez removed review request for @10io

    removed review request for @10io

  • Pedro Pombeiro requested review from @10io

    requested review from @10io

  • Pedro Pombeiro added 136 commits

    added 136 commits

    Compare with previous version

  • Pedro Pombeiro changed the description

    changed the description

  • David Fernandez
  • David Fernandez removed review request for @10io

    removed review request for @10io

  • Pedro Pombeiro added 1 commit

    added 1 commit

    • e81c9e4d - Remove regions/zones/machine types

    Compare with previous version

  • Pedro Pombeiro changed the description

    changed the description

  • Pedro Pombeiro requested review from @10io

    requested review from @10io

  • Pedro Pombeiro added 1 commit

    added 1 commit

    • e571425d - Remove regions/zones/machine types

    Compare with previous version

  • Jarka Košanová approved this merge request

    approved this merge request

  • @allison.browne can you take over the verify review please? :green_heart:

    cc @pedropombeiro

  • Vladimir Shushlin requested review from @allison.browne and removed review request for @vshushlin

    requested review from @allison.browne and removed review request for @vshushlin

  • mentioned in issue #438316 (closed)

  • David Fernandez approved this merge request

    approved this merge request

  • David Fernandez removed review request for @10io

    removed review request for @10io

  • Pedro Pombeiro added 114 commits

    added 114 commits

    Compare with previous version

  • Pedro Pombeiro reset approvals from @jarka and @10io by pushing to the branch

    reset approvals from @jarka and @10io by pushing to the branch

  • Pedro Pombeiro changed the description

    changed the description

  • Pedro Pombeiro added 1 commit

    added 1 commit

    • 979de008 - Remove regions/zones/machine types

    Compare with previous version

  • Allison Browne approved this merge request

    approved this merge request

  • Allison Browne resolved all threads

    resolved all threads

  • Allison Browne mentioned in issue #444235

    mentioned in issue #444235

  • Payton Burdette mentioned in merge request !146405 (merged)

    mentioned in merge request !146405 (merged)

  • Pedro Pombeiro changed the description

    changed the description

  • mo khan approved this merge request

    approved this merge request

  • Pedro Pombeiro added 104 commits

    added 104 commits

    Compare with previous version

  • Pedro Pombeiro reset approvals from @allison.browne by pushing to the branch

    reset approvals from @allison.browne by pushing to the branch

  • Pedro Pombeiro resolved all threads

    resolved all threads

  • Hey @vshushlin :waves:, this MR lost Allison's approval due to a merge conflict. Could you please reapprove?

  • Jarka Košanová approved this merge request

    approved this merge request

  • Pedro Pombeiro requested review from @vshushlin and removed review request for @allison.browne

    requested review from @vshushlin and removed review request for @allison.browne

  • Marius Bobin approved this merge request

    approved this merge request

  • Marius Bobin requested review from @mbobin

    requested review from @mbobin

  • Marius Bobin enabled an automatic merge when the pipeline for 3a5ebd26 succeeds

    enabled an automatic merge when the pipeline for 3a5ebd26 succeeds

  • Marius Bobin mentioned in commit 6aad8686

    mentioned in commit 6aad8686

  • added workflowstaging label and removed workflowcanary label

  • mentioned in issue #448400

  • Please register or sign in to reply
    Loading