Commit bbbf58a3 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'ali/deployment-approvals-data-model' into 'master'

Deployment Approvals data model

See merge request gitlab-org/gitlab!74932
parents 40d3798d c4d5ac49
...@@ -452,7 +452,7 @@ module Ci ...@@ -452,7 +452,7 @@ module Ci
end end
def retryable? def retryable?
return false if retried? || archived? return false if retried? || archived? || deployment_rejected?
success? || failed? || canceled? success? || failed? || canceled?
end end
......
...@@ -146,7 +146,7 @@ class CommitStatus < Ci::ApplicationRecord ...@@ -146,7 +146,7 @@ class CommitStatus < Ci::ApplicationRecord
end end
event :drop do event :drop do
transition [:created, :waiting_for_resource, :preparing, :pending, :running, :scheduled] => :failed transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :failed
end end
event :success do event :success do
......
...@@ -28,6 +28,7 @@ module Enums ...@@ -28,6 +28,7 @@ module Enums
trace_size_exceeded: 19, trace_size_exceeded: 19,
builds_disabled: 20, builds_disabled: 20,
environment_creation_failure: 21, environment_creation_failure: 21,
deployment_rejected: 22,
insufficient_bridge_permissions: 1_001, insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002, downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003, invalid_bridge_trigger: 1_003,
......
...@@ -46,9 +46,10 @@ class Deployment < ApplicationRecord ...@@ -46,9 +46,10 @@ class Deployment < ApplicationRecord
scope :for_project, -> (project_id) { where(project_id: project_id) } scope :for_project, -> (project_id) { where(project_id: project_id) }
scope :for_projects, -> (projects) { where(project: projects) } scope :for_projects, -> (projects) { where(project: projects) }
scope :visible, -> { where(status: %i[running success failed canceled]) } scope :visible, -> { where(status: %i[running success failed canceled blocked]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
scope :active, -> { where(status: %i[created running]) } scope :active, -> { where(status: %i[created running]) }
scope :upcoming, -> { where(status: %i[blocked running]) }
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) } scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
scope :with_api_entity_associations, -> { preload({ deployable: { runner: [], tags: [], user: [], job_artifacts_archive: [] } }) } scope :with_api_entity_associations, -> { preload({ deployable: { runner: [], tags: [], user: [], job_artifacts_archive: [] } }) }
...@@ -64,6 +65,10 @@ class Deployment < ApplicationRecord ...@@ -64,6 +65,10 @@ class Deployment < ApplicationRecord
transition created: :running transition created: :running
end end
event :block do
transition created: :blocked
end
event :succeed do event :succeed do
transition any - [:success] => :success transition any - [:success] => :success
end end
...@@ -140,7 +145,8 @@ class Deployment < ApplicationRecord ...@@ -140,7 +145,8 @@ class Deployment < ApplicationRecord
success: 2, success: 2,
failed: 3, failed: 3,
canceled: 4, canceled: 4,
skipped: 5 skipped: 5,
blocked: 6
} }
def self.archivables_in(project, limit:) def self.archivables_in(project, limit:)
...@@ -391,6 +397,8 @@ class Deployment < ApplicationRecord ...@@ -391,6 +397,8 @@ class Deployment < ApplicationRecord
cancel! cancel!
when 'skipped' when 'skipped'
skip! skip!
when 'blocked'
block!
else else
raise ArgumentError, "The status #{status.inspect} is invalid" raise ArgumentError, "The status #{status.inspect} is invalid"
end end
......
...@@ -31,7 +31,7 @@ class Environment < ApplicationRecord ...@@ -31,7 +31,7 @@ class Environment < ApplicationRecord
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true
has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment has_one :upcoming_deployment, -> { upcoming.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :generate_slug, if: ->(env) { env.slug.blank? } before_validation :generate_slug, if: ->(env) { env.slug.blank? }
......
...@@ -29,7 +29,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -29,7 +29,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
no_matching_runner: 'No matching runner available', no_matching_runner: 'No matching runner available',
trace_size_exceeded: 'The job log size limit was reached', trace_size_exceeded: 'The job log size limit was reached',
builds_disabled: 'The CI/CD is disabled for this project', builds_disabled: 'The CI/CD is disabled for this project',
environment_creation_failure: 'This job could not be executed because it would create an environment with an invalid parameter.' environment_creation_failure: 'This job could not be executed because it would create an environment with an invalid parameter.',
deployment_rejected: 'This deployment job was rejected.'
}.freeze }.freeze
TROUBLESHOOTING_DOC = { TROUBLESHOOTING_DOC = {
......
---
name: deployment_approvals
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74932
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347342
milestone: '14.6'
type: development
group: group::release
default_enabled: false
# frozen_string_literal: true
class AddRequiredApprovalCountToProtectedEnvironments < Gitlab::Database::Migration[1.0]
def change
add_column :protected_environments, :required_approval_count, :integer, default: 0, null: false
end
end
# frozen_string_literal: true
class CreateDeploymentApprovals < Gitlab::Database::Migration[1.0]
def change
create_table :deployment_approvals do |t|
t.bigint :deployment_id, null: false
t.bigint :user_id, null: false, index: true
t.timestamps_with_timezone null: false
t.integer :status, limit: 2, null: false
t.index [:deployment_id, :user_id], unique: true
end
end
end
# frozen_string_literal: true
class AddUserForeignKeyToDeploymentApprovals < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :deployment_approvals, :users, column: :user_id
end
def down
with_lock_retries do
remove_foreign_key :deployment_approvals, :users
end
end
end
# frozen_string_literal: true
class AddDeploymentForeignKeyToDeploymentApprovals < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :deployment_approvals, :deployments, column: :deployment_id
end
def down
with_lock_retries do
remove_foreign_key :deployment_approvals, :deployments
end
end
end
# frozen_string_literal: true
class AddProtectedEnvironmentsRequiredApprovalCountCheckConstraint < Gitlab::Database::Migration[1.0]
CONSTRAINT_NAME = 'protected_environments_required_approval_count_positive'
disable_ddl_transaction!
def up
add_check_constraint :protected_environments, 'required_approval_count >= 0', CONSTRAINT_NAME
end
def down
remove_check_constraint :protected_environments, CONSTRAINT_NAME
end
end
ac2e376ad32f0e2fd45d8695f13a0b46c2d5964b881f79e3a30a51ac85d4359b
\ No newline at end of file
caaf92f12bf0ed144d99f629c9e5d64fd45832a90bbd743e40febcdc4802cd59
\ No newline at end of file
ac21109099642d5934c16b3f0130736a587c4f20143552545c2b524062ff71e0
\ No newline at end of file
61c949b42338b248a0950cfafc82d58816c3fec44a2bf41c4ecb4cf09340a424
\ No newline at end of file
d1ed3ddf51c0bcebbac2a8dee05aa168daa35129110a463ac296ff2e640b0dbd
\ No newline at end of file
...@@ -13410,6 +13410,24 @@ CREATE SEQUENCE deploy_tokens_id_seq ...@@ -13410,6 +13410,24 @@ CREATE SEQUENCE deploy_tokens_id_seq
ALTER SEQUENCE deploy_tokens_id_seq OWNED BY deploy_tokens.id; ALTER SEQUENCE deploy_tokens_id_seq OWNED BY deploy_tokens.id;
CREATE TABLE deployment_approvals (
id bigint NOT NULL,
deployment_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
status smallint NOT NULL
);
CREATE SEQUENCE deployment_approvals_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE deployment_approvals_id_seq OWNED BY deployment_approvals.id;
CREATE TABLE deployment_clusters ( CREATE TABLE deployment_clusters (
deployment_id integer NOT NULL, deployment_id integer NOT NULL,
cluster_id integer NOT NULL, cluster_id integer NOT NULL,
...@@ -18696,7 +18714,9 @@ CREATE TABLE protected_environments ( ...@@ -18696,7 +18714,9 @@ CREATE TABLE protected_environments (
updated_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL,
name character varying NOT NULL, name character varying NOT NULL,
group_id bigint, group_id bigint,
CONSTRAINT protected_environments_project_or_group_existence CHECK (((project_id IS NULL) <> (group_id IS NULL))) required_approval_count integer DEFAULT 0 NOT NULL,
CONSTRAINT protected_environments_project_or_group_existence CHECK (((project_id IS NULL) <> (group_id IS NULL))),
CONSTRAINT protected_environments_required_approval_count_positive CHECK ((required_approval_count >= 0))
); );
CREATE SEQUENCE protected_environments_id_seq CREATE SEQUENCE protected_environments_id_seq
...@@ -21517,6 +21537,8 @@ ALTER TABLE ONLY deploy_keys_projects ALTER COLUMN id SET DEFAULT nextval('deplo ...@@ -21517,6 +21537,8 @@ ALTER TABLE ONLY deploy_keys_projects ALTER COLUMN id SET DEFAULT nextval('deplo
ALTER TABLE ONLY deploy_tokens ALTER COLUMN id SET DEFAULT nextval('deploy_tokens_id_seq'::regclass); ALTER TABLE ONLY deploy_tokens ALTER COLUMN id SET DEFAULT nextval('deploy_tokens_id_seq'::regclass);
ALTER TABLE ONLY deployment_approvals ALTER COLUMN id SET DEFAULT nextval('deployment_approvals_id_seq'::regclass);
ALTER TABLE ONLY deployments ALTER COLUMN id SET DEFAULT nextval('deployments_id_seq'::regclass); ALTER TABLE ONLY deployments ALTER COLUMN id SET DEFAULT nextval('deployments_id_seq'::regclass);
ALTER TABLE ONLY description_versions ALTER COLUMN id SET DEFAULT nextval('description_versions_id_seq'::regclass); ALTER TABLE ONLY description_versions ALTER COLUMN id SET DEFAULT nextval('description_versions_id_seq'::regclass);
...@@ -23086,6 +23108,9 @@ ALTER TABLE ONLY deploy_keys_projects ...@@ -23086,6 +23108,9 @@ ALTER TABLE ONLY deploy_keys_projects
ALTER TABLE ONLY deploy_tokens ALTER TABLE ONLY deploy_tokens
ADD CONSTRAINT deploy_tokens_pkey PRIMARY KEY (id); ADD CONSTRAINT deploy_tokens_pkey PRIMARY KEY (id);
ALTER TABLE ONLY deployment_approvals
ADD CONSTRAINT deployment_approvals_pkey PRIMARY KEY (id);
ALTER TABLE ONLY deployment_clusters ALTER TABLE ONLY deployment_clusters
ADD CONSTRAINT deployment_clusters_pkey PRIMARY KEY (deployment_id); ADD CONSTRAINT deployment_clusters_pkey PRIMARY KEY (deployment_id);
...@@ -25831,6 +25856,10 @@ CREATE INDEX index_deploy_tokens_on_token_and_expires_at_and_id ON deploy_tokens ...@@ -25831,6 +25856,10 @@ CREATE INDEX index_deploy_tokens_on_token_and_expires_at_and_id ON deploy_tokens
CREATE UNIQUE INDEX index_deploy_tokens_on_token_encrypted ON deploy_tokens USING btree (token_encrypted); CREATE UNIQUE INDEX index_deploy_tokens_on_token_encrypted ON deploy_tokens USING btree (token_encrypted);
CREATE UNIQUE INDEX index_deployment_approvals_on_deployment_id_and_user_id ON deployment_approvals USING btree (deployment_id, user_id);
CREATE INDEX index_deployment_approvals_on_user_id ON deployment_approvals USING btree (user_id);
CREATE UNIQUE INDEX index_deployment_clusters_on_cluster_id_and_deployment_id ON deployment_clusters USING btree (cluster_id, deployment_id); CREATE UNIQUE INDEX index_deployment_clusters_on_cluster_id_and_deployment_id ON deployment_clusters USING btree (cluster_id, deployment_id);
CREATE INDEX index_deployment_merge_requests_on_merge_request_id ON deployment_merge_requests USING btree (merge_request_id); CREATE INDEX index_deployment_merge_requests_on_merge_request_id ON deployment_merge_requests USING btree (merge_request_id);
...@@ -28974,6 +29003,9 @@ ALTER TABLE ONLY lists ...@@ -28974,6 +29003,9 @@ ALTER TABLE ONLY lists
ALTER TABLE ONLY ci_unit_test_failures ALTER TABLE ONLY ci_unit_test_failures
ADD CONSTRAINT fk_0f09856e1f FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE; ADD CONSTRAINT fk_0f09856e1f FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
ALTER TABLE ONLY deployment_approvals
ADD CONSTRAINT fk_0f58311058 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_pages_metadata ALTER TABLE ONLY project_pages_metadata
ADD CONSTRAINT fk_0fd5b22688 FOREIGN KEY (pages_deployment_id) REFERENCES pages_deployments(id) ON DELETE SET NULL; ADD CONSTRAINT fk_0fd5b22688 FOREIGN KEY (pages_deployment_id) REFERENCES pages_deployments(id) ON DELETE SET NULL;
...@@ -29082,6 +29114,9 @@ ALTER TABLE ONLY coverage_fuzzing_corpuses ...@@ -29082,6 +29114,9 @@ ALTER TABLE ONLY coverage_fuzzing_corpuses
ALTER TABLE ONLY agent_group_authorizations ALTER TABLE ONLY agent_group_authorizations
ADD CONSTRAINT fk_2c9f941965 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_2c9f941965 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY deployment_approvals
ADD CONSTRAINT fk_2d060dfc73 FOREIGN KEY (deployment_id) REFERENCES deployments(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_freeze_periods ALTER TABLE ONLY ci_freeze_periods
ADD CONSTRAINT fk_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
# frozen_string_literal: true
module Deployments
class Approval < ApplicationRecord
self.table_name = 'deployment_approvals'
belongs_to :deployment
belongs_to :user
validates :user, presence: true, uniqueness: { scope: :deployment_id }
validates :deployment, presence: true
validates :status, presence: true
enum status: {
approved: 0,
rejected: 1
}
end
end
...@@ -7,10 +7,15 @@ module EE ...@@ -7,10 +7,15 @@ module EE
# and be prepended in the `Deployment` model # and be prepended in the `Deployment` model
module Deployment module Deployment
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do prepended do
include UsageStatistics include UsageStatistics
delegate :needs_approval?, to: :environment
has_many :approvals, class_name: 'Deployments::Approval'
state_machine :status do state_machine :status do
after_transition any => :success do |deployment| after_transition any => :success do |deployment|
deployment.run_after_commit do deployment.run_after_commit do
...@@ -25,5 +30,16 @@ module EE ...@@ -25,5 +30,16 @@ module EE
end end
end end
end end
override :sync_status_with
def sync_status_with(build)
return update_status!('blocked') if build.status == 'manual' && needs_approval?
super
end
def pending_approval_count
environment.required_approval_count - approvals.approved.count
end
end end
end end
...@@ -79,6 +79,18 @@ module EE ...@@ -79,6 +79,18 @@ module EE
protected_environment_accesses(user).all? { |access, _| access == true } protected_environment_accesses(user).all? { |access, _| access == true }
end end
def needs_approval?
return false unless ::Feature.enabled?(:deployment_approvals, project, default_enabled: :yaml)
required_approval_count > 0
end
def required_approval_count
return 0 unless protected?
associated_protected_environments.map(&:required_approval_count).max
end
private private
def protected_environment_accesses(user) def protected_environment_accesses(user)
......
...@@ -65,6 +65,8 @@ module EE ...@@ -65,6 +65,8 @@ module EE
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: "::ProtectedBranch::PushAccessLevel" # rubocop:disable Cop/ActiveRecordDependent has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: "::ProtectedBranch::PushAccessLevel" # rubocop:disable Cop/ActiveRecordDependent
has_many :protected_branch_unprotect_access_levels, dependent: :destroy, class_name: "::ProtectedBranch::UnprotectAccessLevel" # rubocop:disable Cop/ActiveRecordDependent has_many :protected_branch_unprotect_access_levels, dependent: :destroy, class_name: "::ProtectedBranch::UnprotectAccessLevel" # rubocop:disable Cop/ActiveRecordDependent
has_many :deployment_approvals, class_name: 'Deployments::Approval'
has_many :smartcard_identities has_many :smartcard_identities
has_many :scim_identities has_many :scim_identities
......
...@@ -12,6 +12,7 @@ class ProtectedEnvironment < ApplicationRecord ...@@ -12,6 +12,7 @@ class ProtectedEnvironment < ApplicationRecord
validates :deploy_access_levels, length: { minimum: 1 } validates :deploy_access_levels, length: { minimum: 1 }
validates :name, presence: true validates :name, presence: true
validate :valid_tier_name, if: :group_level? validate :valid_tier_name, if: :group_level?
validates :required_approval_count, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 5 }
scope :sorted_by_name, -> { order(:name) } scope :sorted_by_name, -> { order(:name) }
......
# frozen_string_literal: true
FactoryBot.define do
factory :deployment_approval, class: 'Deployments::Approval' do
user
deployment
status { :approved }
trait :rejected do
status { :rejected }
end
end
end
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Deployment do RSpec.describe Deployment do
it { is_expected.to have_many(:approvals) }
it { is_expected.to delegate_method(:needs_approval?).to(:environment) }
describe 'state machine' do describe 'state machine' do
context 'when deployment succeeded' do context 'when deployment succeeded' do
let(:deployment) { create(:deployment, :running) } let(:deployment) { create(:deployment, :running) }
...@@ -20,4 +23,115 @@ RSpec.describe Deployment do ...@@ -20,4 +23,115 @@ RSpec.describe Deployment do
end end
end end
end end
describe '#sync_status_with' do
subject { deployment.sync_status_with(ci_build) }
let_it_be(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
let(:deployment) { create(:deployment, project: project, environment: environment) }
context 'when build is manual' do
let(:ci_build) { create(:ci_build, project: project, status: :manual) }
context 'and Protected Environments feature is available' do
before do
stub_licensed_features(protected_environments: true)
create(:protected_environment, name: environment.name, project: project, required_approval_count: required_approval_count)
end
context 'and deployment needs approval' do
let(:required_approval_count) { 1 }
it 'blocks the deployment' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
is_expected.to eq(true)
expect(deployment.status).to eq('blocked')
expect(deployment.errors).to be_empty
end
end
context 'and deployment does not need approval' do
let(:required_approval_count) { 0 }
it 'does not change the deployment' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
is_expected.to eq(false)
expect(deployment.status).to eq('created')
expect(deployment.errors).to be_empty
end
end
end
context 'and Protected Environments feature is not available' do
before do
stub_licensed_features(protected_environments: false)
end
it 'does not change the deployment' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
is_expected.to eq(false)
expect(deployment.status).to eq('created')
expect(deployment.errors).to be_empty
end
end
end
end
describe '#pending_approval_count' do
let_it_be(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
let(:deployment) { create(:deployment, project: project, environment: environment) }
context 'when Protected Environments feature is available' do
before do
stub_licensed_features(protected_environments: true)
create(:protected_environment, name: environment.name, project: project, required_approval_count: 3)
end
context 'with no approvals' do
it 'returns the number of approvals required by the environment' do
expect(deployment.pending_approval_count).to eq(3)
end
end
context 'with some approvals' do
before do
create(:deployment_approval, deployment: deployment)
end
it 'returns the number of pending approvals' do
expect(deployment.pending_approval_count).to eq(2)
end
end
context 'with all approvals satisfied' do
before do
create_list(:deployment_approval, 3, deployment: deployment)
end
it 'returns zero' do
expect(deployment.pending_approval_count).to eq(0)
end
end
end
context 'when Protected Environments feature is not available' do
before do
stub_licensed_features(protected_environments: false)
end
it 'returns zero' do
expect(deployment.pending_approval_count).to eq(0)
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Deployments::Approval do
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:deployment) }
end
describe 'validations' do
subject { create(:deployment_approval) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_uniqueness_of(:user).scoped_to(:deployment_id) }
it { is_expected.to validate_presence_of(:deployment) }
it { is_expected.to validate_presence_of(:status) }
end
end
...@@ -34,6 +34,7 @@ RSpec.describe User do ...@@ -34,6 +34,7 @@ RSpec.describe User do
it { is_expected.to have_many(:escalation_rules).class_name('IncidentManagement::EscalationRule') } it { is_expected.to have_many(:escalation_rules).class_name('IncidentManagement::EscalationRule') }
it { is_expected.to have_many(:escalation_policies).class_name('IncidentManagement::EscalationPolicy').through(:escalation_rules) } it { is_expected.to have_many(:escalation_policies).class_name('IncidentManagement::EscalationPolicy').through(:escalation_rules) }
it { is_expected.to have_many(:epic_board_recent_visits).inverse_of(:user) } it { is_expected.to have_many(:epic_board_recent_visits).inverse_of(:user) }
it { is_expected.to have_many(:deployment_approvals) }
end end
describe 'nested attributes' do describe 'nested attributes' do
......
...@@ -245,4 +245,88 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do ...@@ -245,4 +245,88 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
end end
describe '#needs_approval?' do
subject { environment.needs_approval? }
context 'when Protected Environments feature is available' do
before do
stub_licensed_features(protected_environments: true)
create(:protected_environment, name: environment.name, project: project, required_approval_count: required_approval_count)
end
context 'with some approvals required' do
let(:required_approval_count) { 1 }
it { is_expected.to be_truthy }
context 'and deployment_approvals feature flag turned off' do
before do
stub_feature_flags(deployment_approvals: false)
end
it { is_expected.to be_falsey }
end
end
context 'with no approvals required' do
let(:required_approval_count) { 0 }
it { is_expected.to be_falsey }
end
end
context 'when Protected Environments feature is not available' do
before do
stub_licensed_features(protected_environments: false)
end
it { is_expected.to be_falsey }
end
end
describe '#required_approval_count' do
subject { environment.required_approval_count }
let_it_be(:project) { create(:project, group: create(:group)) }
context 'when Protected Environments feature is not available' do
before do
stub_licensed_features(protected_environments: false)
end
it { is_expected.to eq(0) }
end
context 'when Protected Environments feature is available' do
before do
stub_licensed_features(protected_environments: true)
end
context 'and no associated protected environments exist' do
it { is_expected.to eq(0) }
end
context 'with one associated protected environment' do
before do
create(:protected_environment, name: environment.name, project: project, required_approval_count: 3)
end
it 'returns the required_approval_count of the protected environment' do
expect(subject).to eq(3)
end
end
context 'with multiple associated protected environments' do
before do
create(:protected_environment, name: environment.name, project: project, required_approval_count: 3)
create(:protected_environment, name: environment.tier, project: nil, group: project.group, required_approval_count: 5)
end
it 'returns the highest required_approval_count of the protected environments' do
expect(subject).to eq(5)
end
end
end
end
end end
...@@ -10,6 +10,12 @@ RSpec.describe ProtectedEnvironment do ...@@ -10,6 +10,12 @@ RSpec.describe ProtectedEnvironment do
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:deploy_access_levels) } it { is_expected.to validate_length_of(:deploy_access_levels) }
it do
is_expected.to validate_numericality_of(:required_approval_count)
.only_integer
.is_greater_than_or_equal_to(0)
.is_less_than_or_equal_to(5)
end
it 'can not belong to both group and project' do it 'can not belong to both group and project' do
group = build(:group) group = build(:group)
......
...@@ -34,7 +34,8 @@ module Gitlab ...@@ -34,7 +34,8 @@ module Gitlab
no_matching_runner: 'no matching runner available', no_matching_runner: 'no matching runner available',
trace_size_exceeded: 'log size limit exceeded', trace_size_exceeded: 'log size limit exceeded',
builds_disabled: 'project builds are disabled', builds_disabled: 'project builds are disabled',
environment_creation_failure: 'environment creation failure' environment_creation_failure: 'environment creation failure',
deployment_rejected: 'deployment rejected'
}.freeze }.freeze
private_constant :REASONS private_constant :REASONS
......
...@@ -164,6 +164,7 @@ dependency_proxy_group_settings: :gitlab_main ...@@ -164,6 +164,7 @@ dependency_proxy_group_settings: :gitlab_main
dependency_proxy_image_ttl_group_policies: :gitlab_main dependency_proxy_image_ttl_group_policies: :gitlab_main
dependency_proxy_manifests: :gitlab_main dependency_proxy_manifests: :gitlab_main
deploy_keys_projects: :gitlab_main deploy_keys_projects: :gitlab_main
deployment_approvals: :gitlab_main
deployment_clusters: :gitlab_main deployment_clusters: :gitlab_main
deployment_merge_requests: :gitlab_main deployment_merge_requests: :gitlab_main
deployments: :gitlab_main deployments: :gitlab_main
......
...@@ -1994,6 +1994,14 @@ RSpec.describe Ci::Build do ...@@ -1994,6 +1994,14 @@ RSpec.describe Ci::Build do
it { is_expected.not_to be_retryable } it { is_expected.not_to be_retryable }
end end
context 'when deployment is rejected' do
before do
build.drop!(:deployment_rejected)
end
it { is_expected.not_to be_retryable }
end
end end
end end
......
...@@ -765,6 +765,14 @@ RSpec.describe CommitStatus do ...@@ -765,6 +765,14 @@ RSpec.describe CommitStatus do
it_behaves_like 'incrementing failure reason counter' it_behaves_like 'incrementing failure reason counter'
end end
context 'when status is manual' do
let(:commit_status) { create(:commit_status, :manual) }
it 'is able to be dropped' do
expect { commit_status.drop! }.to change { commit_status.status }.from('manual').to('failed')
end
end
end end
describe 'ensure stage assignment' do describe 'ensure stage assignment' do
......
...@@ -268,6 +268,29 @@ RSpec.describe Deployment do ...@@ -268,6 +268,29 @@ RSpec.describe Deployment do
end end
end end
context 'when deployment is blocked' do
let(:deployment) { create(:deployment, :created) }
it 'has correct status' do
deployment.block!
expect(deployment).to be_blocked
expect(deployment.finished_at).to be_nil
end
it 'does not execute Deployments::LinkMergeRequestWorker asynchronously' do
expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
deployment.block!
end
it 'does not execute Deployments::HooksWorker' do
expect(Deployments::HooksWorker).not_to receive(:perform_async)
deployment.block!
end
end
describe 'synching status to Jira' do describe 'synching status to Jira' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
...@@ -463,11 +486,12 @@ RSpec.describe Deployment do ...@@ -463,11 +486,12 @@ RSpec.describe Deployment do
subject { described_class.active } subject { described_class.active }
it 'retrieves the active deployments' do it 'retrieves the active deployments' do
deployment1 = create(:deployment, status: :created ) deployment1 = create(:deployment, status: :created)
deployment2 = create(:deployment, status: :running ) deployment2 = create(:deployment, status: :running)
create(:deployment, status: :failed ) create(:deployment, status: :failed)
create(:deployment, status: :canceled ) create(:deployment, status: :canceled)
create(:deployment, status: :skipped) create(:deployment, status: :skipped)
create(:deployment, status: :blocked)
is_expected.to contain_exactly(deployment1, deployment2) is_expected.to contain_exactly(deployment1, deployment2)
end end
...@@ -527,9 +551,25 @@ RSpec.describe Deployment do ...@@ -527,9 +551,25 @@ RSpec.describe Deployment do
deployment2 = create(:deployment, status: :success) deployment2 = create(:deployment, status: :success)
deployment3 = create(:deployment, status: :failed) deployment3 = create(:deployment, status: :failed)
deployment4 = create(:deployment, status: :canceled) deployment4 = create(:deployment, status: :canceled)
deployment5 = create(:deployment, status: :blocked)
create(:deployment, status: :skipped)
is_expected.to contain_exactly(deployment1, deployment2, deployment3, deployment4, deployment5)
end
end
describe 'upcoming' do
subject { described_class.upcoming }
it 'retrieves the upcoming deployments' do
deployment1 = create(:deployment, status: :running)
deployment2 = create(:deployment, status: :blocked)
create(:deployment, status: :success)
create(:deployment, status: :failed)
create(:deployment, status: :canceled)
create(:deployment, status: :skipped) create(:deployment, status: :skipped)
is_expected.to contain_exactly(deployment1, deployment2, deployment3, deployment4) is_expected.to contain_exactly(deployment1, deployment2)
end end
end end
end end
...@@ -855,6 +895,27 @@ RSpec.describe Deployment do ...@@ -855,6 +895,27 @@ RSpec.describe Deployment do
expect(deploy.update_status('created')).to eq(false) expect(deploy.update_status('created')).to eq(false)
end end
context 'mapping status to event' do
using RSpec::Parameterized::TableSyntax
where(:status, :method) do
'running' | :run!
'success' | :succeed!
'failed' | :drop!
'canceled' | :cancel!
'skipped' | :skip!
'blocked' | :block!
end
with_them do
it 'calls the correct method for the given status' do
expect(deploy).to receive(method)
deploy.update_status(status)
end
end
end
end end
describe '#sync_status_with' do describe '#sync_status_with' do
......
...@@ -947,6 +947,12 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do ...@@ -947,6 +947,12 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(deployment) } it { is_expected.to eq(deployment) }
end end
context 'when environment has a blocked deployment' do
let!(:deployment) { create(:deployment, :blocked, environment: environment, project: project) }
it { is_expected.to eq(deployment) }
end
end end
describe '#has_terminals?' do describe '#has_terminals?' do
......
...@@ -70,10 +70,13 @@ RSpec.describe Deployments::OlderDeploymentsDropService do ...@@ -70,10 +70,13 @@ RSpec.describe Deployments::OlderDeploymentsDropService do
let(:older_deployment) { create(:deployment, :created, environment: environment, deployable: build) } let(:older_deployment) { create(:deployment, :created, environment: environment, deployable: build) }
let(:build) { create(:ci_build, :manual) } let(:build) { create(:ci_build, :manual) }
it 'does not drop any builds nor track the exception' do it 'drops the older deployment' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception) deployable = older_deployment.deployable
expect(deployable.failed?).to be_falsey
expect { subject }.not_to change { Ci::Build.failed.count } subject
expect(deployable.reload.failed?).to be_truthy
end end
end end
......
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