Commit f90c8c62 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'feature/runner-lock-on-project' into 'master'

Make it possible to lock runner on a specific project

Make it possible to lock runner on a specific project.

![Screen_Shot_2016-06-20_at_4.03.08_PM](/uploads/186378643a20106ff0b67b6fd8bd7f28/Screen_Shot_2016-06-20_at_4.03.08_PM.png)

----

![Screen_Shot_2016-06-20_at_9.54.52_PM](/uploads/c479abdffaf19f383bb6b5a42bdd6cc3/Screen_Shot_2016-06-20_at_9.54.52_PM.png)

----

![Screen_Shot_2016-06-20_at_9.56.26_PM](/uploads/6ad838679b0c28a1fe2e20e9224387ea/Screen_Shot_2016-06-20_at_9.56.26_PM.png)

Closes #3407

See merge request !4093
parents 027b07ca cdcbc8d7
...@@ -71,6 +71,7 @@ v 8.9.0 (unreleased) ...@@ -71,6 +71,7 @@ v 8.9.0 (unreleased)
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Validate only and except regexp - Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
- Add support for using Yubikeys (U2F) for two-factor authentication - Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore - Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
...@@ -86,6 +87,7 @@ v 8.9.0 (unreleased) ...@@ -86,6 +87,7 @@ v 8.9.0 (unreleased)
- Make Omniauth providers specs to not modify global configuration - Make Omniauth providers specs to not modify global configuration
- Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir) - Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11 - Make authentication service for Container Registry to be compatible with < Docker 1.11
- Make it possible to lock a runner from being enabled for other projects
- Add Application Setting to configure Container Registry token expire delay (default 5min) - Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav - Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment - Use Knapsack only in CI environment
......
class Admin::RunnerProjectsController < Admin::ApplicationController class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create] before_action :project, only: [:create]
def index
@runner_projects = project.runner_projects.all
@runner_project = project.runner_projects.new
end
def create def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id]) @runner = Ci::Runner.find(params[:runner_project][:runner_id])
if @runner.assign_to(@project, current_user) return head(403) if @runner.is_shared? || @runner.locked?
runner_project = @runner.assign_to(@project, current_user)
if runner_project.persisted?
redirect_to admin_runner_path(@runner) redirect_to admin_runner_path(@runner)
else else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project' redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
......
...@@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController ...@@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id]) @runner = Ci::Runner.find(params[:runner_project][:runner_id])
return head(403) if @runner.is_shared? || @runner.locked?
return head(403) unless current_user.ci_authorized_runners.include?(@runner) return head(403) unless current_user.ci_authorized_runners.include?(@runner)
path = runners_path(project) path = runners_path(project)
runner_project = @runner.assign_to(project, current_user)
if @runner.assign_to(project, current_user) if runner_project.persisted?
redirect_to path redirect_to path
else else
redirect_to path, alert: 'Failed adding runner to project' redirect_to path, alert: 'Failed adding runner to project'
......
...@@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
def index def index
@runners = project.runners.ordered @project_runners = project.runners.ordered
@specific_runners = current_user.ci_authorized_runners. @assignable_runners = current_user.ci_authorized_runners.
where.not(id: project.runners). assignable_for(project).ordered.page(params[:page]).per(20)
ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active @shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all) @shared_runners_count = @shared_runners.count(:all)
end end
......
...@@ -300,18 +300,12 @@ module Ci ...@@ -300,18 +300,12 @@ module Ci
project.valid_runners_token? token project.valid_runners_token? token
end end
def can_be_served?(runner)
return false unless has_tags? || runner.run_untagged?
(tag_list - runner.tag_list).empty?
end
def has_tags? def has_tags?
tag_list.any? tag_list.any?
end end
def any_runners_online? def any_runners_online?
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
end end
def stuck? def stuck?
......
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged] FORM_EDITABLE = %i[description tag_list active run_untagged locked]
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
...@@ -26,6 +26,13 @@ module Ci ...@@ -26,6 +26,13 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end end
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
where(locked: false).
where.not("id IN (#{project.runners.select(:id).to_sql})").specific
end
validate :tag_constraints validate :tag_constraints
acts_as_taggable acts_as_taggable
...@@ -56,7 +63,7 @@ module Ci ...@@ -56,7 +63,7 @@ module Ci
def assign_to(project, current_user = nil) def assign_to(project, current_user = nil)
self.is_shared = false if shared? self.is_shared = false if shared?
self.save self.save
project.runner_projects.create!(runner_id: self.id) project.runner_projects.create(runner_id: self.id)
end end
def display_name def display_name
...@@ -91,6 +98,10 @@ module Ci ...@@ -91,6 +98,10 @@ module Ci
!shared? !shared?
end end
def can_pick?(build)
assignable_for?(build.project) && accepting_tags?(build)
end
def only_for?(project) def only_for?(project)
projects == [project] projects == [project]
end end
...@@ -111,5 +122,13 @@ module Ci ...@@ -111,5 +122,13 @@ module Ci
'can not be empty when runner is not allowed to pick untagged jobs') 'can not be empty when runner is not allowed to pick untagged jobs')
end end
end end
def assignable_for?(project)
!locked? || projects.exists?(id: project.id)
end
def accepting_tags?(build)
(run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
end
end end
end end
...@@ -21,7 +21,7 @@ module Ci ...@@ -21,7 +21,7 @@ module Ci
end end
build = builds.find do |build| build = builds.find do |build|
build.can_be_served?(current_runner) current_runner.can_pick?(build)
end end
if build if build
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.col-md-6 .col-md-6
%h4 Restrict projects for this runner %h4 Restrict projects for this runner
- if @runner.projects.any? - if @runner.projects.any?
%table.table %table.table.assigned-projects
%thead %thead
%tr %tr
%th Assigned projects %th Assigned projects
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
.pull-right .pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
%table.table %table.table.unassigned-projects
%thead %thead
%tr %tr
%th Project %th Project
......
...@@ -12,6 +12,12 @@ ...@@ -12,6 +12,12 @@
.checkbox .checkbox
= f.check_box :run_untagged = f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags %span.light Indicates whether this runner can pick jobs without tags
.form-group
= label :locked, 'Lock to current projects', class: 'control-label'
.col-sm-10
.checkbox
= f.check_box :locked
%span.light When a runner is locked, it cannot be assigned to other projects
.form-group .form-group
= label_tag :token, class: 'control-label' do = label_tag :token, class: 'control-label' do
Token Token
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
%h4 %h4
= runner_status_icon(runner) = runner_status_icon(runner)
%span.monospace %span.monospace
- if @runners.include?(runner) - if @project_runners.include?(runner)
= link_to runner.short_sha, runner_path(runner) = link_to runner.short_sha, runner_path(runner)
- if runner.locked?
= icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
%small %small
= link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn %i.fa.fa-edit.btn
...@@ -11,7 +13,7 @@ ...@@ -11,7 +13,7 @@
= runner.short_sha = runner.short_sha
.pull-right .pull-right
- if @runners.include?(runner) - if @project_runners.include?(runner)
- if runner.belongs_to_one_project? - if runner.belongs_to_one_project?
= link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else - else
......
...@@ -17,13 +17,13 @@ ...@@ -17,13 +17,13 @@
Start runner! Start runner!
- if @runners.any? - if @project_runners.any?
%h4.underlined-title Runners activated for this project %h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners %ul.bordered-list.activated-specific-runners
= render partial: 'runner', collection: @runners, as: :runner = render partial: 'runner', collection: @project_runners, as: :runner
- if @specific_runners.any? - if @assignable_runners.any?
%h4.underlined-title Available specific runners %h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners %ul.bordered-list.available-specific-runners
= render partial: 'runner', collection: @specific_runners, as: :runner = render partial: 'runner', collection: @assignable_runners, as: :runner
= paginate @specific_runners = paginate @assignable_runners
...@@ -22,6 +22,9 @@ ...@@ -22,6 +22,9 @@
%tr %tr
%td Can run untagged jobs %td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No' %td= @runner.run_untagged? ? 'Yes' : 'No'
%tr
%td Locked to this project
%td= @runner.locked? ? 'Yes' : 'No'
%tr %tr
%td Tags %td Tags
%td %td
......
...@@ -295,7 +295,7 @@ Rails.application.routes.draw do ...@@ -295,7 +295,7 @@ Rails.application.routes.draw do
post :repository_check post :repository_check
end end
resources :runner_projects resources :runner_projects, only: [:create, :destroy]
end end
end end
......
class AddLockedToCiRunner < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_column_with_default(:ci_runners, :locked, :boolean,
default: false, allow_null: false)
end
def down
remove_column(:ci_runners, :locked)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def change
add_concurrent_index :ci_runners, :locked
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: 20160617301627) do ActiveRecord::Schema.define(version: 20160620115026) 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"
...@@ -287,9 +287,11 @@ ActiveRecord::Schema.define(version: 20160617301627) do ...@@ -287,9 +287,11 @@ ActiveRecord::Schema.define(version: 20160617301627) do
t.string "platform" t.string "platform"
t.string "architecture" t.string "architecture"
t.boolean "run_untagged", default: true, null: false t.boolean "run_untagged", default: true, null: false
t.boolean "locked", default: false, null: false
end end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"} add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
......
...@@ -96,6 +96,12 @@ To register the runner, run the command below and follow instructions: ...@@ -96,6 +96,12 @@ To register the runner, run the command below and follow instructions:
sudo gitlab-ci-multi-runner register sudo gitlab-ci-multi-runner register
``` ```
### Lock a specific runner from being enabled for other projects
You can configure a runner to assign it exclusively to a project. When a
runner is locked this way, it can no longer be enabled for other projects.
This setting is available on each runner in *Project Settings* > *Runners*.
### Making an existing Shared Runner Specific ### Making an existing Shared Runner Specific
If you are an admin on your GitLab instance, If you are an admin on your GitLab instance,
...@@ -128,7 +134,7 @@ the appropriate dependencies to run Rails test suites. ...@@ -128,7 +134,7 @@ the appropriate dependencies to run Rails test suites.
### Prevent runner with tags from picking jobs without tags ### Prevent runner with tags from picking jobs without tags
You can configure a runner to prevent it from picking jobs with tags when You can configure a runner to prevent it from picking jobs with tags when
the runnner does not have tags assigned. This setting is available on each the runner does not have tags assigned. This setting is available on each
runner in *Project Settings* > *Runners*. runner in *Project Settings* > *Runners*.
### Be careful with sensitive information ### Be careful with sensitive information
......
...@@ -423,6 +423,7 @@ module API ...@@ -423,6 +423,7 @@ module API
class RunnerDetails < Runner class RunnerDetails < Runner
expose :tag_list expose :tag_list
expose :run_untagged expose :run_untagged
expose :locked
expose :version, :revision, :platform, :architecture expose :version, :revision, :platform, :architecture
expose :contacted_at expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
......
...@@ -49,7 +49,7 @@ module API ...@@ -49,7 +49,7 @@ module API
runner = get_runner(params[:id]) runner = get_runner(params[:id])
authenticate_update_runner!(runner) authenticate_update_runner!(runner)
attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged] attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
if runner.update(attrs) if runner.update(attrs)
present runner, with: Entities::RunnerDetails, current_user: current_user present runner, with: Entities::RunnerDetails, current_user: current_user
else else
...@@ -96,9 +96,14 @@ module API ...@@ -96,9 +96,14 @@ module API
runner = get_runner(params[:runner_id]) runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner) authenticate_enable_runner!(runner)
Ci::RunnerProject.create(runner: runner, project: user_project)
runner_project = runner.assign_to(user_project)
if runner_project.persisted?
present runner, with: Entities::Runner present runner, with: Entities::Runner
else
conflict!("Runner was already enabled for this project")
end
end end
# Disable project's runner # Disable project's runner
...@@ -163,6 +168,7 @@ module API ...@@ -163,6 +168,7 @@ module API
def authenticate_enable_runner!(runner) def authenticate_enable_runner!(runner)
forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner is locked") if runner.locked?
return if current_user.is_admin? return if current_user.is_admin?
forbidden!("No access granted") unless user_can_access_runner?(runner) forbidden!("No access granted") unless user_can_access_runner?(runner)
end end
......
...@@ -28,12 +28,9 @@ module Ci ...@@ -28,12 +28,9 @@ module Ci
post "register" do post "register" do
required_attributes! [:token] required_attributes! [:token]
attributes = { description: params[:description], attributes = attributes_for_keys(
tag_list: params[:tag_list] } [:description, :tag_list, :run_untagged, :locked]
)
unless params[:run_untagged].nil?
attributes[:run_untagged] = params[:run_untagged]
end
runner = runner =
if runner_registration_token_valid? if runner_registration_token_valid?
......
...@@ -60,6 +60,40 @@ describe "Admin Runners" do ...@@ -60,6 +60,40 @@ describe "Admin Runners" do
it { expect(page).to have_content(@project1.name_with_namespace) } it { expect(page).to have_content(@project1.name_with_namespace) }
it { expect(page).not_to have_content(@project2.name_with_namespace) } it { expect(page).not_to have_content(@project2.name_with_namespace) }
end end
describe 'enable/create' do
before do
@project1.runners << runner
visit admin_runner_path(runner)
end
it 'enables specific runner for project' do
within '.unassigned-projects' do
click_on 'Enable'
end
assigned_project = page.find('.assigned-projects')
expect(assigned_project).to have_content(@project2.path)
end
end
describe 'disable/destroy' do
before do
@project1.runners << runner
visit admin_runner_path(runner)
end
it 'enables specific runner for project' do
within '.assigned-projects' do
click_on 'Disable'
end
new_runner_project = page.find('.unassigned-projects')
expect(new_runner_project).to have_content(@project1.path)
end
end
end end
describe 'runners registration token' do describe 'runners registration token' do
......
...@@ -36,32 +36,44 @@ describe Ci::Build, models: true do ...@@ -36,32 +36,44 @@ describe Ci::Build, models: true do
subject { build.ignored? } subject { build.ignored? }
context 'if build is not allowed to fail' do context 'if build is not allowed to fail' do
before { build.allow_failure = false } before do
build.allow_failure = false
end
context 'and build.status is success' do context 'and build.status is success' do
before { build.status = 'success' } before do
build.status = 'success'
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'and build.status is failed' do context 'and build.status is failed' do
before { build.status = 'failed' } before do
build.status = 'failed'
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
end end
context 'if build is allowed to fail' do context 'if build is allowed to fail' do
before { build.allow_failure = true } before do
build.allow_failure = true
end
context 'and build.status is success' do context 'and build.status is success' do
before { build.status = 'success' } before do
build.status = 'success'
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'and build.status is failed' do context 'and build.status is failed' do
before { build.status = 'failed' } before do
build.status = 'failed'
end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
...@@ -75,7 +87,9 @@ describe Ci::Build, models: true do ...@@ -75,7 +87,9 @@ describe Ci::Build, models: true do
context 'if build.trace contains text' do context 'if build.trace contains text' do
let(:text) { 'example output' } let(:text) { 'example output' }
before { build.trace = text } before do
build.trace = text
end
it { is_expected.to include(text) } it { is_expected.to include(text) }
it { expect(subject.length).to be >= text.length } it { expect(subject.length).to be >= text.length }
...@@ -188,7 +202,9 @@ describe Ci::Build, models: true do ...@@ -188,7 +202,9 @@ describe Ci::Build, models: true do
] ]
end end
before { build.update_attributes(stage: 'stage') } before do
build.update_attributes(stage: 'stage')
end
it { is_expected.to eq(predefined_variables + yaml_variables) } it { is_expected.to eq(predefined_variables + yaml_variables) }
...@@ -199,7 +215,9 @@ describe Ci::Build, models: true do ...@@ -199,7 +215,9 @@ describe Ci::Build, models: true do
] ]
end end
before { build.update_attributes(tag: true) } before do
build.update_attributes(tag: true)
end
it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) } it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
end end
...@@ -257,57 +275,6 @@ describe Ci::Build, models: true do ...@@ -257,57 +275,6 @@ describe Ci::Build, models: true do
end end
end end
describe '#can_be_served?' do
let(:runner) { create(:ci_runner) }
before { build.project.runners << runner }
context 'when runner does not have tags' do
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
it 'cannot handle build with tags' do
build.tag_list = ['aa']
expect(build.can_be_served?(runner)).to be_falsey
end
end
context 'when runner has tags' do
before { runner.tag_list = ['bb', 'cc'] }
shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do
build.tag_list = ['bb']
expect(build.can_be_served?(runner)).to be_truthy
end
it 'cannot handle build without matching tags' do
build.tag_list = ['aa']
expect(build.can_be_served?(runner)).to be_falsey
end
end
context 'when runner can pick untagged jobs' do
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
it_behaves_like 'tagged build picker'
end
context 'when runner can not pick untagged jobs' do
before { runner.run_untagged = false }
it 'can not handle builds without tags' do
expect(build.can_be_served?(runner)).to be_falsey
end
it_behaves_like 'tagged build picker'
end
end
end
describe '#has_tags?' do describe '#has_tags?' do
context 'when build has tags' do context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) } subject { create(:ci_build, tag_list: ['tag']) }
...@@ -348,7 +315,7 @@ describe Ci::Build, models: true do ...@@ -348,7 +315,7 @@ describe Ci::Build, models: true do
end end
it 'that cannot handle build' do it 'that cannot handle build' do
expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false) expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
is_expected.to be_falsey is_expected.to be_falsey
end end
...@@ -360,7 +327,9 @@ describe Ci::Build, models: true do ...@@ -360,7 +327,9 @@ describe Ci::Build, models: true do
%w(pending).each do |state| %w(pending).each do |state|
context "if commit_status.status is #{state}" do context "if commit_status.status is #{state}" do
before { build.status = state } before do
build.status = state
end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
...@@ -379,7 +348,9 @@ describe Ci::Build, models: true do ...@@ -379,7 +348,9 @@ describe Ci::Build, models: true do
%w(success failed canceled running).each do |state| %w(success failed canceled running).each do |state|
context "if commit_status.status is #{state}" do context "if commit_status.status is #{state}" do
before { build.status = state } before do
build.status = state
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
...@@ -390,7 +361,10 @@ describe Ci::Build, models: true do ...@@ -390,7 +361,10 @@ describe Ci::Build, models: true do
subject { build.artifacts? } subject { build.artifacts? }
context 'artifacts archive does not exist' do context 'artifacts archive does not exist' do
before { build.update_attributes(artifacts_file: nil) } before do
build.update_attributes(artifacts_file: nil)
end
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
end end
...@@ -623,7 +597,9 @@ describe Ci::Build, models: true do ...@@ -623,7 +597,9 @@ describe Ci::Build, models: true do
let!(:build) { create(:ci_build, :trace, :success, :artifacts) } let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
describe '#erase' do describe '#erase' do
before { build.erase(erased_by: user) } before do
build.erase(erased_by: user)
end
context 'erased by user' do context 'erased by user' do
let!(:user) { create(:user, username: 'eraser') } let!(:user) { create(:user, username: 'eraser') }
...@@ -660,7 +636,9 @@ describe Ci::Build, models: true do ...@@ -660,7 +636,9 @@ describe Ci::Build, models: true do
end end
context 'build has been erased' do context 'build has been erased' do
before { build.erase } before do
build.erase
end
it { is_expected.to be true } it { is_expected.to be true }
end end
...@@ -668,7 +646,9 @@ describe Ci::Build, models: true do ...@@ -668,7 +646,9 @@ describe Ci::Build, models: true do
context 'metadata and build trace are not available' do context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) } let!(:build) { create(:ci_build, :success, :artifacts) }
before { build.remove_artifacts_metadata! } before do
build.remove_artifacts_metadata!
end
describe '#erase' do describe '#erase' do
it 'should not raise error' do it 'should not raise error' do
......
...@@ -20,34 +20,36 @@ describe Ci::Runner, models: true do ...@@ -20,34 +20,36 @@ describe Ci::Runner, models: true do
end end
describe '#display_name' do describe '#display_name' do
it 'should return the description if it has a value' do it 'returns the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448' expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
end end
it 'should return the token if it does not have a description' do it 'returns the token if it does not have a description' do
runner = FactoryGirl.create(:ci_runner) runner = FactoryGirl.create(:ci_runner)
expect(runner.display_name).to eq runner.description expect(runner.display_name).to eq runner.description
end end
it 'should return the token if the description is an empty string' do it 'returns the token if the description is an empty string' do
runner = FactoryGirl.build(:ci_runner, description: '', token: 'token') runner = FactoryGirl.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token expect(runner.display_name).to eq runner.token
end end
end end
describe :assign_to do describe '#assign_to' do
let!(:project) { FactoryGirl.create :empty_project } let!(:project) { FactoryGirl.create :empty_project }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) } let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
before { shared_runner.assign_to(project) } before do
shared_runner.assign_to(project)
end
it { expect(shared_runner).to be_specific } it { expect(shared_runner).to be_specific }
it { expect(shared_runner.projects).to eq([project]) } it { expect(shared_runner.projects).to eq([project]) }
it { expect(shared_runner.only_for?(project)).to be_truthy } it { expect(shared_runner.only_for?(project)).to be_truthy }
end end
describe :online do describe '.online' do
subject { Ci::Runner.online } subject { Ci::Runner.online }
before do before do
...@@ -58,60 +60,269 @@ describe Ci::Runner, models: true do ...@@ -58,60 +60,269 @@ describe Ci::Runner, models: true do
it { is_expected.to eq([@runner2])} it { is_expected.to eq([@runner2])}
end end
describe :online? do describe '#online?' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared) } let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
subject { runner.online? } subject { runner.online? }
context 'never contacted' do context 'never contacted' do
before { runner.contacted_at = nil } before do
runner.contacted_at = nil
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'contacted long time ago time' do context 'contacted long time ago time' do
before { runner.contacted_at = 1.year.ago } before do
runner.contacted_at = 1.year.ago
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'contacted 1s ago' do context 'contacted 1s ago' do
before { runner.contacted_at = 1.second.ago } before do
runner.contacted_at = 1.second.ago
end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
end end
describe :status do describe '#can_pick?' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:runner) { create(:ci_runner) }
before do
build.project.runners << runner
end
context 'when runner does not have tags' do
it 'can handle builds without tags' do
expect(runner.can_pick?(build)).to be_truthy
end
it 'cannot handle build with tags' do
build.tag_list = ['aa']
expect(runner.can_pick?(build)).to be_falsey
end
end
context 'when runner has tags' do
before do
runner.tag_list = ['bb', 'cc']
end
shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do
build.tag_list = ['bb']
expect(runner.can_pick?(build)).to be_truthy
end
it 'cannot handle build without matching tags' do
build.tag_list = ['aa']
expect(runner.can_pick?(build)).to be_falsey
end
end
context 'when runner can pick untagged jobs' do
it 'can handle builds without tags' do
expect(runner.can_pick?(build)).to be_truthy
end
it_behaves_like 'tagged build picker'
end
context 'when runner cannot pick untagged jobs' do
before do
runner.run_untagged = false
end
it 'cannot handle builds without tags' do
expect(runner.can_pick?(build)).to be_falsey
end
it_behaves_like 'tagged build picker'
end
end
context 'when runner is locked' do
before do
runner.locked = true
end
shared_examples 'locked build picker' do
context 'when runner cannot pick untagged jobs' do
before do
runner.run_untagged = false
end
it 'cannot handle builds without tags' do
expect(runner.can_pick?(build)).to be_falsey
end
end
context 'when having runner tags' do
before do
runner.tag_list = ['bb', 'cc']
end
it 'cannot handle it for builds without matching tags' do
build.tag_list = ['aa']
expect(runner.can_pick?(build)).to be_falsey
end
end
end
context 'when serving the same project' do
it 'can handle it' do
expect(runner.can_pick?(build)).to be_truthy
end
it_behaves_like 'locked build picker'
context 'when having runner tags' do
before do
runner.tag_list = ['bb', 'cc']
build.tag_list = ['bb']
end
it 'can handle it for matching tags' do
expect(runner.can_pick?(build)).to be_truthy
end
end
end
context 'serving a different project' do
before do
runner.runner_projects.destroy_all
end
it 'cannot handle it' do
expect(runner.can_pick?(build)).to be_falsey
end
it_behaves_like 'locked build picker'
context 'when having runner tags' do
before do
runner.tag_list = ['bb', 'cc']
build.tag_list = ['bb']
end
it 'cannot handle it for matching tags' do
expect(runner.can_pick?(build)).to be_falsey
end
end
end
end
end
describe '#status' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) } let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
subject { runner.status } subject { runner.status }
context 'never connected' do context 'never connected' do
before { runner.contacted_at = nil } before do
runner.contacted_at = nil
end
it { is_expected.to eq(:not_connected) } it { is_expected.to eq(:not_connected) }
end end
context 'contacted 1s ago' do context 'contacted 1s ago' do
before { runner.contacted_at = 1.second.ago } before do
runner.contacted_at = 1.second.ago
end
it { is_expected.to eq(:online) } it { is_expected.to eq(:online) }
end end
context 'contacted long time ago' do context 'contacted long time ago' do
before { runner.contacted_at = 1.year.ago } before do
runner.contacted_at = 1.year.ago
end
it { is_expected.to eq(:offline) } it { is_expected.to eq(:offline) }
end end
context 'inactive' do context 'inactive' do
before { runner.active = false } before do
runner.active = false
end
it { is_expected.to eq(:paused) } it { is_expected.to eq(:paused) }
end end
end end
describe '.assignable_for' do
let(:runner) { create(:ci_runner) }
let(:project) { create(:project) }
let(:another_project) { create(:project) }
before do
project.runners << runner
end
context 'with shared runners' do
before do
runner.update(is_shared: true)
end
context 'does not give owned runner' do
subject { Ci::Runner.assignable_for(project) }
it { is_expected.to be_empty }
end
context 'does not give shared runner' do
subject { Ci::Runner.assignable_for(another_project) }
it { is_expected.to be_empty }
end
end
context 'with unlocked runner' do
context 'does not give owned runner' do
subject { Ci::Runner.assignable_for(project) }
it { is_expected.to be_empty }
end
context 'does give a specific runner' do
subject { Ci::Runner.assignable_for(another_project) }
it { is_expected.to contain_exactly(runner) }
end
end
context 'with locked runner' do
before do
runner.update(locked: true)
end
context 'does not give owned runner' do
subject { Ci::Runner.assignable_for(project) }
it { is_expected.to be_empty }
end
context 'does not give a locked runner' do
subject { Ci::Runner.assignable_for(another_project) }
it { is_expected.to be_empty }
end
end
end
describe "belongs_to_one_project?" do describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do it "returns false if there are two projects runner assigned to" do
runner = FactoryGirl.create(:ci_runner) runner = FactoryGirl.create(:ci_runner)
......
...@@ -187,14 +187,16 @@ describe API::Runners, api: true do ...@@ -187,14 +187,16 @@ describe API::Runners, api: true do
update_runner(shared_runner.id, admin, description: "#{description}_updated", update_runner(shared_runner.id, admin, description: "#{description}_updated",
active: !active, active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql'], tag_list: ['ruby2.1', 'pgsql', 'mysql'],
run_untagged: 'false') run_untagged: 'false',
locked: 'true')
shared_runner.reload shared_runner.reload
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(shared_runner.description).to eq("#{description}_updated") expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active) expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
expect(shared_runner.run_untagged?).to be false expect(shared_runner.run_untagged?).to be(false)
expect(shared_runner.locked?).to be(true)
end end
end end
...@@ -360,11 +362,13 @@ describe API::Runners, api: true do ...@@ -360,11 +362,13 @@ describe API::Runners, api: true do
describe 'POST /projects/:id/runners' do describe 'POST /projects/:id/runners' do
context 'authorized user' do context 'authorized user' do
it 'should enable specific runner' do let(:specific_runner2) do
specific_runner2 = create(:ci_runner).tap do |runner| create(:ci_runner).tap do |runner|
create(:ci_runner_project, runner: runner, project: project2) create(:ci_runner_project, runner: runner, project: project2)
end end
end
it 'should enable specific runner' do
expect do expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change{ project.runners.count }.by(+1) end.to change{ project.runners.count }.by(+1)
...@@ -375,7 +379,17 @@ describe API::Runners, api: true do ...@@ -375,7 +379,17 @@ describe API::Runners, api: true do
expect do expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
end.to change{ project.runners.count }.by(0) end.to change{ project.runners.count }.by(0)
expect(response.status).to eq(201) expect(response.status).to eq(409)
end
it 'should not enable locked runner' do
specific_runner2.update(locked: true)
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change{ project.runners.count }.by(0)
expect(response.status).to eq(403)
end end
it 'should not enable shared runner' do it 'should not enable shared runner' do
......
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