Commit 9280393e authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 77213ef0 0e08959b
......@@ -717,7 +717,7 @@ GEM
reverse_markdown (~> 1.0)
rugged (>= 0.24, < 2.0)
thor (>= 0.19, < 2.0)
listen (3.2.1)
listen (3.6.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
locale (2.1.3)
......@@ -1198,7 +1198,7 @@ GEM
simplecov-html (~> 0.11)
simplecov-cobertura (1.3.1)
simplecov (~> 0.8)
simplecov-html (0.12.2)
simplecov-html (0.12.3)
sixarm_ruby_unaccent (1.2.0)
slack-messenger (2.3.4)
snowplow-tracker (0.6.1)
......
......@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
isBinary: {
type: Boolean,
required: false,
default: false,
},
activeViewerType: {
type: String,
required: false,
......@@ -81,6 +86,7 @@ export default {
:raw-path="blob.rawPath"
:active-viewer="viewer"
:has-render-error="hasRenderError"
:is-binary="isBinary"
@copy="proxyCopyRequest"
/>
</div>
......
......@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
isBinary: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
downloadUrl() {
......@@ -43,6 +48,9 @@ export default {
getBlobHashTarget() {
return `[data-blob-hash="${this.blobHash}"]`;
},
showCopyButton() {
return !this.hasRenderError && !this.isBinary;
},
},
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
......@@ -52,7 +60,7 @@ export default {
<template>
<gl-button-group data-qa-selector="default_actions_container">
<gl-button
v-if="!hasRenderError"
v-if="showCopyButton"
v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
......@@ -65,6 +73,7 @@ export default {
variant="default"
/>
<gl-button
v-if="!isBinary"
v-gl-tooltip.hover
:aria-label="$options.BTN_RAW_TITLE"
:title="$options.BTN_RAW_TITLE"
......
......@@ -169,6 +169,7 @@ export default {
<blob-header
:blob="blobInfo"
:hide-viewer-switcher="!hasRichViewer || isBinary"
:is-binary="isBinary"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
......
......@@ -78,6 +78,7 @@ class Environment < ApplicationRecord
scope :for_name, -> (name) { where(name: name) }
scope :preload_cluster, -> { preload(last_deployment: :cluster) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
##
# Search environments which have names like the given query.
......
......@@ -11,6 +11,7 @@ class JiraConnectInstallation < ApplicationRecord
validates :client_key, presence: true, uniqueness: true
validates :shared_secret, presence: true
validates :base_url, presence: true, public_url: true
validates :instance_url, public_url: true, allow_blank: true
scope :for_project, -> (project) {
distinct
......
......@@ -256,6 +256,15 @@
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:environments_auto_delete_cron
:worker_name: Environments::AutoDeleteCronWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:environments_auto_stop_cron
:worker_name: Environments::AutoStopCronWorker
:feature_category: :continuous_delivery
......
# frozen_string_literal: true
module Environments
class AutoDeleteCronWorker
include ApplicationWorker
include ::Gitlab::LoopHelpers
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
data_consistency :always
feature_category :continuous_delivery
deduplicate :until_executed, including_scheduled: true
idempotent!
LOOP_TIMEOUT = 45.minutes
LOOP_LIMIT = 1000
BATCH_SIZE = 100
def perform
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
destroy_in_batch
end
end
private
def destroy_in_batch
environments = Environment.auto_deletable(BATCH_SIZE)
return false if environments.empty?
environments.each(&:destroy)
end
end
end
......@@ -470,6 +470,9 @@ production: &base
# Stop expired environments
environments_auto_stop_cron_worker:
cron: "24 * * * *"
# Delete stopped environments
environments_auto_delete_cron_worker:
cron: "34 * * * *"
# Periodically run 'git fsck' on all repositories. If started more than
# once per hour you will have concurrent 'git fsck' jobs.
repository_check_worker:
......
......@@ -444,6 +444,9 @@ Settings.cron_jobs['ci_schedule_delete_objects_worker']['job_class'] = 'Ci::Sche
Settings.cron_jobs['environments_auto_stop_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['environments_auto_stop_cron_worker']['cron'] ||= '24 * * * *'
Settings.cron_jobs['environments_auto_stop_cron_worker']['job_class'] = 'Environments::AutoStopCronWorker'
Settings.cron_jobs['environments_auto_delete_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['environments_auto_delete_cron_worker']['cron'] ||= '34 * * * *'
Settings.cron_jobs['environments_auto_delete_cron_worker']['job_class'] = 'Environments::AutoDeleteCronWorker'
Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *'
Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::DispatchWorker'
......
# frozen_string_literal: true
class AddIsRemovedToEscalationRules < ActiveRecord::Migration[6.1]
def change
add_column :incident_management_escalation_rules, :is_removed, :boolean, null: false, default: false
end
end
# frozen_string_literal: true
class UpdateEscalationRuleFkForPendingAlertEscalations < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
include Gitlab::Database::PartitioningMigrationHelpers
disable_ddl_transaction!
OLD_FOREIGN_KEY_CONSTRAINT = 'fk_rails_057c1e3d87'
# Swap foreign key contrainst from ON DELETE SET NULL to ON DELETE CASCADE
def up
remove_foreign_key_if_exists :incident_management_pending_alert_escalations, :incident_management_escalation_rules, name: OLD_FOREIGN_KEY_CONSTRAINT
add_concurrent_partitioned_foreign_key :incident_management_pending_alert_escalations,
:incident_management_escalation_rules,
column: :rule_id
end
def down
remove_foreign_key_if_exists :incident_management_pending_alert_escalations, :incident_management_escalation_rules, column: :rule_id
add_concurrent_partitioned_foreign_key :incident_management_pending_alert_escalations,
:incident_management_escalation_rules,
column: :rule_id,
on_delete: :nullify,
name: OLD_FOREIGN_KEY_CONSTRAINT
end
end
# frozen_string_literal: true
class RemoveScheduleAndStatusNullConstraintsFromPendingEscalationsAlert < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
# In preparation of removal of these columns in 14.3.
def up
with_lock_retries do
change_column_null :incident_management_pending_alert_escalations, :status, true
change_column_null :incident_management_pending_alert_escalations, :schedule_id, true
end
end
def down
backfill_from_rules_and_disallow_column_null :status, value: :status
backfill_from_rules_and_disallow_column_null :schedule_id, value: :oncall_schedule_id
end
private
def backfill_from_rules_and_disallow_column_null(column, value:)
with_lock_retries do
execute <<~SQL
UPDATE incident_management_pending_alert_escalations AS escalations
SET #{column} = rules.#{value}
FROM incident_management_escalation_rules AS rules
WHERE rule_id = rules.id
AND escalations.#{column} IS NULL
SQL
change_column_null :incident_management_pending_alert_escalations, column, false
end
end
end
# frozen_string_literal: true
class CreateIndexOnEnvironmentsAutoDeleteAt < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_environments_on_state_and_auto_delete_at'
def up
add_concurrent_index :environments,
%i[auto_delete_at],
where: "auto_delete_at IS NOT NULL AND state = 'stopped'",
name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :environments, INDEX_NAME
end
end
# frozen_string_literal: true
class AddNonNullConstraintForEscalationRuleOnPendingAlertEscalations < ActiveRecord::Migration[6.1]
ELAPSED_WHOLE_MINUTES_IN_SECONDS = <<~SQL
ABS(ROUND(
EXTRACT(EPOCH FROM (escalations.process_at - escalations.created_at))/60*60
))
SQL
INSERT_RULES_FOR_ESCALATIONS_WITHOUT_RULES = <<~SQL
INSERT INTO incident_management_escalation_rules (policy_id, oncall_schedule_id, status, elapsed_time_seconds, is_removed)
SELECT
policies.id,
schedule_id,
status,
#{ELAPSED_WHOLE_MINUTES_IN_SECONDS} AS elapsed_time_seconds,
TRUE
FROM incident_management_pending_alert_escalations AS escalations
INNER JOIN incident_management_oncall_schedules AS schedules ON schedules.id = schedule_id
INNER JOIN incident_management_escalation_policies AS policies ON policies.project_id = schedules.project_id
WHERE rule_id IS NULL
GROUP BY policies.id, schedule_id, status, elapsed_time_seconds
ON CONFLICT DO NOTHING;
SQL
UPDATE_EMPTY_RULE_IDS = <<~SQL
UPDATE incident_management_pending_alert_escalations AS escalations
SET rule_id = rules.id
FROM incident_management_pending_alert_escalations AS through_escalations
INNER JOIN incident_management_oncall_schedules AS schedules ON schedules.id = through_escalations.schedule_id
INNER JOIN incident_management_escalation_policies AS policies ON policies.project_id = schedules.project_id
INNER JOIN incident_management_escalation_rules AS rules ON rules.policy_id = policies.id
WHERE escalations.rule_id IS NULL
AND rules.status = escalations.status
AND rules.oncall_schedule_id = escalations.schedule_id
AND rules.elapsed_time_seconds = #{ELAPSED_WHOLE_MINUTES_IN_SECONDS};
SQL
DELETE_LEFTOVER_ESCALATIONS_WITHOUT_RULES = 'DELETE FROM incident_management_pending_alert_escalations WHERE rule_id IS NULL;'
# For each alert which has a pending escalation without a corresponding rule,
# create a rule with the expected attributes for the project's policy.
#
# Deletes all escalations without rules/policy & adds non-null constraint for rule_id.
def up
exec_query INSERT_RULES_FOR_ESCALATIONS_WITHOUT_RULES
exec_query UPDATE_EMPTY_RULE_IDS
exec_query DELETE_LEFTOVER_ESCALATIONS_WITHOUT_RULES
change_column_null :incident_management_pending_alert_escalations, :rule_id, false
end
def down
change_column_null :incident_management_pending_alert_escalations, :rule_id, true
end
end
ac95292b2ab05f17ed13cb8e95ace0660e6dc82e33d6ef1cccd02890abf6c739
\ No newline at end of file
9f3a39b11f250f64e4e6b8623279604c1dba14330f45c26840f6e0b46f7d48a7
\ No newline at end of file
7b20c623b58982ba5d228902c6da6d10245edf3874ece9b02d58e8560d2d5d96
\ No newline at end of file
f16b563bbfa15b97143e82d2a1e78e9d9805d13e02e3a0845369d4ce3204b3cc
\ No newline at end of file
b64ba2a9ee42497aa9f60ca76f4925076cb77e73fd79bb9b10362cd48d11252b
\ No newline at end of file
......@@ -257,13 +257,13 @@ PARTITION BY RANGE (created_at);
CREATE TABLE incident_management_pending_alert_escalations (
id bigint NOT NULL,
rule_id bigint,
rule_id bigint NOT NULL,
alert_id bigint NOT NULL,
schedule_id bigint NOT NULL,
schedule_id bigint,
process_at timestamp with time zone NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
status smallint NOT NULL
status smallint
)
PARTITION BY RANGE (process_at);
......@@ -13975,7 +13975,8 @@ CREATE TABLE incident_management_escalation_rules (
policy_id bigint NOT NULL,
oncall_schedule_id bigint NOT NULL,
status smallint NOT NULL,
elapsed_time_seconds integer NOT NULL
elapsed_time_seconds integer NOT NULL,
is_removed boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE incident_management_escalation_rules_id_seq
......@@ -23676,6 +23677,8 @@ CREATE INDEX index_environments_on_project_id_and_tier ON environments USING btr
CREATE INDEX index_environments_on_project_id_state_environment_type ON environments USING btree (project_id, state, environment_type);
CREATE INDEX index_environments_on_state_and_auto_delete_at ON environments USING btree (auto_delete_at) WHERE ((auto_delete_at IS NOT NULL) AND ((state)::text = 'stopped'::text));
CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING btree (state, auto_stop_at) WHERE ((auto_stop_at IS NOT NULL) AND ((state)::text = 'available'::text));
CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id);
......@@ -26729,9 +26732,6 @@ ALTER TABLE ONLY terraform_state_versions
ALTER TABLE ONLY ci_build_report_results
ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE incident_management_pending_alert_escalations
ADD CONSTRAINT fk_rails_057c1e3d87 FOREIGN KEY (rule_id) REFERENCES incident_management_escalation_rules(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_daily_build_group_report_results
ADD CONSTRAINT fk_rails_0667f7608c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......@@ -28208,6 +28208,9 @@ ALTER TABLE ONLY approval_project_rules_users
ALTER TABLE ONLY insights
ADD CONSTRAINT fk_rails_f36fda3932 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE incident_management_pending_alert_escalations
ADD CONSTRAINT fk_rails_f3d17bc8af FOREIGN KEY (rule_id) REFERENCES incident_management_escalation_rules(id) ON DELETE CASCADE;
ALTER TABLE ONLY board_group_recent_visits
ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
......@@ -25,7 +25,7 @@ module Resolvers
def preloads
{
rules: [:ordered_rules]
rules: [:active_rules]
}
end
end
......
......@@ -23,7 +23,7 @@ module Types
field :rules, [Types::IncidentManagement::EscalationRuleType],
null: true,
description: 'Steps of the escalation policy.',
method: :ordered_rules
method: :active_rules
end
end
end
......@@ -6,7 +6,7 @@ module IncidentManagement
belongs_to :project
has_many :rules, class_name: 'EscalationRule', inverse_of: :policy, foreign_key: 'policy_id', index_errors: true
has_many :ordered_rules, -> { order(:elapsed_time_seconds, :status) }, class_name: 'EscalationRule', inverse_of: :policy, foreign_key: 'policy_id'
has_many :active_rules, -> { not_removed.order(:elapsed_time_seconds, :status) }, class_name: 'EscalationRule', inverse_of: :policy, foreign_key: 'policy_id'
validates :project_id, uniqueness: { message: _('can only have one escalation policy') }, on: :create
validates :name, presence: true, uniqueness: { scope: [:project_id] }, length: { maximum: 72 }
......@@ -14,7 +14,5 @@ module IncidentManagement
validates :rules, presence: true
accepts_nested_attributes_for :rules
scope :with_rules, -> { includes(:rules) }
end
end
......@@ -16,5 +16,8 @@ module IncidentManagement
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 24.hours }
validates :policy_id, uniqueness: { scope: [:oncall_schedule_id, :status, :elapsed_time_seconds], message: _('must have a unique schedule, status, and elapsed time') }
scope :not_removed, -> { where(is_removed: false) }
scope :removed, -> { where(is_removed: true) }
end
end
......@@ -5,6 +5,9 @@ module IncidentManagement
class Alert < ApplicationRecord
include PartitionedTable
include EachBatch
include IgnorableColumns
ignore_columns :schedule_id, :status, remove_with: '14.4', remove_after: '2021-09-22'
alias_attribute :target, :alert
......@@ -15,20 +18,15 @@ module IncidentManagement
partitioned_by :process_at, strategy: :monthly
belongs_to :oncall_schedule, class_name: 'OncallSchedule', foreign_key: 'schedule_id'
belongs_to :alert, class_name: 'AlertManagement::Alert', foreign_key: 'alert_id', inverse_of: :pending_escalations
belongs_to :rule, class_name: 'EscalationRule', foreign_key: 'rule_id', optional: true
belongs_to :rule, class_name: 'EscalationRule', foreign_key: 'rule_id'
scope :processable, -> { where(process_at: ESCALATION_BUFFER.ago..Time.current) }
enum status: AlertManagement::Alert::STATUSES.slice(:acknowledged, :resolved)
validates :process_at, presence: true
validates :status, presence: true
validates :rule_id, presence: true, uniqueness: { scope: [:alert_id] }
delegate :project, to: :alert
delegate :policy, to: :rule, allow_nil: true
end
end
end
......@@ -112,8 +112,8 @@ module EE
issuables_service(noteable, project, author).publish_issue_to_status_page
end
def notify_via_escalation(noteable, project, recipients, escalation_policy, oncall_schedule)
escalations_service(noteable, project).notify_via_escalation(recipients, escalation_policy: escalation_policy, oncall_schedule: oncall_schedule)
def notify_via_escalation(noteable, project, recipients, escalation_policy)
escalations_service(noteable, project).notify_via_escalation(recipients, escalation_policy: escalation_policy)
end
private
......
......@@ -46,36 +46,45 @@ module IncidentManagement
params[:rules_attributes] && params[:rules_attributes].empty?
end
# Limits rules_attributes to only new records & prepares
# to delete existing rules which are no longer needed
# when the policy is saved.
#
# Context: Rules are managed via `accepts_nested_attributes_for`
# on the IncidentManagement::EscalationPolicy.
# `accepts_nested_attributes_for` requires explicit
# removal of records, so we'll limit `rules_attributes`
# to new records, then rely on `autosave` to actually
# destroy the unwanted rules after marking them for
# deletion.
# Replaces rules params with records for existing rules,
# creates records for new rules, and marks appropriate
# rule records for removal. Records are not actually
# deleted as there may be pending escalations for the rule.
def reconcile_rules!
return unless rules_attributes = params.delete(:rules_attributes)
return unless expected_rules_by_uniq_id.present?
params[:rules_attributes] = remove_obsolete_rules(rules_attributes).to_a
update_existing_rules!
params[:rules] = expected_rules_by_uniq_id.merge(existing_rules_by_uniq_id).values
end
# Prepares existing rules to be removed or un-removed
# based on whether they're included in the input params
def update_existing_rules!
existing_rules_by_uniq_id.each do |uniq_id, rule|
rule.is_removed = !expected_rules_by_uniq_id.key?(uniq_id)
end
end
def remove_obsolete_rules(rules_attrs)
expected_rules = rules_attrs.to_set { |attrs| normalize(::IncidentManagement::EscalationRule.new(**attrs)) }
# @return [Hash<Array, IncidentManagement::EscalationRule>]
def existing_rules_by_uniq_id
strong_memoize(:existing_rules_by_uniq_id) do
escalation_policy.rules.index_by { |rule| unique_id(rule) }
end
end
escalation_policy.rules.each_with_object(expected_rules) do |existing_rule, new_rules|
# Exclude an expected rule which already corresponds to a persisted record - it's a no-op.
next if new_rules.delete?(normalize(existing_rule))
# @return [Hash<Array, IncidentManagement::EscalationRule>]
def expected_rules_by_uniq_id
strong_memoize(:expected_rules_by_uniq_id) do
params.delete(:rules_attributes).to_h do |attrs|
rule = ::IncidentManagement::EscalationRule.new(**attrs)
# Destroy a persisted record, since we don't expect this rule to be on the policy anymore.
existing_rule.mark_for_destruction
[unique_id(rule), rule]
end
end
end
def normalize(rule)
def unique_id(rule)
rule.slice(:oncall_schedule_id, :elapsed_time_seconds, :status)
end
end
......
......@@ -12,20 +12,16 @@ module IncidentManagement
def execute
return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project) && !target.resolved?
policy = escalation_policies.first
policy = project.incident_management_escalation_policies.first
return unless policy
create_escalations(policy.rules)
create_escalations(policy.active_rules)
end
private
attr_reader :target, :project, :escalation, :process_time
def escalation_policies
project.incident_management_escalation_policies.with_rules
end
attr_reader :target, :project, :process_time
def create_escalations(rules)
escalation_ids = rules.map do |rule|
......@@ -40,8 +36,6 @@ module IncidentManagement
IncidentManagement::PendingEscalations::Alert.create!(
target: target,
rule: rule,
schedule_id: rule.oncall_schedule_id,
status: rule.status,
process_at: rule.elapsed_time_seconds.seconds.after(process_time)
)
end
......
......@@ -8,7 +8,7 @@ module IncidentManagement
def initialize(escalation)
@escalation = escalation
@project = escalation.project
@oncall_schedule = escalation.oncall_schedule
@rule = escalation.rule
@target = escalation.target
end
......@@ -16,6 +16,7 @@ module IncidentManagement
return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project)
return if too_early_to_process?
return if target_already_resolved?
return unless rule # Remove in %14.3; Rule might be unavailable after deploy, but before post-migrations complete.
return if target_status_exceeded_rule?
notify_recipients
......@@ -25,7 +26,7 @@ module IncidentManagement
private
attr_reader :escalation, :project, :target, :oncall_schedule
attr_reader :escalation, :project, :target, :rule
def target_already_resolved?
return false unless target.resolved?
......@@ -34,7 +35,7 @@ module IncidentManagement
end
def target_status_exceeded_rule?
target.status >= escalation.status_before_type_cast
target.status >= rule.status_before_type_cast
end
def too_early_to_process?
......@@ -49,12 +50,12 @@ module IncidentManagement
end
def create_system_notes
SystemNoteService.notify_via_escalation(target, project, oncall_notification_recipients, escalation.policy, oncall_schedule)
SystemNoteService.notify_via_escalation(target, project, oncall_notification_recipients, rule.policy)
end
def oncall_notification_recipients
strong_memoize(:oncall_notification_recipients) do
::IncidentManagement::OncallUsersFinder.new(project, schedule: oncall_schedule).execute.to_a
::IncidentManagement::OncallUsersFinder.new(project, schedule: rule.oncall_schedule).execute.to_a
end
end
......
......@@ -8,12 +8,8 @@ module SystemNotes
@author = User.alert_bot
end
def notify_via_escalation(recipients, escalation_policy: nil, oncall_schedule: nil)
body = if escalation_policy
"notified #{recipients.map(&:to_reference).to_sentence} of this alert via escalation policy **#{escalation_policy.name}**"
else
"notified #{recipients.map(&:to_reference).to_sentence} of this alert via schedule **#{oncall_schedule.name}**, per an escalation rule which no longer exists"
end
def notify_via_escalation(recipients, escalation_policy:)
body = "notified #{recipients.map(&:to_reference).to_sentence} of this alert via escalation policy **#{escalation_policy.name}**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'new_alert_added'))
end
......
......@@ -2,13 +2,18 @@
FactoryBot.define do
factory :incident_management_escalation_rule, class: 'IncidentManagement::EscalationRule' do
association :policy, factory: :incident_management_escalation_policy
policy { association :incident_management_escalation_policy }
oncall_schedule { association :incident_management_oncall_schedule, project: policy.project }
status { IncidentManagement::EscalationRule.statuses[:acknowledged] }
elapsed_time_seconds { 5.minutes }
is_removed { false }
trait :resolved do
status { IncidentManagement::EscalationRule.statuses[:resolved] }
end
trait :removed do
is_removed { true }
end
end
end
......@@ -3,14 +3,12 @@
FactoryBot.define do
factory :incident_management_pending_alert_escalation, class: 'IncidentManagement::PendingEscalations::Alert' do
transient do
project { create(:project) } # rubocop:disable FactoryBot/InlineAssociation
policy { create(:incident_management_escalation_policy, project: project) } # rubocop:disable FactoryBot/InlineAssociation
project { association :project }
policy { association :incident_management_escalation_policy, project: project }
end
rule { association :incident_management_escalation_rule, policy: policy }
oncall_schedule { association :incident_management_oncall_schedule, project: project }
alert { association :alert_management_alert, project: project }
status { IncidentManagement::EscalationRule.statuses[:acknowledged] }
alert { association :alert_management_alert, project: rule.policy.project }
process_at { 5.minutes.from_now }
end
end
......@@ -47,7 +47,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Update do
expect(resolve[:escalation_policy]).to have_attributes(escalation_policy.reload.attributes)
expect(escalation_policy).to have_attributes(args.slice(:name, :description))
expect(escalation_policy.rules).to match_array(expected_rules)
expect(escalation_policy.active_rules).to match_array(expected_rules)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddNonNullConstraintForEscalationRuleOnPendingAlertEscalations, :migration, schema: 20210721174453 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:schedules) { table(:incident_management_oncall_schedules) }
let(:policies) { table(:incident_management_escalation_policies) }
let(:rules) { table(:incident_management_escalation_rules) }
let(:alerts) { table(:alert_management_alerts) }
let(:escalations) { partitioned_table(:incident_management_pending_alert_escalations, by: :process_at) }
let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
let!(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
let!(:schedule) { schedules.create!(name: 'Schedule', iid: 1, project_id: project.id) }
let!(:policy) { policies.create!(name: 'Policy', project_id: project.id) }
let!(:rule) { rules.create!(oncall_schedule_id: schedule.id, policy_id: policy.id, status: 2, elapsed_time_seconds: 5.minutes) }
let!(:alert) { alerts.create!(iid: 1, project_id: project.id, title: 'Alert 1', started_at: Time.current) }
let!(:other_alert) { alerts.create!(iid: 2, project_id: project.id, title: 'Alert 2', started_at: Time.current) }
let!(:other_schedule) { schedules.create!(name: 'Other Schedule', iid: 2, project_id: project.id) }
let!(:other_project) { projects.create!(name: 'project2', path: 'project2', namespace_id: namespace.id) }
let!(:other_project_policy) { policies.create!(name: 'Other Policy', project_id: other_project.id) }
let!(:other_project_schedule) { schedules.create!(name: 'Other Schedule', iid: 1, project_id: other_project.id) }
let!(:other_project_alert) { alerts.create!(iid: 1, project_id: other_project.id, title: 'Other Project Alert', started_at: Time.current) }
let!(:policyless_project) { projects.create!(name: 'project2', path: 'project2', namespace_id: namespace.id) }
let!(:policyless_project_schedule) { schedules.create!(name: 'Schedule for no policy', iid: 1, project_id: policyless_project.id) }
let!(:policyless_project_alert) { alerts.create!(iid: 1, project_id: policyless_project.id, title: 'Alert with no policy', started_at: Time.current) }
it 'creates rules for each escalation and removes escalations without policies', :aggregate_failures do
# Should all point to the same new rule
escalation = create_escalation
matching_escalation = create_escalation
other_alert_escalation = create_escalation(alert_id: other_alert.id)
escalations_with_same_new_rule = [escalation, matching_escalation, other_alert_escalation]
# Should each point to a different new rule
other_status_escalation = create_escalation(status: 2)
other_elapsed_time_escalation = create_escalation(process_at: 1.minute.from_now)
other_schedule_escalation = create_escalation(schedule_id: other_schedule.id)
other_project_escalation = create_escalation(schedule_id: other_project_schedule.id, alert_id: other_project_alert.id)
escalations_with_unique_new_rules = [other_status_escalation, other_elapsed_time_escalation, other_schedule_escalation, other_project_escalation]
# Should be deleted
policyless_escalation = create_escalation(schedule_id: policyless_project_schedule.id, alert_id: policyless_project_alert.id)
# Should all point to the existing rule
existing_rule_escalation = create_escalation(rule_id: rule.id)
existing_rule_without_schedule_escalation = create_escalation(rule_id: rule.id, status: nil, schedule_id: nil)
existing_rule_escalation_without_rule_id = create_escalation(status: rule.status, process_at: rule.elapsed_time_seconds.seconds.after(Time.current))
escalations_with_existing_rule = [existing_rule_escalation, existing_rule_without_schedule_escalation, existing_rule_escalation_without_rule_id]
# Assert all escalations were updated with a rule id or deleted
expect { migrate! }
.to change(rules, :count).by(5)
.and change(escalations, :count).by(-1)
expect(escalations.pluck(:rule_id)).to all( be_present )
escalations_with_same_new_rule.each(&:reload)
escalations_with_unique_new_rules.each(&:reload)
escalations_with_existing_rule.each(&:reload)
expect { policyless_escalation.reload }.to raise_error(ActiveRecord::RecordNotFound)
# Assert rules are associated with the correct escalations
expect(escalations_with_existing_rule.map(&:rule_id)).to all( eq(rule.id) )
expect(rules.where(is_removed: false)).to contain_exactly(rule)
expect(escalations_with_same_new_rule.map(&:rule_id).uniq.length).to eq(1)
rule_ids_for_escalations_with_unique_new_rules = escalations_with_unique_new_rules.map(&:rule_id)
expect(rule_ids_for_escalations_with_unique_new_rules.uniq.length).to eq(rule_ids_for_escalations_with_unique_new_rules.length)
expect(rule_ids_for_escalations_with_unique_new_rules).not_to include(escalation.rule_id, rule.id)
# Assert new rules have the correct attributes
escalations.where.not(rule_id: rule.id).each do |esc|
rule = rules.find(esc.rule_id)
expect(esc).to have_attributes(
status: rule.status,
schedule_id: rule.oncall_schedule_id,
process_at: be_within(5.seconds).of(rule.elapsed_time_seconds.seconds.after(esc.created_at))
)
end
# Assert all rules are associated with the correct projects
project_rule_ids = rules.where(policy_id: policy.id).ids
expected_rule_ids_for_project = escalations.where.not(id: other_project_escalation.id).pluck(:rule_id)
expect(project_rule_ids - expected_rule_ids_for_project).to be_empty
other_project_rule_ids = rules.where(policy_id: other_project_policy.id).ids
expect(other_project_rule_ids).to contain_exactly(other_project_escalation.rule_id)
# Assert non-null constraint was effectively applied
expect(escalations.where(rule_id: nil)).to be_empty
expect { create_escalation(rule_id: nil) }.to raise_error(ActiveRecord::NotNullViolation)
end
context 'without escalations' do
it 'adds a non-null constraint for rule_id' do
migrate!
expect(escalations.where(rule_id: nil)).to be_empty
expect { create_escalation(rule_id: nil) }.to raise_error(ActiveRecord::NotNullViolation)
end
end
private
def create_escalation(options = {})
escalations.create!(
schedule_id: schedule.id,
status: 1,
alert_id: alert.id,
process_at: Time.current,
**options
)
end
end
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RemoveScheduleAndStatusNullConstraintsFromPendingEscalationsAlert, :migration, schema: 20210721174441 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:schedules) { table(:incident_management_oncall_schedules) }
let(:policies) { table(:incident_management_escalation_policies) }
let(:rules) { table(:incident_management_escalation_rules) }
let(:alerts) { table(:alert_management_alerts) }
let(:escalations) { partitioned_table(:incident_management_pending_alert_escalations, by: :process_at) }
let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
let!(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
let!(:schedule) { schedules.create!(name: 'Schedule', iid: 1, project_id: project.id) }
let!(:policy) { policies.create!(name: 'Policy', project_id: project.id) }
let!(:rule) { rules.create!(oncall_schedule_id: schedule.id, policy_id: policy.id, status: 2, elapsed_time_seconds: 5.minutes) }
let!(:alert) { alerts.create!(iid: 1, project_id: project.id, title: 'Alert 1', started_at: Time.current) }
let!(:other_schedule) { schedules.create!(name: 'Other Schedule', iid: 2, project_id: project.id) }
let!(:other_rule) { rules.create!(oncall_schedule_id: other_schedule.id, policy_id: policy.id, status: 2, elapsed_time_seconds: 5.minutes) }
let!(:other_alert) { alerts.create!(iid: 2, project_id: project.id, title: 'Alert 2', started_at: Time.current) }
let(:rule_only_escalation) { create_escalation }
let(:duplicate_escalation) { create_escalation }
let(:other_rule_escalation) { create_escalation(rule_id: other_rule.id) }
let(:other_alert_escalation) { create_escalation(alert_id: other_alert.id) }
let(:all_escalations) { [rule_only_escalation, duplicate_escalation, other_rule_escalation, other_alert_escalation] }
it 'reversibly removes the non-null constraint for schedule_id and status', :aggregate_failures do
reversible_migration do |migration|
migration.before -> {
expect { create_escalation(status: rule.status, schedule_id: rule.oncall_schedule_id) }.not_to raise_error
expect { create_escalation }.to raise_error(ActiveRecord::NotNullViolation)
escalations.all.each do |escalation|
rule = rules.find(escalation.rule_id)
expect(escalation).to have_attributes(schedule_id: rule.oncall_schedule_id, status: rule.status)
end
}
migration.after -> {
expect { all_escalations }.not_to raise_error
}
end
end
context 'without escalations' do
it 'removes the non-null constraint for schedule_id and status' do
migrate!
expect { create_escalation(status: 1) }.not_to raise_error
expect { create_escalation(schedule_id: schedule.id) }.not_to raise_error
end
end
private
def create_escalation(options = {})
escalations.create!(
rule_id: rule.id,
alert_id: alert.id,
process_at: Time.current,
**options
)
end
end
......@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe IncidentManagement::EscalationPolicy do
let_it_be(:project) { create(:project) }
subject { build(:incident_management_escalation_policy) }
it { is_expected.to be_valid }
......@@ -12,7 +10,17 @@ RSpec.describe IncidentManagement::EscalationPolicy do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:rules) }
it { is_expected.to have_many(:ordered_rules).order(elapsed_time_seconds: :asc, status: :asc) }
it { is_expected.to have_many(:active_rules).order(elapsed_time_seconds: :asc, status: :asc).class_name('EscalationRule').inverse_of(:policy) }
describe '.active_rules' do
let(:policy) { create(:incident_management_escalation_policy) }
let(:rule) { policy.rules.first }
let(:removed_rule) { create(:incident_management_escalation_rule, :removed, policy: policy) }
subject { policy.reload.active_rules }
it { is_expected.to contain_exactly(rule) }
end
end
describe 'validations' do
......
......@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe IncidentManagement::EscalationRule do
let_it_be(:policy) { create(:incident_management_escalation_policy) }
let_it_be(:schedule) { create(:incident_management_oncall_schedule, project: policy.project) }
subject { build(:incident_management_escalation_rule, policy: policy) }
......@@ -21,4 +20,21 @@ RSpec.describe IncidentManagement::EscalationRule do
it { is_expected.to validate_numericality_of(:elapsed_time_seconds).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(24.hours) }
it { is_expected.to validate_uniqueness_of(:policy_id).scoped_to([:oncall_schedule_id, :status, :elapsed_time_seconds] ).with_message('must have a unique schedule, status, and elapsed time') }
end
describe 'scopes' do
let_it_be(:rule) { policy.rules.first }
let_it_be(:removed_rule) { create(:incident_management_escalation_rule, :removed, policy: policy) }
describe '.not_removed' do
subject { described_class.not_removed }
it { is_expected.to contain_exactly(rule) }
end
describe '.removed' do
subject { described_class.removed }
it { is_expected.to contain_exactly(removed_rule) }
end
end
end
......@@ -9,14 +9,11 @@ RSpec.describe IncidentManagement::PendingEscalations::Alert do
describe 'validations' do
it { is_expected.to validate_presence_of(:process_at) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to delegate_method(:project).to(:alert) }
it { is_expected.to delegate_method(:policy).to(:rule).allow_nil }
it { is_expected.to validate_uniqueness_of(:rule_id).scoped_to([:alert_id]) }
end
describe 'associations' do
it { is_expected.to belong_to(:oncall_schedule) }
it { is_expected.to belong_to(:alert) }
it { is_expected.to belong_to(:rule) }
end
......
......@@ -74,7 +74,7 @@ RSpec.describe 'Updating an escalation policy' do
expect(escalation_policy.reload).to have_attributes(
name: variables[:name],
description: variables[:description],
rules: [
active_rules: [
have_attributes(
oncall_schedule: schedule,
status: rule_variables[:status].downcase,
......
......@@ -38,6 +38,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::UpdateService do
end
let(:new_rule) { have_attributes(**new_rule_params.except(:status), status: 'acknowledged') }
let(:removed_rules) { [] }
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
......@@ -62,7 +63,8 @@ RSpec.describe IncidentManagement::EscalationPolicies::UpdateService do
expect(execute.payload).to eq(escalation_policy: escalation_policy.reload)
expect(escalation_policy).to have_attributes(params.slice(:name, :description))
expect(escalation_policy.rules).to match_array(expected_rules)
expect(escalation_policy.active_rules).to match_array(expected_rules)
expect(escalation_policy.rules.removed).to match_array(removed_rules)
end
end
......@@ -97,6 +99,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::UpdateService do
context 'when all old rules are replaced' do
let(:rule_params) { [new_rule_params] }
let(:expected_rules) { [new_rule] }
let(:removed_rules) { escalation_rules }
it_behaves_like 'successful update with no errors'
end
......@@ -104,6 +107,15 @@ RSpec.describe IncidentManagement::EscalationPolicies::UpdateService do
context 'when some rules are preserved, added, and deleted' do
let(:rule_params) { [existing_rules_params.first, new_rule_params] }
let(:expected_rules) { [escalation_rules.first, new_rule] }
let(:removed_rules) { [escalation_rules.last] }
it_behaves_like 'successful update with no errors'
end
context 'when rules are only deleted' do
let(:rule_params) { [existing_rules_params.first] }
let(:expected_rules) { [escalation_rules.first] }
let(:removed_rules) { [escalation_rules.last] }
it_behaves_like 'successful update with no errors'
end
......
......@@ -8,6 +8,7 @@ RSpec.describe IncidentManagement::PendingEscalations::CreateService do
let_it_be(:rule_count) { 2 }
let!(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rule_count: rule_count) }
let!(:removed_rule) { create(:incident_management_escalation_rule, :removed, policy: escalation_policy) }
let(:rules) { escalation_policy.rules }
let(:service) { described_class.new(target) }
......@@ -46,6 +47,7 @@ RSpec.describe IncidentManagement::PendingEscalations::CreateService do
context 'when there is no escalation policy for the project' do
let!(:escalation_policy) { nil }
let!(:removed_rule) { nil }
it 'does nothing' do
expect { execute }.not_to change { IncidentManagement::PendingEscalations::Alert.count }
......@@ -64,8 +66,6 @@ RSpec.describe IncidentManagement::PendingEscalations::CreateService do
expect(escalation).to have_attributes(
rule_id: rule.id,
alert_id: target.id,
schedule_id: rule.oncall_schedule_id,
status: rule.status,
process_at: be_within(1.minute).of(rule.elapsed_time_seconds.seconds.after(execution_time))
)
end
......
......@@ -15,7 +15,7 @@ RSpec.describe IncidentManagement::PendingEscalations::ProcessService do
let(:target) { alert }
let(:process_at) { 5.minutes.ago }
let(:escalation) { create(:incident_management_pending_alert_escalation, rule: escalation_rule, oncall_schedule: schedule_1, target: target, status: IncidentManagement::EscalationRule.statuses[:acknowledged], process_at: process_at) }
let(:escalation) { create(:incident_management_pending_alert_escalation, rule: escalation_rule, alert: target, process_at: process_at) }
let(:service) { described_class.new(escalation) }
......@@ -31,7 +31,7 @@ RSpec.describe IncidentManagement::PendingEscalations::ProcessService do
it 'does not delete the escalation' do
subject
expect { escalation.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
expect { escalation.reload }.not_to raise_error
end
end
......@@ -50,7 +50,7 @@ RSpec.describe IncidentManagement::PendingEscalations::ProcessService do
it 'creates a system note' do
expect(SystemNoteService)
.to receive(:notify_via_escalation).with(alert, project, [a_kind_of(User)], escalation_policy, schedule_1)
.to receive(:notify_via_escalation).with(alert, project, [a_kind_of(User)], escalation_policy)
.and_call_original
expect { execute }.to change(Note, :count).by(1)
......
......@@ -9,10 +9,9 @@ RSpec.describe SystemNotes::EscalationsService do
let_it_be(:author) { User.alert_bot }
describe '#notify_via_escalation' do
subject { described_class.new(noteable: noteable, project: project).notify_via_escalation([user, user_2], escalation_policy: escalation_policy, oncall_schedule: oncall_schedule) }
subject { described_class.new(noteable: noteable, project: project).notify_via_escalation([user, user_2], escalation_policy: escalation_policy) }
let_it_be(:escalation_policy) { create(:incident_management_escalation_policy, project: project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be(:noteable) { create(:alert_management_alert, project: project) }
it_behaves_like 'a system note' do
......@@ -22,13 +21,5 @@ RSpec.describe SystemNotes::EscalationsService do
it 'posts the correct text to the system note' do
expect(subject.note).to match("notified #{user.to_reference} and #{user_2.to_reference} of this alert via escalation policy **#{escalation_policy.name}**")
end
context 'when policy is missing' do
let_it_be(:escalation_policy) { nil }
it 'posts the correct text to the system note' do
expect(subject.note).to match("notified #{user.to_reference} and #{user_2.to_reference} of this alert via schedule **#{oncall_schedule.name}**, per an escalation rule which no longer exists")
end
end
end
end
......@@ -75,6 +75,11 @@ FactoryBot.define do
auto_stop_at { 1.day.ago }
end
trait :auto_deletable do
state { :stopped }
auto_delete_at { 1.day.ago }
end
trait :will_auto_stop do
auto_stop_at { 1.day.from_now }
end
......
......@@ -39,6 +39,9 @@ describe('Blob Header Default Actions', () => {
});
describe('renders', () => {
const findCopyButton = () => wrapper.find('[data-testid="copyContentsButton"]');
const findViewRawButton = () => wrapper.find('[data-testid="viewRawButton"]');
it('gl-button-group component', () => {
expect(btnGroup.exists()).toBe(true);
});
......@@ -76,7 +79,14 @@ describe('Blob Header Default Actions', () => {
hasRenderError: true,
});
expect(wrapper.find('[data-testid="copyContentsButton"]').exists()).toBe(false);
expect(findCopyButton().exists()).toBe(false);
});
it('does not render the copy and view raw button if isBinary is set to true', () => {
createComponent({ isBinary: true });
expect(findCopyButton().exists()).toBe(false);
expect(findViewRawButton().exists()).toBe(false);
});
});
});
......@@ -29,6 +29,8 @@ describe('Blob Header Default Actions', () => {
});
describe('rendering', () => {
const findDefaultActions = () => wrapper.find(DefaultActions);
const slots = {
prepend: 'Foo Prepend',
actions: 'Actions Bar',
......@@ -42,7 +44,7 @@ describe('Blob Header Default Actions', () => {
it('renders all components', () => {
createComponent();
expect(wrapper.find(ViewerSwitcher).exists()).toBe(true);
expect(wrapper.find(DefaultActions).exists()).toBe(true);
expect(findDefaultActions().exists()).toBe(true);
expect(wrapper.find(BlobFilepath).exists()).toBe(true);
});
......@@ -100,7 +102,13 @@ describe('Blob Header Default Actions', () => {
hasRenderError: true,
},
);
expect(wrapper.find(DefaultActions).props('hasRenderError')).toBe(true);
expect(findDefaultActions().props('hasRenderError')).toBe(true);
});
it('passes the correct isBinary value to default actions when viewing a binary file', () => {
createComponent({}, {}, { isBinary: true });
expect(findDefaultActions().props('isBinary')).toBe(true);
});
});
......
......@@ -349,6 +349,17 @@ describe('Blob content viewer component', () => {
});
});
it('passes the correct isBinary value to blob header when viewing a binary file', async () => {
fullFactory({
mockData: { blobInfo: richMockData, isBinary: true },
stubs: { BlobContent: true, BlobReplace: true },
});
await nextTick();
expect(findBlobHeader().props('isBinary')).toBe(true);
});
describe('BlobButtonGroup', () => {
const { name, path, replacePath, webPath } = simpleMockData;
const {
......
......@@ -215,6 +215,24 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
describe '.auto_deletable' do
subject { described_class.auto_deletable(limit) }
let(:limit) { 100 }
context 'when environment is auto-deletable' do
let!(:environment) { create(:environment, :auto_deletable) }
it { is_expected.to eq([environment]) }
end
context 'when environment is not auto-deletable' do
let!(:environment) { create(:environment) }
it { is_expected.to be_empty }
end
end
describe '.stop_actions' do
subject { environments.stop_actions }
......
......@@ -15,6 +15,9 @@ RSpec.describe JiraConnectInstallation do
it { is_expected.to allow_value('https://test.atlassian.net').for(:base_url) }
it { is_expected.not_to allow_value('not/a/url').for(:base_url) }
it { is_expected.to allow_value('https://test.atlassian.net').for(:instance_url) }
it { is_expected.not_to allow_value('not/a/url').for(:instance_url) }
end
describe '.for_project' do
......
......@@ -16,6 +16,23 @@ module MigrationsHelpers
end
end
def partitioned_table(name, by: :created_at, strategy: :monthly)
klass = Class.new(active_record_base) do
include PartitionedTable
self.table_name = name
self.primary_key = :id
partitioned_by by, strategy: strategy
def self.name
table_name.singularize.camelcase
end
end
klass.tap { Gitlab::Database::Partitioning::PartitionManager.new.sync_partitions }
end
def migrations_paths
ActiveRecord::Migrator.migrations_paths
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Environments::AutoDeleteCronWorker do
include CreateEnvironmentsHelpers
let(:worker) { described_class.new }
describe '#perform' do
subject { worker.perform }
let_it_be(:project) { create(:project, :repository) }
let!(:environment) { create(:environment, :auto_deletable, project: project) }
it 'deletes the environment' do
expect { subject }.to change { Environment.count }.by(-1)
end
context 'when environment is not stopped' do
let!(:environment) { create(:environment, :available, auto_delete_at: 1.day.ago, project: project) }
it 'does not delete the environment' do
expect { subject }.not_to change { Environment.count }
end
end
context 'when auto_delete_at is null' do
let!(:environment) { create(:environment, :stopped, auto_delete_at: nil, project: project) }
it 'does not delete the environment' do
expect { subject }.not_to change { Environment.count }
end
end
context 'with multiple deletable environments' do
let!(:other_environment) { create(:environment, :auto_deletable, project: project) }
it 'deletes all deletable environments' do
expect { subject }.to change { Environment.count }.by(-2)
end
context 'when loop reached loop limit' do
before do
stub_const("#{described_class}::LOOP_LIMIT", 1)
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'deletes only one deletable environment' do
expect { subject }.to change { Environment.count }.by(-1)
end
end
context 'when batch size is less than the number of environments' do
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'deletes all deletable environments' do
expect { subject }.to change { Environment.count }.by(-2)
end
end
end
context 'with multiple deployments' do
it 'deletes the deployment records and refs' do
deployment_1 = create(:deployment, environment: environment, project: project)
deployment_2 = create(:deployment, environment: environment, project: project)
deployment_1.create_ref
deployment_2.create_ref
expect(project.repository.commit(deployment_1.ref_path)).to be_present
expect(project.repository.commit(deployment_2.ref_path)).to be_present
expect { subject }.to change { Deployment.count }.by(-2)
expect(project.repository.commit(deployment_1.ref_path)).not_to be_present
expect(project.repository.commit(deployment_2.ref_path)).not_to be_present
end
end
context 'when loop reached timeout' do
before do
stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
stub_const("#{described_class}::LOOP_LIMIT", 100_000)
allow_next_instance_of(described_class) do |worker|
allow(worker).to receive(:destroy_in_batch) { true }
end
end
it 'does not delete the environment' do
expect { subject }.not_to change { Environment.count }
end
end
context 'with idempotent flag' do
include_examples 'an idempotent worker' do
it 'deletes the environment' do
expect { subject }.to change { Environment.count }.by(-1)
end
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