diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index a53f5d859496b4c272e5f8ca30c1fcd4eba5f1c0..9ae313db4c1f254ab92c537686b6050c83ecfdea 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -20,6 +20,7 @@ } .ci-status-icon-pending, +.ci-status-icon-waiting-for-resource, .ci-status-icon-failed-with-warnings, .ci-status-icon-success-with-warnings { svg { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 364fe3da71eca3184b8223d947575db92a738f88..72657a6479478a7b94f0b8f6a69eb20ccafbef8d 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -795,6 +795,7 @@ } &.ci-status-icon-pending, + &.ci-status-icon-waiting-for-resource, &.ci-status-icon-success-with-warnings { @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700); } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 5d6a4b7cd13193cf90e06538a8fa2f4489b0f47b..4f3d6fb0d4469ca0551a109e83069489edd35816 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -42,6 +42,7 @@ } &.ci-pending, + &.ci-waiting-for-resource, &.ci-failed-with-warnings, &.ci-success-with-warnings { @include status-color($orange-100, $orange-500, $orange-700); diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 4471d5b64b24a5bb1e6dfd4faa8a23eeaa936012..80d1b7e7edbde78d8642f5ae1a22eedcb7f9044f 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -62,6 +62,7 @@ def ci_status_for_statuseable(subject) status.humanize end + # rubocop:disable Metrics/CyclomaticComplexity def ci_icon_for_status(status, size: 16) if detailed_status?(status) return sprite_icon(status.icon, size: size) @@ -77,6 +78,8 @@ def ci_icon_for_status(status, size: 16) 'status_failed' when 'pending' 'status_pending' + when 'waiting_for_resource' + 'status_pending' when 'preparing' 'status_preparing' when 'running' @@ -97,6 +100,7 @@ def ci_icon_for_status(status, size: 16) sprite_icon(icon_name, size: size) end + # rubocop:enable Metrics/CyclomaticComplexity def ci_icon_class_for_status(status) group = detailed_status?(status) ? status.group : status.dasherize diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index fbf76f2c36ab3703d7e7f709966e95ecccdbdd32..78ccfff6d17a296d869768d9518e791b495efe4c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -206,9 +206,25 @@ def retry(build, current_user) state_machine :status do event :enqueue do + transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :requires_resource? transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites? end + event :enqueue_scheduled do + transition scheduled: :waiting_for_resource, if: :requires_resource? + transition scheduled: :preparing, if: :any_unmet_prerequisites? + transition scheduled: :pending + end + + event :enqueue_waiting_for_resource do + transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites? + transition waiting_for_resource: :pending + end + + event :enqueue_preparing do + transition preparing: :pending + end + event :actionize do transition created: :manual end @@ -221,14 +237,8 @@ def retry(build, current_user) transition scheduled: :manual end - event :enqueue_scheduled do - transition scheduled: :preparing, if: ->(build) do - build.scheduled_at&.past? && build.any_unmet_prerequisites? - end - - transition scheduled: :pending, if: ->(build) do - build.scheduled_at&.past? && !build.any_unmet_prerequisites? - end + before_transition on: :enqueue_scheduled do |build| + build.scheduled_at.nil? || build.scheduled_at.past? # If false is returned, it stops the transition end before_transition scheduled: any do |build| @@ -239,6 +249,27 @@ def retry(build, current_user) build.scheduled_at = build.options_scheduled_at end + before_transition any => :waiting_for_resource do |build| + build.waiting_for_resource_at = Time.now + end + + before_transition on: :enqueue_waiting_for_resource do |build| + next unless build.requires_resource? + + build.resource_group.assign_resource_to(build) # If false is returned, it stops the transition + end + + after_transition any => :waiting_for_resource do |build| + build.run_after_commit do + Ci::ResourceGroups::AssignResourceFromResourceGroupWorker + .perform_async(build.resource_group_id) + end + end + + before_transition on: :enqueue_preparing do |build| + build.any_unmet_prerequisites? # If false is returned, it stops the transition + end + after_transition created: :scheduled do |build| build.run_after_commit do Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id) @@ -267,6 +298,16 @@ def retry(build, current_user) end end + after_transition any => ::Ci::Build.completed_statuses do |build| + next unless build.resource_group_id.present? + next unless build.resource_group.release_resource_from(build) + + build.run_after_commit do + Ci::ResourceGroups::AssignResourceFromResourceGroupWorker + .perform_async(build.resource_group_id) + end + end + after_transition any => [:success, :failed, :canceled] do |build| build.run_after_commit do BuildFinishedWorker.perform_async(id) @@ -439,6 +480,11 @@ def expanded_kubernetes_namespace end end + def requires_resource? + Feature.enabled?(:ci_resource_group, project) && + self.resource_group_id.present? + end + def has_environment? environment.present? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 29ec41ef1a1d9981f0017addbbdba8a69f15f5ff..53cd433d6fb2fc68ca914c75fac4c6ea9d0fed09 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -95,10 +95,14 @@ class Pipeline < ApplicationRecord state_machine :status, initial: :created do event :enqueue do - transition [:created, :preparing, :skipped, :scheduled] => :pending + transition [:created, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending transition [:success, :failed, :canceled] => :running end + event :request_resource do + transition any - [:waiting_for_resource] => :waiting_for_resource + end + event :prepare do transition any - [:preparing] => :preparing end @@ -135,7 +139,7 @@ class Pipeline < ApplicationRecord # Do not add any operations to this state_machine # Create a separate worker for each new operation - before_transition [:created, :preparing, :pending] => :running do |pipeline| + before_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline| pipeline.started_at = Time.now end @@ -158,7 +162,7 @@ class Pipeline < ApplicationRecord end end - after_transition [:created, :preparing, :pending] => :running do |pipeline| + after_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline| pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end @@ -166,7 +170,7 @@ class Pipeline < ApplicationRecord pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end - after_transition [:created, :preparing, :pending, :running] => :success do |pipeline| + after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline| pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) } end @@ -317,7 +321,7 @@ def self.internal_sources end def self.bridgeable_statuses - ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending] + ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending] end def stages_count @@ -576,6 +580,7 @@ def update_status new_status = latest_builds_status.to_s case new_status when 'created' then nil + when 'waiting_for_resource' then request_resource when 'preparing' then prepare when 'pending' then enqueue when 'running' then run diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 77ac8bfe87579776f0dec4f80cb1e46b8e2887ce..96041e0233783f57f41637575891f1dc982a7222 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -39,10 +39,14 @@ class Stage < ApplicationRecord state_machine :status, initial: :created do event :enqueue do - transition [:created, :preparing] => :pending + transition [:created, :waiting_for_resource, :preparing] => :pending transition [:success, :failed, :canceled, :skipped] => :running end + event :request_resource do + transition any - [:waiting_for_resource] => :waiting_for_resource + end + event :prepare do transition any - [:preparing] => :preparing end @@ -81,6 +85,7 @@ def update_status new_status = latest_stage_status.to_s case new_status when 'created' then nil + when 'waiting_for_resource' then request_resource when 'preparing' then prepare when 'pending' then enqueue when 'running' then run diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8d38835fb3bf49857a231aaae3754ea4baf6979d..9da49b44cbefad62635097425a5e5f9a7221e7c9 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -96,7 +96,7 @@ class CommitStatus < ApplicationRecord # A CommitStatus will never have prerequisites, but this event # is shared by Ci::Build, which cannot progress unless prerequisites # are satisfied. - transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending, unless: :any_unmet_prerequisites? + transition [:created, :skipped, :manual, :scheduled] => :pending, if: :all_met_to_become_pending? end event :run do @@ -104,22 +104,22 @@ class CommitStatus < ApplicationRecord end event :skip do - transition [:created, :preparing, :pending] => :skipped + transition [:created, :waiting_for_resource, :preparing, :pending] => :skipped end event :drop do - transition [:created, :preparing, :pending, :running, :scheduled] => :failed + transition [:created, :waiting_for_resource, :preparing, :pending, :running, :scheduled] => :failed end event :success do - transition [:created, :preparing, :pending, :running] => :success + transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success end event :cancel do - transition [:created, :preparing, :pending, :running, :manual, :scheduled] => :canceled + transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :canceled end - before_transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status| + before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status| commit_status.queued_at = Time.now end @@ -218,10 +218,18 @@ def has_trace? false end + def all_met_to_become_pending? + !any_unmet_prerequisites? && !requires_resource? + end + def any_unmet_prerequisites? false end + def requires_resource? + false + end + def auto_canceled? canceled? && auto_canceled_by_id? end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index c01fb4740e5f0c0597f24c056648f9a641bb0888..7fd21c29c829e060e708a25b6771c4968bd9b839 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -5,16 +5,16 @@ module HasStatus DEFAULT_STATUS = 'created' BLOCKED_STATUS = %w[manual scheduled].freeze - AVAILABLE_STATUSES = %w[created preparing pending running success failed canceled skipped manual scheduled].freeze + AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze ACTIVE_STATUSES = %w[preparing pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze - ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze + ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7, - scheduled: 8, preparing: 9 }.freeze + scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze UnknownStatusError = Class.new(StandardError) @@ -29,6 +29,7 @@ def legacy_status_sql manual = scope_relevant.manual.select('count(*)').to_sql scheduled = scope_relevant.scheduled.select('count(*)').to_sql preparing = scope_relevant.preparing.select('count(*)').to_sql + waiting_for_resource = scope_relevant.waiting_for_resource.select('count(*)').to_sql pending = scope_relevant.pending.select('count(*)').to_sql running = scope_relevant.running.select('count(*)').to_sql skipped = scope_relevant.skipped.select('count(*)').to_sql @@ -46,6 +47,7 @@ def legacy_status_sql WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})>0 THEN 'running' + WHEN (#{waiting_for_resource})>0 THEN 'waiting_for_resource' WHEN (#{manual})>0 THEN 'manual' WHEN (#{scheduled})>0 THEN 'scheduled' WHEN (#{preparing})>0 THEN 'preparing' @@ -95,6 +97,7 @@ def completed_statuses state_machine :status, initial: :created do state :created, value: 'created' + state :waiting_for_resource, value: 'waiting_for_resource' state :preparing, value: 'preparing' state :pending, value: 'pending' state :running, value: 'running' @@ -107,6 +110,7 @@ def completed_statuses end scope :created, -> { with_status(:created) } + scope :waiting_for_resource, -> { with_status(:waiting_for_resource) } scope :preparing, -> { with_status(:preparing) } scope :relevant, -> { without_status(:created) } scope :running, -> { with_status(:running) } @@ -117,8 +121,8 @@ def completed_statuses scope :skipped, -> { with_status(:skipped) } scope :manual, -> { with_status(:manual) } scope :scheduled, -> { with_status(:scheduled) } - scope :alive, -> { with_status(:created, :preparing, :pending, :running) } - scope :alive_or_scheduled, -> { with_status(:created, :preparing, :pending, :running, :scheduled) } + scope :alive, -> { with_status(:created, :waiting_for_resource, :preparing, :pending, :running) } + scope :alive_or_scheduled, -> { with_status(:created, :waiting_for_resource, :preparing, :pending, :running, :scheduled) } scope :created_or_pending, -> { with_status(:created, :pending) } scope :running_or_pending, -> { with_status(:running, :pending) } scope :finished, -> { with_status(:success, :failed, :canceled) } @@ -126,7 +130,7 @@ def completed_statuses scope :incomplete, -> { without_statuses(completed_statuses) } scope :cancelable, -> do - where(status: [:running, :preparing, :pending, :created, :scheduled]) + where(status: [:running, :waiting_for_resource, :preparing, :pending, :created, :scheduled]) end scope :without_statuses, -> (names) do diff --git a/app/services/ci/prepare_build_service.rb b/app/services/ci/prepare_build_service.rb index 5d024c45e5f5b7b60dd9d1feea9c385f269f529f..3f87c7112706e1271da1cc6d9cfe00144ea8e391 100644 --- a/app/services/ci/prepare_build_service.rb +++ b/app/services/ci/prepare_build_service.rb @@ -11,7 +11,7 @@ def initialize(build) def execute prerequisites.each(&:complete!) - build.enqueue! + build.enqueue_preparing! rescue => e Gitlab::ErrorTracking.track_exception(e, build_id: build.id) diff --git a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..a4bcca8e8b36ee91c0ac375560d70726aef25e0d --- /dev/null +++ b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Ci + module ResourceGroups + class AssignResourceFromResourceGroupService < ::BaseService + # rubocop: disable CodeReuse/ActiveRecord + def execute(resource_group) + free_resources = resource_group.resources.free.count + + resource_group.builds.waiting_for_resource.take(free_resources).each do |build| + build.enqueue_waiting_for_resource + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 02acf360afc81af2ba03ea05c0e8cd1b17951086..96d75b2fd332724daad97c3736af729b66adf995 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -103,6 +103,7 @@ - pipeline_processing:stage_update - pipeline_processing:update_head_pipeline_for_merge_request - pipeline_processing:ci_build_schedule +- pipeline_processing:ci_resource_groups_assign_resource_from_resource_group - deployment:deployments_success - deployment:deployments_finished diff --git a/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb b/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..62233d19516996b80e7395d448b0c9f62a709731 --- /dev/null +++ b/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Ci + module ResourceGroups + class AssignResourceFromResourceGroupWorker + include ApplicationWorker + include PipelineQueue + + queue_namespace :pipeline_processing + feature_category :continuous_delivery + + def perform(resource_group_id) + ::Ci::ResourceGroup.find_by_id(resource_group_id).try do |resource_group| + Ci::ResourceGroups::AssignResourceFromResourceGroupService.new(resource_group.project, nil) + .execute(resource_group) + end + end + end + end +end diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 1e25e97728628bec0ce594dd214789a30c301c6a..49f788cfaae44e13c4d1559c0693b98272b99675 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -4443,7 +4443,8 @@ type Pipeline { startedAt: Time """ - Status of the pipeline (CREATED, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) + Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, + RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) """ status: PipelineStatusEnum! @@ -4521,6 +4522,7 @@ enum PipelineStatusEnum { SCHEDULED SKIPPED SUCCESS + WAITING_FOR_RESOURCE } type Project { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index e71fd3fc71bdbf677fc9d39a95d9fd2552679be8..3ab37b262d25fb9ab9686f5e4bdf161ec236ae5c 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -12203,7 +12203,7 @@ }, { "name": "status", - "description": "Status of the pipeline (CREATED, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED)", + "description": "Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED)", "args": [ ], @@ -12344,6 +12344,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "WAITING_FOR_RESOURCE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "PREPARING", "description": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f142cee854437707971f093544d2a1eca5db76b2..72c7c9ed7ea8160db66d6e861b05e42bd52c1b5f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -635,7 +635,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `iid` | String! | Internal ID of the pipeline | | `sha` | String! | SHA of the pipeline's commit | | `beforeSha` | String | Base SHA of the source branch | -| `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) | +| `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) | | `detailedStatus` | DetailedStatus! | Detailed status of the pipeline | | `duration` | Int | Duration of the pipeline in seconds | | `coverage` | Float | Coverage percentage | diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 96d058428385310d4bb6ce70f7200ceaef8e52cb..7e5afbad80655230c62575b557d05b009052fa15 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -11,6 +11,7 @@ def self.extended_statuses Status::Build::Manual, Status::Build::Canceled, Status::Build::Created, + Status::Build::WaitingForResource, Status::Build::Preparing, Status::Build::Pending, Status::Build::Skipped], diff --git a/lib/gitlab/ci/status/build/waiting_for_resource.rb b/lib/gitlab/ci/status/build/waiting_for_resource.rb new file mode 100644 index 0000000000000000000000000000000000000000..008e6a17bddb97d274fad856fdcbebf9f5118848 --- /dev/null +++ b/lib/gitlab/ci/status/build/waiting_for_resource.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Build + class WaitingForResource < Status::Extended + ## + # TODO: image is shared with 'pending' + # until we get a dedicated one + # + def illustration + { + image: 'illustrations/pending_job_empty.svg', + size: 'svg-430', + title: _('This job is waiting for resource: ') + subject.resource_group.key + } + end + + def self.matches?(build, _) + build.waiting_for_resource? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb index 3c00b67911ffa4ee0a2980bd99844fbcfc264aad..074651f10406850f732e65235c6e38ee789ee6eb 100644 --- a/lib/gitlab/ci/status/composite.rb +++ b/lib/gitlab/ci/status/composite.rb @@ -25,6 +25,8 @@ def initialize(all_statuses, with_allow_failure: true) # 2. In other cases we assume that status is of that type # based on what statuses are no longer valid based on the # data set that we have + # rubocop: disable Metrics/CyclomaticComplexity + # rubocop: disable Metrics/PerceivedComplexity def status return if none? @@ -43,6 +45,8 @@ def status 'pending' elsif any_of?(:running, :pending) 'running' + elsif any_of?(:waiting_for_resource) + 'waiting_for_resource' elsif any_of?(:manual) 'manual' elsif any_of?(:scheduled) @@ -56,6 +60,8 @@ def status end end end + # rubocop: enable Metrics/CyclomaticComplexity + # rubocop: enable Metrics/PerceivedComplexity def warnings? @status_set.include?(:success_with_warnings) diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index c29dc51f076de527e00610ccfc2028eacac2f7b5..73c73a3b3fc09b651a031ceb43847db39c1355e7 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -20,7 +20,7 @@ def fabricate! def core_status Gitlab::Ci::Status - .const_get(@status.capitalize, false) + .const_get(@status.to_s.camelize, false) .new(@subject, @user) .extend(self.class.common_helpers) end diff --git a/lib/gitlab/ci/status/waiting_for_resource.rb b/lib/gitlab/ci/status/waiting_for_resource.rb new file mode 100644 index 0000000000000000000000000000000000000000..4c9e706bc517f1a65b44242aee4b25cdeef2fced --- /dev/null +++ b/lib/gitlab/ci/status/waiting_for_resource.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + class WaitingForResource < Status::Core + def text + s_('CiStatusText|waiting') + end + + def label + s_('CiStatusLabel|waiting for resource') + end + + def icon + 'status_pending' + end + + def favicon + 'favicon_pending' + end + + def group + 'waiting-for-resource' + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c83fc685a6201e2f3f2080b03bae32123f0a1dde..77f16d4f3859c2ef7975701f4fb1ce39f200f610 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3358,6 +3358,9 @@ msgstr "" msgid "CiStatusLabel|waiting for manual action" msgstr "" +msgid "CiStatusLabel|waiting for resource" +msgstr "" + msgid "CiStatusText|blocked" msgstr "" @@ -3388,6 +3391,9 @@ msgstr "" msgid "CiStatusText|skipped" msgstr "" +msgid "CiStatusText|waiting" +msgstr "" + msgid "CiStatus|running" msgstr "" @@ -18505,6 +18511,9 @@ msgstr "" msgid "This job is stuck because you don't have any active runners that can run this job." msgstr "" +msgid "This job is waiting for resource: " +msgstr "" + msgid "This job requires a manual action" msgstr "" diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index a38935c89ba1549af5b03d88e3f96d51c7a2e060..0c5d14eeb32ad703867d88a21007b3e382d8eba9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -77,6 +77,10 @@ status { 'created' } end + trait :waiting_for_resource do + status { 'waiting_for_resource' } + end + trait :preparing do status { 'preparing' } end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 5d635d93ff2e786e15f75dc171c4b2fc232a0093..a54c0ce74c6db72b1967b443eacfef582f357cf2 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -35,6 +35,10 @@ status { 'pending' } end + trait :waiting_for_resource do + status { 'waiting_for_resource' } + end + trait :preparing do status { 'preparing' } end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 94fac9a2eb5d99ef7c15157708143a6b421fd8b5..4b97c58d920d4a7dbd8bc3fb570171227ea30b22 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -606,6 +606,117 @@ end end + context 'when build requires resource', :sidekiq_inline do + let_it_be(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:resource_group) { create(:ci_resource_group, project: project) } + + let!(:test_job) do + create(:ci_build, :pending, stage: 'test', name: 'test', + stage_idx: 1, pipeline: pipeline, project: project) + end + + let!(:deploy_job) do + create(:ci_build, :created, stage: 'deploy', name: 'deploy', + stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group) + end + + describe 'GET /:project/pipelines/:id' do + subject { visit project_pipeline_path(project, pipeline) } + + it 'shows deploy job as created' do + subject + + within('.pipeline-header-container') do + expect(page).to have_content('pending') + end + + within('.pipeline-graph') do + within '.stage-column:nth-child(1)' do + expect(page).to have_content('test') + expect(page).to have_css('.ci-status-icon-pending') + end + + within '.stage-column:nth-child(2)' do + expect(page).to have_content('deploy') + expect(page).to have_css('.ci-status-icon-created') + end + end + end + + context 'when test job succeeded' do + before do + test_job.success! + end + + it 'shows deploy job as pending' do + subject + + within('.pipeline-header-container') do + expect(page).to have_content('running') + end + + within('.pipeline-graph') do + within '.stage-column:nth-child(1)' do + expect(page).to have_content('test') + expect(page).to have_css('.ci-status-icon-success') + end + + within '.stage-column:nth-child(2)' do + expect(page).to have_content('deploy') + expect(page).to have_css('.ci-status-icon-pending') + end + end + end + end + + context 'when test job succeeded but there are no available resources' do + let(:another_job) { create(:ci_build, :running, project: project, resource_group: resource_group) } + + before do + resource_group.assign_resource_to(another_job) + test_job.success! + end + + it 'shows deploy job as waiting for resource' do + subject + + within('.pipeline-header-container') do + expect(page).to have_content('waiting') + end + + within('.pipeline-graph') do + within '.stage-column:nth-child(2)' do + expect(page).to have_content('deploy') + expect(page).to have_css('.ci-status-icon-waiting-for-resource') + end + end + end + + context 'when resource is released from another job' do + before do + another_job.success! + end + + it 'shows deploy job as pending' do + subject + + within('.pipeline-header-container') do + expect(page).to have_content('running') + end + + within('.pipeline-graph') do + within '.stage-column:nth-child(2)' do + expect(page).to have_content('deploy') + expect(page).to have_css('.ci-status-icon-pending') + end + end + end + end + end + end + end + describe 'GET /:project/pipelines/:id/builds' do include_context 'pipeline builds' diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb index 9d7dfc4284889be438dd1db4041f395f9118825b..9c11e42fc5a250185dc37721d51f96d04dedb493 100644 --- a/spec/lib/gitlab/ci/status/external/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb @@ -22,7 +22,7 @@ end let(:expected_status) do - Gitlab::Ci::Status.const_get(simple_status.capitalize, false) + Gitlab::Ci::Status.const_get(simple_status.to_s.camelize, false) end it "fabricates a core status #{simple_status}" do diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index c6d7a1ec5d91e9640286c887ce316f6784aaf1be..219eb53d9df141f15841ac1a5486bf3b1ecbafe0 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -13,7 +13,7 @@ let(:resource) { double('resource', status: simple_status) } let(:expected_status) do - Gitlab::Ci::Status.const_get(simple_status.capitalize, false) + Gitlab::Ci::Status.const_get(simple_status.to_s.camelize, false) end it "fabricates a core status #{simple_status}" do diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index 3acc767ab7a883999be8cf004b9296761b7a63bd..838154759cbf8bd01b427b7efa04848a94c3c09f 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -18,7 +18,7 @@ let(:pipeline) { create(:ci_pipeline, status: simple_status) } let(:expected_status) do - Gitlab::Ci::Status.const_get(simple_status.capitalize, false) + Gitlab::Ci::Status.const_get(simple_status.camelize, false) end it "matches correct core status for #{simple_status}" do diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index dcb537121576c4d32fd8c1ebe2221cab95a69596..317756ea13c7aa317ad581ab6d5bb92d0213ed37 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -34,7 +34,7 @@ it "fabricates a core status #{core_status}" do expect(status).to be_a( - Gitlab::Ci::Status.const_get(core_status.capitalize, false)) + Gitlab::Ci::Status.const_get(core_status.camelize, false)) end it 'extends core status with common stage methods' do diff --git a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ed00dac85609407b4801a509779c4ba8624f41b4 --- /dev/null +++ b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::WaitingForResource do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.text).to eq 'waiting' } + end + + describe '#label' do + it { expect(subject.label).to eq 'waiting for resource' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'status_pending' } + end + + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_pending' } + end + + describe '#group' do + it { expect(subject.group).to eq 'waiting-for-resource' } + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index ee8e9805c49f76313ed0588bcfd599ba4a791f44..a86a9331f83e1871252861b486bbdf99da763501 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1119,6 +1119,60 @@ end end + describe 'state transition with resource group' do + let(:resource_group) { create(:ci_resource_group, project: project) } + + context 'when build status is created' do + let(:build) { create(:ci_build, :created, project: project, resource_group: resource_group) } + + it 'is waiting for resource when build is enqueued' do + expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(resource_group.id) + + expect { build.enqueue! }.to change { build.status }.from('created').to('waiting_for_resource') + + expect(build.waiting_for_resource_at).not_to be_nil + end + + context 'when build is waiting for resource' do + before do + build.update_column(:status, 'waiting_for_resource') + end + + it 'is enqueued when build requests resource' do + expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('pending') + end + + it 'releases a resource when build finished' do + expect(build.resource_group).to receive(:release_resource_from).with(build).and_call_original + expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(build.resource_group_id) + + build.enqueue_waiting_for_resource! + build.success! + end + + context 'when build has prerequisites' do + before do + allow(build).to receive(:any_unmet_prerequisites?) { true } + end + + it 'is preparing when build is enqueued' do + expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('preparing') + end + end + + context 'when there are no available resources' do + before do + resource_group.assign_resource_to(create(:ci_build)) + end + + it 'stays as waiting for resource when build requests resource' do + expect { build.enqueue_waiting_for_resource }.not_to change { build.status } + end + end + end + end + end + describe '#on_stop' do subject { build.on_stop } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3f9e882ea5239852a751384656e080d6d757eedc..b4f9dadfe6bd75f79ad6a93445ff6f4ed9270078 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1749,7 +1749,7 @@ def create_pipeline(status, ref, sha, project) subject { described_class.bridgeable_statuses } it { is_expected.to be_an(Array) } - it { is_expected.not_to include('created', 'preparing', 'pending') } + it { is_expected.not_to include('created', 'waiting_for_resource', 'preparing', 'pending') } end describe '#status', :sidekiq_might_not_need_inline do @@ -1759,6 +1759,17 @@ def create_pipeline(status, ref, sha, project) subject { pipeline.reload.status } + context 'on waiting for resource' do + before do + allow(build).to receive(:requires_resource?) { true } + allow(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async) + + build.enqueue + end + + it { is_expected.to eq('waiting_for_resource') } + end + context 'on prepare' do before do # Prevent skipping directly to 'pending' diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index b65c11f837ceaa92494769228954c3d0318ff959..7e2751128e2c49f104eec881054904e1e0a7efcb 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -105,6 +105,18 @@ end end + context 'when build is waiting for resource' do + before do + create(:ci_build, :waiting_for_resource, stage_id: stage.id) + end + + it 'updates status to waiting for resource' do + expect { stage.update_status } + .to change { stage.reload.status } + .to 'waiting_for_resource' + end + end + context 'when stage is skipped because is empty' do it 'updates status to skipped' do expect { stage.update_status } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 31aebac54e1ba0213b8f85a2d23df26bbcc6fb93..98dc6f0041201e09dcfc31f4a451daf557feb697 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -634,6 +634,30 @@ def create_status(**opts) end end + describe '#all_met_to_become_pending?' do + subject { commit_status.all_met_to_become_pending? } + + let(:commit_status) { create(:commit_status) } + + it { is_expected.to eq(true) } + + context 'when build requires a resource' do + before do + allow(commit_status).to receive(:requires_resource?) { true } + end + + it { is_expected.to eq(false) } + end + + context 'when build has a prerequisite' do + before do + allow(commit_status).to receive(:any_unmet_prerequisites?) { true } + end + + it { is_expected.to eq(false) } + end + end + describe '#enqueue' do let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) } @@ -654,12 +678,6 @@ def create_status(**opts) it_behaves_like 'commit status enqueued' end - context 'when initial state is :preparing' do - let(:commit_status) { create(:commit_status, :preparing) } - - it_behaves_like 'commit status enqueued' - end - context 'when initial state is :skipped' do let(:commit_status) { create(:commit_status, :skipped) } diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 21e4dda6dabda10f1f837d13d925c46f0863b6ed..99d09af80d00ed4b33285a5e7020face3e668a66 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -39,6 +39,22 @@ it { is_expected.to eq 'running' } end + context 'all waiting for resource' do + let!(:statuses) do + [create(type, status: :waiting_for_resource), create(type, status: :waiting_for_resource)] + end + + it { is_expected.to eq 'waiting_for_resource' } + end + + context 'at least one waiting for resource' do + let!(:statuses) do + [create(type, status: :success), create(type, status: :waiting_for_resource)] + end + + it { is_expected.to eq 'waiting_for_resource' } + end + context 'all preparing' do let!(:statuses) do [create(type, status: :preparing), create(type, status: :preparing)] @@ -219,7 +235,7 @@ end end - %i[created preparing running pending success + %i[created waiting_for_resource preparing running pending success failed canceled skipped].each do |status| it_behaves_like 'having a job', status end @@ -265,7 +281,7 @@ describe '.alive' do subject { CommitStatus.alive } - %i[running pending preparing created].each do |status| + %i[running pending waiting_for_resource preparing created].each do |status| it_behaves_like 'containing the job', status end @@ -277,7 +293,7 @@ describe '.alive_or_scheduled' do subject { CommitStatus.alive_or_scheduled } - %i[running pending preparing created scheduled].each do |status| + %i[running pending waiting_for_resource preparing created scheduled].each do |status| it_behaves_like 'containing the job', status end @@ -313,7 +329,7 @@ describe '.cancelable' do subject { CommitStatus.cancelable } - %i[running pending preparing created scheduled].each do |status| + %i[running pending waiting_for_resource preparing created scheduled].each do |status| it_behaves_like 'containing the job', status end diff --git a/spec/services/ci/prepare_build_service_spec.rb b/spec/services/ci/prepare_build_service_spec.rb index 3c3d8b90bb03058565fee8b20636c7cce99695a6..02928b58ff8b3f69d5156c628ddaff034ec339d9 100644 --- a/spec/services/ci/prepare_build_service_spec.rb +++ b/spec/services/ci/prepare_build_service_spec.rb @@ -14,7 +14,7 @@ shared_examples 'build enqueueing' do it 'enqueues the build' do - expect(build).to receive(:enqueue).once + expect(build).to receive(:enqueue_preparing).once subject end @@ -34,7 +34,7 @@ context 'prerequisites fail to complete' do before do - allow(build).to receive(:enqueue).and_return(false) + allow(build).to receive(:enqueue_preparing).and_return(false) end it 'drops the build' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ba5891c86949dd69549f203425e78749a0a8b692..509a42314722b54fbc7607c655ea0fcfc07957fe 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -261,12 +261,16 @@ expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' }) - enqueue_scheduled('rollout10%') + Timecop.travel 2.minutes.from_now do + enqueue_scheduled('rollout10%') + end succeed_pending expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'scheduled' }) - enqueue_scheduled('rollout100%') + Timecop.travel 2.minutes.from_now do + enqueue_scheduled('rollout100%') + end succeed_pending expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'success', 'cleanup': 'pending' }) @@ -328,7 +332,9 @@ expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' }) - enqueue_scheduled('rollout10%') + Timecop.travel 2.minutes.from_now do + enqueue_scheduled('rollout10%') + end fail_running_or_pending expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'failed' }) @@ -394,7 +400,9 @@ expect(process_pipeline).to be_truthy expect(builds_names_and_statuses).to eq({ 'delayed1': 'scheduled', 'delayed2': 'scheduled' }) - enqueue_scheduled('delayed1') + Timecop.travel 2.minutes.from_now do + enqueue_scheduled('delayed1') + end expect(builds_names_and_statuses).to eq({ 'delayed1': 'pending', 'delayed2': 'scheduled' }) expect(pipeline.reload.status).to eq 'running' @@ -413,7 +421,9 @@ expect(process_pipeline).to be_truthy expect(builds_names_and_statuses).to eq({ 'delayed': 'scheduled' }) - enqueue_scheduled('delayed') + Timecop.travel 2.minutes.from_now do + enqueue_scheduled('delayed') + end fail_running_or_pending expect(builds_names_and_statuses).to eq({ 'delayed': 'failed', 'job': 'pending' }) @@ -906,7 +916,7 @@ def play_manual_action(name) end def enqueue_scheduled(name) - builds.scheduled.find_by(name: name).enqueue + builds.scheduled.find_by(name: name).enqueue_scheduled end def retry_build(name) diff --git a/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb b/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..50d312647ae6ab1fd268712518b70a0bc9ee746c --- /dev/null +++ b/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::ResourceGroups::AssignResourceFromResourceGroupService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + subject { service.execute(resource_group) } + + let(:resource_group) { create(:ci_resource_group, project: project) } + let!(:build) { create(:ci_build, :waiting_for_resource, project: project, user: user, resource_group: resource_group) } + + context 'when there is an available resource' do + it 'requests resource' do + subject + + expect(build.reload).to be_pending + expect(build.resource).to be_present + end + + context 'when failed to request resource' do + before do + allow_next_instance_of(Ci::Build) do |build| + allow(build).to receive(:enqueue_waiting_for_resource) { false } + end + end + + it 'has a build waiting for resource' do + subject + + expect(build).to be_waiting_for_resource + end + end + + context 'when the build has already retained a resource' do + before do + resource_group.assign_resource_to(build) + build.update_column(:status, :pending) + end + + it 'has a pending build' do + subject + + expect(build).to be_pending + end + end + end + + context 'when there are no available resources' do + before do + resource_group.assign_resource_to(create(:ci_build)) + end + + it 'does not request resource' do + expect_any_instance_of(Ci::Build).not_to receive(:enqueue_waiting_for_resource) + + subject + end + end + end +end diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb index ab8d9f4ba2eb0403e44bd5319b01199549696dcb..43d110cbc8fce42c7b40bede5024446191254d99 100644 --- a/spec/services/ci/run_scheduled_build_service_spec.rb +++ b/spec/services/ci/run_scheduled_build_service_spec.rb @@ -26,6 +26,18 @@ expect(build).to be_pending end + + context 'when build requires resource' do + let(:resource_group) { create(:ci_resource_group, project: project) } + + before do + build.update!(resource_group: resource_group) + end + + it 'transits to waiting for resource status' do + expect { subject }.to change { build.status }.from('scheduled').to('waiting_for_resource') + end + end end context 'when scheduled_at is not expired' do diff --git a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..634d932121eca03aa200d8b6947baa2174c28533 --- /dev/null +++ b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do + let(:worker) { described_class.new } + + describe '#perform' do + subject { worker.perform(resource_group_id) } + + context 'when resource group exists' do + let(:resource_group) { create(:ci_resource_group) } + let(:resource_group_id) { resource_group.id } + + it 'executes AssignResourceFromResourceGroupService' do + expect_next_instance_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, resource_group.project, nil) do |service| + expect(service).to receive(:execute).with(resource_group) + end + + subject + end + end + + context 'when build does not exist' do + let(:resource_group_id) { 123 } + + it 'does not execute AssignResourceFromResourceGroupService' do + expect(Ci::ResourceGroups::AssignResourceFromResourceGroupService).not_to receive(:new) + + subject + end + end + end +end