Commit 62d832bb authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'environment-multi-stop-actions' into 'master'

Multiple on_stop action for an environment

See merge request gitlab-org/gitlab!84922
parents e138c828 d886be07
...@@ -104,11 +104,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -104,11 +104,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def stop def stop
return render_404 unless @environment.available? return render_404 unless @environment.available?
stop_action = @environment.stop_with_action!(current_user) stop_actions = @environment.stop_with_actions!(current_user)
action_or_env_url = action_or_env_url =
if stop_action if stop_actions&.count == 1
polymorphic_url([project, stop_action]) polymorphic_url([project, stop_actions.first])
else else
project_environment_url(project, @environment) project_environment_url(project, @environment)
end end
......
...@@ -59,7 +59,7 @@ class Environment < ApplicationRecord ...@@ -59,7 +59,7 @@ class Environment < ApplicationRecord
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true delegate :manual_actions, to: :last_deployment, allow_nil: true
delegate :auto_rollback_enabled?, to: :project delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
...@@ -191,6 +191,23 @@ class Environment < ApplicationRecord ...@@ -191,6 +191,23 @@ class Environment < ApplicationRecord
last_deployment&.deployable last_deployment&.deployable
end end
def last_deployment_pipeline
last_deployable&.pipeline
end
# This method returns the deployment records of the last deployment pipeline, that successfully executed to this environment.
# e.g.
# A pipeline contains
# - deploy job A => production environment
# - deploy job B => production environment
# In this case, `last_deployment_group` returns both deployments, whereas `last_deployable` returns only B.
def last_deployment_group
return Deployment.none unless last_deployment_pipeline
successful_deployments.where(
deployable_id: last_deployment_pipeline.latest_builds.pluck(:id))
end
# NOTE: Below assocation overrides is a workaround for issue https://gitlab.com/gitlab-org/gitlab/-/issues/339908 # NOTE: Below assocation overrides is a workaround for issue https://gitlab.com/gitlab-org/gitlab/-/issues/339908
# It helps to avoid cross joins with the CI database. # It helps to avoid cross joins with the CI database.
# Caveat: It also overrides and losses the default AR caching mechanism. # Caveat: It also overrides and losses the default AR caching mechanism.
...@@ -261,8 +278,8 @@ class Environment < ApplicationRecord ...@@ -261,8 +278,8 @@ class Environment < ApplicationRecord
external_url.gsub(%r{\A.*?://}, '') external_url.gsub(%r{\A.*?://}, '')
end end
def stop_action_available? def stop_actions_available?
available? && stop_action.present? available? && stop_actions.present?
end end
def cancel_deployment_jobs! def cancel_deployment_jobs!
...@@ -275,18 +292,34 @@ class Environment < ApplicationRecord ...@@ -275,18 +292,34 @@ class Environment < ApplicationRecord
end end
end end
def stop_with_action!(current_user) def stop_with_actions!(current_user)
return unless available? return unless available?
stop! stop!
return unless stop_action actions = []
Gitlab::OptimisticLocking.retry_lock( stop_actions.each do |stop_action|
stop_action, Gitlab::OptimisticLocking.retry_lock(
name: 'environment_stop_with_action' stop_action,
) do |build| name: 'environment_stop_with_actions'
build&.play(current_user) ) do |build|
actions << build.play(current_user)
end
end
actions
end
def stop_actions
strong_memoize(:stop_actions) do
if ::Feature.enabled?(:environment_multiple_stop_actions, project, default_enabled: :yaml)
# Fix N+1 queries it brings to the serializer.
# Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
last_deployment_group.map(&:stop_action).compact
else
[last_deployment&.stop_action].compact
end
end end
end end
......
...@@ -4,12 +4,12 @@ class EnvironmentPolicy < BasePolicy ...@@ -4,12 +4,12 @@ class EnvironmentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:stop_with_deployment_allowed) do condition(:stop_with_deployment_allowed) do
@subject.stop_action_available? && @subject.stop_actions_available? &&
can?(:create_deployment) && can?(:update_build, @subject.stop_action) can?(:create_deployment) && can?(:update_build, @subject.stop_actions.last)
end end
condition(:stop_with_update_allowed) do condition(:stop_with_update_allowed) do
!@subject.stop_action_available? && can?(:update_environment, @subject) !@subject.stop_actions_available? && can?(:update_environment, @subject)
end end
condition(:stopped) do condition(:stopped) do
......
...@@ -18,7 +18,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -18,7 +18,7 @@ class EnvironmentEntity < Grape::Entity
expose :environment_type expose :environment_type
expose :name_without_type expose :name_without_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action_available?, as: :has_stop_action expose :stop_actions_available?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :tier expose :tier
......
...@@ -7,7 +7,7 @@ module Environments ...@@ -7,7 +7,7 @@ module Environments
def execute(environment) def execute(environment)
return unless can?(current_user, :stop_environment, environment) return unless can?(current_user, :stop_environment, environment)
environment.stop_with_action!(current_user) environment.stop_with_actions!(current_user)
end end
def execute_for_branch(branch_name) def execute_for_branch(branch_name)
......
...@@ -10,8 +10,10 @@ module Environments ...@@ -10,8 +10,10 @@ module Environments
def perform(environment_id, params = {}) def perform(environment_id, params = {})
Environment.find_by_id(environment_id).try do |environment| Environment.find_by_id(environment_id).try do |environment|
user = environment.stop_action&.user stop_actions = environment.stop_actions
environment.stop_with_action!(user)
user = stop_actions.last&.user
environment.stop_with_actions!(user)
end end
end end
end end
......
---
name: environment_multiple_stop_actions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84922
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358911
milestone: '14.10'
type: development
group: group::release
default_enabled: false
...@@ -131,7 +131,7 @@ module API ...@@ -131,7 +131,7 @@ module API
environment = user_project.environments.find(params[:environment_id]) environment = user_project.environments.find(params[:environment_id])
authorize! :stop_environment, environment authorize! :stop_environment, environment
environment.stop_with_action!(current_user) environment.stop_with_actions!(current_user)
status 200 status 200
present environment, with: Entities::Environment, current_user: current_user present environment, with: Entities::Environment, current_user: current_user
......
...@@ -254,38 +254,54 @@ RSpec.describe Projects::EnvironmentsController do ...@@ -254,38 +254,54 @@ RSpec.describe Projects::EnvironmentsController do
end end
describe 'PATCH #stop' do describe 'PATCH #stop' do
subject { patch :stop, params: environment_params(format: :json) }
context 'when env not available' do context 'when env not available' do
it 'returns 404' do it 'returns 404' do
allow_any_instance_of(Environment).to receive(:available?) { false } allow_any_instance_of(Environment).to receive(:available?) { false }
patch :stop, params: environment_params(format: :json) subject
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'when stop action' do context 'when stop action' do
it 'returns action url' do it 'returns action url for single stop action' do
action = create(:ci_build, :manual) action = create(:ci_build, :manual)
allow_any_instance_of(Environment) allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_action!: action) .to receive_messages(available?: true, stop_with_actions!: [action])
patch :stop, params: environment_params(format: :json) subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq( expect(json_response).to eq(
{ 'redirect_url' => { 'redirect_url' =>
project_job_url(project, action) }) project_job_url(project, action) })
end end
it 'returns environment url for multiple stop actions' do
actions = create_list(:ci_build, 2, :manual)
allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_actions!: actions)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(
{ 'redirect_url' =>
project_environment_url(project, environment) })
end
end end
context 'when no stop action' do context 'when no stop action' do
it 'returns env url' do it 'returns env url' do
allow_any_instance_of(Environment) allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_action!: nil) .to receive_messages(available?: true, stop_with_actions!: nil)
patch :stop, params: environment_params(format: :json) subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq( expect(json_response).to eq(
......
This diff is collapsed.
...@@ -8,7 +8,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| ...@@ -8,7 +8,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false|
create_environment_with_associations(project) create_environment_with_associations(project)
create_environment_with_associations(project) create_environment_with_associations(project)
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count) # Fix N+1 queries introduced by multi stop_actions for environment.
# Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
relax_count = 14
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count)
end end
it 'avoids N+1 database queries without grouping', :request_store do it 'avoids N+1 database queries without grouping', :request_store do
...@@ -19,7 +23,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| ...@@ -19,7 +23,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false|
create_environment_with_associations(project) create_environment_with_associations(project)
create_environment_with_associations(project) create_environment_with_associations(project)
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count) # Fix N+1 queries introduced by multi stop_actions for environment.
# Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
relax_count = 14
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count)
end end
it 'does not preload for environments that does not exist in the page', :request_store do it 'does not preload for environments that does not exist in the page', :request_store do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment