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
def stop
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 =
if stop_action
polymorphic_url([project, stop_action])
if stop_actions&.count == 1
polymorphic_url([project, stop_actions.first])
else
project_environment_url(project, @environment)
end
......
......@@ -59,7 +59,7 @@ class Environment < ApplicationRecord
allow_nil: 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
scope :available, -> { with_state(:available) }
......@@ -191,6 +191,23 @@ class Environment < ApplicationRecord
last_deployment&.deployable
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
# It helps to avoid cross joins with the CI database.
# Caveat: It also overrides and losses the default AR caching mechanism.
......@@ -261,8 +278,8 @@ class Environment < ApplicationRecord
external_url.gsub(%r{\A.*?://}, '')
end
def stop_action_available?
available? && stop_action.present?
def stop_actions_available?
available? && stop_actions.present?
end
def cancel_deployment_jobs!
......@@ -275,18 +292,34 @@ class Environment < ApplicationRecord
end
end
def stop_with_action!(current_user)
def stop_with_actions!(current_user)
return unless available?
stop!
return unless stop_action
actions = []
Gitlab::OptimisticLocking.retry_lock(
stop_action,
name: 'environment_stop_with_action'
) do |build|
build&.play(current_user)
stop_actions.each do |stop_action|
Gitlab::OptimisticLocking.retry_lock(
stop_action,
name: 'environment_stop_with_actions'
) 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
......
......@@ -4,12 +4,12 @@ class EnvironmentPolicy < BasePolicy
delegate { @subject.project }
condition(:stop_with_deployment_allowed) do
@subject.stop_action_available? &&
can?(:create_deployment) && can?(:update_build, @subject.stop_action)
@subject.stop_actions_available? &&
can?(:create_deployment) && can?(:update_build, @subject.stop_actions.last)
end
condition(:stop_with_update_allowed) do
!@subject.stop_action_available? && can?(:update_environment, @subject)
!@subject.stop_actions_available? && can?(:update_environment, @subject)
end
condition(:stopped) do
......
......@@ -18,7 +18,7 @@ class EnvironmentEntity < Grape::Entity
expose :environment_type
expose :name_without_type
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 :tier
......
......@@ -7,7 +7,7 @@ module Environments
def execute(environment)
return unless can?(current_user, :stop_environment, environment)
environment.stop_with_action!(current_user)
environment.stop_with_actions!(current_user)
end
def execute_for_branch(branch_name)
......
......@@ -10,8 +10,10 @@ module Environments
def perform(environment_id, params = {})
Environment.find_by_id(environment_id).try do |environment|
user = environment.stop_action&.user
environment.stop_with_action!(user)
stop_actions = environment.stop_actions
user = stop_actions.last&.user
environment.stop_with_actions!(user)
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
environment = user_project.environments.find(params[:environment_id])
authorize! :stop_environment, environment
environment.stop_with_action!(current_user)
environment.stop_with_actions!(current_user)
status 200
present environment, with: Entities::Environment, current_user: current_user
......
......@@ -254,38 +254,54 @@ RSpec.describe Projects::EnvironmentsController do
end
describe 'PATCH #stop' do
subject { patch :stop, params: environment_params(format: :json) }
context 'when env not available' do
it 'returns 404' do
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)
end
end
context 'when stop action' do
it 'returns action url' do
it 'returns action url for single stop action' do
action = create(:ci_build, :manual)
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(json_response).to eq(
{ 'redirect_url' =>
project_job_url(project, action) })
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
context 'when no stop action' do
it 'returns env url' do
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(json_response).to eq(
......
This diff is collapsed.
......@@ -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)
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
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|
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
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