Commit efba1559 authored by Douwe Maan's avatar Douwe Maan

Merge branch '3314-add-on-runner-minutes-for-gitlab-com' into 'master'

Add shared_runners_extra_minutes column to namespaces

Closes #3314

See merge request gitlab-org/gitlab-ee!9815
parents b8dc9544 d11e1cf4
...@@ -7,5 +7,11 @@ module Groups ...@@ -7,5 +7,11 @@ module Groups
def initialize(group, user, params = {}) def initialize(group, user, params = {})
@group, @current_user, @params = group, user, params.dup @group, @current_user, @params = group, user, params.dup
end end
private
def remove_unallowed_params
# overridden in EE
end
end end
end end
...@@ -8,6 +8,8 @@ module Groups ...@@ -8,6 +8,8 @@ module Groups
end end
def execute def execute
remove_unallowed_params
@group = Group.new(params) @group = Group.new(params)
after_build_hook(@group, params) after_build_hook(@group, params)
......
...@@ -6,6 +6,7 @@ module Groups ...@@ -6,6 +6,7 @@ module Groups
def execute def execute
reject_parent_id! reject_parent_id!
remove_unallowed_params
return false unless valid_visibility_level_change?(group, params[:visibility_level]) return false unless valid_visibility_level_change?(group, params[:visibility_level])
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1972,6 +1972,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do ...@@ -1972,6 +1972,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do
t.string "runners_token_encrypted" t.string "runners_token_encrypted"
t.integer "custom_project_templates_group_id" t.integer "custom_project_templates_group_id"
t.boolean "auto_devops_enabled" 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 ["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 ["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 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 ...@@ -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 ["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"], 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 ["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 ["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 t.index ["type"], name: "index_namespaces_on_type", using: :btree
end end
......
...@@ -240,6 +240,7 @@ Example response: ...@@ -240,6 +240,7 @@ Example response:
"file_template_project_id": 1, "file_template_project_id": 1,
"parent_id": null, "parent_id": null,
"shared_runners_minutes_limit": 133, "shared_runners_minutes_limit": 133,
"extra_shared_runners_minutes_limit": 133,
"projects": [ "projects": [
{ {
"id": 7, "id": 7,
...@@ -420,6 +421,7 @@ Parameters: ...@@ -420,6 +421,7 @@ Parameters:
| `request_access_enabled` | boolean | no | Allow users to request member access. | | `request_access_enabled` | boolean | no | Allow users to request member access. |
| `parent_id` | integer | no | The parent group id for creating nested group. | | `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. | | `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 ## Transfer project to group
...@@ -457,6 +459,7 @@ PUT /groups/:id ...@@ -457,6 +459,7 @@ PUT /groups/:id
| `request_access_enabled` | boolean | no | Allow users to request member access. | | `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 | | `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 | | `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 ```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5?name=Experimental" curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
......
...@@ -261,6 +261,7 @@ Parameters: ...@@ -261,6 +261,7 @@ Parameters:
"external": false, "external": false,
"private_profile": false, "private_profile": false,
"shared_runners_minutes_limit": 133 "shared_runners_minutes_limit": 133
"extra_shared_runners_minutes_limit": 133
} }
``` ```
...@@ -303,6 +304,7 @@ Parameters: ...@@ -303,6 +304,7 @@ Parameters:
- `avatar` (optional) - Image file for user's avatar - `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false - `private_profile` (optional) - User's profile is private - true or false
- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user - `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 ## User modification
...@@ -334,6 +336,7 @@ Parameters: ...@@ -334,6 +336,7 @@ Parameters:
- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default) - `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
- `external` (optional) - Flags the user as external - 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 - `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 - `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false - `private_profile` (optional) - User's profile is private - true or false
......
...@@ -2,11 +2,21 @@ ...@@ -2,11 +2,21 @@
module EE module EE
module NamespacesHelper 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) 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? 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' status = namespace.shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
else else
limit = 'Unlimited' limit = 'Unlimited'
...@@ -18,15 +28,21 @@ module EE ...@@ -18,15 +28,21 @@ module EE
end end
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) def namespace_shared_runner_limits_percent_used(namespace)
return 0 unless namespace.shared_runners_minutes_limit_enabled? 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 end
def namespace_shared_runner_limits_progress_bar(namespace) def namespace_shared_runner_usage_progress_bar(percent)
percent = [namespace_shared_runner_limits_percent_used(namespace), 100].min
status = status =
if percent == 100 if percent == 100
'danger' 'danger'
...@@ -46,6 +62,13 @@ module EE ...@@ -46,6 +62,13 @@ module EE
end end
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 # rubocop: disable CodeReuse/ActiveRecord
def namespaces_options_with_developer_maintainer_access(options = {}) def namespaces_options_with_developer_maintainer_access(options = {})
selected = options.delete(:selected) || :current_user selected = options.delete(:selected) || :current_user
......
...@@ -36,9 +36,11 @@ module EE ...@@ -36,9 +36,11 @@ module EE
accepts_nested_attributes_for :gitlab_subscription accepts_nested_attributes_for :gitlab_subscription
scope :with_plan, -> { where.not(plan_id: nil) } 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, 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 # Opportunistically clear the +file_template_project_id+ if invalid
before_validation :clear_file_template_project_id before_validation :clear_file_template_project_id
...@@ -151,9 +153,14 @@ module EE ...@@ -151,9 +153,14 @@ module EE
end end
end end
def actual_shared_runners_minutes_limit def actual_shared_runners_minutes_limit(include_extra: true)
shared_runners_minutes_limit || extra_minutes = include_extra ? extra_shared_runners_minutes_limit.to_i : 0
::Gitlab::CurrentSettings.shared_runners_minutes
if shared_runners_minutes_limit
shared_runners_minutes_limit + extra_minutes
else
::Gitlab::CurrentSettings.shared_runners_minutes + extra_minutes
end
end end
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
...@@ -167,6 +174,12 @@ module EE ...@@ -167,6 +174,12 @@ module EE
shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit
end 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? def shared_runners_enabled?
if ::Feature.enabled?(:shared_runner_minutes_on_root_namespace) if ::Feature.enabled?(:shared_runner_minutes_on_root_namespace)
all_projects.with_shared_runners.any? all_projects.with_shared_runners.any?
......
...@@ -26,6 +26,7 @@ module EE ...@@ -26,6 +26,7 @@ module EE
validate :cannot_be_admin_and_auditor validate :cannot_be_admin_and_auditor
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=, delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
:extra_shared_runners_minutes_limit, :extra_shared_runners_minutes_limit=,
to: :namespace to: :namespace
has_many :reviews, foreign_key: :author_id, inverse_of: :author has_many :reviews, foreign_key: :author_id, inverse_of: :author
......
...@@ -5,7 +5,18 @@ class NamespaceStatistics < ApplicationRecord ...@@ -5,7 +5,18 @@ class NamespaceStatistics < ApplicationRecord
validates :namespace, presence: true validates :namespace, presence: true
def shared_runners_minutes def shared_runners_minutes(include_extra: true)
shared_runners_seconds.to_i / 60 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
end end
...@@ -41,7 +41,8 @@ module EE ...@@ -41,7 +41,8 @@ module EE
all_namespaces all_namespaces
.joins('LEFT JOIN namespace_statistics ON namespace_statistics.namespace_id = namespaces.id') .joins('LEFT JOIN namespace_statistics ON namespace_statistics.namespace_id = namespaces.id')
.where('COALESCE(namespaces.shared_runners_minutes_limit, ?, 0) = 0 OR ' \ .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) application_shared_runners_minutes, application_shared_runners_minutes)
.select('1') .select('1')
end end
......
...@@ -19,6 +19,14 @@ module EE ...@@ -19,6 +19,14 @@ module EE
group.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit group.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
end 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 def log_audit_event
::AuditEventService.new( ::AuditEventService.new(
current_user, current_user,
......
...@@ -24,6 +24,14 @@ module EE ...@@ -24,6 +24,14 @@ module EE
group.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit group.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
end 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? def changes_file_template_project_id?
return false unless params.key?(:file_template_project_id) return false unless params.key?(:file_template_project_id)
......
...@@ -3,10 +3,16 @@ ...@@ -3,10 +3,16 @@
%h3.page-title %h3.page-title
Group pipelines quota 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' = link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
%p.light
Monthly pipeline minutes usage across shared Runners for the .row
%strong= @group.name .col-sm-6
group 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", = render "namespaces/pipelines_quota/list",
locals: { namespace: @group, projects: @projects } 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 @@ ...@@ -21,6 +21,8 @@
Unlimited Unlimited
= namespace_shared_runner_limits_progress_bar(namespace) = namespace_shared_runner_limits_progress_bar(namespace)
= render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace
%table.table.pipeline-project-metrics %table.table.pipeline-project-metrics
%thead %thead
%tr %tr
......
...@@ -10,6 +10,15 @@ class ClearSharedRunnersMinutesWorker ...@@ -10,6 +10,15 @@ class ClearSharedRunnersMinutesWorker
def perform def perform
return unless try_obtain_lease 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) NamespaceStatistics.where.not(shared_runners_seconds: 0)
.update_all( .update_all(
shared_runners_seconds: 0, shared_runners_seconds: 0,
...@@ -24,6 +33,10 @@ class ClearSharedRunnersMinutesWorker ...@@ -24,6 +33,10 @@ class ClearSharedRunnersMinutesWorker
private 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 def try_obtain_lease
Gitlab::ExclusiveLease.new('gitlab_clear_shared_runners_minutes_worker', Gitlab::ExclusiveLease.new('gitlab_clear_shared_runners_minutes_worker',
timeout: LEASE_TIMEOUT).try_obtain 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 ...@@ -26,6 +26,7 @@ module EE
prepended do prepended do
expose :shared_runners_minutes_limit expose :shared_runners_minutes_limit
expose :extra_shared_runners_minutes_limit
end end
end end
...@@ -66,6 +67,7 @@ module EE ...@@ -66,6 +67,7 @@ module EE
prepended do prepended do
expose :shared_runners_minutes_limit expose :shared_runners_minutes_limit
expose :extra_shared_runners_minutes_limit
end end
end end
...@@ -107,6 +109,7 @@ module EE ...@@ -107,6 +109,7 @@ module EE
prepended do prepended do
expose :shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? } 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| expose :billable_members_count do |namespace, options|
namespace.billable_members_count(options[:requested_hosted_plan]) namespace.billable_members_count(options[:requested_hosted_plan])
end end
......
...@@ -40,12 +40,6 @@ module EE ...@@ -40,12 +40,6 @@ module EE
override :update_group override :update_group
def update_group(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 params.delete(:file_template_project_id) unless
group.feature_available?(:custom_file_templates_for_namespace) group.feature_available?(:custom_file_templates_for_namespace)
......
...@@ -38,6 +38,7 @@ module EE ...@@ -38,6 +38,7 @@ module EE
params do params do
optional :plan, type: String, desc: "Namespace or Group plan" optional :plan, type: String, desc: "Namespace or Group plan"
optional :shared_runners_minutes_limit, type: Integer, desc: "Pipeline minutes quota for this namespace" 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" optional :trial_ends_on, type: Date, desc: "Trial expiration date"
end end
put ':id' do put ':id' do
...@@ -47,7 +48,7 @@ module EE ...@@ -47,7 +48,7 @@ module EE
break not_found!('Namespace') unless namespace 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 present namespace, with: ::API::Entities::Namespace, current_user: current_user
else else
render_validation_error!(namespace) render_validation_error!(namespace)
......
...@@ -79,4 +79,52 @@ describe EE::NamespacesHelper, :postgresql do ...@@ -79,4 +79,52 @@ describe EE::NamespacesHelper, :postgresql do
end end
end 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 end
...@@ -13,6 +13,7 @@ describe Namespace do ...@@ -13,6 +13,7 @@ describe Namespace do
it { is_expected.to have_one(:gitlab_subscription).dependent(:destroy) } it { is_expected.to have_one(:gitlab_subscription).dependent(:destroy) }
it { is_expected.to belong_to(:plan) } 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_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).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) }
...@@ -379,6 +380,20 @@ describe Namespace do ...@@ -379,6 +380,20 @@ describe Namespace do
is_expected.to eq(500) is_expected.to eq(500)
end end
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
end end
...@@ -498,6 +513,77 @@ describe Namespace do ...@@ -498,6 +513,77 @@ describe Namespace do
end end
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 describe '#shared_runners_minutes_used?' do
subject { namespace.shared_runners_minutes_used? } subject { namespace.shared_runners_minutes_used? }
......
...@@ -10,4 +10,41 @@ describe NamespaceStatistics do ...@@ -10,4 +10,41 @@ describe NamespaceStatistics do
it { expect(namespace_statistics.shared_runners_minutes).to eq(2) } it { expect(namespace_statistics.shared_runners_minutes).to eq(2) }
end 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 end
...@@ -20,11 +20,11 @@ describe API::Namespaces do ...@@ -20,11 +20,11 @@ describe API::Namespaces do
expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'members_count_with_descendants', 'parent_id', 'members_count_with_descendants',
'plan', 'shared_runners_minutes_limit', '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', expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'plan', 'shared_runners_minutes_limit', 'parent_id', 'plan', 'shared_runners_minutes_limit',
'billable_members_count') 'extra_shared_runners_minutes_limit', 'billable_members_count')
end end
end end
......
...@@ -147,6 +147,45 @@ describe Ci::RegisterJobService do ...@@ -147,6 +147,45 @@ describe Ci::RegisterJobService do
end end
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) def execute(runner)
described_class.new(runner).execute.build described_class.new(runner).execute.build
end end
......
...@@ -57,6 +57,32 @@ describe Groups::CreateService, '#execute' do ...@@ -57,6 +57,32 @@ describe Groups::CreateService, '#execute' do
end end
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) def create_group(user, opts)
described_class.new(user, opts).execute described_class.new(user, opts).execute
end end
......
...@@ -154,6 +154,30 @@ describe Groups::UpdateService, '#execute' do ...@@ -154,6 +154,30 @@ describe Groups::UpdateService, '#execute' do
end end
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) def update_group(group, user, opts)
Groups::UpdateService.new(group, user, opts).execute Groups::UpdateService.new(group, user, opts).execute
end end
......
...@@ -47,5 +47,49 @@ describe ClearSharedRunnersMinutesWorker do ...@@ -47,5 +47,49 @@ describe ClearSharedRunnersMinutesWorker do
expect(statistics.reload.shared_runners_seconds_last_reset).to be_like_time(Time.now) expect(statistics.reload.shared_runners_seconds_last_reset).to be_like_time(Time.now)
end end
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
end end
...@@ -26,6 +26,7 @@ module API ...@@ -26,6 +26,7 @@ module API
optional :ldap_cn, type: String, desc: 'LDAP Common Name' optional :ldap_cn, type: String, desc: 'LDAP Common Name'
optional :ldap_access, type: Integer, desc: 'A valid access level' 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 :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 all_or_none_of :ldap_cn, :ldap_access
end end
end end
......
...@@ -54,6 +54,7 @@ module API ...@@ -54,6 +54,7 @@ module API
if Gitlab.ee? if Gitlab.ee?
optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user' 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
end end
......
...@@ -617,6 +617,9 @@ msgstr "" ...@@ -617,6 +617,9 @@ msgstr ""
msgid "Additional text" msgid "Additional text"
msgstr "" msgstr ""
msgid "Aditional minutes"
msgstr ""
msgid "Admin Area" msgid "Admin Area"
msgstr "" msgstr ""
......
...@@ -467,15 +467,6 @@ describe API::Groups do ...@@ -467,15 +467,6 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end 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 it 'returns 200 if shared_runners_minutes_limit is not changing' do
group1.update(shared_runners_minutes_limit: 133) 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