Commit 64f755ad authored by nicolasdular's avatar nicolasdular

Show storage size alert on git push

When an admin of the namespace pushes to a repository that hits
a certain threshold, we want to warn them that they will hit a
storage limit at some point with a message of their current usage
as well as information about what happens when they will hit the
limit.
parent 361f86b0
# frozen_string_literal: true
class Namespace::RootStorageSize
ALERT_USAGE_THRESHOLD = 0.5
def initialize(root_namespace)
@root_namespace = root_namespace
end
......@@ -27,12 +25,6 @@ class Namespace::RootStorageSize
@limit ||= Gitlab::CurrentSettings.namespace_storage_size_limit.megabytes
end
def show_alert?
return false if limit == 0
usage_ratio >= ALERT_USAGE_THRESHOLD
end
private
attr_reader :root_namespace
......
......@@ -3,45 +3,70 @@
module Namespaces
class CheckStorageSizeService
include ActiveSupport::NumberHelper
include Gitlab::Allowable
include Gitlab::Utils::StrongMemoize
def initialize(namespace)
def initialize(namespace, user)
@root_namespace = namespace.root_ancestor
@root_storage_size = Namespace::RootStorageSize.new(root_namespace)
@user = user
end
def execute
return ServiceResponse.success unless Feature.enabled?(:namespace_storage_limit, root_namespace)
return ServiceResponse.success unless root_storage_size.show_alert?
return ServiceResponse.success if alert_level == :none
if root_storage_size.above_size_limit?
ServiceResponse.error(message: above_size_limit_message, payload: payload)
else
ServiceResponse.success(message: info_message, payload: payload)
ServiceResponse.success(payload: payload)
end
end
private
attr_reader :root_namespace, :root_storage_size
attr_reader :root_namespace, :root_storage_size, :user
USAGE_THRESHOLDS = {
none: 0.0,
info: 0.5,
warning: 0.75,
alert: 0.95,
error: 1.0
}.freeze
def payload
return {} unless can?(user, :admin_namespace, root_namespace)
{
current_usage_message: current_usage_message,
usage_ratio: root_storage_size.usage_ratio
explanation_message: explanation_message,
usage_message: usage_message,
alert_level: alert_level
}
end
def current_usage_message
params = {
usage_in_percent: number_to_percentage(root_storage_size.usage_ratio * 100, precision: 0),
namespace_name: root_namespace.name,
used_storage: formatted(root_storage_size.current_size),
storage_limit: formatted(root_storage_size.limit)
}
s_("You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})" % params)
def explanation_message
root_storage_size.above_size_limit? ? above_size_limit_message : below_size_limit_message
end
def usage_message
s_("You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
end
def alert_level
strong_memoize(:alert_level) do
usage_ratio = root_storage_size.usage_ratio
current_level = USAGE_THRESHOLDS.each_key.first
USAGE_THRESHOLDS.each do |level, threshold|
current_level = level if usage_ratio >= threshold
end
current_level
end
end
def info_message
def below_size_limit_message
s_("If you reach 100%% storage capacity, you will not be able to: %{base_message}" % { base_message: base_message } )
end
......@@ -53,6 +78,15 @@ module Namespaces
s_("push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines.")
end
def current_usage_params
{
usage_in_percent: number_to_percentage(root_storage_size.usage_ratio * 100, precision: 0),
namespace_name: root_namespace.name,
used_storage: formatted(root_storage_size.current_size),
storage_limit: formatted(root_storage_size.limit)
}
end
def formatted(number)
number_to_human_size(number, delimiter: ',', precision: 2)
end
......
......@@ -29,6 +29,8 @@ class PostReceiveService
response.add_alert_message(message)
end
response.add_alert_message(storage_size_limit_alert)
broadcast_message = BroadcastMessage.current_banner_messages&.last&.message
response.add_alert_message(broadcast_message)
......@@ -74,6 +76,19 @@ class PostReceiveService
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
private
def storage_size_limit_alert
return unless repository&.repo_type&.project?
payload = Namespaces::CheckStorageSizeService.new(project.namespace, user).execute.payload
return unless payload.present?
alert_level = "##### #{payload[:alert_level].to_s.upcase} #####"
[alert_level, payload[:usage_message], payload[:explanation_message]].join("\n")
end
end
PostReceiveService.prepend_if_ee('EE::PostReceiveService')
......@@ -64,22 +64,4 @@ RSpec.describe Namespace::RootStorageSize, type: :model do
it { is_expected.to eq(limit.megabytes) }
end
describe '#show_alert?' do
subject { model.show_alert? }
it { is_expected.to eq(true) }
context 'when limit is 0' do
let(:limit) { 0 }
it { is_expected.to eq(false) }
end
context 'when is below threshold' do
let(:current_size) { 49.megabytes }
it { is_expected.to eq(false) }
end
end
end
......@@ -3,92 +3,157 @@
require 'spec_helper'
describe Namespaces::CheckStorageSizeService, '#execute' do
let_it_be(:root_group) { create(:group) }
let(:nested_group) { create(:group, parent: root_group) }
let(:service) { described_class.new(nested_group) }
let(:namespace) { build_stubbed(:namespace) }
let(:user) { build(:user, namespace: namespace) }
let(:service) { described_class.new(namespace, user) }
let(:current_size) { 150.megabytes }
let(:limit) { 100 }
let(:limit) { 100.megabytes }
subject { service.execute }
subject(:response) { service.execute }
before do
stub_application_setting(namespace_storage_size_limit: limit)
allow(namespace).to receive(:root_ancestor).and_return(namespace)
create(:namespace_root_storage_statistics, namespace: root_group, storage_size: current_size)
root_storage_size = instance_double("RootStorageSize",
current_size: current_size,
limit: limit,
usage_ratio: limit == 0 ? 0 : current_size.to_f / limit.to_f,
above_size_limit?: current_size > limit
)
expect(Namespace::RootStorageSize).to receive(:new).and_return(root_storage_size)
end
context 'feature flag' do
it 'is successful when disabled' do
stub_feature_flags(namespace_storage_limit: false)
expect(subject).to be_success
expect(response).to be_success
end
it 'errors when enabled' do
stub_feature_flags(namespace_storage_limit: true)
expect(subject).to be_error
expect(response).to be_error
end
it 'is successful when feature flag is activated for another group' do
stub_feature_flags(namespace_storage_limit: create(:group))
it 'is successful when feature flag is activated for another namespace' do
stub_feature_flags(namespace_storage_limit: build(:namespace))
expect(subject).to be_success
expect(response).to be_success
end
it 'errors when feature flag is activated for the current group' do
stub_feature_flags(namespace_storage_limit: root_group)
it 'errors when feature flag is activated for the current namespace' do
stub_feature_flags(namespace_storage_limit: namespace )
expect(subject).to be_error
expect(response).to be_error
expect(response.message).to be_present
end
end
context 'when limit is set to 0' do
let(:limit) { 0 }
it { is_expected.to be_success }
it 'is successful and has no payload' do
expect(response).to be_success
expect(response.payload).to be_empty
end
end
it 'does not respond with a payload' do
result = subject
context 'when current size is below threshold' do
let(:current_size) { 10.megabytes }
expect(result.message).to be_nil
expect(result.payload).to be_empty
it 'is successful and has no payload' do
expect(response).to be_success
expect(response.payload).to be_empty
end
end
context 'when current size is below threshold to show an alert' do
let(:current_size) { 10.megabytes }
context 'when not admin of the namespace' do
let(:other_namespace) { build_stubbed(:namespace) }
subject(:response) { described_class.new(other_namespace, user).execute }
before do
allow(other_namespace).to receive(:root_ancestor).and_return(other_namespace)
end
it 'errors and has no payload' do
expect(response).to be_error
expect(response.payload).to be_empty
end
end
context 'when providing the child namespace' do
let(:namespace) { build_stubbed(:group) }
let(:child_namespace) { build_stubbed(:group, parent: namespace) }
subject(:response) { described_class.new(child_namespace, user).execute }
before do
allow(child_namespace).to receive(:root_ancestor).and_return(namespace)
namespace.add_owner(user)
end
it 'uses the root namespace' do
expect(response).to be_error
end
end
describe 'payload alert_level' do
subject { service.execute.payload[:alert_level] }
context 'when above info threshold' do
let(:current_size) { 50.megabytes }
it { is_expected.to eq(:info) }
end
context 'when above warning threshold' do
let(:current_size) { 75.megabytes }
it { is_expected.to eq(:warning) }
end
context 'when above alert threshold' do
let(:current_size) { 95.megabytes }
it { is_expected.to be_success }
it { is_expected.to eq(:alert) }
end
context 'when above error threshold' do
let(:current_size) { 100.megabytes }
it { is_expected.to eq(:error) }
end
end
context 'when current size exceeds limit' do
it 'returns an error with a payload' do
result = subject
current_usage_message = result.payload[:current_usage_message]
describe 'payload explanation_message' do
subject(:response) { service.execute.payload[:explanation_message] }
context 'when above limit' do
let(:current_size) { 110.megabytes }
it 'returns message with read-only warning' do
expect(response).to include("#{namespace.name} is now read-only")
end
end
context 'when below limit' do
let(:current_size) { 60.megabytes }
expect(result).to be_error
expect(result.message).to include("#{root_group.name} is now read-only.")
expect(current_usage_message).to include("150%")
expect(current_usage_message).to include(root_group.name)
expect(current_usage_message).to include("150 MB of 100 MB")
expect(result.payload[:usage_ratio]).to eq(1.5)
it { is_expected.to include('If you reach 100% storage capacity') }
end
end
context 'when current size is below limit but should show an alert' do
let(:current_size) { 50.megabytes }
describe 'payload usage_message' do
let(:current_size) { 60.megabytes }
it 'returns success with a payload' do
result = subject
current_usage_message = result.payload[:current_usage_message]
subject(:response) { service.execute.payload[:usage_message] }
expect(result).to be_success
expect(result.message).to be_present
expect(current_usage_message).to include("50%")
expect(current_usage_message).to include(root_group.name)
expect(current_usage_message).to include("50 MB of 100 MB")
expect(result.payload[:usage_ratio]).to eq(0.5)
it 'returns current usage information' do
expect(response).to include("60 MB of 100 MB")
expect(response).to include("60%")
end
end
end
......@@ -166,6 +166,41 @@ describe PostReceiveService do
expect(subject).to include(build_alert_message(message))
end
end
context 'storage size limit alerts' do
let(:check_storage_size_response) { ServiceResponse.success }
before do
expect_next_instance_of(Namespaces::CheckStorageSizeService, project.namespace, user) do |check_storage_size_service|
expect(check_storage_size_service).to receive(:execute).and_return(check_storage_size_response)
end
end
context 'when there is no payload' do
it 'adds no alert' do
expect(subject.size).to eq(1)
end
end
context 'when there is payload' do
let(:check_storage_size_response) do
ServiceResponse.success(
payload: {
alert_level: :info,
usage_message: "Usage",
explanation_message: "Explanation"
}
)
end
it 'adds an alert' do
response = subject
expect(response.size).to eq(2)
expect(response).to include(build_alert_message("##### INFO #####\nUsage\nExplanation"))
end
end
end
end
context 'with PersonalSnippet' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment