Commit d11e1cf4 authored by Rubén Dávila's avatar Rubén Dávila

Add support to purchase extra CI minutes

* extra_shared_runners_minutes_limit column was added to namespaces.
* extra minutes should roll over to the next month in case customer has
not used all their extra minutes
parent 7e676b3c
......@@ -7,5 +7,11 @@ module Groups
def initialize(group, user, params = {})
@group, @current_user, @params = group, user, params.dup
end
private
def remove_unallowed_params
# overridden in EE
end
end
end
......@@ -8,6 +8,8 @@ module Groups
end
def execute
remove_unallowed_params
@group = Group.new(params)
after_build_hook(@group, params)
......
......@@ -6,6 +6,7 @@ module Groups
def execute
reject_parent_id!
remove_unallowed_params
return false unless valid_visibility_level_change?(group, params[:visibility_level])
......
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190325165127) do
ActiveRecord::Schema.define(version: 20190328210840) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1972,6 +1972,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do
t.string "runners_token_encrypted"
t.integer "custom_project_templates_group_id"
t.boolean "auto_devops_enabled"
t.integer "extra_shared_runners_minutes_limit"
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)", using: :btree
t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id", using: :btree
......@@ -1987,6 +1988,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do
t.index ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
t.index ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
t.index ["runners_token_encrypted"], name: "index_namespaces_on_runners_token_encrypted", unique: true, using: :btree
t.index ["shared_runners_minutes_limit", "extra_shared_runners_minutes_limit"], name: "index_namespaces_on_shared_and_extra_runners_minutes_limit", using: :btree
t.index ["trial_ends_on"], name: "index_namespaces_on_trial_ends_on", where: "(trial_ends_on IS NOT NULL)", using: :btree
t.index ["type"], name: "index_namespaces_on_type", using: :btree
end
......
......@@ -240,6 +240,7 @@ Example response:
"file_template_project_id": 1,
"parent_id": null,
"shared_runners_minutes_limit": 133,
"extra_shared_runners_minutes_limit": 133,
"projects": [
{
"id": 7,
......@@ -420,6 +421,7 @@ Parameters:
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `parent_id` | integer | no | The parent group id for creating nested group. |
| `shared_runners_minutes_limit` | integer | no | (admin-only) Pipeline minutes quota for this group. |
| `extra_shared_runners_minutes_limit` | integer | no | (admin-only) Extra pipeline minutes quota for this group. |
## Transfer project to group
......@@ -457,6 +459,7 @@ PUT /groups/:id
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `file_template_project_id` | integer | no | **(Premium)** The ID of a project to load custom file templates from |
| `shared_runners_minutes_limit` | integer | no | (admin-only) Pipeline minutes quota for this group |
| `extra_shared_runners_minutes_limit` | integer | no | (admin-only) Extra pipeline minutes quota for this group |
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
......
......@@ -261,6 +261,7 @@ Parameters:
"external": false,
"private_profile": false,
"shared_runners_minutes_limit": 133
"extra_shared_runners_minutes_limit": 133
}
```
......@@ -303,6 +304,7 @@ Parameters:
- `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false
- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
- `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user
## User modification
......@@ -334,6 +336,7 @@ Parameters:
- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default)
- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
- `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user
- `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false
......
......@@ -2,11 +2,21 @@
module EE
module NamespacesHelper
def namespace_extra_shared_runner_limits_quota(namespace)
limit = namespace.extra_shared_runners_minutes_limit.to_i
used = namespace.extra_shared_runners_minutes.to_i
status = namespace.extra_shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
content_tag(:span, class: "shared_runners_limit_#{status}") do
"#{used} / #{limit}"
end
end
def namespace_shared_runner_limits_quota(namespace)
used = namespace.shared_runners_minutes.to_i
used = namespace.shared_runners_minutes(include_extra: false).to_i
if namespace.shared_runners_minutes_limit_enabled?
limit = namespace.actual_shared_runners_minutes_limit
limit = namespace.actual_shared_runners_minutes_limit(include_extra: false)
status = namespace.shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
else
limit = 'Unlimited'
......@@ -18,15 +28,21 @@ module EE
end
end
def namespace_extra_shared_runner_limits_percent_used(namespace)
limit = namespace.extra_shared_runners_minutes_limit.to_i
return 0 if limit.zero?
100 * namespace.extra_shared_runners_minutes.to_i / limit
end
def namespace_shared_runner_limits_percent_used(namespace)
return 0 unless namespace.shared_runners_minutes_limit_enabled?
100 * namespace.shared_runners_minutes.to_i / namespace.actual_shared_runners_minutes_limit
100 * namespace.shared_runners_minutes(include_extra: false).to_i / namespace.actual_shared_runners_minutes_limit(include_extra: false)
end
def namespace_shared_runner_limits_progress_bar(namespace)
percent = [namespace_shared_runner_limits_percent_used(namespace), 100].min
def namespace_shared_runner_usage_progress_bar(percent)
status =
if percent == 100
'danger'
......@@ -46,6 +62,13 @@ module EE
end
end
def namespace_shared_runner_limits_progress_bar(namespace, extra: false)
used = extra ? namespace_extra_shared_runner_limits_percent_used(namespace) : namespace_shared_runner_limits_percent_used(namespace)
percent = [used, 100].min
namespace_shared_runner_usage_progress_bar(percent)
end
# rubocop: disable CodeReuse/ActiveRecord
def namespaces_options_with_developer_maintainer_access(options = {})
selected = options.delete(:selected) || :current_user
......
......@@ -36,9 +36,11 @@ module EE
accepts_nested_attributes_for :gitlab_subscription
scope :with_plan, -> { where.not(plan_id: nil) }
scope :with_shared_runners_minutes_limit, -> { where("namespaces.shared_runners_minutes_limit > 0") }
scope :with_extra_shared_runners_minutes_limit, -> { where("namespaces.extra_shared_runners_minutes_limit > 0") }
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
to: :namespace_statistics, allow_nil: true
:extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true
# Opportunistically clear the +file_template_project_id+ if invalid
before_validation :clear_file_template_project_id
......@@ -151,9 +153,14 @@ module EE
end
end
def actual_shared_runners_minutes_limit
shared_runners_minutes_limit ||
::Gitlab::CurrentSettings.shared_runners_minutes
def actual_shared_runners_minutes_limit(include_extra: true)
extra_minutes = include_extra ? extra_shared_runners_minutes_limit.to_i : 0
if shared_runners_minutes_limit
shared_runners_minutes_limit + extra_minutes
else
::Gitlab::CurrentSettings.shared_runners_minutes + extra_minutes
end
end
def shared_runners_minutes_limit_enabled?
......@@ -167,6 +174,12 @@ module EE
shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit
end
def extra_shared_runners_minutes_used?
shared_runners_minutes_limit_enabled? &&
extra_shared_runners_minutes_limit &&
extra_shared_runners_minutes.to_i >= extra_shared_runners_minutes_limit
end
def shared_runners_enabled?
if ::Feature.enabled?(:shared_runner_minutes_on_root_namespace)
all_projects.with_shared_runners.any?
......
......@@ -26,6 +26,7 @@ module EE
validate :cannot_be_admin_and_auditor
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
:extra_shared_runners_minutes_limit, :extra_shared_runners_minutes_limit=,
to: :namespace
has_many :reviews, foreign_key: :author_id, inverse_of: :author
......
......@@ -5,7 +5,18 @@ class NamespaceStatistics < ApplicationRecord
validates :namespace, presence: true
def shared_runners_minutes
shared_runners_seconds.to_i / 60
def shared_runners_minutes(include_extra: true)
minutes = shared_runners_seconds.to_i / 60
include_extra ? minutes : minutes - extra_shared_runners_minutes
end
def extra_shared_runners_minutes
limit = namespace.shared_runners_minutes_limit.to_i
extra_limit = namespace.extra_shared_runners_minutes_limit.to_i
return 0 if extra_limit.zero? || shared_runners_minutes <= limit
shared_runners_minutes - limit
end
end
......@@ -41,7 +41,8 @@ module EE
all_namespaces
.joins('LEFT JOIN namespace_statistics ON namespace_statistics.namespace_id = namespaces.id')
.where('COALESCE(namespaces.shared_runners_minutes_limit, ?, 0) = 0 OR ' \
'COALESCE(namespace_statistics.shared_runners_seconds, 0) < COALESCE(namespaces.shared_runners_minutes_limit, ?, 0) * 60',
'COALESCE(namespace_statistics.shared_runners_seconds, 0) < ' \
'COALESCE((namespaces.shared_runners_minutes_limit + COALESCE(namespaces.extra_shared_runners_minutes_limit, 0)), ?, 0) * 60',
application_shared_runners_minutes, application_shared_runners_minutes)
.select('1')
end
......
......@@ -19,6 +19,14 @@ module EE
group.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
end
override :remove_unallowed_params
def remove_unallowed_params
unless current_user&.admin?
params.delete(:shared_runners_minutes_limit)
params.delete(:extra_shared_runners_minutes_limit)
end
end
def log_audit_event
::AuditEventService.new(
current_user,
......
......@@ -24,6 +24,14 @@ module EE
group.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
end
override :remove_unallowed_params
def remove_unallowed_params
unless current_user&.admin?
params.delete(:shared_runners_minutes_limit)
params.delete(:extra_shared_runners_minutes_limit)
end
end
def changes_file_template_project_id?
return false unless params.key?(:file_template_project_id)
......
......@@ -3,10 +3,16 @@
%h3.page-title
Group pipelines quota
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
%p.light
.row
.col-sm-6
Monthly pipeline minutes usage across shared Runners for the
%strong= @group.name
group
.col-sm-6
%p.text-right
= link_to 'Buy additional minutes', 'https://customers.gitlab.com/subscriptions', target: '_blank', class: 'btn btn-inverted btn-success right text-right'
= render "namespaces/pipelines_quota/list",
locals: { namespace: @group, projects: @projects }
- return unless Gitlab.com? && namespace.shared_runners_minutes_limit_enabled?
.row
.col-sm-6
%strong
= _("Aditional minutes")
%div
= namespace_extra_shared_runner_limits_quota(namespace)
minutes
.col-sm-6.right
#{namespace_extra_shared_runner_limits_percent_used(namespace)}% used
= namespace_shared_runner_limits_progress_bar(namespace, extra: true)
......@@ -21,6 +21,8 @@
Unlimited
= namespace_shared_runner_limits_progress_bar(namespace)
= render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace
%table.table.pipeline-project-metrics
%thead
%tr
......
......@@ -10,6 +10,15 @@ class ClearSharedRunnersMinutesWorker
def perform
return unless try_obtain_lease
if Gitlab::Database.postgresql?
# Using UPDATE with a joined table is not supported in MySql
Namespace.with_shared_runners_minutes_limit
.with_extra_shared_runners_minutes_limit
.where('namespace_statistics.namespace_id = namespaces.id')
.where('namespace_statistics.shared_runners_seconds > (namespaces.shared_runners_minutes_limit * 60)')
.update_all("extra_shared_runners_minutes_limit = #{extra_minutes_left_sql} FROM namespace_statistics")
end
NamespaceStatistics.where.not(shared_runners_seconds: 0)
.update_all(
shared_runners_seconds: 0,
......@@ -24,6 +33,10 @@ class ClearSharedRunnersMinutesWorker
private
def extra_minutes_left_sql
"GREATEST((namespaces.shared_runners_minutes_limit + namespaces.extra_shared_runners_minutes_limit) - ROUND(namespace_statistics.shared_runners_seconds / 60.0), 0)"
end
def try_obtain_lease
Gitlab::ExclusiveLease.new('gitlab_clear_shared_runners_minutes_worker',
timeout: LEASE_TIMEOUT).try_obtain
......
---
title: Add ability to purchase extra CI minutes
merge_request: 9815
author:
type: added
# frozen_string_literal: true
class AddExtraSharedRunnersMinutesLimitToNamespaces < ActiveRecord::Migration[5.0]
DOWNTIME = false
def change
add_column :namespaces, :extra_shared_runners_minutes_limit, :integer
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexesToSharedRunnersMinutesLimitAndExtraSharedRunnersMinutesLimitColumnsOnNamespaces < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_concurrent_index :namespaces, [:shared_runners_minutes_limit, :extra_shared_runners_minutes_limit], name: 'index_namespaces_on_shared_and_extra_runners_minutes_limit'
end
def down
remove_concurrent_index :namespaces, [:shared_runners_minutes_limit, :extra_shared_runners_minutes_limit]
end
end
......@@ -26,6 +26,7 @@ module EE
prepended do
expose :shared_runners_minutes_limit
expose :extra_shared_runners_minutes_limit
end
end
......@@ -66,6 +67,7 @@ module EE
prepended do
expose :shared_runners_minutes_limit
expose :extra_shared_runners_minutes_limit
end
end
......@@ -107,6 +109,7 @@ module EE
prepended do
expose :shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? }
expose :extra_shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? }
expose :billable_members_count do |namespace, options|
namespace.billable_members_count(options[:requested_hosted_plan])
end
......
......@@ -40,12 +40,6 @@ module EE
override :update_group
def update_group(group)
if params[:shared_runners_minutes_limit].present? &&
group.shared_runners_minutes_limit.to_i !=
params[:shared_runners_minutes_limit].to_i
authenticated_as_admin!
end
params.delete(:file_template_project_id) unless
group.feature_available?(:custom_file_templates_for_namespace)
......
......@@ -38,6 +38,7 @@ module EE
params do
optional :plan, type: String, desc: "Namespace or Group plan"
optional :shared_runners_minutes_limit, type: Integer, desc: "Pipeline minutes quota for this namespace"
optional :extra_shared_runners_minutes_limit, type: Integer, desc: "Extra pipeline minutes for this namespace"
optional :trial_ends_on, type: Date, desc: "Trial expiration date"
end
put ':id' do
......@@ -47,7 +48,7 @@ module EE
break not_found!('Namespace') unless namespace
if namespace.update(declared_params)
if namespace.update(declared_params(include_missing: false))
present namespace, with: ::API::Entities::Namespace, current_user: current_user
else
render_validation_error!(namespace)
......
......@@ -79,4 +79,52 @@ describe EE::NamespacesHelper, :postgresql do
end
end
end
describe '#namespace_shared_runner_limits_quota' do
context "when it's unlimited" do
before do
allow(user_group).to receive(:shared_runners_minutes_limit_enabled?).and_return(false)
end
it 'returns Unlimited for the limit section' do
expect(helper.namespace_shared_runner_limits_quota(user_group)).to match(%r{0 / Unlimited})
end
it 'returns the proper value for the used section' do
allow(user_group).to receive(:shared_runners_minutes).and_return(100)
expect(helper.namespace_shared_runner_limits_quota(user_group)).to match(%r{100 / Unlimited})
end
end
context "when it's limited" do
before do
allow(user_group).to receive(:shared_runners_minutes_limit_enabled?).and_return(true)
allow(user_group).to receive(:shared_runners_minutes).and_return(100)
user_group.update!(shared_runners_minutes_limit: 500)
end
it 'returns the proper values for used and limit sections' do
expect(helper.namespace_shared_runner_limits_quota(user_group)).to match(%r{100 / 500})
end
end
end
describe '#namespace_extra_shared_runner_limits_quota' do
context 'when extra minutes are assigned' do
it 'returns the proper values for used and limit sections' do
allow(user_group).to receive(:extra_shared_runners_minutes).and_return(50)
user_group.update!(extra_shared_runners_minutes_limit: 100)
expect(helper.namespace_extra_shared_runner_limits_quota(user_group)).to match(%r{50 / 100})
end
end
context 'when extra minutes are not assigned' do
it 'returns the proper values for used and limit sections' do
expect(helper.namespace_extra_shared_runner_limits_quota(user_group)).to match(%r{0 / 0})
end
end
end
end
......@@ -13,6 +13,7 @@ describe Namespace do
it { is_expected.to have_one(:gitlab_subscription).dependent(:destroy) }
it { is_expected.to belong_to(:plan) }
it { is_expected.to delegate_method(:extra_shared_runners_minutes).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_minutes).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) }
......@@ -379,6 +380,20 @@ describe Namespace do
is_expected.to eq(500)
end
end
context 'when extra minutes limit is set' do
before do
namespace.update_attribute(:extra_shared_runners_minutes_limit, 100)
end
it 'returns the extra minutes by default' do
is_expected.to eq(1100)
end
it 'can exclude the extra minutes if required' do
expect(namespace.actual_shared_runners_minutes_limit(include_extra: false)).to eq(1000)
end
end
end
end
......@@ -498,6 +513,77 @@ describe Namespace do
end
end
describe '#extra_shared_runners_minutes_used?' do
subject { namespace.extra_shared_runners_minutes_used? }
context 'with project' do
let!(:project) do
create(:project, namespace: namespace, shared_runners_enabled: true)
end
context 'shared_runners_minutes_limit is not enabled' do
before do
allow(namespace).to receive(:shared_runners_minutes_limit_enabled?).and_return(false)
end
it { is_expected.to be_falsey }
end
context 'shared_runners_minutes_limit is enabled' do
context 'when limit is defined' do
before do
namespace.update_attribute(:extra_shared_runners_minutes_limit, 100)
end
context "when usage is below the quota" do
before do
allow(namespace).to receive(:extra_shared_runners_minutes).and_return(50)
end
it { is_expected.to be_falsey }
end
context "when usage is above the quota" do
before do
allow(namespace).to receive(:extra_shared_runners_minutes).and_return(101)
end
it { is_expected.to be_truthy }
end
context 'and main limit is unlimited' do
before do
namespace.update_attribute(:shared_runners_minutes_limit, 0)
end
context "and it's above the quota" do
it { is_expected.to be_falsey }
end
end
end
context 'without limit' do
before do
namespace.update_attribute(:shared_runners_minutes_limit, 100)
namespace.update_attribute(:extra_shared_runners_minutes_limit, nil)
end
context 'when main usage is above the quota' do
before do
allow(namespace).to receive(:shared_runners_minutes).and_return(101)
end
it { is_expected.to be_falsey }
end
end
end
end
context 'without project' do
it { is_expected.to be_falsey }
end
end
describe '#shared_runners_minutes_used?' do
subject { namespace.shared_runners_minutes_used? }
......
......@@ -10,4 +10,41 @@ describe NamespaceStatistics do
it { expect(namespace_statistics.shared_runners_minutes).to eq(2) }
end
describe '#extra_shared_runners_minutes' do
subject { namespace_statistics.extra_shared_runners_minutes }
let(:namespace) { create(:namespace, shared_runners_minutes_limit: 100) }
let(:namespace_statistics) { create(:namespace_statistics, namespace: namespace) }
context 'when limit is defined' do
before do
namespace.update_attribute(:extra_shared_runners_minutes_limit, 50)
end
context 'when usage is above the main quota' do
before do
namespace_statistics.update_attribute(:shared_runners_seconds, 101 * 60)
end
it { is_expected.to eq(1) }
end
context 'when usage is below the main quota' do
before do
namespace_statistics.update_attribute(:shared_runners_seconds, 99 * 60)
end
it { is_expected.to eq(0) }
end
end
context 'without limit' do
before do
namespace.update_attribute(:extra_shared_runners_minutes_limit, nil)
end
it { is_expected.to eq(0) }
end
end
end
......@@ -20,11 +20,11 @@ describe API::Namespaces do
expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'members_count_with_descendants',
'plan', 'shared_runners_minutes_limit',
'billable_members_count')
'extra_shared_runners_minutes_limit', 'billable_members_count')
expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'plan', 'shared_runners_minutes_limit',
'billable_members_count')
'extra_shared_runners_minutes_limit', 'billable_members_count')
end
end
......
......@@ -147,6 +147,45 @@ describe Ci::RegisterJobService do
end
end
context 'for project with shared runners when limit is set only on namespace' do
let(:build) { execute(shared_runner) }
let(:runners_seconds_used) { 0 }
before do
project.update(shared_runners_enabled: true)
project.namespace.update(shared_runners_minutes_limit: 100)
project.namespace.create_namespace_statistics(shared_runners_seconds: runners_seconds_used)
end
context 'when we are under the limit' do
let(:runners_seconds_used) { 5000 }
it "does return a build" do
expect(build).not_to be_nil
end
end
context 'when we are over the limit' do
let(:runners_seconds_used) { 6001 }
it "does not return a build" do
expect(build).to be_nil
end
end
context 'when namespace has extra minutes' do
let(:runners_seconds_used) { 6001 }
before do
project.namespace.update(extra_shared_runners_minutes_limit: 5)
end
it "does return a build" do
expect(build).not_to be_nil
end
end
end
def execute(runner)
described_class.new(runner).execute.build
end
......
......@@ -57,6 +57,32 @@ describe Groups::CreateService, '#execute' do
end
end
context 'updating protected params' do
let(:attrs) do
group_params.merge(shared_runners_minutes_limit: 1000, extra_shared_runners_minutes_limit: 100)
end
context 'as an admin' do
let(:user) { create(:admin) }
it 'updates the attributes' do
group = create_group(user, attrs)
expect(group.shared_runners_minutes_limit).to eq(1000)
expect(group.extra_shared_runners_minutes_limit).to eq(100)
end
end
context 'as a regular user' do
it 'ignores the attributes' do
group = create_group(user, attrs)
expect(group.shared_runners_minutes_limit).to be_nil
expect(group.extra_shared_runners_minutes_limit).to be_nil
end
end
end
def create_group(user, opts)
described_class.new(user, opts).execute
end
......
......@@ -154,6 +154,30 @@ describe Groups::UpdateService, '#execute' do
end
end
context 'updating protected params' do
let(:attrs) { { shared_runners_minutes_limit: 1000, extra_shared_runners_minutes_limit: 100 } }
context 'as an admin' do
let(:user) { create(:admin) }
it 'updates the attributes' do
update_group(group, user, attrs)
expect(group.shared_runners_minutes_limit).to eq(1000)
expect(group.extra_shared_runners_minutes_limit).to eq(100)
end
end
context 'as a regular user' do
it 'ignores the attributes' do
update_group(group, user, attrs)
expect(group.shared_runners_minutes_limit).to be_nil
expect(group.extra_shared_runners_minutes_limit).to be_nil
end
end
end
def update_group(group, user, opts)
Groups::UpdateService.new(group, user, opts).execute
end
......
......@@ -47,5 +47,49 @@ describe ClearSharedRunnersMinutesWorker do
expect(statistics.reload.shared_runners_seconds_last_reset).to be_like_time(Time.now)
end
end
context 'when namespace has extra shared runner minutes', :postgresql do
let!(:namespace) do
create(:namespace, shared_runners_minutes_limit: 100, extra_shared_runners_minutes_limit: 10 )
end
let!(:statistics) do
create(:namespace_statistics, namespace: namespace, shared_runners_seconds: minutes_used * 60)
end
let(:minutes_used) { 0 }
context 'when consumption is below the default quota' do
let(:minutes_used) { 50 }
it 'does not modify the extra minutes quota' do
subject
expect(namespace.reload.extra_shared_runners_minutes_limit).to eq(10)
end
end
context 'when consumption is above the default quota' do
context 'when all extra minutes are used' do
let(:minutes_used) { 115 }
it 'sets extra minutes to 0' do
subject
expect(namespace.reload.extra_shared_runners_minutes_limit).to eq(0)
end
end
context 'when some extra minutes are used' do
let(:minutes_used) { 105 }
it 'it discounts the extra minutes used' do
subject
expect(namespace.reload.extra_shared_runners_minutes_limit).to eq(5)
end
end
end
end
end
end
......@@ -26,6 +26,7 @@ module API
optional :ldap_cn, type: String, desc: 'LDAP Common Name'
optional :ldap_access, type: Integer, desc: 'A valid access level'
optional :shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Pipeline minutes quota for this group'
optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this group'
all_or_none_of :ldap_cn, :ldap_access
end
end
......
......@@ -54,6 +54,7 @@ module API
if Gitlab.ee?
optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user'
optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this user'
end
end
......
......@@ -617,6 +617,9 @@ msgstr ""
msgid "Additional text"
msgstr ""
msgid "Aditional minutes"
msgstr ""
msgid "Admin Area"
msgstr ""
......
......@@ -467,15 +467,6 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
# EE
it 'returns 403 for updating shared_runners_minutes_limit' do
expect do
put api("/groups/#{group1.id}", user1), params: { shared_runners_minutes_limit: 133 }
end.not_to change { group1.shared_runners_minutes_limit }
expect(response).to have_gitlab_http_status(403)
end
it 'returns 200 if shared_runners_minutes_limit is not changing' do
group1.update(shared_runners_minutes_limit: 133)
......
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