From 3da396178fde0f1a5d42a0ff097c15bf83ebafe1 Mon Sep 17 00:00:00 2001
From: Jason Goodman <jgoodman@gitlab.com>
Date: Mon, 24 Jul 2023 15:27:19 +0000
Subject: [PATCH] Use the namespace storage forks cost factor application
 setting

Refactor classes using the placeholder constant
Replace constant with application setting
Extract CostFactor module
---
 .../namespace_storage_forks_cost_factor.yml   |  8 ++
 .../ee/namespace/root_storage_statistics.rb   |  2 +-
 ee/app/models/ee/project_statistics.rb        |  6 +-
 .../models/namespaces/storage/cost_factor.rb  | 34 +++++++
 ee/app/models/namespaces/storage/root_size.rb |  2 -
 .../groups/usage_quotas/storage_tab_spec.rb   |  4 +-
 .../namespace/root_storage_statistics_spec.rb | 12 ++-
 ee/spec/models/ee/project_statistics_spec.rb  |  3 +-
 .../namespaces/storage/cost_factor_spec.rb    | 92 +++++++++++++++++++
 .../namespaces/storage/root_size_spec.rb      |  3 +-
 10 files changed, 150 insertions(+), 16 deletions(-)
 create mode 100644 config/feature_flags/development/namespace_storage_forks_cost_factor.yml
 create mode 100644 ee/app/models/namespaces/storage/cost_factor.rb
 create mode 100644 ee/spec/models/namespaces/storage/cost_factor_spec.rb

diff --git a/config/feature_flags/development/namespace_storage_forks_cost_factor.yml b/config/feature_flags/development/namespace_storage_forks_cost_factor.yml
new file mode 100644
index 000000000000..60b4980389be
--- /dev/null
+++ b/config/feature_flags/development/namespace_storage_forks_cost_factor.yml
@@ -0,0 +1,8 @@
+---
+name: namespace_storage_forks_cost_factor
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126775
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419181
+milestone: '16.3'
+type: development
+group: group::utilization
+default_enabled: false
diff --git a/ee/app/models/ee/namespace/root_storage_statistics.rb b/ee/app/models/ee/namespace/root_storage_statistics.rb
index fb5e855ad405..ba37798367ab 100644
--- a/ee/app/models/ee/namespace/root_storage_statistics.rb
+++ b/ee/app/models/ee/namespace/root_storage_statistics.rb
@@ -29,7 +29,7 @@ def forks_size_reduction
       end
 
       def inverted_cost_factor_for_forks
-        1 - ::Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS
+        ::Namespaces::Storage::CostFactor.inverted_cost_factor_for_forks(namespace)
       end
 
       override :from_namespace_statistics
diff --git a/ee/app/models/ee/project_statistics.rb b/ee/app/models/ee/project_statistics.rb
index 9485711b2682..95572274a0dd 100644
--- a/ee/app/models/ee/project_statistics.rb
+++ b/ee/app/models/ee/project_statistics.rb
@@ -11,11 +11,7 @@ def cost_factored_storage_size
     private
 
     def cost_factor
-      if project.forked? && (project.root_ancestor.paid? || !project.private?)
-        ::Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS
-      else
-        ::Namespaces::Storage::RootSize::COST_FACTOR
-      end
+      ::Namespaces::Storage::CostFactor.cost_factor_for(project)
     end
 
     override :storage_size_components
diff --git a/ee/app/models/namespaces/storage/cost_factor.rb b/ee/app/models/namespaces/storage/cost_factor.rb
new file mode 100644
index 000000000000..0ac61f8b15b1
--- /dev/null
+++ b/ee/app/models/namespaces/storage/cost_factor.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Namespaces
+  module Storage
+    module CostFactor
+      extend self
+
+      FULL_COST = 1.0
+
+      def cost_factor_for(project)
+        if project.forked? && (project.root_ancestor.paid? || !project.private?)
+          forks_cost_factor(project.root_ancestor)
+        else
+          FULL_COST
+        end
+      end
+
+      def inverted_cost_factor_for_forks(root_namespace)
+        FULL_COST - forks_cost_factor(root_namespace)
+      end
+
+      private
+
+      def forks_cost_factor(root_namespace)
+        if ::Gitlab::CurrentSettings.should_check_namespace_plan? &&
+            ::Feature.enabled?(:namespace_storage_forks_cost_factor, root_namespace)
+          ::Gitlab::CurrentSettings.namespace_storage_forks_cost_factor
+        else
+          FULL_COST
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/models/namespaces/storage/root_size.rb b/ee/app/models/namespaces/storage/root_size.rb
index cd3768efc211..9e4018bcbdc7 100644
--- a/ee/app/models/namespaces/storage/root_size.rb
+++ b/ee/app/models/namespaces/storage/root_size.rb
@@ -6,8 +6,6 @@ class RootSize
       CURRENT_SIZE_CACHE_KEY = 'root_storage_current_size'
       EXPIRATION_TIME = 10.minutes
       LIMIT_CACHE_NAME = 'root_storage_size_limit'
-      COST_FACTOR_FOR_FORKS = 1.0
-      COST_FACTOR = 1.0
 
       def initialize(root_namespace)
         @root_namespace = root_namespace.root_ancestor # just in case the true root isn't passed
diff --git a/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb b/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb
index 1b33b892d627..e8c877294dfe 100644
--- a/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb
+++ b/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb
@@ -69,7 +69,7 @@
 
       context 'with a cost factor for forks' do
         before do
-          stub_const('Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS', 0.1)
+          stub_ee_application_setting(namespace_storage_forks_cost_factor: 0.1)
         end
 
         it 'displays the storage size with the cost factor applied' do
@@ -93,7 +93,7 @@
 
   context 'with a cost factor for forks' do
     before do
-      stub_const('Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS', 0.1)
+      stub_ee_application_setting(namespace_storage_forks_cost_factor: 0.1)
     end
 
     it 'displays the total storage size taking into account the cost factor' do
diff --git a/ee/spec/models/ee/namespace/root_storage_statistics_spec.rb b/ee/spec/models/ee/namespace/root_storage_statistics_spec.rb
index db23e12b0ac0..c72f37b4d100 100644
--- a/ee/spec/models/ee/namespace/root_storage_statistics_spec.rb
+++ b/ee/spec/models/ee/namespace/root_storage_statistics_spec.rb
@@ -96,9 +96,13 @@
   describe '#cost_factored_storage_size', :saas do
     let_it_be(:group) { create(:group_with_plan, plan: :ultimate_plan) }
 
+    before do
+      stub_ee_application_setting(check_namespace_plan: true)
+    end
+
     context 'with a cost factor for forks' do
       before do
-        stub_const("::Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS", 0.05)
+        stub_ee_application_setting(namespace_storage_forks_cost_factor: 0.05)
       end
 
       context 'with a free plan' do
@@ -166,7 +170,7 @@
 
     context 'with a fork cost factor of 1' do
       before do
-        stub_const("::Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS", 1.0)
+        stub_ee_application_setting(namespace_storage_forks_cost_factor: 1.0)
       end
 
       it 'considers forks to take up their full actual disk storage' do
@@ -179,7 +183,7 @@
 
     context 'with a fork cost factor of 0' do
       before do
-        stub_const("::Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS", 0)
+        stub_ee_application_setting(namespace_storage_forks_cost_factor: 0)
       end
 
       it 'considers forks to take up no storage at all' do
@@ -192,7 +196,7 @@
 
     context 'when the cost factor would result in a fractional storage_size' do
       before do
-        stub_const("::Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS", 0.1)
+        stub_ee_application_setting(namespace_storage_forks_cost_factor: 0.1)
       end
 
       it 'rounds to the nearest integer' do
diff --git a/ee/spec/models/ee/project_statistics_spec.rb b/ee/spec/models/ee/project_statistics_spec.rb
index a2c775032170..05bf680f9839 100644
--- a/ee/spec/models/ee/project_statistics_spec.rb
+++ b/ee/spec/models/ee/project_statistics_spec.rb
@@ -82,7 +82,8 @@
 
     context 'when there is a cost factor for forks' do
       before do
-        stub_const('Namespaces::Storage::RootSize::COST_FACTOR_FOR_FORKS', 0.1)
+        stub_ee_application_setting(check_namespace_plan: true)
+        stub_ee_application_setting(namespace_storage_forks_cost_factor: 0.1)
       end
 
       where(:plan, :fork_visibility) do
diff --git a/ee/spec/models/namespaces/storage/cost_factor_spec.rb b/ee/spec/models/namespaces/storage/cost_factor_spec.rb
new file mode 100644
index 000000000000..131bbdb570ad
--- /dev/null
+++ b/ee/spec/models/namespaces/storage/cost_factor_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::Storage::CostFactor, feature_category: :consumables_cost_management do
+  let(:full_cost) { 1.0 }
+  let(:forks_cost_factor) { 0.2 }
+  let(:paid_group) { build_group }
+  let(:free_group) { build_group(paid: false) }
+
+  before do
+    stub_ee_application_setting(namespace_storage_forks_cost_factor: forks_cost_factor)
+    stub_ee_application_setting(check_namespace_plan: true)
+    stub_feature_flags(namespace_storage_forks_cost_factor: true)
+  end
+
+  describe '.cost_factor_for' do
+    it 'returns the forks cost factor for a project that is a fork' do
+      project = build_fork(group: paid_group)
+
+      expect(described_class.cost_factor_for(project)).to eq(forks_cost_factor)
+    end
+
+    it 'returns full cost for a project that is not a fork' do
+      project = build_project(group: paid_group)
+
+      expect(described_class.cost_factor_for(project)).to eq(full_cost)
+    end
+
+    it 'returns full cost for a private fork in a free namespace' do
+      project = build_fork(group: free_group, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+      expect(described_class.cost_factor_for(project)).to eq(full_cost)
+    end
+
+    it 'returns full cost when the feature flag is false' do
+      stub_feature_flags(namespace_storage_forks_cost_factor: false)
+      project = build_fork(group: paid_group)
+
+      expect(described_class.cost_factor_for(project)).to eq(full_cost)
+    end
+
+    it 'returns full cost when namespace plans are not checked' do
+      stub_ee_application_setting(check_namespace_plan: false)
+      project = build_fork(group: paid_group)
+
+      expect(described_class.cost_factor_for(project)).to eq(full_cost)
+    end
+  end
+
+  describe '.inverted_cost_factor_for_forks' do
+    it 'returns the inverse of the cost factor for forks' do
+      expect(described_class.inverted_cost_factor_for_forks(paid_group)).to eq(0.8)
+    end
+
+    it 'returns the inverse of full cost when the feature flag is false' do
+      stub_feature_flags(namespace_storage_forks_cost_factor: false)
+
+      expect(described_class.inverted_cost_factor_for_forks(paid_group)).to eq(0)
+    end
+
+    it 'returns the inverse of full cost when namespace plans are not checked' do
+      stub_ee_application_setting(check_namespace_plan: false)
+
+      expect(described_class.inverted_cost_factor_for_forks(paid_group)).to eq(0)
+    end
+  end
+
+  def build_group(paid: true)
+    group = build_stubbed(:group)
+
+    allow(group).to receive(:paid?).and_return(paid)
+
+    group
+  end
+
+  def build_project(group:, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+    project = build_stubbed(:project, visibility_level: visibility_level)
+
+    allow(project).to receive(:root_ancestor).and_return(group)
+
+    project
+  end
+
+  def build_fork(group:, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+    project = build_project(group: group, visibility_level: visibility_level)
+
+    allow(project).to receive(:forked?).and_return(true)
+
+    project
+  end
+end
diff --git a/ee/spec/models/namespaces/storage/root_size_spec.rb b/ee/spec/models/namespaces/storage/root_size_spec.rb
index a806b98f25b1..e1ee772af43e 100644
--- a/ee/spec/models/namespaces/storage/root_size_spec.rb
+++ b/ee/spec/models/namespaces/storage/root_size_spec.rb
@@ -212,7 +212,8 @@
       end
 
       before do
-        stub_const("#{described_class}::COST_FACTOR_FOR_FORKS", 0.05)
+        stub_ee_application_setting(check_namespace_plan: true)
+        stub_ee_application_setting(namespace_storage_forks_cost_factor: 0.05)
       end
 
       it 'returns the cost factored storage size' do
-- 
GitLab