Commit 83360bc2 authored by Doug Stull's avatar Doug Stull Committed by Igor Drozdov

Refactor ci minutes email notification

- to incorporate already existing logic
  in recently refactored ci minutes
  area.
parent 7418c7de
...@@ -29,6 +29,18 @@ module Ci ...@@ -29,6 +29,18 @@ module Ci
contextual_map.dig(stage, :style) contextual_map.dig(stage, :style)
end end
def no_remaining_minutes?
stage == :exceeded
end
def running_out?
[:danger, :warning].include?(stage)
end
def stage_percentage
PERCENTAGES[stage]
end
private private
attr_reader :context, :stage attr_reader :context, :stage
......
...@@ -370,6 +370,10 @@ module EE ...@@ -370,6 +370,10 @@ module EE
end end
end end
def owners_emails
owners.pluck(:email)
end
private private
def custom_project_templates_group_allowed def custom_project_templates_group_allowed
......
...@@ -20,8 +20,6 @@ module EE ...@@ -20,8 +20,6 @@ module EE
LICENSE_PLANS_TO_NAMESPACE_PLANS = NAMESPACE_PLANS_TO_LICENSE_PLANS.invert.freeze LICENSE_PLANS_TO_NAMESPACE_PLANS = NAMESPACE_PLANS_TO_LICENSE_PLANS.invert.freeze
PLANS = (NAMESPACE_PLANS_TO_LICENSE_PLANS.keys + [Plan::FREE]).freeze PLANS = (NAMESPACE_PLANS_TO_LICENSE_PLANS.keys + [Plan::FREE]).freeze
CI_USAGE_ALERT_LEVELS = [30, 5].freeze
prepended do prepended do
include EachBatch include EachBatch
...@@ -55,6 +53,8 @@ module EE ...@@ -55,6 +53,8 @@ module EE
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset, delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
:extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true :extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true
delegate :email, to: :owner, allow_nil: true, prefix: true
# Opportunistically clear the +file_template_project_id+ if invalid # Opportunistically clear the +file_template_project_id+ if invalid
before_validation :clear_file_template_project_id before_validation :clear_file_template_project_id
......
...@@ -4,49 +4,55 @@ module Ci ...@@ -4,49 +4,55 @@ module Ci
module Minutes module Minutes
class EmailNotificationService < ::BaseService class EmailNotificationService < ::BaseService
def execute def execute
return unless ::Gitlab.com?
return unless namespace.shared_runners_minutes_limit_enabled? return unless namespace.shared_runners_minutes_limit_enabled?
notify_on_total_usage notify
notify_on_partial_usage
end end
private private
def recipients attr_reader :notification
namespace.user? ? [namespace.owner.email] : namespace.owners.pluck(:email) # rubocop:disable CodeReuse/ActiveRecord
def notify
@notification = ::Ci::Minutes::Notification.new(project, nil)
if notification.no_remaining_minutes?
notify_total_usage
elsif notification.running_out?
notify_partial_usage
end
end end
def notify_on_total_usage def notify_total_usage
return unless namespace.shared_runners_minutes_used? && namespace.last_ci_minutes_notification_at.nil? return if namespace.last_ci_minutes_notification_at
namespace.update_columns(last_ci_minutes_notification_at: Time.current) namespace.update_columns(last_ci_minutes_notification_at: Time.current)
CiMinutesUsageMailer.notify(namespace, recipients).deliver_later CiMinutesUsageMailer.notify(namespace, recipients).deliver_later
end end
def notify_on_partial_usage def notify_partial_usage
return if namespace.shared_runners_minutes_used? return if already_notified_running_out
return if namespace.last_ci_minutes_usage_notification_level == current_alert_level
return if alert_levels.max < namespace.shared_runners_remaining_minutes_percent
namespace.update_columns(last_ci_minutes_usage_notification_level: current_alert_level) namespace.update_columns(last_ci_minutes_usage_notification_level: current_alert_percentage)
CiMinutesUsageMailer.notify_limit(namespace, recipients, current_alert_level).deliver_later CiMinutesUsageMailer.notify_limit(namespace, recipients, current_alert_percentage).deliver_later
end end
def namespace def already_notified_running_out
@namespace ||= project.shared_runners_limit_namespace namespace.last_ci_minutes_usage_notification_level == current_alert_percentage
end end
def alert_levels def recipients
@alert_levels ||= EE::Namespace::CI_USAGE_ALERT_LEVELS.sort namespace.user? ? [namespace.owner_email] : namespace.owners_emails
end end
def current_alert_level def namespace
remaining_percent = namespace.shared_runners_remaining_minutes_percent @namespace ||= project.shared_runners_limit_namespace
end
@current_alert_level ||= alert_levels.find { |level| level >= remaining_percent } def current_alert_percentage
notification.stage_percentage
end end
end end
end end
......
...@@ -6,7 +6,7 @@ module EE ...@@ -6,7 +6,7 @@ module EE
UpdateBuildMinutesService.new(build.project, nil).execute(build) UpdateBuildMinutesService.new(build.project, nil).execute(build)
# We need to use `reset` on `project` because their AR associations have been cached # We need to use `reset` on `project` because their AR associations have been cached
# and `Namespace#namespace_statistics` will return stale data. # and `Namespace#namespace_statistics` will return stale data.
::Ci::Minutes::EmailNotificationService.new(build.project.reset).execute ::Ci::Minutes::EmailNotificationService.new(build.project.reset).execute if ::Gitlab.com?
StoreSecurityScansWorker.perform_async(build.id) StoreSecurityScansWorker.perform_async(build.id)
......
...@@ -41,10 +41,30 @@ RSpec.describe Ci::Minutes::Notification do ...@@ -41,10 +41,30 @@ RSpec.describe Ci::Minutes::Notification do
allow(group).to receive(:shared_runners_remaining_minutes).and_return(4) allow(group).to receive(:shared_runners_remaining_minutes).and_return(4)
end end
it 'has warning notification' do describe '#show?' do
expect(subject.show?(user)).to be_truthy it 'has warning notification' do
expect(subject.text).to match(/.*\shas 30% or less Shared Runner Pipeline minutes remaining/) expect(subject.show?(user)).to be_truthy
expect(subject.style).to eq :warning expect(subject.text).to match(/.*\shas 30% or less Shared Runner Pipeline minutes remaining/)
expect(subject.style).to eq :warning
end
end
describe '#running_out?' do
it 'is running out of minutes' do
expect(subject.running_out?).to be_truthy
end
end
describe '#no_remaining_minutes?' do
it 'has not ran out of minutes' do
expect(subject.no_remaining_minutes?).to be_falsey
end
end
describe '#stage_percentage' do
it 'provides percentage for current alert level' do
expect(subject.stage_percentage).to eq 30
end
end end
end end
...@@ -53,10 +73,30 @@ RSpec.describe Ci::Minutes::Notification do ...@@ -53,10 +73,30 @@ RSpec.describe Ci::Minutes::Notification do
allow(group).to receive(:shared_runners_remaining_minutes).and_return(1) allow(group).to receive(:shared_runners_remaining_minutes).and_return(1)
end end
it 'has danger notification' do describe '#show?' do
expect(subject.show?(user)).to be_truthy it 'has danger notification' do
expect(subject.text).to match(/.*\shas 5% or less Shared Runner Pipeline minutes remaining/) expect(subject.show?(user)).to be_truthy
expect(subject.style).to eq :danger expect(subject.text).to match(/.*\shas 5% or less Shared Runner Pipeline minutes remaining/)
expect(subject.style).to eq :danger
end
end
describe '#running_out?' do
it 'is running out of minutes' do
expect(subject.running_out?).to be_truthy
end
end
describe '#no_remaining_minutes?' do
it 'has not ran out of minutes' do
expect(subject.no_remaining_minutes?).to be_falsey
end
end
describe '#stage_percentage' do
it 'provides percentage for current alert level' do
expect(subject.stage_percentage).to eq 5
end
end end
end end
...@@ -65,20 +105,60 @@ RSpec.describe Ci::Minutes::Notification do ...@@ -65,20 +105,60 @@ RSpec.describe Ci::Minutes::Notification do
allow(group).to receive(:shared_runners_remaining_minutes).and_return(6) allow(group).to receive(:shared_runners_remaining_minutes).and_return(6)
end end
it 'has warning notification' do describe '#show?' do
expect(subject.show?(user)).to be_truthy it 'has warning notification' do
expect(subject.text).to match(/.*\shas 30% or less Shared Runner Pipeline minutes remaining/) expect(subject.show?(user)).to be_truthy
expect(subject.style).to eq :warning expect(subject.text).to match(/.*\shas 30% or less Shared Runner Pipeline minutes remaining/)
expect(subject.style).to eq :warning
end
end
describe '#running_out?' do
it 'is running out of minutes' do
expect(subject.running_out?).to be_truthy
end
end
describe '#no_remaining_minutes?' do
it 'has not ran out of minutes' do
expect(subject.no_remaining_minutes?).to be_falsey
end
end
describe '#stage_percentage' do
it 'provides percentage for current alert level' do
expect(subject.stage_percentage).to eq 30
end
end end
end end
context 'when usage has exceeded the limit' do context 'when usage has exceeded the limit' do
let(:group) { create(:group, :with_used_build_minutes_limit) } let(:group) { create(:group, :with_used_build_minutes_limit) }
it 'has exceeded notification' do describe '#show?' do
expect(subject.show?(user)).to be_truthy it 'has exceeded notification' do
expect(subject.text).to match(/.*\shas exceeded its pipeline minutes quota/) expect(subject.show?(user)).to be_truthy
expect(subject.style).to eq :danger expect(subject.text).to match(/.*\shas exceeded its pipeline minutes quota/)
expect(subject.style).to eq :danger
end
end
describe '#running_out?' do
it 'does not have any minutes left' do
expect(subject.running_out?).to be_falsey
end
end
describe '#no_remaining_minutes?' do
it 'has run out of minutes out of minutes' do
expect(subject.no_remaining_minutes?).to be_truthy
end
end
describe '#stage_percentage' do
it 'provides percentage for current alert level' do
expect(subject.stage_percentage).to eq 0
end
end end
end end
end end
...@@ -91,83 +171,85 @@ RSpec.describe Ci::Minutes::Notification do ...@@ -91,83 +171,85 @@ RSpec.describe Ci::Minutes::Notification do
end end
context 'when not permitted to see notifications' do context 'when not permitted to see notifications' do
it 'has no notifications set' do describe '#show?' do
expect(subject.show?(user)).to be_falsey it 'has no notifications set' do
expect(subject.show?(user)).to be_falsey
end
end end
end end
end end
context 'when at project level' do context 'when at project level' do
describe '#show?' do context 'when eligible to see notifications' do
context 'when eligible to see notifications' do before do
before do group.add_developer(user)
group.add_developer(user) end
end
describe '#show?' do
it_behaves_like 'queries for notifications' do it_behaves_like 'queries for notifications' do
subject do subject do
threshold = described_class.new(injected_project, nil) threshold = described_class.new(injected_project, nil)
threshold.show?(user) threshold.show?(user)
end end
end end
it_behaves_like 'has notifications' do
subject { described_class.new(injected_project, nil) }
end
end end
it_behaves_like 'not eligible to see notifications' do it_behaves_like 'has notifications' do
subject { described_class.new(injected_project, nil) } subject { described_class.new(injected_project, nil) }
end end
end
context 'when user is not authenticated' do it_behaves_like 'not eligible to see notifications' do
let(:user) { nil } subject { described_class.new(injected_project, nil) }
end
it_behaves_like 'not eligible to see notifications' do context 'when user is not authenticated' do
subject { described_class.new(injected_project, nil) } let(:user) { nil }
end
it_behaves_like 'not eligible to see notifications' do
subject { described_class.new(injected_project, nil) }
end end
end end
end end
context 'when at namespace level' do context 'when at namespace level' do
describe '#show?' do context 'when eligible to see notifications' do
context 'when eligible to see notifications' do before do
before do group.add_developer(user)
group.add_developer(user) end
end
context 'with a project that has runners enabled inside namespace' do context 'with a project that has runners enabled inside namespace' do
describe '#show?' do
it_behaves_like 'queries for notifications' do it_behaves_like 'queries for notifications' do
subject do subject do
threshold = described_class.new(nil, injected_group) threshold = described_class.new(nil, injected_group)
threshold.show?(user) threshold.show?(user)
end end
end end
it_behaves_like 'has notifications' do
subject { described_class.new(nil, injected_group) }
end
end end
context 'with no projects that have runners enabled inside namespace' do it_behaves_like 'has notifications' do
it_behaves_like 'not eligible to see notifications' do subject { described_class.new(nil, injected_group) }
let(:shared_runners_enabled) { false }
subject { described_class.new(nil, injected_group) }
end
end end
end end
it_behaves_like 'not eligible to see notifications' do context 'with no projects that have runners enabled inside namespace' do
subject { described_class.new(nil, injected_group) } it_behaves_like 'not eligible to see notifications' do
let(:shared_runners_enabled) { false }
subject { described_class.new(nil, injected_group) }
end
end end
end
context 'when user is not authenticated' do it_behaves_like 'not eligible to see notifications' do
let(:user) { nil } subject { described_class.new(nil, injected_group) }
end
it_behaves_like 'not eligible to see notifications' do context 'when user is not authenticated' do
subject { described_class.new(injected_project, nil) } let(:user) { nil }
end
it_behaves_like 'not eligible to see notifications' do
subject { described_class.new(injected_project, nil) }
end end
end end
end end
......
...@@ -23,6 +23,7 @@ RSpec.describe Namespace do ...@@ -23,6 +23,7 @@ RSpec.describe Namespace do
it { is_expected.to delegate_method(:trial?).to(:gitlab_subscription) } it { is_expected.to delegate_method(:trial?).to(:gitlab_subscription) }
it { is_expected.to delegate_method(:trial_ends_on).to(:gitlab_subscription) } it { is_expected.to delegate_method(:trial_ends_on).to(:gitlab_subscription) }
it { is_expected.to delegate_method(:upgradable?).to(:gitlab_subscription) } it { is_expected.to delegate_method(:upgradable?).to(:gitlab_subscription) }
it { is_expected.to delegate_method(:email).to(:owner).with_prefix.allow_nil }
shared_examples 'plan helper' do |namespace_plan| shared_examples 'plan helper' do |namespace_plan|
let(:namespace) { create(:namespace_with_plan, plan: "#{plan_name}_plan") } let(:namespace) { create(:namespace_with_plan, plan: "#{plan_name}_plan") }
......
...@@ -430,7 +430,7 @@ RSpec.describe Group do ...@@ -430,7 +430,7 @@ RSpec.describe Group do
context 'group with associated push_rules record' do context 'group with associated push_rules record' do
context 'with its own push rule' do context 'with its own push rule' do
let(:push_rule) { create(:push_rule )} let(:push_rule) { create(:push_rule) }
it 'returns its own push rule' do it 'returns its own push rule' do
group.update(push_rule: push_rule) group.update(push_rule: push_rule)
...@@ -941,4 +941,16 @@ RSpec.describe Group do ...@@ -941,4 +941,16 @@ RSpec.describe Group do
end end
end end
end end
describe '#owners_emails' do
let(:user) { create(:user, email: 'bob@example.com') }
before do
group.add_owner(user)
end
subject { group.owners_emails }
it { is_expected.to match([user.email]) }
end
end end
...@@ -32,12 +32,6 @@ RSpec.describe Ci::Minutes::EmailNotificationService do ...@@ -32,12 +32,6 @@ RSpec.describe Ci::Minutes::EmailNotificationService do
create(:namespace_statistics, namespace: namespace, shared_runners_seconds: ci_minutes_used * 60) create(:namespace_statistics, namespace: namespace, shared_runners_seconds: ci_minutes_used * 60)
end end
let(:gitlab_dot_com) { true }
before do
allow(Gitlab).to receive(:com?).and_return(gitlab_dot_com)
end
describe '#execute' do describe '#execute' do
let(:extra_ci_minutes) { 0 } let(:extra_ci_minutes) { 0 }
let(:namespace) do let(:namespace) do
...@@ -46,17 +40,6 @@ RSpec.describe Ci::Minutes::EmailNotificationService do ...@@ -46,17 +40,6 @@ RSpec.describe Ci::Minutes::EmailNotificationService do
subject { described_class.new(project).execute } subject { described_class.new(project).execute }
context 'when it is not GitLab.com' do
let(:gitlab_dot_com) { false }
let(:ci_minutes_used) { 2500 }
it 'does not send the email to all the owners' do
expect(CiMinutesUsageMailer).not_to receive(:notify)
subject
end
end
context 'with a personal namespace' do context 'with a personal namespace' do
before do before do
namespace.update(owner_id: user.id) namespace.update(owner_id: user.id)
...@@ -169,8 +152,6 @@ RSpec.describe Ci::Minutes::EmailNotificationService do ...@@ -169,8 +152,6 @@ RSpec.describe Ci::Minutes::EmailNotificationService do
end end
before do before do
stub_const("EE::Namespace::CI_USAGE_ALERT_LEVELS", [30, 5])
namespace.add_owner(user) namespace.add_owner(user)
namespace.add_owner(user_2) namespace.add_owner(user_2)
end end
...@@ -182,16 +163,7 @@ RSpec.describe Ci::Minutes::EmailNotificationService do ...@@ -182,16 +163,7 @@ RSpec.describe Ci::Minutes::EmailNotificationService do
end end
context 'when available minutes have reached the first level of alert' do context 'when available minutes have reached the first level of alert' do
context 'when it is not GitLab.com' do
let(:gitlab_dot_com) { false }
let(:ci_minutes_used) { 1500 }
it_behaves_like 'no notification is sent'
end
context 'when quota is unlimited' do context 'when quota is unlimited' do
# Gitlab.com? => true is required in order to excercise the notification
let(:gitlab_dot_com) { true }
let(:ci_minutes_used) { 1500 } let(:ci_minutes_used) { 1500 }
before do before do
......
...@@ -21,32 +21,46 @@ RSpec.describe BuildFinishedWorker do ...@@ -21,32 +21,46 @@ RSpec.describe BuildFinishedWorker do
end end
describe '#perform' do describe '#perform' do
before do context 'when on .com' do
allow(Gitlab).to receive(:com?).and_return(true) before do
allow_any_instance_of(EE::Project).to receive(:shared_runners_minutes_limit_enabled?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
end allow_any_instance_of(EE::Project).to receive(:shared_runners_minutes_limit_enabled?).and_return(true)
end
it 'updates the project stats' do it 'updates the project stats' do
expect { subject }.to change { project_stats.reload.shared_runners_seconds } expect { subject }.to change { project_stats.reload.shared_runners_seconds }
end end
it 'updates the namespace stats' do it 'updates the namespace stats' do
expect { subject }.to change { namespace_stats.reload.shared_runners_seconds } expect { subject }.to change { namespace_stats.reload.shared_runners_seconds }
end end
it 'notifies the owners of Groups' do it 'notifies the owners of Groups' do
namespace.update_attribute(:shared_runners_minutes_limit, 2000) namespace.update_attribute(:shared_runners_minutes_limit, 2000)
namespace_stats.update_attribute(:shared_runners_seconds, 2100 * 60) namespace_stats.update_attribute(:shared_runners_seconds, 2100 * 60)
expect(CiMinutesUsageMailer).to receive(:notify).once.with(namespace, [namespace.owner.email]).and_return(spy) expect(CiMinutesUsageMailer).to receive(:notify).once.with(namespace, [namespace.owner.email]).and_return(spy)
subject subject
end
it 'stores security scans' do
expect(StoreSecurityScansWorker).to receive(:perform_async)
subject
end
end end
it 'stores security scans' do context 'when not on .com' do
expect(StoreSecurityScansWorker).to receive(:perform_async) before do
allow(Gitlab).to receive(:com?).and_return(false)
end
it 'does not notify the owners of Groups' do
expect(::Ci::Minutes::EmailNotificationService).not_to receive(:new)
subject subject
end
end 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