Commit 781fa72d authored by Corinna Wiesner's avatar Corinna Wiesner Committed by Rémy Coutable

Add classes to check excess storage size

Add classes to calculate and check the excess storage size of a
namespace/group.
parent ac7ad4ab
# frozen_string_literal: true
module EE
class Namespace::RootExcessStorageSize
include ::Gitlab::Utils::StrongMemoize
def initialize(root_namespace)
@root_namespace = root_namespace
end
def above_size_limit?
return false unless enforce_limit?
current_size > limit
end
def usage_ratio
return 1 if limit == 0 && current_size > 0
return 0 if limit == 0
BigDecimal(current_size) / BigDecimal(limit)
end
def current_size
strong_memoize(:current_size) { root_namespace.total_repository_size_excess }
end
def limit
strong_memoize(:limit) do
root_namespace.additional_purchased_storage_size.megabytes
end
end
def enforce_limit?
return false unless ::Gitlab::CurrentSettings.automatic_purchased_storage_allocation?
return false unless ::Feature.enabled?(:additional_repo_storage_by_namespace, root_namespace)
true
end
private
attr_reader :root_namespace
end
end
# frozen_string_literal: true
module Namespaces
class CheckExcessStorageSizeService < CheckStorageSizeService
def initialize(namespace, user)
super
@root_storage_size = EE::Namespace::RootExcessStorageSize.new(root_namespace)
end
private
def enforce_limit?
root_storage_size.enforce_limit?
end
def usage_message
if root_namespace.contains_locked_projects?
if root_namespace.additional_purchased_storage_size == 0
params = { locked_project_count: root_namespace.repository_size_excess_project_count }
s_("NamespaceStorageSize|You have reached the free storage limit of 10GB on %{locked_project_count} projects. To unlock them, please purchase additional storage" % params)
else
s_("NamespaceStorageSize|%{namespace_name} contains a locked project" % { namespace_name: root_namespace.name })
end
else
s_("NamespaceStorageSize|You have reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
end
end
def above_size_limit_message
if root_namespace.additional_purchased_storage_size > 0
s_("NamespaceStorageSize|You have consumed all of your additional storage, please purchase more to unlock your projects over the free 10GB limit. You can't %{base_message}" % { base_message: base_message })
else
s_("NamespaceStorageSize|Please purchase additional storage to unlock your projects over the free 10GB project limit. You can't %{base_message}" % { base_message: base_message })
end
end
end
end
......@@ -13,7 +13,7 @@ module Namespaces
end
def execute
return ServiceResponse.success unless Feature.enabled?(:namespace_storage_limit, root_namespace)
return ServiceResponse.success unless enforce_limit?
return ServiceResponse.success if alert_level == :none
if root_storage_size.above_size_limit?
......@@ -35,6 +35,10 @@ module Namespaces
error: 1.0
}.freeze
def enforce_limit?
::Feature.enabled?(:namespace_storage_limit, root_namespace)
end
def payload
return {} unless can?(user, :admin_namespace, root_namespace)
......@@ -51,7 +55,7 @@ module Namespaces
end
def usage_message
s_("You reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
s_("NamespaceStorageSize|You have reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
end
def alert_level
......@@ -68,15 +72,15 @@ module Namespaces
end
def below_size_limit_message
s_("If you reach 100%% storage capacity, you will not be able to: %{base_message}" % { base_message: base_message } )
s_("NamespaceStorageSize|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 })
s_("NamespaceStorageSize|%{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.")
s_("NamespaceStorageSize|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
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::Namespace::RootExcessStorageSize do
let(:namespace) { create(:namespace, additional_purchased_storage_size: additional_purchased_storage_size) }
let(:total_repository_size_excess) { 50.megabytes }
let(:additional_purchased_storage_size) { 100 }
let(:model) { described_class.new(namespace) }
before do
allow(namespace).to receive(:total_repository_size_excess).and_return(total_repository_size_excess)
end
describe '#above_size_limit?' do
subject { model.above_size_limit? }
before do
allow(Gitlab::CurrentSettings).to receive(:automatic_purchased_storage_allocation?) { storage_allocation_enabled }
end
context 'when limit enforcement is off' do
let(:storage_allocation_enabled) { false }
it { is_expected.to eq(false) }
end
context 'when limit enforcement is on' do
let(:storage_allocation_enabled) { true }
context 'when below limit' do
it { is_expected.to eq(false) }
end
context 'when above limit' do
let(:total_repository_size_excess) { 101.megabytes }
it { is_expected.to eq(true) }
end
end
end
describe '#usage_ratio' do
subject { model.usage_ratio }
it { is_expected.to eq(0.5) }
context 'when limit is 0' do
let(:additional_purchased_storage_size) { 0 }
context 'when current size is greater than 0' do
it { is_expected.to eq(1) }
end
context 'when current size is less than 0' do
let(:total_repository_size_excess) { 0 }
it { is_expected.to eq(0) }
end
end
end
describe '#current_size' do
subject { model.current_size }
it { is_expected.to eq(total_repository_size_excess) }
end
describe '#limit' do
subject { model.limit }
context 'when there is additional purchased storage and a plan' do
let(:additional_purchased_storage_size) { 10_000 }
it { is_expected.to eq(10_000.megabytes) }
end
context 'when there is no additionl purchased storage' do
let(:additional_purchased_storage_size) { 0 }
it { is_expected.to eq(0.megabytes) }
end
end
describe '#enforce_limit?' do
subject { model.enforce_limit? }
let(:storage_allocation_enabled) { true }
before do
allow(Gitlab::CurrentSettings).to receive(:automatic_purchased_storage_allocation?) { storage_allocation_enabled }
end
it { is_expected.to eq(true) }
context 'with application setting is disabled' do
let(:storage_allocation_enabled) { false }
it { is_expected.to eq(false) }
end
context 'with feature flag :additional_repo_storage_by_namespace disabled' do
before do
stub_feature_flags(additional_repo_storage_by_namespace: false)
end
it { is_expected.to eq(false) }
end
end
end
......@@ -2,10 +2,9 @@
require 'spec_helper'
RSpec.describe Namespace::RootStorageSize, type: :model do
RSpec.describe EE::Namespace::RootStorageSize 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)}
let_it_be(:gold_plan, reload: true) { create(:gold_plan) }
......@@ -37,7 +36,10 @@ RSpec.describe Namespace::RootStorageSize, type: :model do
end
context 'when limit is 0' do
let(:limit) { 0 }
before do
plan_limits.update!(storage_size_limit: 0)
namespace.update!(additional_purchased_storage_size: 0)
end
it { is_expected.to eq(false) }
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::CheckExcessStorageSizeService, '#execute' do
let(:namespace) { build(:namespace, additional_purchased_storage_size: additional_purchased_storage_size) }
let(:user) { build(:user, namespace: namespace) }
let(:service) { described_class.new(namespace, user) }
let(:total_repository_size_excess) { 150.megabytes }
let(:additional_purchased_storage_size) { 100 }
let(:storage_allocation_enabled) { true }
subject(:response) { service.execute }
before do
allow(Gitlab::CurrentSettings).to receive(:automatic_purchased_storage_allocation?) { storage_allocation_enabled }
allow(namespace).to receive(:root_ancestor).and_return(namespace)
allow(namespace).to receive(:total_repository_size_excess).and_return(total_repository_size_excess)
end
context 'without limit enforcement' do
context 'with application setting disabled' do
let(:storage_allocation_enabled) { false }
it { is_expected.to be_success }
end
context 'with feature flag :additional_repo_storage_by_namespace disabled' do
before do
stub_feature_flags(additional_repo_storage_by_namespace: false)
end
it { is_expected.to be_success }
end
end
context 'when additional_purchased_storage_size is set to 0' do
let(:additional_purchased_storage_size) { 0 }
context 'when current size is greater than 0' do
it 'is successful and has no payload' do
expect(response).to be_error
expect(response.message).to be_present
end
end
context 'when current size is 0' do
let(:total_repository_size_excess) { 0 }
it 'is successful and has no payload' do
expect(response).to be_success
expect(response.payload).to be_empty
end
end
end
context 'when current size is below threshold' do
let(:total_repository_size_excess) { 10.megabytes }
it 'is successful and has no payload' do
expect(response).to be_success
expect(response.payload).to be_empty
end
end
context 'when not admin of the namespace' do
let(:other_namespace) { build(:namespace, additional_purchased_storage_size: additional_purchased_storage_size) }
subject(:response) { described_class.new(other_namespace, user).execute }
before do
allow(other_namespace).to receive(:root_ancestor).and_return(other_namespace)
allow(other_namespace).to receive(:total_repository_size_excess).and_return(total_repository_size_excess)
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(:group) }
let(:child_namespace) { build(: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(:total_repository_size_excess) { 50.megabytes }
it { is_expected.to eq(:info) }
end
context 'when above warning threshold' do
let(:total_repository_size_excess) { 75.megabytes }
it { is_expected.to eq(:warning) }
end
context 'when above alert threshold' do
let(:total_repository_size_excess) { 95.megabytes }
it { is_expected.to eq(:alert) }
end
context 'when above error threshold' do
let(:total_repository_size_excess) { 100.megabytes }
it { is_expected.to eq(:error) }
end
end
describe 'payload explanation_message' do
subject(:response) { service.execute.payload[:explanation_message] }
context 'when above limit' do
let(:total_repository_size_excess) { 110.megabytes }
context 'when namespace purchased additional storage' do
it 'returns message that the additional storage has been consumed' do
expect(response).to include("You have consumed all of your additional storage")
end
end
context 'when namespace did not purchase additional storage' do
let(:additional_purchased_storage_size) { 0 }
it 'returns message to purchase additional storage' do
expect(response).to include("Please purchase additional storage")
end
end
end
context 'when below limit' do
let(:total_repository_size_excess) { 60.megabytes }
it { is_expected.to include('If you reach 100% storage capacity') }
end
end
describe 'payload usage_message' do
let(:total_repository_size_excess) { 60.megabytes }
subject(:response) { service.execute.payload[:usage_message] }
before do
allow(namespace).to receive(:contains_locked_projects?).and_return(contains_locked_projects)
end
context 'when namespace contains locked projects' do
let(:contains_locked_projects) { true }
context 'when there is additional storage' do
it 'returns message about containing a locked project' do
expect(response).to include("contains a locked project")
end
end
context 'when there is no additional storage' do
let(:additional_purchased_storage_size) { 0 }
let(:locked_project_count) { 3 }
before do
allow(namespace).to receive(:repository_size_excess_project_count).and_return(locked_project_count)
end
it 'returns message to have reached the free storage limit' do
expect(response).to include("You have reached the free storage limit of 10GB")
expect(response).to include("#{locked_project_count} projects")
end
end
end
context 'when namespace does not contain locked projects' do
let(:contains_locked_projects) { false }
it 'returns current usage information' do
expect(response).to include("60 MB of 100 MB")
expect(response).to include("60%")
end
end
end
describe 'payload root_namespace' do
subject(:response) { service.execute.payload[:root_namespace] }
it { is_expected.to eq(namespace) }
end
end
......@@ -630,9 +630,6 @@ msgstr ""
msgid "%{name_with_link} has run out of Shared Runner Pipeline minutes so no new jobs or pipelines in its projects will run."
msgstr ""
msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
msgstr ""
msgid "%{name} contained %{resultsString}"
msgstr ""
......@@ -13502,9 +13499,6 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
msgstr ""
msgid "If you recently signed in and recognize the IP address, you may disregard this email."
msgstr ""
......@@ -17149,6 +17143,30 @@ msgstr ""
msgid "Namespace:"
msgstr ""
msgid "NamespaceStorageSize|%{namespace_name} contains a locked project"
msgstr ""
msgid "NamespaceStorageSize|%{namespace_name} is now read-only. You cannot: %{base_message}"
msgstr ""
msgid "NamespaceStorageSize|If you reach 100%% storage capacity, you will not be able to: %{base_message}"
msgstr ""
msgid "NamespaceStorageSize|Please purchase additional storage to unlock your projects over the free 10GB project limit. You can't %{base_message}"
msgstr ""
msgid "NamespaceStorageSize|You have consumed all of your additional storage, please purchase more to unlock your projects over the free 10GB limit. You can't %{base_message}"
msgstr ""
msgid "NamespaceStorageSize|You have reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})"
msgstr ""
msgid "NamespaceStorageSize|You have reached the free storage limit of 10GB on %{locked_project_count} projects. To unlock them, please purchase additional storage"
msgstr ""
msgid "NamespaceStorageSize|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 "Namespaces"
msgstr ""
......@@ -30055,9 +30073,6 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
msgid "You reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})"
msgstr ""
msgid "You successfully declined the invitation"
msgstr ""
......@@ -31594,9 +31609,6 @@ msgstr ""
msgid "projects"
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"
msgstr ""
......
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