Commit 6ef0fa86 authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Fabio Pitino

Add a runner cost factor for new public projects

This commit adds a runner minutes cost factor for public projects in new
namespaces. "New" is defined as "after July 17, 2021".

* Renames Ci::Runner#minutes_cost_factor to #cost_factor_for_project and
  updates it to take a project
* Adds Ci::Minutes::CostFactor#for_project

https://gitlab.com/gitlab-org/gitlab/-/issues/330581

Changelog: changed
EE: true
parent 89ce649a
...@@ -272,14 +272,15 @@ If you add a member to a group by using the [share a group with another group](. ...@@ -272,14 +272,15 @@ If you add a member to a group by using the [share a group with another group](.
## CI pipeline minutes ## CI pipeline minutes
CI pipeline minutes are the execution time for your CI pipeline minutes are the execution time for your [pipelines](../../ci/pipelines/index.md)
[pipelines](../../ci/pipelines/index.md) on GitLab shared runners. Each on GitLab shared runners. Each [GitLab SaaS tier](https://about.gitlab.com/pricing/)
[GitLab SaaS tier](https://about.gitlab.com/pricing/) includes a monthly quota includes a monthly quota of CI pipeline minutes for private and public projects:
of CI pipeline minutes:
| Plan | Private projects | Public projects |
- Free: 400 minutes |----------|------------------|-----------------|
- Premium: 10,000 minutes | Free | 400 | 50,000 |
- Ultimate: 50,000 minutes | Premium | 10,000 | 1,250,000 |
| Ultimate | 50,000 | 6,250,000 |
Quotas apply to: Quotas apply to:
......
...@@ -10,7 +10,7 @@ class Groups::UsageQuotasController < Groups::ApplicationController ...@@ -10,7 +10,7 @@ class Groups::UsageQuotasController < Groups::ApplicationController
feature_category :purchase feature_category :purchase
def index def index
@projects = @group.all_projects.with_shared_runners_limit_enabled.page(params[:page]) @projects = @group.all_projects.with_shared_runners.page(params[:page])
end end
private private
......
...@@ -7,7 +7,7 @@ class Profiles::UsageQuotasController < Profiles::ApplicationController ...@@ -7,7 +7,7 @@ class Profiles::UsageQuotasController < Profiles::ApplicationController
def index def index
@namespace = current_user.namespace @namespace = current_user.namespace
@projects = @namespace.projects.with_shared_runners_limit_enabled.page(params[:page]) @projects = @namespace.projects.with_shared_runners.page(params[:page])
end end
private private
......
...@@ -73,7 +73,7 @@ module EE ...@@ -73,7 +73,7 @@ module EE
end end
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
project.shared_runners_minutes_limit_enabled? && runner&.minutes_cost_factor(project.visibility_level)&.positive? project.shared_runners_minutes_limit_enabled? && runner&.cost_factor_enabled?(project)
end end
def log_geo_deleted_event def log_geo_deleted_event
......
...@@ -5,19 +5,25 @@ module EE ...@@ -5,19 +5,25 @@ module EE
module Runner module Runner
extend ActiveSupport::Concern extend ActiveSupport::Concern
def minutes_cost_factor(visibility_level) def cost_factor_for_project(project)
::Gitlab::Ci::Minutes::CostFactor.new(runner_matcher).for_visibility(visibility_level) cost_factor.for_project(project)
end
def cost_factor_enabled?(project)
cost_factor.enabled?(project)
end end
def visibility_levels_without_minutes_quota def visibility_levels_without_minutes_quota
::Gitlab::VisibilityLevel.options.values.reject do |visibility_level| ::Gitlab::VisibilityLevel.options.values.reject do |visibility_level|
minutes_cost_factor(visibility_level) > 0 cost_factor.for_visibility(visibility_level) > 0
end end
end end
class_methods do private
def has_shared_runners_with_non_zero_public_cost?
::Ci::Runner.instance_type.where('public_projects_minutes_cost_factor > 0').exists? def cost_factor
strong_memoize(:cost_factor) do
::Gitlab::Ci::Minutes::CostFactor.new(runner_matcher)
end end
end end
end end
......
...@@ -114,14 +114,6 @@ module EE ...@@ -114,14 +114,6 @@ module EE
elastic_index_dependant_association :merge_requests, on_change: :visibility_level elastic_index_dependant_association :merge_requests, on_change: :visibility_level
elastic_index_dependant_association :notes, on_change: :visibility_level elastic_index_dependant_association :notes, on_change: :visibility_level
scope :with_shared_runners_limit_enabled, -> do
if ::Ci::Runner.has_shared_runners_with_non_zero_public_cost?
with_shared_runners
else
with_shared_runners.non_public_only
end
end
scope :mirror, -> { where(mirror: true) } scope :mirror, -> { where(mirror: true) }
scope :mirrors_to_sync, ->(freeze_at, limit: nil) do scope :mirrors_to_sync, ->(freeze_at, limit: nil) do
......
...@@ -14,7 +14,7 @@ module EE ...@@ -14,7 +14,7 @@ module EE
private private
def cost_factor_disabled?(build_matcher) def cost_factor_disabled?(build_matcher)
cost_factor.disabled?(build_matcher.project.visibility_level) cost_factor.disabled?(build_matcher.project)
end end
def cost_factor def cost_factor
......
...@@ -20,11 +20,7 @@ module Gitlab ...@@ -20,11 +20,7 @@ module Gitlab
private private
def cost_factor def cost_factor
@build.runner.minutes_cost_factor(project_visibility_level) @build.runner.cost_factor_for_project(@build.project)
end
def project_visibility_level
@build.project.visibility_level
end end
end end
end end
......
...@@ -4,20 +4,38 @@ module Gitlab ...@@ -4,20 +4,38 @@ module Gitlab
module Ci module Ci
module Minutes module Minutes
class CostFactor class CostFactor
NEW_NAMESPACE_PUBLIC_PROJECT_COST_FACTOR = 0.008
def initialize(runner_matcher) def initialize(runner_matcher)
ensure_runner_matcher_instance(runner_matcher) ensure_runner_matcher_instance(runner_matcher)
@runner_matcher = runner_matcher @runner_matcher = runner_matcher
end end
def enabled?(visibility_level) def enabled?(project)
for_visibility(visibility_level) > 0 for_project(project) > 0
end
def disabled?(project)
!enabled?(project)
end end
def disabled?(visibility_level) def for_project(project)
!enabled?(visibility_level) return 0.0 unless @runner_matcher.instance_type?
cost_factor = for_visibility(project.visibility_level)
if cost_factor == 0 && project.force_cost_factor?
NEW_NAMESPACE_PUBLIC_PROJECT_COST_FACTOR
else
cost_factor
end
end end
# This method SHOULD NOT BE USED by new code. It is currently depended
# on by `BuildQueueService`. That dependency will be removed by
# https://gitlab.com/groups/gitlab-org/-/epics/5909, and this method
# should be made private at that time. Please use #for_project instead.
def for_visibility(visibility_level) def for_visibility(visibility_level)
return 0.0 unless @runner_matcher.instance_type? return 0.0 unless @runner_matcher.instance_type?
......
...@@ -22,7 +22,9 @@ RSpec.describe Gitlab::Ci::Minutes::CostFactor do ...@@ -22,7 +22,9 @@ RSpec.describe Gitlab::Ci::Minutes::CostFactor do
end end
describe '#enabled?' do describe '#enabled?' do
subject { described_class.new(runner.runner_matcher).enabled?(visibility_level) } let(:project) { build_stubbed(:project, visibility_level: visibility_level) }
subject { described_class.new(runner.runner_matcher).enabled?(project) }
where(:runner_type, :visibility_level, :public_cost_factor, :private_cost_factor, :result) do where(:runner_type, :visibility_level, :public_cost_factor, :private_cost_factor, :result) do
:project | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | false :project | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | false
...@@ -45,7 +47,9 @@ RSpec.describe Gitlab::Ci::Minutes::CostFactor do ...@@ -45,7 +47,9 @@ RSpec.describe Gitlab::Ci::Minutes::CostFactor do
end end
describe '#disabled?' do describe '#disabled?' do
subject { described_class.new(runner.runner_matcher).disabled?(visibility_level) } let(:project) { build_stubbed(:project, visibility_level: visibility_level) }
subject { described_class.new(runner.runner_matcher).disabled?(project) }
where(:runner_type, :visibility_level, :public_cost_factor, :private_cost_factor, :result) do where(:runner_type, :visibility_level, :public_cost_factor, :private_cost_factor, :result) do
:project | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | true :project | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | true
...@@ -67,6 +71,72 @@ RSpec.describe Gitlab::Ci::Minutes::CostFactor do ...@@ -67,6 +71,72 @@ RSpec.describe Gitlab::Ci::Minutes::CostFactor do
end end
end end
describe '#for_project' do
let(:project) { build_stubbed(:project, namespace: namespace, visibility_level: visibility_level) }
subject { described_class.new(runner.runner_matcher).for_project(project) }
context 'before the public project cost factor release date' do
let_it_be(:namespace) do
create(:group, created_at: Date.new(2021, 7, 16))
end
where(:runner_type, :visibility_level, :public_cost_factor, :private_cost_factor, :result) do
:project | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | 0
:project | Gitlab::VisibilityLevel::INTERNAL | 1 | 1 | 0
:project | Gitlab::VisibilityLevel::PUBLIC | 1 | 1 | 0
:group | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | 0
:group | Gitlab::VisibilityLevel::INTERNAL | 1 | 1 | 0
:group | Gitlab::VisibilityLevel::PUBLIC | 1 | 1 | 0
:instance | Gitlab::VisibilityLevel::PUBLIC | 0 | 5 | 0
:instance | Gitlab::VisibilityLevel::INTERNAL | 0 | 5 | 5
:instance | Gitlab::VisibilityLevel::PRIVATE | 0 | 5 | 5
end
with_them do
it { is_expected.to eq(result) }
end
end
context 'after the public project cost factor release date' do
let_it_be(:namespace) do
create(:group, created_at: Date.new(2021, 7, 17))
end
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
where(:runner_type, :visibility_level, :public_cost_factor, :private_cost_factor, :result) do
:project | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | 0
:project | Gitlab::VisibilityLevel::INTERNAL | 1 | 1 | 0
:project | Gitlab::VisibilityLevel::PUBLIC | 1 | 1 | 0
:group | Gitlab::VisibilityLevel::PRIVATE | 1 | 1 | 0
:group | Gitlab::VisibilityLevel::INTERNAL | 1 | 1 | 0
:group | Gitlab::VisibilityLevel::PUBLIC | 1 | 1 | 0
:instance | Gitlab::VisibilityLevel::PUBLIC | 0 | 5 | 0.008
:instance | Gitlab::VisibilityLevel::INTERNAL | 0 | 5 | 5
:instance | Gitlab::VisibilityLevel::PRIVATE | 0 | 5 | 5
end
with_them do
it { is_expected.to eq(result) }
end
end
context 'when the project has an invalid visibility level' do
let(:namespace) { nil }
let(:private_cost_factor) { 1 }
let(:public_cost_factor) { 1 }
let(:runner_type) { :instance }
let(:visibility_level) { 123 }
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError)
end
end
end
describe '#for_visibility' do describe '#for_visibility' do
subject { described_class.new(runner.runner_matcher).for_visibility(visibility_level) } subject { described_class.new(runner.runner_matcher).for_visibility(visibility_level) }
......
...@@ -3,15 +3,15 @@ ...@@ -3,15 +3,15 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe EE::Ci::Runner do RSpec.describe EE::Ci::Runner do
describe '#minutes_cost_factor' do describe '#cost_factor_for_project' do
subject { runner.minutes_cost_factor(visibility_level) } subject { runner.cost_factor_for_project(project) }
context 'with group type runner' do context 'with group type runner' do
let(:runner) { create(:ci_runner, :group) } let(:runner) { create(:ci_runner, :group) }
::Gitlab::VisibilityLevel.options.each do |level_name, level_value| ::Gitlab::VisibilityLevel.options.each do |level_name, level_value|
context "with #{level_name}" do context "with #{level_name}" do
let(:visibility_level) {level_value} let(:project) { create(:project, visibility_level: level_value) }
it { is_expected.to eq(0.0) } it { is_expected.to eq(0.0) }
end end
...@@ -23,7 +23,7 @@ RSpec.describe EE::Ci::Runner do ...@@ -23,7 +23,7 @@ RSpec.describe EE::Ci::Runner do
::Gitlab::VisibilityLevel.options.each do |level_name, level_value| ::Gitlab::VisibilityLevel.options.each do |level_name, level_value|
context "with #{level_name}" do context "with #{level_name}" do
let(:visibility_level) {level_value} let(:project) { create(:project, visibility_level: level_value) }
it { is_expected.to eq(0.0) } it { is_expected.to eq(0.0) }
end end
...@@ -35,29 +35,47 @@ RSpec.describe EE::Ci::Runner do ...@@ -35,29 +35,47 @@ RSpec.describe EE::Ci::Runner do
create(:ci_runner, create(:ci_runner,
:instance, :instance,
private_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 1.1,
public_projects_minutes_cost_factor: 2.2) public_projects_minutes_cost_factor: 0)
end end
context 'with private visibility level' do context 'with private visibility level' do
let(:visibility_level) { ::Gitlab::VisibilityLevel::PRIVATE } let(:project) { create(:project, visibility_level: ::Gitlab::VisibilityLevel::PRIVATE) }
it { is_expected.to eq(1.1) } it { is_expected.to eq(1.1) }
end end
context 'with public visibility level' do context 'with public visibility level' do
let(:visibility_level) { ::Gitlab::VisibilityLevel::PUBLIC } let(:project) { create(:project, namespace: namespace, visibility_level: ::Gitlab::VisibilityLevel::PUBLIC) }
it { is_expected.to eq(2.2) } context 'after the release date for public project cost factors' do
let(:namespace) do
create(:group, created_at: Date.new(2021, 7, 17))
end
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it { is_expected.to eq(0.008) }
end
context 'before the release date for public project cost factors' do
let(:namespace) do
create(:group, created_at: Date.new(2021, 7, 16))
end
it { is_expected.to eq(0.0) }
end
end end
context 'with internal visibility level' do context 'with internal visibility level' do
let(:visibility_level) { ::Gitlab::VisibilityLevel::INTERNAL } let(:project) { create(:project, visibility_level: ::Gitlab::VisibilityLevel::INTERNAL) }
it { is_expected.to eq(1.1) } it { is_expected.to eq(1.1) }
end end
context 'with invalid visibility level' do context 'with invalid visibility level' do
let(:visibility_level) { 123 } let(:project) { create(:project, visibility_level: 123) }
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(ArgumentError) expect { subject }.to raise_error(ArgumentError)
...@@ -66,6 +84,33 @@ RSpec.describe EE::Ci::Runner do ...@@ -66,6 +84,33 @@ RSpec.describe EE::Ci::Runner do
end end
end end
describe '#cost_factor_enabled?' do
let_it_be(:project) do
namespace = create(:group, created_at: Date.new(2021, 7, 16))
create(:project, namespace: namespace)
end
context 'when the project has any cost factor' do
it 'returns true' do
runner = create(:ci_runner, :instance,
private_projects_minutes_cost_factor: 1,
public_projects_minutes_cost_factor: 0)
expect(runner.cost_factor_enabled?(project)).to be_truthy
end
end
context 'when the project has no cost factor' do
it 'returns false' do
runner = create(:ci_runner, :instance,
private_projects_minutes_cost_factor: 0,
public_projects_minutes_cost_factor: 0)
expect(runner.cost_factor_enabled?(project)).to be_falsy
end
end
end
describe '#visibility_levels_without_minutes_quota' do describe '#visibility_levels_without_minutes_quota' do
subject { runner.visibility_levels_without_minutes_quota } subject { runner.visibility_levels_without_minutes_quota }
......
...@@ -343,46 +343,6 @@ RSpec.describe Project do ...@@ -343,46 +343,6 @@ RSpec.describe Project do
end end
end end
describe '.with_shared_runners_limit_enabled' do
let(:public_cost_factor) { 1.0 }
before do
create(:ci_runner, :instance, public_projects_minutes_cost_factor: public_cost_factor)
end
it 'does not return projects without shared runners' do
project_with_shared_runners = create(:project, shared_runners_enabled: true)
project_without_shared_runners = create(:project, shared_runners_enabled: false)
expect(described_class.with_shared_runners_limit_enabled).to include(project_with_shared_runners)
expect(described_class.with_shared_runners_limit_enabled).not_to include(project_without_shared_runners)
end
it 'return projects with shared runners with positive public cost factor with any visibility levels' do
public_project_with_shared_runners = create(:project, :public, shared_runners_enabled: true)
internal_project_with_shared_runners = create(:project, :internal, shared_runners_enabled: true)
private_project_with_shared_runners = create(:project, :private, shared_runners_enabled: true)
expect(described_class.with_shared_runners_limit_enabled).to include(public_project_with_shared_runners)
expect(described_class.with_shared_runners_limit_enabled).to include(internal_project_with_shared_runners)
expect(described_class.with_shared_runners_limit_enabled).to include(private_project_with_shared_runners)
end
context 'and shared runners public cost factors set to 0' do
let(:public_cost_factor) { 0.0 }
it 'return projects with any visibility levels except public' do
public_project_with_shared_runners = create(:project, :public, shared_runners_enabled: true)
internal_project_with_shared_runners = create(:project, :internal, shared_runners_enabled: true)
private_project_with_shared_runners = create(:project, :private, shared_runners_enabled: true)
expect(described_class.with_shared_runners_limit_enabled).not_to include(public_project_with_shared_runners)
expect(described_class.with_shared_runners_limit_enabled).to include(internal_project_with_shared_runners)
expect(described_class.with_shared_runners_limit_enabled).to include(private_project_with_shared_runners)
end
end
end
describe '.has_vulnerabilities' do describe '.has_vulnerabilities' do
let_it_be(:project_1) { create(:project) } let_it_be(:project_1) { create(:project) }
let_it_be(:project_2) { create(:project) } let_it_be(:project_2) { create(:project) }
......
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