Commit 9255afcf authored by nicolasdular's avatar nicolasdular

Add namespace size checker

This is needed to display a banner or push message to users when
the namespace storage becomes short.
parent d505787c
# frozen_string_literal: true
class Namespace::RootStorageSize
ALERT_USAGE_THRESHOLD = 0.5
def initialize(root_namespace)
@root_namespace = root_namespace
end
def above_size_limit?
return false if limit == 0
usage_ratio > 1
end
def usage_ratio
return 0 if limit == 0
current_size.to_f / limit.to_f
end
def current_size
@current_size ||= root_namespace.root_storage_statistics&.storage_size
end
def limit
@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
end
# frozen_string_literal: true
module Namespaces
class CheckStorageSizeService
include ActiveSupport::NumberHelper
def initialize(namespace)
@root_namespace = namespace.root_ancestor
@root_storage_size = Namespace::RootStorageSize.new(root_namespace)
end
def execute
return ServiceResponse.success unless Feature.enabled?(:namespace_storage_limit, root_namespace)
return ServiceResponse.success unless root_storage_size.show_alert?
if root_storage_size.above_size_limit?
ServiceResponse.error(message: above_size_limit_message, payload: payload)
else
ServiceResponse.success(message: info_message, payload: payload)
end
end
private
attr_reader :root_namespace, :root_storage_size
def payload
{
current_usage_message: current_usage_message,
usage_ratio: root_storage_size.usage_ratio
}
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)
end
def info_message
s_("If you reach 100%% storage capacity, you will not be able to: %{base_message}" % { base_message: base_message } )
end
def above_size_limit_message
s_("%{namespace_name} is now read-only. You cannot: %{base_message}" % { namespace_name: root_namespace.name, base_message: base_message })
end
def base_message
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 formatted(number)
number_to_human_size(number, delimiter: ',', precision: 2)
end
end
end
...@@ -373,6 +373,9 @@ msgstr "" ...@@ -373,6 +373,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically." msgid "%{mrText}, this issue will be closed automatically."
msgstr "" msgstr ""
msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
msgstr ""
msgid "%{name} contained %{resultsString}" msgid "%{name} contained %{resultsString}"
msgstr "" msgstr ""
...@@ -10926,6 +10929,9 @@ msgstr "" ...@@ -10926,6 +10929,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes." msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr "" msgstr ""
msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
msgstr ""
msgid "If your HTTP repository is not publicly accessible, add your credentials." msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr "" msgstr ""
...@@ -23952,6 +23958,9 @@ msgstr "" ...@@ -23952,6 +23958,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive." msgid "You need to upload a Google Takeout archive."
msgstr "" msgstr ""
msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
msgstr ""
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:" msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr "" msgstr ""
...@@ -25277,6 +25286,9 @@ msgstr[1] "" ...@@ -25277,6 +25286,9 @@ msgstr[1] ""
msgid "project avatar" msgid "project avatar"
msgstr "" msgstr ""
msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
msgstr ""
msgid "quick actions" msgid "quick actions"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespace::RootStorageSize, type: :model do
let(:namespace) { create(:namespace) }
let(:current_size) { 50.megabytes }
let(:limit) { 100 }
let(:model) { described_class.new(namespace) }
let(:create_statistics) { create(:namespace_root_storage_statistics, namespace: namespace, storage_size: current_size)}
before do
create_statistics
stub_application_setting(namespace_storage_size_limit: limit)
end
describe '#above_size_limit?' do
subject { model.above_size_limit? }
context 'when limit is 0' do
let(:limit) { 0 }
it { is_expected.to eq(false) }
end
context 'when below limit' do
it { is_expected.to eq(false) }
end
context 'when above limit' do
let(:current_size) { 101.megabytes }
it { is_expected.to eq(true) }
end
end
describe '#usage_ratio' do
subject { model.usage_ratio }
it { is_expected.to eq(0.5) }
context 'when limit is 0' do
let(:limit) { 0 }
it { is_expected.to eq(0) }
end
context 'when there are no root_storage_statistics' do
let(:create_statistics) { nil }
it { is_expected.to eq(0) }
end
end
describe '#current_size' do
subject { model.current_size }
it { is_expected.to eq(current_size) }
end
describe '#limit' do
subject { model.limit }
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
# frozen_string_literal: true
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(:current_size) { 150.megabytes }
let(:limit) { 100 }
subject { service.execute }
before do
stub_application_setting(namespace_storage_size_limit: limit)
create(:namespace_root_storage_statistics, namespace: root_group, storage_size: current_size)
end
context 'feature flag' do
it 'is successful when disabled' do
stub_feature_flags(namespace_storage_limit: false)
expect(subject).to be_success
end
it 'errors when enabled' do
stub_feature_flags(namespace_storage_limit: true)
expect(subject).to be_error
end
it 'is successful when disabled for the current group' do
stub_feature_flags(namespace_storage_limit: { enabled: false, thing: root_group })
expect(subject).to be_success
end
it 'is successful when feature flag is activated for another group' do
stub_feature_flags(namespace_storage_limit: false)
stub_feature_flags(namespace_storage_limit: { enabled: true, thing: create(:group) })
expect(subject).to be_success
end
it 'errors when feature flag is activated for the current group' do
stub_feature_flags(namespace_storage_limit: { enabled: true, thing: root_group })
expect(subject).to be_error
end
end
context 'when limit is set to 0' do
let(:limit) { 0 }
it { is_expected.to be_success }
it 'does not respond with a payload' do
result = subject
expect(result.message).to be_nil
expect(result.payload).to be_empty
end
end
context 'when current size is below threshold to show an alert' do
let(:current_size) { 10.megabytes }
it { is_expected.to be_success }
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]
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)
end
end
context 'when current size is below limit but should show an alert' do
let(:current_size) { 50.megabytes }
it 'returns success with a payload' do
result = subject
current_usage_message = result.payload[:current_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)
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