diff --git a/app/models/environment.rb b/app/models/environment.rb index ed4369f7fd68f39324281c4e7787d971ac5654d7..6fce9327cf7955b47157b0521b04f5a2bd6fd3b6 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -571,6 +571,18 @@ def deploy_freezes end end + def ensure_environment_tier + self.tier ||= guess_tier + end + + def set_default_auto_stop_setting + self.auto_stop_setting = if Feature.enabled?(:new_default_for_auto_stop, project) + production? || staging? ? :with_action : :always + else + :always + end + end + private def run_stop_action!(job, link_identity:) @@ -617,10 +629,6 @@ def generate_slug self.slug = Gitlab::Slug::Environment.new(name).generate end - def ensure_environment_tier - self.tier ||= guess_tier - end - def merge_request_not_changed if merge_request_id_changed? && persisted? errors.add(:merge_request, 'merge_request cannot be changed') diff --git a/app/services/environments/create_service.rb b/app/services/environments/create_service.rb index 8a576c923287e7d590b175c9649b28ae8833a34a..6736e98404ffab69df6a190cee9290afa3923671 100644 --- a/app/services/environments/create_service.rb +++ b/app/services/environments/create_service.rb @@ -20,7 +20,10 @@ def execute end begin - environment = project.environments.create!(**params.slice(*ALLOWED_ATTRIBUTES)) + environment = project.environments.new(**params.slice(*ALLOWED_ATTRIBUTES)) + environment.ensure_environment_tier + environment.set_default_auto_stop_setting unless params[:auto_stop_setting] + environment.save! ServiceResponse.success(payload: { environment: environment }) rescue ActiveRecord::RecordInvalid => err ServiceResponse.error(message: err.record.errors.full_messages, payload: { environment: nil }) diff --git a/config/feature_flags/beta/new_default_for_auto_stop.yml b/config/feature_flags/beta/new_default_for_auto_stop.yml new file mode 100644 index 0000000000000000000000000000000000000000..c730a05d18ad94006126c0f7d538d96d49baa11e --- /dev/null +++ b/config/feature_flags/beta/new_default_for_auto_stop.yml @@ -0,0 +1,9 @@ +--- +name: new_default_for_auto_stop +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428625 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181746 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/523169 +milestone: '17.10' +group: group::environments +type: beta +default_enabled: false diff --git a/doc/ci/environments/_index.md b/doc/ci/environments/_index.md index 0ba973e707334517241714c8c02fe4fff4a542a5..ae783dd12c4810812e3f505b7d19221d5d9444be 100644 --- a/doc/ci/environments/_index.md +++ b/doc/ci/environments/_index.md @@ -421,6 +421,18 @@ To stop an environment in the GitLab UI: 1. Next to the environment you want to stop, select **Stop**. 1. On the confirmation dialog, select **Stop environment**. +### Default stopping behavior + +GitLab automatically stops environments when the associated branch is deleted or merged. +This behavior persists even if no explicit `on_stop` CI/CD job is defined. + +However, [issue 428625](https://gitlab.com/gitlab-org/gitlab/-/issues/428625) proposes to change this behavior +so that production and staging environments stop only if an explicit `on_stop` CI/CD job is defined. + +You can configure an environment's stopping behavior with the +[`auto_stop_setting`](../../api/environments.md#update-an-existing-environment) +parameter in the Environments API. + ### Stop an environment when a branch is deleted You can configure environments to stop when a branch is deleted. diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 89221534fea7f8bd2a4ff226322defd57f3438f9..9530fbde5f9c6a4b7260ee6334a9e1c7fb4bb541 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -72,7 +72,7 @@ class Environments < ::API::Base optional :kubernetes_namespace, type: String, desc: 'The Kubernetes namespace to associate with this environment' optional :flux_resource_path, type: String, desc: 'The Flux resource path to associate with this environment' optional :description, type: String, desc: 'The description of the environment' - optional :auto_stop_setting, type: String, default: "always", values: Environment.auto_stop_settings.keys, desc: 'The auto stop setting for the environment. Allowed values are `always` and `with_action`' + optional :auto_stop_setting, type: String, values: Environment.auto_stop_settings.keys, desc: 'The auto stop setting for the environment. Allowed values are `always` and `with_action`' end route_setting :authentication, job_token_allowed: true route_setting :authorization, job_token_policies: :admin_environments diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 48c292a7b1fdfae1897c0905b383751b25dc0ec7..ac5f1ab56d3bf25bae58aea51db2ca50c795521b 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -2325,4 +2325,48 @@ def create_deployment_with_stop_action(status, pipeline, stop_action_name) is_expected.to match_array([environment]) end end + + describe '#set_default_auto_stop_setting' do + let(:environment) { create(:environment, project: project) } + + subject { environment.set_default_auto_stop_setting } + + context 'when auto_stop_setting is not set' do + %w[production staging].each do |tier| + context "when #{tier} environment" do + let(:environment) { create(:environment, project: project, name: 'production', tier: :production) } + + it 'sets auto_stop_setting to :with_action' do + expect { subject }.to change { environment.auto_stop_setting }.from('always').to('with_action') + end + end + end + + %w[testing development other].each do |tier| + context "when #{tier} environment" do + let(:environment) { create(:environment, project: project, name: tier, tier: tier.to_sym) } + + it 'sets auto_stop_setting to :always' do + expect { subject }.not_to change { environment.auto_stop_setting } + end + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(new_default_for_auto_stop: false) + end + + %w[production staging testing development other].each do |tier| + context "when #{tier} environment" do + let(:environment) { create(:environment, project: project, name: tier, tier: tier.to_sym) } + + it 'sets auto_stop_setting to :always' do + expect { subject }.not_to change { environment.auto_stop_setting } + end + end + end + end + end + end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 5ed6e1c5501ab3e8d7ef80a3babd9d4501aa2e8e..0e4bfb77692185564eaf74dc8e6020ad653d7fba 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -151,7 +151,16 @@ expect(json_response['slug']).to eq('mepmep') expect(json_response['tier']).to eq('staging') expect(json_response['external']).to be_nil - expect(json_response['auto_stop_setting']).to eq('always') + expect(json_response['auto_stop_setting']).to eq('with_action') + end + + context 'when the tier is development' do + it 'creates an environment with auto_stop_setting set to always' do + post api("/projects/#{project.id}/environments", user), params: { name: "mepmep", tier: 'development' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['auto_stop_setting']).to eq('always') + end end context 'when associating a cluster agent' do diff --git a/spec/services/environments/create_service_spec.rb b/spec/services/environments/create_service_spec.rb index 5e57311e9ec3b06bd25fb033db14f6b8f1d245c5..00ca4dda1b3331285f113e38694938524509f9e7 100644 --- a/spec/services/environments/create_service_spec.rb +++ b/spec/services/environments/create_service_spec.rb @@ -14,7 +14,10 @@ describe '#execute' do subject { service.execute } - let(:params) { { name: 'production', description: 'description', external_url: 'https://gitlab.com', tier: :production, auto_stop_setting: :always } } + let(:auto_stop_setting) { :always } + let(:env_name) { 'production' } + let(:tier) { nil } + let(:params) { { name: env_name, description: 'description', tier: tier, external_url: 'https://gitlab.com', auto_stop_setting: auto_stop_setting } } it 'creates an environment' do expect { subject }.to change { ::Environment.count }.by(1) @@ -31,6 +34,119 @@ expect(response.payload[:environment].auto_stop_setting).to eq('always') end + context 'when tier is provided' do + let(:tier) { 'production' } + let(:env_name) { 'testing' } + + it 'creates an environment' do + expect { subject }.to change { ::Environment.count }.by(1) + end + + it 'returns successful response' do + response = subject + + expect(response).to be_success + expect(response.payload[:environment].name).to eq('testing') + expect(response.payload[:environment].tier).to eq('production') + expect(response.payload[:environment].auto_stop_setting).to eq('always') + end + end + + context 'when tier is not provided' do + context 'when environment name is production' do + let(:env_name) { 'production' } + + it 'guesses tier to production' do + response = subject + + expect(response).to be_success + expect(response.payload[:environment].name).to eq('production') + expect(response.payload[:environment].tier).to eq('production') + end + end + + context 'when environment name is testing' do + let(:env_name) { 'testing' } + + it 'guesses tier to testing' do + response = subject + + expect(response).to be_success + expect(response.payload[:environment].name).to eq('testing') + expect(response.payload[:environment].tier).to eq('testing') + end + end + end + + context 'when auto_stop_setting is not provided' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(new_default_for_auto_stop: false) + end + + let(:tier) { 'production' } + let(:env_name) { 'production' } + let(:auto_stop_setting) { nil } + + it 'creates an environment' do + expect { subject }.to change { ::Environment.count }.by(1) + end + + it 'sets :always for auto_stop_setting' do + expect_next_instance_of(Environment) do |instance| + expect(instance).to receive(:set_default_auto_stop_setting).and_call_original + end + + response = subject + expect(response).to be_success + expect(response.payload[:environment].name).to eq(env_name) + expect(response.payload[:environment].auto_stop_setting).to eq('always') + end + end + + context 'when environment tier is production' do + let(:tier) { 'production' } + let(:env_name) { 'production' } + let(:auto_stop_setting) { nil } + + it 'creates an environment' do + expect { subject }.to change { ::Environment.count }.by(1) + end + + it 'sets :with_action for auto_stop_setting' do + expect_next_instance_of(Environment) do |instance| + expect(instance).to receive(:set_default_auto_stop_setting).and_call_original + end + + response = subject + expect(response).to be_success + expect(response.payload[:environment].name).to eq(env_name) + expect(response.payload[:environment].auto_stop_setting).to eq('with_action') + end + end + + context 'when environment tier is development' do + let(:tier) { 'development' } + let(:env_name) { 'development' } + let(:auto_stop_setting) { nil } + + it 'creates an environment' do + expect { subject }.to change { ::Environment.count }.by(1) + end + + it 'sets :always for auto_stop_setting' do + expect_next_instance_of(Environment) do |instance| + expect(instance).to receive(:set_default_auto_stop_setting).and_call_original + end + + response = subject + expect(response).to be_success + expect(response.payload[:environment].name).to eq(env_name) + expect(response.payload[:environment].auto_stop_setting).to eq('always') + end + end + end + context 'with a cluster agent' do let_it_be(:agent_management_project) { create(:project) } let_it_be(:cluster_agent) { create(:cluster_agent, project: agent_management_project) }