diff --git a/app/models/group.rb b/app/models/group.rb index 71f58a5fd1ab67c8158a9e7c06bdac638b1ac285..c38ddbdf6fbcb3f8aa13beab50b47f0a78f07f97 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -18,6 +18,8 @@ class Group < Namespace ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 + UpdateSharedRunnersError = Class.new(StandardError) + has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members has_many :users, through: :group_members @@ -89,6 +91,8 @@ class Group < Namespace scope :with_users, -> { includes(:users) } + scope :by_id, ->(groups) { where(id: groups) } + class << self def sort_by_attribute(method) if method == 'storage_size_desc' @@ -504,6 +508,55 @@ def preload_shared_group_links preloader.preload(self, shared_with_group_links: [shared_with_group: :route]) end + def shared_runners_allowed? + shared_runners_enabled? || allow_descendants_override_disabled_shared_runners? + end + + def parent_allows_shared_runners? + return true unless has_parent? + + parent.shared_runners_allowed? + end + + def parent_enabled_shared_runners? + return true unless has_parent? + + parent.shared_runners_enabled? + end + + def enable_shared_runners! + raise UpdateSharedRunnersError, 'Shared Runners disabled for the parent group' unless parent_enabled_shared_runners? + + update_column(:shared_runners_enabled, true) + end + + def disable_shared_runners! + group_ids = self_and_descendants + return if group_ids.empty? + + Group.by_id(group_ids).update_all(shared_runners_enabled: false) + + all_projects.update_all(shared_runners_enabled: false) + end + + def allow_descendants_override_disabled_shared_runners! + raise UpdateSharedRunnersError, 'Shared Runners enabled' if shared_runners_enabled? + raise UpdateSharedRunnersError, 'Group level shared Runners not allowed' unless parent_allows_shared_runners? + + update_column(:allow_descendants_override_disabled_shared_runners, true) + end + + def disallow_descendants_override_disabled_shared_runners! + raise UpdateSharedRunnersError, 'Shared Runners enabled' if shared_runners_enabled? + + group_ids = self_and_descendants + return if group_ids.empty? + + Group.by_id(group_ids).update_all(allow_descendants_override_disabled_shared_runners: false) + + all_projects.update_all(shared_runners_enabled: false) + end + private def update_two_factor_requirement diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6176e8db9207b5076acf4e1cef167d5edc2334b8..bc23b7d6af0354bdded317faa5e42c5c402e3e01 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -211,7 +211,7 @@ def lfs_enabled? Gitlab.config.lfs.enabled end - def shared_runners_enabled? + def any_project_with_shared_runners_enabled? projects.with_shared_runners.any? end diff --git a/app/services/groups/update_shared_runners_service.rb b/app/services/groups/update_shared_runners_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..63f5710451034413eaa70559455d1d73bd1d806b --- /dev/null +++ b/app/services/groups/update_shared_runners_service.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Groups + class UpdateSharedRunnersService < Groups::BaseService + def execute + return error('Operation not allowed', 403) unless can?(current_user, :admin_group, group) + + validate_params + + enable_or_disable_shared_runners! + allow_or_disallow_descendants_override_disabled_shared_runners! + + success + + rescue Group::UpdateSharedRunnersError => error + error(error.message) + end + + private + + def validate_params + if Gitlab::Utils.to_boolean(params[:shared_runners_enabled]) && !params[:allow_descendants_override_disabled_shared_runners].nil? + raise Group::UpdateSharedRunnersError, 'Cannot set shared_runners_enabled to true and allow_descendants_override_disabled_shared_runners' + end + end + + def enable_or_disable_shared_runners! + return if params[:shared_runners_enabled].nil? + + if Gitlab::Utils.to_boolean(params[:shared_runners_enabled]) + group.enable_shared_runners! + else + group.disable_shared_runners! + end + end + + def allow_or_disallow_descendants_override_disabled_shared_runners! + return if params[:allow_descendants_override_disabled_shared_runners].nil? + + # Needs to reset group because if both params are present could result in error + group.reset + + if Gitlab::Utils.to_boolean(params[:allow_descendants_override_disabled_shared_runners]) + group.allow_descendants_override_disabled_shared_runners! + else + group.disallow_descendants_override_disabled_shared_runners! + end + end + end +end diff --git a/changelogs/unreleased/add-Groups-SharedRunnersService-tests-migrations.yml b/changelogs/unreleased/add-Groups-SharedRunnersService-tests-migrations.yml new file mode 100644 index 0000000000000000000000000000000000000000..308225344b5cc61094ca9b6f64e2fb1d6fe24517 --- /dev/null +++ b/changelogs/unreleased/add-Groups-SharedRunnersService-tests-migrations.yml @@ -0,0 +1,5 @@ +--- +title: Add setting to enable and disable shared Runners for a group and its descendants +merge_request: 33411 +author: Arthur de Lapertosa Lisboa +type: added diff --git a/db/migrate/20200424102023_add_shared_runners_enabled_and_override_to_namespaces.rb b/db/migrate/20200424102023_add_shared_runners_enabled_and_override_to_namespaces.rb new file mode 100644 index 0000000000000000000000000000000000000000..2555a50be44fe1d56a0e8cf3c830f4c4ca6a94dd --- /dev/null +++ b/db/migrate/20200424102023_add_shared_runners_enabled_and_override_to_namespaces.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddSharedRunnersEnabledAndOverrideToNamespaces < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column :namespaces, :shared_runners_enabled, :boolean, default: true, null: false + add_column :namespaces, :allow_descendants_override_disabled_shared_runners, :boolean, default: false, null: false + end + end + + def down + with_lock_retries do + remove_column :namespaces, :shared_runners_enabled, :boolean + remove_column :namespaces, :allow_descendants_override_disabled_shared_runners, :boolean + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 93fcdaa4a1cef543e99c0582906c81ef0690197d..eb628da59f1f1dfd830ec09b3653f1031f695b10 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13086,7 +13086,9 @@ CREATE TABLE public.namespaces ( default_branch_protection smallint, unlock_membership_to_ldap boolean, max_personal_access_token_lifetime integer, - push_rule_id bigint + push_rule_id bigint, + shared_runners_enabled boolean DEFAULT true NOT NULL, + allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL ); CREATE SEQUENCE public.namespaces_id_seq @@ -23366,6 +23368,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200424043515 20200424050250 20200424101920 +20200424102023 20200424135319 20200427064130 20200428134356 diff --git a/ee/app/models/ee/namespace.rb b/ee/app/models/ee/namespace.rb index e86eaa8a6a9fbacf01708b21c82f7ffb0c1ca9ec..edc28858efe6189d363e7f30a35b27eba9c0072d 100644 --- a/ee/app/models/ee/namespace.rb +++ b/ee/app/models/ee/namespace.rb @@ -212,7 +212,7 @@ def actual_shared_runners_minutes_limit(include_extra: true) def shared_runners_minutes_limit_enabled? shared_runner_minutes_supported? && - shared_runners_enabled? && + any_project_with_shared_runners_enabled? && actual_shared_runners_minutes_limit.nonzero? end @@ -238,7 +238,7 @@ def extra_shared_runners_minutes_used? extra_shared_runners_minutes.to_i >= extra_shared_runners_minutes_limit end - def shared_runners_enabled? + def any_project_with_shared_runners_enabled? all_projects.with_shared_runners.any? end diff --git a/ee/app/views/namespaces/_shared_runner_status.html.haml b/ee/app/views/namespaces/_shared_runner_status.html.haml index ab039197bdb69179cb83bd66551be975459e6d24..125356e465c43edd941e9d5946f415e0124ba4f2 100644 --- a/ee/app/views/namespaces/_shared_runner_status.html.haml +++ b/ee/app/views/namespaces/_shared_runner_status.html.haml @@ -1,7 +1,7 @@ - namespace = local_assigns.fetch(:namespace) - minutes_quota = namespace.ci_minutes_quota -- if namespace.shared_runners_enabled? +- if namespace.any_project_with_shared_runners_enabled? %li %span.light Pipeline minutes quota: %strong diff --git a/ee/app/views/namespaces/pipelines_quota/_list.haml b/ee/app/views/namespaces/pipelines_quota/_list.haml index 64592bd514c44986846c077426b7f6d536859a46..20f1643c462476af0dfcdbb43452d5dbbb1a51ba 100644 --- a/ee/app/views/namespaces/pipelines_quota/_list.haml +++ b/ee/app/views/namespaces/pipelines_quota/_list.haml @@ -29,7 +29,7 @@ .col-sm-6.right - if namespace.shared_runners_minutes_limit_enabled? #{minutes_quota.monthly_percent_used}% used - - elsif !namespace.shared_runners_enabled? + - elsif !namespace.any_project_with_shared_runners_enabled? 0% used - else = s_('UsageQuota|Unlimited') @@ -47,7 +47,7 @@ = _('Minutes') %tbody - - if !namespace.shared_runners_enabled? + - if !namespace.any_project_with_shared_runners_enabled? %tr %td{ colspan: 2 } .nothing-here-block diff --git a/ee/spec/models/ee/namespace_spec.rb b/ee/spec/models/ee/namespace_spec.rb index 6819a6d4632e9ba17f9b12f8c5d36385612d50b1..7ffa5d29f4efcd17cfc8ed2d60aea56a6a21dc7c 100644 --- a/ee/spec/models/ee/namespace_spec.rb +++ b/ee/spec/models/ee/namespace_spec.rb @@ -429,8 +429,8 @@ end end - describe '#shared_runners_enabled?' do - subject { namespace.shared_runners_enabled? } + describe '#any_project_with_shared_runners_enabled?' do + subject { namespace.any_project_with_shared_runners_enabled? } context 'without projects' do it { is_expected.to be_falsey } @@ -557,8 +557,8 @@ end end - describe '#shared_runners_enabled?' do - subject { namespace.shared_runners_enabled? } + describe '#any_project_with_shared_runners_enabled?' do + subject { namespace.any_project_with_shared_runners_enabled? } context 'subgroup with shared runners enabled project' do let(:subgroup) { create(:group, parent: namespace) } diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index d51c437f83ae8b3edbea873f275948047947b971..60d427dde008796c66d62c1d8e9fa45342c9e3d7 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -51,5 +51,13 @@ trait :owner_subgroup_creation_only do subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS } end + + trait :shared_runners_disabled do + shared_runners_enabled { false } + end + + trait :allow_descendants_override_disabled_shared_runners do + allow_descendants_override_disabled_shared_runners { true } + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index da1bf026f6078ce62c2e1b8a6cc73ecf7c290191..4184f2d07cc6ff0a29d2b9f81221faa6a4fee714 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1313,4 +1313,231 @@ def setup_group_members(group) expect(groups).to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3) end end + + describe '#shared_runners_allowed?' do + using RSpec::Parameterized::TableSyntax + + where(:shared_runners_enabled, :allow_descendants_override, :expected_shared_runners_allowed) do + true | false | true + true | true | true + false | false | false + false | true | true + end + + with_them do + let!(:group) { create(:group, shared_runners_enabled: shared_runners_enabled, allow_descendants_override_disabled_shared_runners: allow_descendants_override) } + + it 'returns the expected result' do + expect(group.shared_runners_allowed?).to eq(expected_shared_runners_allowed) + end + end + end + + describe '#parent_allows_shared_runners?' do + context 'when parent group is present' do + using RSpec::Parameterized::TableSyntax + + where(:shared_runners_enabled, :allow_descendants_override, :expected_shared_runners_allowed) do + true | false | true + true | true | true + false | false | false + false | true | true + end + + with_them do + let!(:parent_group) { create(:group, shared_runners_enabled: shared_runners_enabled, allow_descendants_override_disabled_shared_runners: allow_descendants_override) } + let!(:group) { create(:group, parent: parent_group) } + + it 'returns the expected result' do + expect(group.parent_allows_shared_runners?).to eq(expected_shared_runners_allowed) + end + end + end + + context 'when parent group is missing' do + let!(:group) { create(:group) } + + it 'returns true' do + expect(group.parent_allows_shared_runners?).to be_truthy + end + end + end + + describe '#parent_enabled_shared_runners?' do + subject { group.parent_enabled_shared_runners? } + + context 'when parent group is present' do + context 'When shared Runners are disabled' do + let!(:parent_group) { create(:group, :shared_runners_disabled) } + let!(:group) { create(:group, parent: parent_group) } + + it { is_expected.to be_falsy } + end + + context 'When shared Runners are enabled' do + let!(:parent_group) { create(:group) } + let!(:group) { create(:group, parent: parent_group) } + + it { is_expected.to be_truthy } + end + end + + context 'when parent group is missing' do + let!(:group) { create(:group) } + + it { is_expected.to be_truthy } + end + end + + describe '#enable_shared_runners!' do + subject { group.enable_shared_runners! } + + context 'group that its ancestors have shared runners disabled' do + let_it_be(:parent) { create(:group, :shared_runners_disabled) } + let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) } + + it 'raises error and does not enable shared Runners' do + expect { subject } + .to raise_error(described_class::UpdateSharedRunnersError, 'Shared Runners disabled for the parent group') + .and not_change { parent.reload.shared_runners_enabled } + .and not_change { group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + end + end + + context 'root group with shared runners disabled' do + let_it_be(:group) { create(:group, :shared_runners_disabled) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } + + it 'enables shared Runners only for itself' do + expect { subject } + .to change { group.reload.shared_runners_enabled }.from(false).to(true) + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + end + end + end + + describe '#disable_shared_runners!' do + let_it_be(:group) { create(:group) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) } + let_it_be(:sub_group_2) { create(:group, parent: group) } + let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) } + let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) } + + subject { group.disable_shared_runners! } + + it 'disables shared Runners for all descendant groups and projects' do + expect { subject } + .to change { group.reload.shared_runners_enabled }.from(true).to(false) + .and not_change { group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { sub_group.reload.allow_descendants_override_disabled_shared_runners } + .and change { sub_group_2.reload.shared_runners_enabled }.from(true).to(false) + .and not_change { sub_group_2.reload.allow_descendants_override_disabled_shared_runners } + .and change { project.reload.shared_runners_enabled }.from(true).to(false) + .and change { project_2.reload.shared_runners_enabled }.from(true).to(false) + end + end + + describe '#allow_descendants_override_disabled_shared_runners!' do + subject { group.allow_descendants_override_disabled_shared_runners! } + + context 'top level group' do + let_it_be(:group) { create(:group, :shared_runners_disabled) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } + + it 'enables allow descendants to override only for itself' do + expect { subject } + .to change { group.reload.allow_descendants_override_disabled_shared_runners }.from(false).to(true) + .and not_change { group.reload.shared_runners_enabled } + .and not_change { sub_group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + end + end + + context 'group that its ancestors have shared Runners disabled but allows to override' do + let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } + let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) } + + it 'enables allow descendants to override' do + expect { subject } + .to not_change { parent.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { parent.reload.shared_runners_enabled } + .and change { group.reload.allow_descendants_override_disabled_shared_runners }.from(false).to(true) + .and not_change { group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + end + end + + context 'when parent does not allow' do + let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false ) } + let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) } + + it 'raises error and does not allow descendants to override' do + expect { subject } + .to raise_error(described_class::UpdateSharedRunnersError, 'Group level shared Runners not allowed') + .and not_change { parent.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { parent.reload.shared_runners_enabled } + .and not_change { group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { group.reload.shared_runners_enabled } + end + end + + context 'top level group that has shared Runners enabled' do + let_it_be(:group) { create(:group, shared_runners_enabled: true) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } + + it 'raises error and does not change config' do + expect { subject } + .to raise_error(described_class::UpdateSharedRunnersError, 'Shared Runners enabled') + .and not_change { group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { group.reload.shared_runners_enabled } + .and not_change { sub_group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + end + end + end + + describe '#disallow_descendants_override_disabled_shared_runners!' do + subject { group.disallow_descendants_override_disabled_shared_runners! } + + context 'top level group' do + let_it_be(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners ) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) } + let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) } + + it 'disables allow project to override for descendants and disables project shared Runners' do + expect { subject } + .to not_change { group.reload.shared_runners_enabled } + .and change { group.reload.allow_descendants_override_disabled_shared_runners }.from(true).to(false) + .and not_change { sub_group.reload.shared_runners_enabled } + .and change { sub_group.reload.allow_descendants_override_disabled_shared_runners }.from(true).to(false) + .and change { project.reload.shared_runners_enabled }.from(true).to(false) + end + end + + context 'top level group that has shared Runners enabled' do + let_it_be(:group) { create(:group, shared_runners_enabled: true) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } + + it 'results error and does not change config' do + expect { subject } + .to raise_error(described_class::UpdateSharedRunnersError, 'Shared Runners enabled') + .and not_change { group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { group.reload.shared_runners_enabled } + .and not_change { sub_group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + end + end + end end diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9fd8477a455d72f8c3d2179587c80037b5655323 --- /dev/null +++ b/spec/services/groups/update_shared_runners_service_spec.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::UpdateSharedRunnersService do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:params) { {} } + + describe '#execute' do + subject { described_class.new(group, user, params).execute } + + context 'when current_user is not the group owner' do + let_it_be(:group) { create(:group) } + + let(:params) { { shared_runners_enabled: '0' } } + + before do + group.add_maintainer(user) + end + + it 'results error and does not call any method' do + expect(group).not_to receive(:enable_shared_runners!) + expect(group).not_to receive(:disable_shared_runners!) + expect(group).not_to receive(:allow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:disallow_descendants_override_disabled_shared_runners!) + + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Operation not allowed') + expect(subject[:http_status]).to eq(403) + end + end + + context 'when current_user is the group owner' do + before do + group.add_owner(user) + end + + context 'enable shared Runners' do + where(:desired_params) do + ['1', true] + end + + with_them do + let(:params) { { shared_runners_enabled: desired_params } } + + context 'group that its ancestors have shared runners disabled' do + let_it_be(:parent) { create(:group, :shared_runners_disabled) } + let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) } + + it 'results error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Shared Runners disabled for the parent group') + end + end + + context 'root group with shared runners disabled' do + let_it_be(:group) { create(:group, :shared_runners_disabled) } + + it 'receives correct method and succeeds' do + expect(group).to receive(:enable_shared_runners!) + expect(group).not_to receive(:disable_shared_runners!) + expect(group).not_to receive(:allow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:disallow_descendants_override_disabled_shared_runners!) + + expect(subject[:status]).to eq(:success) + end + end + end + end + + context 'disable shared Runners' do + let_it_be(:group) { create(:group) } + + where(:desired_params) do + ['0', false] + end + + with_them do + let(:params) { { shared_runners_enabled: desired_params } } + + it 'receives correct method and succeeds' do + expect(group).to receive(:disable_shared_runners!) + expect(group).not_to receive(:enable_shared_runners!) + expect(group).not_to receive(:allow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:disallow_descendants_override_disabled_shared_runners!) + + expect(subject[:status]).to eq(:success) + end + end + end + + context 'allow descendants to override' do + where(:desired_params) do + ['1', true] + end + + with_them do + let(:params) { { allow_descendants_override_disabled_shared_runners: desired_params } } + + context 'top level group' do + let_it_be(:group) { create(:group, :shared_runners_disabled) } + + it 'receives correct method and succeeds' do + expect(group).to receive(:allow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:disallow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:enable_shared_runners!) + expect(group).not_to receive(:disable_shared_runners!) + + expect(subject[:status]).to eq(:success) + end + end + + context 'when parent does not allow' do + let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false ) } + let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) } + + it 'results error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Group level shared Runners not allowed') + end + end + end + end + + context 'disallow descendants to override' do + where(:desired_params) do + ['0', false] + end + + with_them do + let(:params) { { allow_descendants_override_disabled_shared_runners: desired_params } } + + context 'top level group' do + let_it_be(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners ) } + + it 'receives correct method and succeeds' do + expect(group).to receive(:disallow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:allow_descendants_override_disabled_shared_runners!) + expect(group).not_to receive(:enable_shared_runners!) + expect(group).not_to receive(:disable_shared_runners!) + + expect(subject[:status]).to eq(:success) + end + end + + context 'top level group that has shared Runners enabled' do + let_it_be(:group) { create(:group, shared_runners_enabled: true) } + + it 'results error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Shared Runners enabled') + end + end + end + end + + context 'both params are present' do + context 'shared_runners_enabled: 1 and allow_descendants_override_disabled_shared_runners' do + let_it_be(:group) { create(:group, :shared_runners_disabled) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } + + where(:allow_descendants_override) do + ['1', true, '0', false] + end + + with_them do + let(:params) { { shared_runners_enabled: '1', allow_descendants_override_disabled_shared_runners: allow_descendants_override } } + + it 'results in an error because shared Runners are enabled' do + expect { subject } + .to not_change { group.reload.shared_runners_enabled } + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { project.reload.shared_runners_enabled } + .and not_change { group.reload.allow_descendants_override_disabled_shared_runners } + .and not_change { sub_group.reload.allow_descendants_override_disabled_shared_runners } + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Cannot set shared_runners_enabled to true and allow_descendants_override_disabled_shared_runners') + end + end + end + + context 'shared_runners_enabled: 0 and allow_descendants_override_disabled_shared_runners: 0' do + let_it_be(:group) { create(:group, :allow_descendants_override_disabled_shared_runners) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) } + let_it_be(:sub_group_2) { create(:group, parent: group) } + let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) } + let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) } + + let(:params) { { shared_runners_enabled: '0', allow_descendants_override_disabled_shared_runners: '0' } } + + it 'disables shared Runners and disable allow_descendants_override_disabled_shared_runners' do + expect { subject } + .to change { group.reload.shared_runners_enabled }.from(true).to(false) + .and change { group.reload.allow_descendants_override_disabled_shared_runners }.from(true).to(false) + .and not_change { sub_group.reload.shared_runners_enabled } + .and change { sub_group.reload.allow_descendants_override_disabled_shared_runners }.from(true).to(false) + .and change { sub_group_2.reload.shared_runners_enabled }.from(true).to(false) + .and not_change { sub_group_2.reload.allow_descendants_override_disabled_shared_runners } + .and change { project.reload.shared_runners_enabled }.from(true).to(false) + .and change { project_2.reload.shared_runners_enabled }.from(true).to(false) + end + end + + context 'shared_runners_enabled: 0 and allow_descendants_override_disabled_shared_runners: 1' do + let_it_be(:group) { create(:group) } + let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } + let_it_be(:sub_group_2) { create(:group, parent: group) } + let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) } + let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) } + + let(:params) { { shared_runners_enabled: '0', allow_descendants_override_disabled_shared_runners: '1' } } + + it 'disables shared Runners and enable allow_descendants_override_disabled_shared_runners only for itself' do + expect { subject } + .to change { group.reload.shared_runners_enabled }.from(true).to(false) + .and change { group.reload.allow_descendants_override_disabled_shared_runners }.from(false).to(true) + .and not_change { sub_group.reload.shared_runners_enabled } + .and not_change { sub_group.reload.allow_descendants_override_disabled_shared_runners } + .and change { sub_group_2.reload.shared_runners_enabled }.from(true).to(false) + .and not_change { sub_group_2.reload.allow_descendants_override_disabled_shared_runners } + .and change { project.reload.shared_runners_enabled }.from(true).to(false) + .and change { project_2.reload.shared_runners_enabled }.from(true).to(false) + end + end + end + end + end +end