Commit 71165067 authored by Douwe Maan's avatar Douwe Maan Committed by Kamil Trzcinski

Merge branch 'introduce-build-minutes' into 'master'

Introduce build minutes

Closes #1405

See merge request !965
parent 85ef6a6f
...@@ -37,6 +37,13 @@ ...@@ -37,6 +37,13 @@
$(this).parents('.no-password-message').remove(); $(this).parents('.no-password-message').remove();
return e.preventDefault(); return e.preventDefault();
}); });
$('.hide-shared-runner-limit-message').on('click', function(e) {
var $alert = $(this).parents('.shared-runner-quota-message');
var scope = $alert.data('scope');
Cookies.set('hide_shared_runner_quota_message', 'false', { path: scope });
$alert.remove();
e.preventDefault();
});
this.projectSelectDropdown(); this.projectSelectDropdown();
} }
......
...@@ -85,3 +85,39 @@ ...@@ -85,3 +85,39 @@
} }
} }
} }
.panel {
.shared_runners_limit_under_quota {
color: $gl-success;
}
.shared_runners_limit_over_quota {
color: $gl-danger;
}
}
.pipeline-quota {
border-top: 1px solid $table-border-color;
border-bottom: 1px solid $table-border-color;
margin: 0 0 $gl-padding;
.row {
padding-top: 10px;
padding-bottom: 10px;
}
.right {
text-align: right;
}
.progress {
height: 6px;
width: 100%;
margin-bottom: 0;
margin-top: 4px;
}
}
table.pipeline-project-metrics tr td {
padding: $gl-padding;
}
...@@ -160,6 +160,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -160,6 +160,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:elasticsearch_port, :elasticsearch_port,
:elasticsearch_search, :elasticsearch_search,
:repository_size_limit, :repository_size_limit,
:shared_runners_minutes,
:usage_ping_enabled :usage_ping_enabled
] ]
end end
......
...@@ -78,7 +78,8 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -78,7 +78,8 @@ class Admin::GroupsController < Admin::ApplicationController
def group_params_ee def group_params_ee
[ [
:repository_size_limit :repository_size_limit,
:shared_runners_minutes_limit
] ]
end end
end end
class Groups::PipelineQuotaController < Groups::ApplicationController
before_action :authorize_admin_group!
layout 'group_settings'
def index
@projects = @group.projects.with_shared_runners_limit_enabled.page(params[:page])
end
end
module EE
module GroupsHelper
def group_shared_runner_limits_quota(group)
used = group.shared_runners_minutes
if group.shared_runners_minutes_limit_enabled?
limit = group.actual_shared_runners_minutes_limit
status = group.shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
else
limit = 'Unlimited'
status = 'disabled'
end
content_tag(:span, class: "shared_runners_limit_#{status}") do
"#{used} / #{limit}"
end
end
def group_shared_runner_limits_percent_used(group)
return 0 unless group.shared_runners_minutes_limit_enabled?
100 * group.shared_runners_minutes / group.actual_shared_runners_minutes_limit
end
def group_shared_runner_limits_progress_bar(group)
percent = [group_shared_runner_limits_percent_used(group), 100].min
status =
if percent == 100
'danger'
elsif percent >= 80
'warning'
else
'success'
end
options = {
class: "progress-bar progress-bar-#{status}",
style: "width: #{percent}%;"
}
content_tag :div, class: 'progress' do
content_tag :div, nil, options
end
end
end
end
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
prepend EE::ApplicationSetting
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
......
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Build < CommitStatus class Build < CommitStatus
include TokenAuthenticatable include TokenAuthenticatable
include AfterCommitQueue include AfterCommitQueue
prepend EE::Build
belongs_to :runner belongs_to :runner
belongs_to :trigger_request belongs_to :trigger_request
......
module EE
# ApplicationSetting EE mixin
#
# This module is intended to encapsulate EE-specific model logic
# and be included in the `ApplicationSetting` model
module ApplicationSetting
extend ::Prependable
prepended do
validates :shared_runners_minutes,
numericality: { greater_than_or_equal_to: 0 }
end
end
end
module EE
# Build EE mixin
#
# This module is intended to encapsulate EE-specific model logic
# and be included in the `Build` model
module Build
extend ActiveSupport::Concern
def shared_runners_minutes_limit_enabled?
runner && runner.shared? && project.shared_runners_minutes_limit_enabled?
end
end
end
module EE
# Namespace EE mixin
#
# This module is intended to encapsulate EE-specific model logic
# and be included in the `Namespace` model
module Namespace
extend ::Prependable
prepended do
has_one :namespace_statistics, dependent: :destroy
delegate :shared_runners_minutes, :shared_runners_minutes_last_reset,
to: :namespace_statistics, allow_nil: true
end
def actual_shared_runners_minutes_limit
shared_runners_minutes_limit ||
current_application_settings.shared_runners_minutes
end
def shared_runners_minutes_limit_enabled?
shared_runners_enabled? &&
actual_shared_runners_minutes_limit.nonzero?
end
def shared_runners_minutes_used?
shared_runners_minutes_limit_enabled? &&
shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit
end
end
end
module EE
# Project EE mixin
#
# This module is intended to encapsulate EE-specific model logic
# and be included in the `Project` model
module Project
extend ::Prependable
prepended do
scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
delegate :shared_runners_minutes, :shared_runners_minutes_last_reset,
to: :statistics, allow_nil: true
delegate :actual_shared_runners_minutes_limit,
:shared_runners_minutes_used?, to: :namespace
end
def shared_runners_available?
super && !namespace.shared_runners_minutes_used?
end
def shared_runners_minutes_limit_enabled?
!public? && shared_runners_enabled? && namespace.shared_runners_minutes_limit_enabled?
end
end
end
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
prepend EE::Namespace
include CacheMarkdownField include CacheMarkdownField
include Sortable include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::CurrentSettings
include Routable include Routable
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
...@@ -179,6 +181,10 @@ class Namespace < ActiveRecord::Base ...@@ -179,6 +181,10 @@ class Namespace < ActiveRecord::Base
end end
end end
def shared_runners_enabled?
projects.with_shared_runners.any?
end
def full_name def full_name
@full_name ||= @full_name ||=
if parent if parent
......
class NamespaceStatistics < ActiveRecord::Base
belongs_to :namespace
validates :namespace, presence: true
end
...@@ -18,6 +18,7 @@ class Project < ActiveRecord::Base ...@@ -18,6 +18,7 @@ class Project < ActiveRecord::Base
include SelectForProjectAuthorization include SelectForProjectAuthorization
include Routable include Routable
prepend EE::GeoAwareAvatar prepend EE::GeoAwareAvatar
prepend EE::Project
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
...@@ -245,6 +246,7 @@ class Project < ActiveRecord::Base ...@@ -245,6 +246,7 @@ class Project < ActiveRecord::Base
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) } scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
...@@ -1239,12 +1241,20 @@ class Project < ActiveRecord::Base ...@@ -1239,12 +1241,20 @@ class Project < ActiveRecord::Base
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end end
def shared_runners_available?
shared_runners_enabled?
end
def shared_runners
shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end
def any_runners?(&block) def any_runners?(&block)
if runners.active.any?(&block) if runners.active.any?(&block)
return true return true
end end
shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) shared_runners.active.any?(&block)
end end
def valid_runners_token?(token) def valid_runners_token?(token)
......
...@@ -2,34 +2,31 @@ module Ci ...@@ -2,34 +2,31 @@ module Ci
# This class responsible for assigning # This class responsible for assigning
# proper pending build to runner on runner API request # proper pending build to runner on runner API request
class RegisterBuildService class RegisterBuildService
def execute(current_runner) include Gitlab::CurrentSettings
builds = Ci::Build.pending.unstarted prepend EE::Ci::RegisterBuildService
attr_reader :runner
def initialize(runner)
@runner = runner
end
def execute
builds = builds =
if current_runner.shared? if runner.shared?
builds. builds_for_shared_runner
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else else
# do run projects which are only assigned to this runner (FIFO) builds_for_specific_runner
builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
end end
build = builds.find do |build| build = builds.find do |build|
current_runner.can_pick?(build) runner.can_pick?(build)
end end
if build if build
# In case when 2 runners try to assign the same build, second runner will be declined # In case when 2 runners try to assign the same build, second runner will be declined
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
build.runner_id = current_runner.id build.runner_id = runner.id
build.run! build.run!
end end
...@@ -41,9 +38,35 @@ module Ci ...@@ -41,9 +38,35 @@ module Ci
private private
def builds_for_shared_runner
new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
def builds_for_specific_runner
new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC')
end
def running_builds_for_shared_runners def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared). Ci::Build.running.where(runner: Ci::Runner.shared).
group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds') group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
end end
def new_builds
Ci::Build.pending.unstarted
end
def shared_runner_build_limits_feature_enabled?
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
end
end end
end end
module EE
module Ci
# RegisterBuildService EE mixin
#
# This module is intended to encapsulate EE-specific service logic
# and be included in the `RegisterBuildService` service
module RegisterBuildService
extend ::Prependable
def builds_for_shared_runner
return super unless shared_runner_build_limits_feature_enabled?
# select projects which have allowed number of shared runner minutes or are public
super.
where("projects.visibility_level=? OR (#{builds_check_limit.to_sql})=1",
::Gitlab::VisibilityLevel::PUBLIC)
end
def builds_check_limit
::Namespace.reorder(nil).
where('namespaces.id = projects.namespace_id').
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_minutes, 0) < COALESCE(namespaces.shared_runners_minutes_limit, ?, 0)',
application_shared_runners_minutes, application_shared_runners_minutes).
select('1')
end
def application_shared_runners_minutes
current_application_settings.shared_runners_minutes
end
def shared_runner_build_limits_feature_enabled?
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
end
end
end
end
class UpdateBuildMinutesService < BaseService
def execute(build)
return unless build.shared_runners_minutes_limit_enabled?
return unless build.complete?
return unless build.duration
ProjectStatistics.update_counters(project.statistics,
shared_runners_minutes: build.duration)
NamespaceStatistics.update_counters(namespace_statistics,
shared_runners_minutes: build.duration)
end
private
def namespace_statistics
namespace.namespace_statistics || namespace.create_namespace_statistics
end
def project_statistics
project.statistics || project.create_statistics(namespace: namespace)
end
def namespace
project.namespace
end
end
...@@ -215,6 +215,9 @@ ...@@ -215,6 +215,9 @@
= f.label :shared_runners_enabled do = f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled = f.check_box :shared_runners_enabled
Enable shared runners for new projects Enable shared runners for new projects
= render 'shared_runners_minutes_setting', form: f
.form-group .form-group
= f.label :shared_runners_text, class: 'control-label col-sm-2' = f.label :shared_runners_text, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
.form-group
= form.label :shared_runners_minutes, 'Build minutes quota', class: 'control-label col-sm-2'
.col-sm-10
= form.number_field :shared_runners_minutes, class: 'form-control'
.help-block
Set the maximum number of build minutes that a group can use on shared runners per month.
0 for unlimited.
= link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-minutes")
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
= render 'groups/group_lfs_settings', f: f = render 'groups/group_lfs_settings', f: f
= render 'groups/shared_runners_minutes_setting', f: f
- if @group.new_record? - if @group.new_record?
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
......
- if group.shared_runners_enabled?
%li
%span.light Build minutes quota:
%strong
= group_shared_runner_limits_quota(group)
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-minutes")
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
= group_lfs_status(@group) = group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
= render "shared_runner_status", group: @group
.panel.panel-default .panel.panel-default
.panel-heading Linked LDAP groups .panel-heading Linked LDAP groups
%ul.well-list %ul.well-list
......
- if project.builds_enabled?
%li
%span.light Shared Runners:
%strong
- if project.shared_runners_enabled?
Enabled
- if project.shared_runners_minutes_limit_enabled?
- limit = project.actual_shared_runners_minutes_limit.to_i
(Limited to #{limit} minutes per month)
- else
(Unlimited minutes)
- else
Disabled
...@@ -97,6 +97,8 @@ ...@@ -97,6 +97,8 @@
%span.light archived: %span.light archived:
%strong repository is read-only %strong repository is read-only
= render "shared_runner_status", project: @project
%li %li
%span.light access: %span.light access:
%strong %strong
......
- if current_user.admin?
.form-group
= f.label :shared_runners_minutes_limit, class: 'control-label' do
Build Minutes Quota
.col-sm-10
= f.number_field :shared_runners_minutes_limit, class: 'form-control', min: 0
%span.help-block#shared_runners_minutes_limit_help_block
Set the maximum number of build minutes that a group can use on shared runners per month.
Set 0 for unlimited.
Set empty to inherit global setting of #{current_application_settings.shared_runners_minutes}.
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-minutes")
- page_title "Pipeline Quota"
%h3.page-title Group Pipeline Quota
%p.light Monthly build minutes usage across shared runners for #{@group.name}
.pipeline-quota.container-fluid
.row
.col-sm-6
%strong
- last_reset = @group.shared_runners_minutes_last_reset
- if last_reset
Usage since
= last_reset.strftime('%b %d, %Y')
- else
Current Period Usage
%div
= group_shared_runner_limits_quota(@group)
minutes
.col-sm-6.right
- if @group.shared_runners_minutes_limit_enabled?
= "#{group_shared_runner_limits_percent_used(@group)}% used"
- else
Unlimited
= group_shared_runner_limits_progress_bar(@group)
%table.table.pipeline-project-metrics
%thead
%tr
%th Project
%th Minutes
%tbody
- @projects.each do |project|
%tr
%td
.avatar-container.s20.hidden-xs
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.name, project
%td
= project.shared_runners_minutes
- if @projects.blank?
%tr
%td{ colspan: 2 }
.nothing-here-block This group has no projects which use shared runners
= paginate @projects, theme: "gitlab"
...@@ -28,5 +28,10 @@ ...@@ -28,5 +28,10 @@
= link_to group_audit_events_path(@group), title: "Audit Events" do = link_to group_audit_events_path(@group), title: "Audit Events" do
%span %span
Audit Events Audit Events
- if @group.shared_runners_enabled? && @group.shared_runners_minutes_limit_enabled?
= nav_link(controller: :pipeline_quota) do
= link_to group_pipeline_quota_path(@group), title: "Pipeline Quota" do
%span
Pipeline Quota
%li %li
= link_to 'Edit Group', edit_group_path(@group) = link_to 'Edit Group', edit_group_path(@group)
- if project.namespace.shared_runners_minutes_used?
- quota_used = project.namespace.shared_runners_minutes
- quota_limit = project.namespace.actual_shared_runners_minutes_limit
.bs-callout.bs-callout-warning
%p
You have used all your shared runner minutes
= "(#{quota_used} of #{quota_limit})."
- if can?(current_user, :admin_build, @project)
%br
For more information, go to the
= succeed "." do
= link_to namespace_project_runners_path(project.namespace, project) do
runners page
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page Runners page
= render "shared_runner_limit_warning", project: @build.project
- if @build.starts_environment? - if @build.starts_environment?
.prepend-top-default .prepend-top-default
.environment-information .environment-information
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh' = render 'shared/no_ssh'
= render 'shared/no_password' = render 'shared/no_password'
= render 'shared/shared_runners_minutes_limit', project: @project
= render "home_panel" = render "home_panel"
......
- @no_container = true - @no_container = true
- page_title "Pipelines" - page_title "Pipelines"
= content_for :flash_message do
= render 'shared/shared_runners_minutes_limit', project: @project
= render "projects/pipelines/head" = render "projects/pipelines/head"
%div{ class: container_class } %div{ class: container_class }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh' = render 'shared/no_ssh'
= render 'shared/no_password' = render 'shared/no_password'
= render 'shared/shared_runners_minutes_limit', project: @project
- if @project.above_size_limit? - if @project.above_size_limit?
= render 'above_size_limit_warning' = render 'above_size_limit_warning'
......
- project = local_assigns.fetch(:project, nil)
- namespace = local_assigns.fetch(:namespace, project && project.namespace)
- scope = (project || namespace).full_path
- has_limit = (project || namespace).shared_runners_minutes_limit_enabled?
- can_see_status = project.nil? || can?(current_user, :create_pipeline, project)
- if cookies[:hide_shared_runner_quota_message].blank? && has_limit && namespace.shared_runners_minutes_used? && can_see_status
.shared-runner-quota-message.alert.alert-warning.hidden-xs{ data: { scope: scope } }
= namespace.name
has exceeded their build minutes quota. Pipelines will not run anymore on shared runners.
.pull-right
= link_to 'Remind later', '#', class: 'hide-shared-runner-limit-message alert-link'
...@@ -4,6 +4,7 @@ class BuildFinishedWorker ...@@ -4,6 +4,7 @@ class BuildFinishedWorker
def perform(build_id) def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build| Ci::Build.find_by(id: build_id).try do |build|
UpdateBuildMinutesService.new(build.project, nil).execute(build)
BuildCoverageWorker.new.perform(build.id) BuildCoverageWorker.new.perform(build.id)
BuildHooksWorker.new.perform(build.id) BuildHooksWorker.new.perform(build.id)
end end
......
class ClearSharedRunnersMinutesWorker
LEASE_TIMEOUT = 3600
include Sidekiq::Worker
include CronjobQueue
def perform
return unless try_obtain_lease
ProjectStatistics.update_all(
shared_runners_minutes: 0,
shared_runners_minutes_last_reset: Time.now)
NamespaceStatistics.update_all(
shared_runners_minutes: 0,
shared_runners_minutes_last_reset: Time.now)
end
private
def try_obtain_lease
Gitlab::ExclusiveLease.new('gitlab_clear_shared_runners_minutes_worker',
timeout: LEASE_TIMEOUT).try_obtain
end
end
---
title: Allow to limit shared runners minutes quota for group
merge_request: 965
author:
...@@ -410,6 +410,10 @@ Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.n ...@@ -410,6 +410,10 @@ Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.n
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *' Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker' Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['clear_shared_runners_minutes_worker']['cron'] ||= '0 0 1 * *'
Settings.cron_jobs['clear_shared_runners_minutes_worker']['job_class'] = 'ClearSharedRunnersMinutesWorker'
# #
# GitLab Shell # GitLab Shell
# #
......
...@@ -30,6 +30,7 @@ scope(path: 'groups/*group_id', ...@@ -30,6 +30,7 @@ scope(path: 'groups/*group_id',
## EE-specific ## EE-specific
resource :notification_setting, only: [:update] resource :notification_setting, only: [:update]
resources :audit_events, only: [:index] resources :audit_events, only: [:index]
resources :pipeline_quota, only: [:index]
## EE-specific ## EE-specific
## EE-specific ## EE-specific
......
class AddSharedRunnersMinutesToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :shared_runners_minutes, :integer, null: false, default: 0
end
end
class AddSharedRunnersMinutesLimitToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :namespaces, :shared_runners_minutes_limit, :integer
end
end
class CreateTableNamespaceStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :namespace_statistics do |t|
t.references :namespace, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
t.integer :shared_runners_minutes, default: 0, null: false
t.timestamp :shared_runners_minutes_last_reset
end
end
end
class AddSharedRunnersMinutesToProjectStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
counter_column = { limit: 8, null: false, default: 0 }
add_column :project_statistics, :shared_runners_minutes, :integer, counter_column
add_column :project_statistics, :shared_runners_minutes_last_reset, :timestamp
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,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: 20170106172224) do ActiveRecord::Schema.define(version: 20170106172237) 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"
...@@ -118,6 +118,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do ...@@ -118,6 +118,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.boolean "html_emails_enabled", default: true t.boolean "html_emails_enabled", default: true
t.string "plantuml_url" t.string "plantuml_url"
t.boolean "plantuml_enabled" t.boolean "plantuml_enabled"
t.integer "shared_runners_minutes", default: 0, null: false
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -824,6 +825,14 @@ ActiveRecord::Schema.define(version: 20170106172224) do ...@@ -824,6 +825,14 @@ ActiveRecord::Schema.define(version: 20170106172224) do
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespace_statistics", force: :cascade do |t|
t.integer "namespace_id", null: false
t.integer "shared_runners_minutes", default: 0, null: false
t.datetime "shared_runners_minutes_last_reset"
end
add_index "namespace_statistics", ["namespace_id"], name: "index_namespace_statistics_on_namespace_id", unique: true, using: :btree
create_table "namespaces", force: :cascade do |t| create_table "namespaces", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "path", null: false t.string "path", null: false
...@@ -847,14 +856,15 @@ ActiveRecord::Schema.define(version: 20170106172224) do ...@@ -847,14 +856,15 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.integer "repository_size_limit" t.integer "repository_size_limit"
t.text "description_html" t.text "description_html"
t.integer "parent_id" t.integer "parent_id"
t.integer "shared_runners_minutes_limit"
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree
add_index "namespaces", ["ldap_sync_last_successful_update_at"], name: "index_namespaces_on_ldap_sync_last_successful_update_at", using: :btree add_index "namespaces", ["ldap_sync_last_successful_update_at"], name: "index_namespaces_on_ldap_sync_last_successful_update_at", using: :btree
add_index "namespaces", ["ldap_sync_last_update_at"], name: "index_namespaces_on_ldap_sync_last_update_at", using: :btree add_index "namespaces", ["ldap_sync_last_update_at"], name: "index_namespaces_on_ldap_sync_last_update_at", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
...@@ -1040,6 +1050,8 @@ ActiveRecord::Schema.define(version: 20170106172224) do ...@@ -1040,6 +1050,8 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.integer "repository_size", limit: 8, default: 0, null: false t.integer "repository_size", limit: 8, default: 0, null: false
t.integer "lfs_objects_size", limit: 8, default: 0, null: false t.integer "lfs_objects_size", limit: 8, default: 0, null: false
t.integer "build_artifacts_size", limit: 8, default: 0, null: false t.integer "build_artifacts_size", limit: 8, default: 0, null: false
t.integer "shared_runners_minutes", limit: 8, default: 0, null: false
t.datetime "shared_runners_minutes_last_reset"
end end
add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
...@@ -1497,13 +1509,14 @@ ActiveRecord::Schema.define(version: 20170106172224) do ...@@ -1497,13 +1509,14 @@ ActiveRecord::Schema.define(version: 20170106172224) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade
add_foreign_key "path_locks", "projects" add_foreign_key "path_locks", "projects"
add_foreign_key "path_locks", "users" add_foreign_key "path_locks", "users"
add_foreign_key "personal_access_tokens", "users" add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id"
add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_merge_access_levels", "users" add_foreign_key "protected_branch_merge_access_levels", "users"
add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id" add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id"
......
...@@ -16,7 +16,7 @@ module Ci ...@@ -16,7 +16,7 @@ module Ci
not_found! unless current_runner.active? not_found! unless current_runner.active?
update_runner_info update_runner_info
build = Ci::RegisterBuildService.new.execute(current_runner) build = Ci::RegisterBuildService.new(current_runner).execute
if build if build
Gitlab::Metrics.add_event(:build_found, Gitlab::Metrics.add_event(:build_found,
......
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
included do included do
scope :public_only, -> { where(visibility_level: PUBLIC) } scope :public_only, -> { where(visibility_level: PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only } scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only }
end end
......
# This module is based on: https://gist.github.com/bcardarella/5735987
module Prependable
include ActiveSupport::Concern
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
def prepend_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
return false
else
return false if base < self
super
base.singleton_class.send(:prepend, const_get('ClassMethods')) if const_defined?(:ClassMethods)
@_dependencies.each { |dep| base.send(:include, dep) }
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
alias_method :prepended, :included
end
...@@ -16,6 +16,10 @@ FactoryGirl.define do ...@@ -16,6 +16,10 @@ FactoryGirl.define do
is_shared true is_shared true
end end
trait :specific do
is_shared false
end
trait :inactive do trait :inactive do
active false active false
end end
......
FactoryGirl.define do FactoryGirl.define do
factory :group do factory :group, class: Group, parent: :namespace do
sequence(:name) { |n| "group#{n}" } sequence(:name) { |n| "group#{n}" }
path { name.downcase.gsub(/\s/, '_') } path { name.downcase.gsub(/\s/, '_') }
type 'Group' type 'Group'
owner nil
trait :public do trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC visibility_level Gitlab::VisibilityLevel::PUBLIC
......
FactoryGirl.define do
factory :namespace_statistics do
namespace factory: :namespace
end
end
...@@ -3,5 +3,23 @@ FactoryGirl.define do ...@@ -3,5 +3,23 @@ FactoryGirl.define do
sequence(:name) { |n| "namespace#{n}" } sequence(:name) { |n| "namespace#{n}" }
path { name.downcase.gsub(/\s/, '_') } path { name.downcase.gsub(/\s/, '_') }
owner owner
trait :with_build_minutes do
namespace_statistics factory: :namespace_statistics, shared_runners_minutes: 400
end
trait :with_build_minutes_limit do
shared_runners_minutes_limit 500
end
trait :with_not_used_build_minutes_limit do
namespace_statistics factory: :namespace_statistics, shared_runners_minutes: 300
shared_runners_minutes_limit 500
end
trait :with_used_build_minutes_limit do
namespace_statistics factory: :namespace_statistics, shared_runners_minutes: 1000
shared_runners_minutes_limit 500
end
end end
end end
require 'spec_helper'
feature 'CI shared runner settings', feature: true do
let(:admin) { create(:admin) }
let(:group) { create(:group, :with_build_minutes) }
let!(:project) { create(:project, namespace: group, shared_runners_enabled: true) }
before do
login_as(admin)
end
context 'without global shared runners quota' do
scenario 'should display ratio with global quota' do
visit_admin_group_path
expect(page).to have_content("Build minutes quota: 400 / Unlimited")
expect(page).to have_selector('.shared_runners_limit_disabled')
end
end
context 'with global shared runners quota' do
before do
set_admin_shared_runners_minutes 500
end
scenario 'should display ratio with global quota' do
visit_admin_group_path
expect(page).to have_content("Build minutes quota: 400 / 500")
expect(page).to have_selector('.shared_runners_limit_under_quota')
end
scenario 'should display new ratio with overridden group quota' do
set_group_shared_runners_minutes 300
visit_admin_group_path
expect(page).to have_content("Build minutes quota: 400 / 300")
expect(page).to have_selector('.shared_runners_limit_over_quota')
end
scenario 'should display unlimited ratio with overridden group quota' do
set_group_shared_runners_minutes 0
visit_admin_group_path
expect(page).to have_content("Build minutes quota: 400 / Unlimited")
expect(page).to have_selector('.shared_runners_limit_disabled')
end
end
def set_admin_shared_runners_minutes(limit)
visit admin_application_settings_path
fill_in 'application_setting_shared_runners_minutes', with: limit
click_on 'Save'
end
def set_group_shared_runners_minutes(limit)
visit admin_group_edit_path(group)
fill_in 'group_shared_runners_minutes_limit', with: limit
click_on 'Save changes'
end
def visit_admin_group_path
visit admin_group_path(group)
end
end
require 'spec_helper'
feature 'CI shared runner limits', feature: true do
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: group, shared_runners_enabled: true) }
let(:group) { create(:group) }
before do
login_as(user)
end
context 'when project member' do
before do
group.add_developer(user)
end
context 'without limit' do
scenario 'it does not display a warning message on project homepage' do
visit_project_home
expect_no_quota_exceeded_alert
end
scenario 'it does not display a warning message on pipelines page' do
visit_project_pipelines
expect_no_quota_exceeded_alert
end
end
context 'when limit is defined' do
context 'when limit is exceeded' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
scenario 'it displays a warning message on project homepage' do
visit_project_home
expect_quota_exceeded_alert("#{group.name} has exceeded their build minutes quota.")
end
scenario 'it displays a warning message on pipelines page' do
visit_project_pipelines
expect_quota_exceeded_alert("#{group.name} has exceeded their build minutes quota.")
end
end
context 'when limit not yet exceeded' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
scenario 'it does not display a warning message on project homepage' do
visit_project_home
expect_no_quota_exceeded_alert
end
scenario 'it does not display a warning message on pipelines page' do
visit_project_pipelines
expect_no_quota_exceeded_alert
end
end
context 'when minutes are not yet set' do
let(:group) { create(:group, :with_build_minutes_limit) }
scenario 'it does not display a warning message on project homepage' do
visit_project_home
expect_no_quota_exceeded_alert
end
scenario 'it does not display a warning message on pipelines page' do
visit_project_pipelines
expect_no_quota_exceeded_alert
end
end
end
end
context 'when not a project member' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
context 'when limit is defined and limit is exceeded' do
scenario 'it does not display a warning message on project homepage' do
visit_project_home
expect_no_quota_exceeded_alert
end
scenario 'it does not display a warning message on pipelines page' do
visit_project_pipelines
expect_no_quota_exceeded_alert
end
end
end
def visit_project_home
visit namespace_project_path(project.namespace, project)
end
def visit_project_pipelines
visit namespace_project_pipelines_path(project.namespace, project)
end
def expect_quota_exceeded_alert(message = nil)
expect(page).to have_selector('.shared-runner-quota-message', count: 1)
expect(page.find('.shared-runner-quota-message')).to have_content(message) unless message.nil?
end
def expect_no_quota_exceeded_alert
expect(page).not_to have_selector('.shared-runner-quota-message')
end
end
require 'spec_helper'
feature 'Groups > Pipeline Quota', feature: true do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:project) { create(:empty_project, namespace: group, shared_runners_enabled: true) }
before do
group.add_owner(user)
login_with(user)
end
context 'with no quota' do
let(:group) { create(:group, :with_build_minutes) }
it 'is not linked within the group settings dropdown' do
visit group_path(group)
page.within('.layout-nav') do
expect(page).not_to have_selector(:link_or_button, 'Pipeline Quota')
end
end
it 'shows correct group quota info' do
visit_pipeline_quota_page
page.within('.pipeline-quota') do
expect(page).to have_content("400 / Unlimited minutes")
expect(page).to have_selector('.progress-bar-success')
end
end
end
context 'with no projects using shared runners' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
let!(:project) { create(:empty_project, namespace: group, shared_runners_enabled: false) }
it 'is not linked within the group settings dropdown' do
visit group_path(group)
page.within('.layout-nav') do
expect(page).not_to have_selector(:link_or_button, 'Pipeline Quota')
end
end
it 'shows correct group quota info' do
visit_pipeline_quota_page
page.within('.pipeline-quota') do
expect(page).to have_content("300 / Unlimited minutes")
expect(page).to have_selector('.progress-bar-success')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content('This group has no projects which use shared runners')
end
end
end
context 'minutes under quota' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
it 'is linked within the group settings dropdown' do
visit group_path(group)
page.within('.layout-nav') do
expect(page).to have_selector(:link_or_button, 'Pipeline Quota')
end
end
it 'shows correct group quota info' do
visit_pipeline_quota_page
page.within('.pipeline-quota') do
expect(page).to have_content("300 / 500 minutes")
expect(page).to have_content("60% used")
expect(page).to have_selector('.progress-bar-success')
end
end
end
context 'minutes over quota' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
let!(:other_project) { create(:empty_project, namespace: group, shared_runners_enabled: false) }
it 'is linked within the group settings dropdown' do
visit group_path(group)
page.within('.layout-nav') do
expect(page).to have_selector(:link_or_button, 'Pipeline Quota')
end
end
it 'shows correct group quota and projects info' do
visit_pipeline_quota_page
page.within('.pipeline-quota') do
expect(page).to have_content("1000 / 500 minutes")
expect(page).to have_content("200% used")
expect(page).to have_selector('.progress-bar-danger')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content(project.name)
expect(page).not_to have_content(other_project.name)
end
end
end
def visit_pipeline_quota_page
visit group_pipeline_quota_path(group)
end
end
...@@ -264,6 +264,17 @@ feature 'Builds', :feature do ...@@ -264,6 +264,17 @@ feature 'Builds', :feature do
end end
end end
end end
context 'build project is over shared runners limit' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
let(:project) { create(:project, namespace: group, shared_runners_enabled: true) }
it 'displays a warning message' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_content('You have used all your shared runner minutes')
end
end
end end
describe "POST /:project/builds/:id/cancel" do describe "POST /:project/builds/:id/cancel" do
......
require 'spec_helper'
describe Prependable do
subject { FooObject }
context 'class methods' do
it "has a method" do
expect(subject).to respond_to(:class_value)
end
it 'can execute a method' do
expect(subject.class_value).to eq(200)
end
end
context 'instance methods' do
it "has a method" do
expect(subject.new).to respond_to(:value)
end
it 'chains a method execution' do
expect(subject.new.value).to eq(100)
end
end
module Foo
extend Prependable
prepended do
def self.class_value
20
end
end
def value
super * 10
end
end
class FooObject
prepend Foo
def value
10
end
end
end
require 'spec_helper'
describe Ci::Build, models: true do
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_pipeline, project: project,
sha: project.commit.id,
ref: project.default_branch,
status: 'success')
end
let(:build) { create(:ci_build, pipeline: pipeline) }
describe '#shared_runners_minutes_limit_enabled?' do
subject { build.shared_runners_minutes_limit_enabled? }
context 'for shared runner' do
before do
build.runner = create(:ci_runner, :shared)
end
it do
expect(build.project).to receive(:shared_runners_minutes_limit_enabled?).
and_return(true)
is_expected.to be_truthy
end
end
context 'with specific runner' do
before do
build.runner = create(:ci_runner, :specific)
end
it { is_expected.to be_falsey }
end
context 'without runner' do
it { is_expected.to be_falsey }
end
end
context 'updates build minutes' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
%w(success drop cancel).each do |event|
it "for event #{event}" do
expect(UpdateBuildMinutesService).
to receive(:new).and_call_original
build.public_send(event)
end
end
end
end
require 'spec_helper'
describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
it { is_expected.to have_one(:namespace_statistics).dependent(:destroy) }
it { is_expected.to delegate_method(:shared_runners_minutes).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_minutes_last_reset).to(:namespace_statistics) }
describe '#shared_runners_enabled?' do
subject { namespace.shared_runners_enabled? }
context 'without projects' do
it { is_expected.to be_falsey }
end
context 'with project' do
context 'and disabled shared runners' do
let!(:project) do
create(:empty_project,
namespace: namespace,
shared_runners_enabled: false)
end
it { is_expected.to be_falsey }
end
context 'and enabled shared runners' do
let!(:project) do
create(:empty_project,
namespace: namespace,
shared_runners_enabled: true)
end
it { is_expected.to be_truthy }
end
end
end
describe '#actual_shared_runners_minutes_limit' do
subject { namespace.actual_shared_runners_minutes_limit }
context 'when no limit defined' do
it { is_expected.to be_zero }
end
context 'when application settings limit is set' do
before do
stub_application_setting(shared_runners_minutes: 1000)
end
it 'returns global limit' do
is_expected.to eq(1000)
end
context 'when namespace limit is set' do
before do
namespace.shared_runners_minutes_limit = 500
end
it 'returns namespace limit' do
is_expected.to eq(500)
end
end
end
end
describe '#shared_runners_minutes_limit_enabled?' do
subject { namespace.shared_runners_minutes_limit_enabled? }
context 'with project' do
let!(:project) do
create(:empty_project,
namespace: namespace,
shared_runners_enabled: true)
end
context 'when no limit defined' do
it { is_expected.to be_falsey }
end
context 'when limit is defined' do
before do
namespace.shared_runners_minutes_limit = 500
end
it { is_expected.to be_truthy }
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? }
context 'with project' do
let!(:project) do
create(:empty_project,
namespace: namespace,
shared_runners_enabled: true)
end
context 'when limit is defined' do
context 'when limit is used' do
let(:namespace) { create(:namespace, :with_used_build_minutes_limit) }
it { is_expected.to be_truthy }
end
context 'when limit not yet used' do
let(:namespace) { create(:namespace, :with_not_used_build_minutes_limit) }
it { is_expected.to be_falsey }
end
context 'when minutes are not yet set' do
it { is_expected.to be_falsey }
end
end
context 'without limit' do
let(:namespace) { create(:namespace, :with_build_minutes_limit) }
it { is_expected.to be_falsey }
end
end
context 'without project' do
it { is_expected.to be_falsey }
end
end
end
require 'spec_helper'
describe Project, models: true do
describe 'associations' do
it { is_expected.to delegate_method(:shared_runners_minutes).to(:statistics) }
it { is_expected.to delegate_method(:shared_runners_minutes_last_reset).to(:statistics) }
it { is_expected.to delegate_method(:actual_shared_runners_minutes_limit).to(:namespace) }
it { is_expected.to delegate_method(:shared_runners_minutes_limit_enabled?).to(:namespace) }
it { is_expected.to delegate_method(:shared_runners_minutes_used?).to(:namespace) }
end
describe '#any_runners_limit' do
let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
let(:specific_runner) { create(:ci_runner) }
let(:shared_runner) { create(:ci_runner, :shared) }
context 'for shared runners enabled' do
let(:shared_runners_enabled) { true }
before do
shared_runner
end
it 'has a shared runner' do
expect(project.any_runners?).to be_truthy
end
it 'checks the presence of shared runner' do
expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
end
context 'with used build minutes' do
let(:namespace) { create(:namespace, :with_used_build_minutes_limit) }
let(:project) do
create(:empty_project,
namespace: namespace,
shared_runners_enabled: shared_runners_enabled)
end
it 'does not have a shared runner' do
expect(project.any_runners?).to be_falsey
end
end
end
end
describe '#shared_runners_available?' do
subject { project.shared_runners_available? }
context 'with used build minutes' do
let(:namespace) { create(:namespace, :with_used_build_minutes_limit) }
let(:project) do
create(:empty_project,
namespace: namespace,
shared_runners_enabled: true)
end
before do
expect(namespace).to receive(:shared_runners_minutes_used?).and_call_original
end
it 'shared runners are not available' do
expect(project.shared_runners_available?).to be_falsey
end
end
end
describe '#shared_runners_minutes_limit_enabled?' do
let(:project) { create(:empty_project) }
subject { project.shared_runners_minutes_limit_enabled? }
before do
allow(project.namespace).to receive(:shared_runners_minutes_limit_enabled?).
and_return(true)
end
context 'with shared runners enabled' do
before do
project.shared_runners_enabled = true
end
context 'for public project' do
before do
project.visibility_level = Project::PUBLIC
end
it { is_expected.to be_falsey }
end
context 'for internal project' do
before do
project.visibility_level = Project::INTERNAL
end
it { is_expected.to be_truthy }
end
context 'for private project' do
before do
project.visibility_level = Project::INTERNAL
end
it { is_expected.to be_truthy }
end
end
context 'without shared runners' do
before do
project.shared_runners_enabled = false
end
it { is_expected.to be_falsey }
end
end
end
...@@ -81,13 +81,19 @@ describe Group, models: true do ...@@ -81,13 +81,19 @@ describe Group, models: true do
describe 'public_only' do describe 'public_only' do
subject { described_class.public_only.to_a } subject { described_class.public_only.to_a }
it{ is_expected.to eq([group]) } it { is_expected.to eq([group]) }
end end
describe 'public_and_internal_only' do describe 'public_and_internal_only' do
subject { described_class.public_and_internal_only.to_a } subject { described_class.public_and_internal_only.to_a }
it{ is_expected.to match_array([group, internal_group]) } it { is_expected.to match_array([group, internal_group]) }
end
describe 'non_public_only' do
subject { described_class.non_public_only.to_a }
it { is_expected.to match_array([private_group, internal_group]) }
end end
end end
......
require 'spec_helper'
describe NamespaceStatistics, models: true do
it { is_expected.to belong_to(:namespace) }
it { is_expected.to validate_presence_of(:namespace) }
end
...@@ -976,6 +976,26 @@ describe Project, models: true do ...@@ -976,6 +976,26 @@ describe Project, models: true do
it { expect(project.builds_enabled?).to be_truthy } it { expect(project.builds_enabled?).to be_truthy }
end end
describe '.with_shared_runners' do
subject { Project.with_shared_runners }
context 'when shared runners are enabled for project' do
let!(:project) { create(:empty_project, shared_runners_enabled: true) }
it "returns a project" do
is_expected.to eq([project])
end
end
context 'when shared runners are disabled for project' do
let!(:project) { create(:empty_project, shared_runners_enabled: false) }
it "returns a project" do
is_expected.to eq([project])
end
end
end
describe '.cached_count', caching: true do describe '.cached_count', caching: true do
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
let!(:project1) { create(:empty_project, :public, group: group) } let!(:project1) { create(:empty_project, :public, group: group) }
...@@ -1118,6 +1138,28 @@ describe Project, models: true do ...@@ -1118,6 +1138,28 @@ describe Project, models: true do
end end
end end
describe '#shared_runners' do
let!(:runner) { create(:ci_runner, :shared) }
subject { project.shared_runners }
context 'when shared runners are enabled for project' do
let!(:project) { create(:empty_project, shared_runners_enabled: true) }
it "returns a list of shared runners" do
is_expected.to eq([runner])
end
end
context 'when shared runners are disabled for project' do
let!(:project) { create(:empty_project, shared_runners_enabled: false) }
it "returns a empty list" do
is_expected.to be_nil
end
end
end
describe '#visibility_level_allowed?' do describe '#visibility_level_allowed?' do
let(:project) { create(:project, :internal) } let(:project) { create(:project, :internal) }
......
...@@ -2,7 +2,6 @@ require 'spec_helper' ...@@ -2,7 +2,6 @@ require 'spec_helper'
module Ci module Ci
describe RegisterBuildService, services: true do describe RegisterBuildService, services: true do
let!(:service) { RegisterBuildService.new }
let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
...@@ -19,29 +18,29 @@ module Ci ...@@ -19,29 +18,29 @@ module Ci
pending_build.tag_list = ["linux"] pending_build.tag_list = ["linux"]
pending_build.save pending_build.save
specific_runner.tag_list = ["linux"] specific_runner.tag_list = ["linux"]
expect(service.execute(specific_runner)).to eq(pending_build) expect(execute(specific_runner)).to eq(pending_build)
end end
it "does not pick build with different tag" do it "does not pick build with different tag" do
pending_build.tag_list = ["linux"] pending_build.tag_list = ["linux"]
pending_build.save pending_build.save
specific_runner.tag_list = ["win32"] specific_runner.tag_list = ["win32"]
expect(service.execute(specific_runner)).to be_falsey expect(execute(specific_runner)).to be_falsey
end end
it "picks build without tag" do it "picks build without tag" do
expect(service.execute(specific_runner)).to eq(pending_build) expect(execute(specific_runner)).to eq(pending_build)
end end
it "does not pick build with tag" do it "does not pick build with tag" do
pending_build.tag_list = ["linux"] pending_build.tag_list = ["linux"]
pending_build.save pending_build.save
expect(service.execute(specific_runner)).to be_falsey expect(execute(specific_runner)).to be_falsey
end end
it "pick build without tag" do it "pick build without tag" do
specific_runner.tag_list = ["win32"] specific_runner.tag_list = ["win32"]
expect(service.execute(specific_runner)).to eq(pending_build) expect(execute(specific_runner)).to eq(pending_build)
end end
end end
...@@ -56,13 +55,13 @@ module Ci ...@@ -56,13 +55,13 @@ module Ci
end end
it 'does not pick a build' do it 'does not pick a build' do
expect(service.execute(shared_runner)).to be_nil expect(execute(shared_runner)).to be_nil
end end
end end
context 'for specific runner' do context 'for specific runner' do
it 'does not pick a build' do it 'does not pick a build' do
expect(service.execute(specific_runner)).to be_nil expect(execute(specific_runner)).to be_nil
end end
end end
end end
...@@ -86,34 +85,34 @@ module Ci ...@@ -86,34 +85,34 @@ module Ci
it 'prefers projects without builds first' do it 'prefers projects without builds first' do
# it gets for one build from each of the projects # it gets for one build from each of the projects
expect(service.execute(shared_runner)).to eq(build1_project1) expect(execute(shared_runner)).to eq(build1_project1)
expect(service.execute(shared_runner)).to eq(build1_project2) expect(execute(shared_runner)).to eq(build1_project2)
expect(service.execute(shared_runner)).to eq(build1_project3) expect(execute(shared_runner)).to eq(build1_project3)
# then it gets a second build from each of the projects # then it gets a second build from each of the projects
expect(service.execute(shared_runner)).to eq(build2_project1) expect(execute(shared_runner)).to eq(build2_project1)
expect(service.execute(shared_runner)).to eq(build2_project2) expect(execute(shared_runner)).to eq(build2_project2)
# in the end the third build # in the end the third build
expect(service.execute(shared_runner)).to eq(build3_project1) expect(execute(shared_runner)).to eq(build3_project1)
end end
it 'equalises number of running builds' do it 'equalises number of running builds' do
# after finishing the first build for project 1, get a second build from the same project # after finishing the first build for project 1, get a second build from the same project
expect(service.execute(shared_runner)).to eq(build1_project1) expect(execute(shared_runner)).to eq(build1_project1)
build1_project1.reload.success build1_project1.reload.success
expect(service.execute(shared_runner)).to eq(build2_project1) expect(execute(shared_runner)).to eq(build2_project1)
expect(service.execute(shared_runner)).to eq(build1_project2) expect(execute(shared_runner)).to eq(build1_project2)
build1_project2.reload.success build1_project2.reload.success
expect(service.execute(shared_runner)).to eq(build2_project2) expect(execute(shared_runner)).to eq(build2_project2)
expect(service.execute(shared_runner)).to eq(build1_project3) expect(execute(shared_runner)).to eq(build1_project3)
expect(service.execute(shared_runner)).to eq(build3_project1) expect(execute(shared_runner)).to eq(build3_project1)
end end
end end
context 'shared runner' do context 'shared runner' do
let(:build) { service.execute(shared_runner) } let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Build) } it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid } it { expect(build).to be_valid }
...@@ -122,7 +121,7 @@ module Ci ...@@ -122,7 +121,7 @@ module Ci
end end
context 'specific runner' do context 'specific runner' do
let(:build) { service.execute(specific_runner) } let(:build) { execute(specific_runner) }
it { expect(build).to be_kind_of(Build) } it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid } it { expect(build).to be_valid }
...@@ -137,13 +136,13 @@ module Ci ...@@ -137,13 +136,13 @@ module Ci
end end
context 'shared runner' do context 'shared runner' do
let(:build) { service.execute(shared_runner) } let(:build) { execute(shared_runner) }
it { expect(build).to be_nil } it { expect(build).to be_nil }
end end
context 'specific runner' do context 'specific runner' do
let(:build) { service.execute(specific_runner) } let(:build) { execute(specific_runner) }
it { expect(build).to be_kind_of(Build) } it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid } it { expect(build).to be_valid }
...@@ -159,17 +158,21 @@ module Ci ...@@ -159,17 +158,21 @@ module Ci
end end
context 'and uses shared runner' do context 'and uses shared runner' do
let(:build) { service.execute(shared_runner) } let(:build) { execute(shared_runner) }
it { expect(build).to be_nil } it { expect(build).to be_nil }
end end
context 'and uses specific runner' do context 'and uses specific runner' do
let(:build) { service.execute(specific_runner) } let(:build) { execute(specific_runner) }
it { expect(build).to be_nil } it { expect(build).to be_nil }
end end
end end
def execute(runner)
described_class.new(runner).execute
end
end end
end end
end end
require 'spec_helper'
module Ci
describe RegisterBuildService, services: true do
let!(:project) { create :empty_project, shared_runners_enabled: false }
let!(:pipeline) { create :ci_empty_pipeline, project: project }
let!(:pending_build) { create :ci_build, pipeline: pipeline }
let(:shared_runner) { create(:ci_runner, :shared) }
describe '#execute' do
context 'for project with shared runners when global minutes limit is set' do
before do
project.update(shared_runners_enabled: true)
stub_application_setting(shared_runners_minutes: 500)
end
context 'allow to pick builds' do
let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Build) }
end
context 'when over the global quota' do
before do
project.namespace.create_namespace_statistics(
shared_runners_minutes: 600)
end
let(:build) { execute(shared_runner) }
it "does not return a build" do
expect(build).to be_nil
end
context 'when project is public' do
before do
project.update(visibility_level: Project::PUBLIC)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
context 'when namespace limit is set to unlimited' do
before do
project.namespace.update(shared_runners_minutes_limit: 0)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
context 'when namespace quota is bigger than a global one' do
before do
project.namespace.update(shared_runners_minutes_limit: 1000)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
end
end
def execute(runner)
described_class.new(runner).execute
end
end
end
end
require 'spec_helper'
describe UpdateBuildMinutesService, services: true do
context '#perform' do
let(:namespace) { create(:namespace) }
let(:project) { create(:empty_project, namespace: namespace) }
let(:pipeline) { create(:ci_pipeline) }
let(:build) do
create(:ci_build, :success,
runner: runner, pipeline: pipeline,
started_at: 2.hours.ago, finished_at: 1.hour.ago)
end
subject { described_class.new(project, nil).execute(build) }
context 'with shared runner' do
let(:runner) { create(:ci_runner, :shared) }
it "creates a metrics and sets duration" do
subject
expect(project.statistics.reload.shared_runners_minutes).
to eq(build.duration.to_i)
expect(namespace.namespace_statistics.reload.shared_runners_minutes).
to eq(build.duration.to_i)
end
context 'when metrics are created' do
before do
project.create_statistics(shared_runners_minutes: 100)
namespace.create_namespace_statistics(shared_runners_minutes: 100)
end
it "updates metrics and adds duration" do
subject
expect(project.statistics.reload.shared_runners_minutes).
to eq(100 + build.duration.to_i)
expect(namespace.namespace_statistics.reload.shared_runners_minutes).
to eq(100 + build.duration.to_i)
end
end
end
context 'for specific runner' do
let(:runner) { create(:ci_runner) }
it "does not create metrics" do
subject
expect(project.statistics).to be_nil
expect(namespace.namespace_statistics).to be_nil
end
end
end
end
require 'spec_helper'
describe ClearSharedRunnersMinutesWorker do
let(:worker) { described_class.new }
describe '#perform' do
before do
expect_any_instance_of(described_class).
to receive(:try_obtain_lease).and_return(true)
end
subject { worker.perform }
context 'when project statistics are defined' do
let(:project) { create(:empty_project) }
let(:statistics) { project.statistics }
before do
statistics.update(shared_runners_minutes: 100)
end
it 'clears counters' do
subject
expect(statistics.reload.shared_runners_minutes).to be_zero
end
it 'resets timer' do
subject
expect(statistics.reload.shared_runners_minutes_last_reset).to be_like_time(Time.now)
end
end
context 'when namespace statistics are defined' do
let!(:statistics) { create(:namespace_statistics, shared_runners_minutes: 100) }
it 'clears counters' do
subject
expect(statistics.reload.shared_runners_minutes).to be_zero
end
it 'resets timer' do
subject
expect(statistics.reload.shared_runners_minutes_last_reset).to be_like_time(Time.now)
end
end
end
end
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