spec/requests/api/ci/runners_spec.rb (1,791 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runners, :aggregate_failures, factory_default: :keep, feature_category: :fleet_visibility do
using RSpec::Parameterized::TableSyntax
let_it_be(:organization) { create_default(:organization) }
let_it_be(:admin) { create(:user, :admin, last_activity_on: Time.current) }
let_it_be(:users) { create_list(:user, 2) }
let_it_be(:group_guest) { create(:user, guest_of: group) }
let_it_be(:group_reporter) { create(:user, reporter_of: group) }
let_it_be(:group_developer) { create(:user, developer_of: group) }
let_it_be(:group_maintainer) { create(:user, maintainer_of: group) }
let_it_be(:group) { create(:group, owners: users.first) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) do
create(:project, creator_id: users.first.id, maintainers: users.first, reporters: users.second)
end
let_it_be(:project2) { create(:project, creator_id: users.first.id, maintainers: users.first) }
let_it_be(:shared_runner, reload: true) { create(:ci_runner, :instance, :with_runner_manager, description: 'Shared runner') }
let_it_be(:project_runner, reload: true) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
let_it_be(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
let_it_be(:group_runner_a) { create(:ci_runner, :group, description: 'Group runner A', groups: [group]) }
let_it_be(:group_runner_b) { create(:ci_runner, :group, description: 'Group runner B', groups: [subgroup]) }
let(:query) { {} }
let(:extra_query_parts) { {} }
let(:query_path) { query.merge(extra_query_parts).to_param }
shared_context 'access token setup' do
let(:current_user) { nil }
let(:pat_user) { users.first }
let(:pat) { create(:personal_access_token, user: pat_user, scopes: [scope]) }
let(:extra_query_parts) { { private_token: pat.token } }
end
shared_examples 'when scope is forbidden' do |forbidden_scopes: []|
where(:scope) { forbidden_scopes }
with_them do
it 'returns 403' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
shared_examples 'when scope is not allowed' do |scopes: []|
where(:scope) { scopes }
with_them do
it 'returns 401' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /runners' do
let(:path) { "/runners?#{query_path}" }
subject(:perform_request) { get api(path, current_user) }
context 'authorized user' do
let(:current_user) { users.first }
it 'returns response status and headers' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
end
it 'returns user available runners' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner'),
a_hash_including('description' => 'Two projects runner'),
a_hash_including('description' => 'Group runner A'),
a_hash_including('description' => 'Group runner B')
]
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner manage_runner]
end
context 'when filtering by scope' do
let(:query) { { scope: :paused } }
before_all do
create(:ci_runner, :project, :paused, description: 'Paused project runner', projects: [project])
end
it 'filters runners by scope' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match_array [
a_hash_including('description' => 'Paused project runner')
]
end
context 'when is invalid' do
let(:query) { { scope: :unknown } }
it 'avoids filtering' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'when filtering by type' do
let(:query) { { type: type } }
context 'with project_type type' do
let(:type) { :project_type }
it 'filters runners by type' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner'),
a_hash_including('description' => 'Two projects runner')
]
end
end
context 'when type is invalid' do
let(:type) { :bogus }
it 'does not filter by invalid type' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'with a paused runner' do
let_it_be(:runner) do
create(:ci_runner, :project, :paused, description: 'Paused project runner', projects: [project])
end
context 'when filtering by paused' do
let(:query) { { paused: true } }
it 'filters runners by paused state' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Paused project runner'))
end
end
context 'when filtering by status' do
let(:query) { { status: :paused } }
it 'filters runners by status' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Paused project runner'))
end
end
context 'when filtering by invalid status' do
let(:query) { { status: :bogus } }
it 'does not filter' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when filtering by tag_list' do
let(:query) { { tag_list: 'tag1,tag2' } }
before_all do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
end
it 'filters runners by tag_list' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Runner tagged with tag1 and tag2', 'active' => true, 'paused' => false)
)
end
end
end
end
context 'unauthorized user' do
let(:current_user) { nil }
it 'does not return runners' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /runners/:id/managers' do
let(:path) { "/runners/#{runner.id}/managers" }
subject(:perform_request) { get api(path, current_user) }
context 'authorized user' do
let(:current_user) { users.first }
context 'when runner has managers' do
let(:runner) { shared_runner }
let(:manager) { runner.runner_managers.first }
it 'returns all managers of the runner' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to contain_exactly(
a_hash_including('id' => manager.id, 'version' => manager.version, 'architecture' => manager.architecture)
)
end
end
context 'when runner does not have managers' do
let(:runner) { project_runner }
it 'returns no managers' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { shared_runner }
it 'does not return runners' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /runners/all' do
let(:path) { "/runners/all?#{query_path}" }
subject(:perform_request) { get api(path, current_user) }
it_behaves_like 'GET request permissions for admin mode'
context 'authorized user' do
context 'with admin privileges', :enable_admin_mode do
let(:current_user) { admin }
it 'returns response status and headers' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
end
it 'returns all runners' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner', 'is_shared' => false, 'active' => true, 'paused' => false, 'runner_type' => 'project_type'),
a_hash_including('description' => 'Two projects runner', 'is_shared' => false, 'runner_type' => 'project_type'),
a_hash_including('description' => 'Group runner A', 'is_shared' => false, 'runner_type' => 'group_type'),
a_hash_including('description' => 'Group runner B', 'is_shared' => false, 'runner_type' => 'group_type'),
a_hash_including('description' => 'Shared runner', 'is_shared' => true, 'runner_type' => 'instance_type')
]
end
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { admin }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner manage_runner]
end
context 'when filtering runners by scope' do
let(:query) { { scope: scope } }
context 'with shared scope' do
let(:scope) { :shared }
it 'filters runners by scope' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Shared runner', 'is_shared' => true)
)
end
end
context 'with specific scope' do
let(:scope) { :specific }
it 'filters runners by scope' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner'),
a_hash_including('description' => 'Two projects runner'),
a_hash_including('description' => 'Group runner A'),
a_hash_including('description' => 'Group runner B')
]
end
end
context 'with invalid scope' do
let(:scope) { :unknown }
it 'avoids filtering' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'when filtering runners by type' do
let(:query) { { type: type } }
context 'with project_type type' do
let(:type) { :project_type }
it 'filters runners by project type' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner'),
a_hash_including('description' => 'Two projects runner')
]
end
end
context 'with group_type type' do
let(:type) { :group_type }
it 'filters runners by group type' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Group runner A'),
a_hash_including('description' => 'Group runner B')
]
end
end
context 'with invalid type' do
let(:type) { :bogus }
it 'does not filter by invalid type' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'with an paused runner' do
let_it_be(:runner) { create(:ci_runner, :project, :paused, description: 'Paused project runner', projects: [project]) }
context 'when filtering runners by paused status' do
let(:query) { { paused: true } }
it 'filters runners by status' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Paused project runner'))
end
end
context 'when filtering runners by status' do
let(:query) { { status: :paused } }
it 'filters runners by status' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Paused project runner'))
end
context 'and status is invalid' do
let(:query) { { status: :bogus } }
it 'does not filter by invalid status' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
context 'when filtering by tag_list' do
let(:query) { { tag_list: 'tag1,tag2' } }
before_all do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: %w[tag2])
end
it 'filters runners by tag_list' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
)
end
end
describe 'with ci_runner_machines' do
before_all do
version_ci_runner = create(:ci_runner, description: 'Runner with machine')
version_16_ci_runner = create(:ci_runner, description: 'Runner with machine version 16')
create(:ci_runner_machine, runner: version_ci_runner, version: '15.0.3')
create(:ci_runner_machine, runner: version_16_ci_runner, version: '16.0.1')
end
context 'when filtering by version_prefix' do
let(:query) { { version_prefix: version_prefix } }
context 'with version_prefix set to "15.0"' do
let(:version_prefix) { '15.0' }
it 'filters runners by version_prefix' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Runner with machine', 'active' => true, 'paused' => false)
)
end
end
context 'with version_prefix set to "16"' do
let(:version_prefix) { '16' }
it 'filters runners by version_prefix' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Runner with machine version 16', 'active' => true, 'paused' => false)
)
end
end
context 'with version_prefix set to "25"' do
let(:version_prefix) { '25' }
it 'filters runners by version_prefix' do
perform_request
expect(json_response).to be_empty
end
end
context 'with version_prefix set to invalid prefix "V15"' do
let(:version_prefix) { 'V15' }
it 'does not filter runners by version_prefix' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
end
context 'without admin privileges' do
let(:current_user) { users.first }
it 'does not return runners list' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'unauthorized user' do
let(:current_user) { nil }
it 'does not return runners' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /runners/:id' do
let(:runner_id) { runner.id }
let(:path) { "/runners/#{runner_id}?#{query_path}" }
subject(:perform_request) { get api(path, current_user) }
it_behaves_like 'GET request permissions for admin mode' do
let(:runner) { project_runner }
end
context 'admin user' do
let(:current_user) { admin }
context 'when runner is shared' do
let(:runner) { shared_runner }
it "returns runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(shared_runner.description)
expect(json_response['maximum_timeout']).to be_nil
expect(json_response['status']).to eq('never_contacted')
expect(json_response['active']).to eq(true)
expect(json_response['paused']).to eq(false)
expect(json_response['maintenance_note']).to be_nil
end
end
context 'when runner is a project runner' do
let(:runner) { project_runner }
it "returns forbidden" do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'with admin mode enabled', :enable_admin_mode do
it "returns runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(runner.description)
end
it "returns the project's details" do
perform_request
expect(json_response['projects'].first['id']).to eq(project.id)
end
end
end
context 'when runner does not exist' do
let(:runner_id) { non_existing_record_id }
let(:runner) { project_runner }
it 'returns 404', :enable_admin_mode do
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
shared_examples 'an endpoint returning expected results' do
context 'when the runner is a group runner' do
let(:runner) { group_runner_a }
it "returns the runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(runner.description)
expect(json_response['groups'].first['id']).to eq(group.id)
end
end
context "runner project's administrative user" do
let(:current_user) { users.first }
context 'when runner is not shared' do
let(:runner) { project_runner }
it "returns runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(runner.description)
end
end
context 'when runner is shared' do
let(:runner) { shared_runner }
it "returns runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(runner.description)
end
end
end
end
context 'authorized user' do
let(:current_user) { users.first }
it_behaves_like 'an endpoint returning expected results'
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner] do
let(:runner) { project_runner }
end
context 'with sufficient scope' do
where(:scope) { %i[manage_runner read_api] }
with_them do
it_behaves_like 'an endpoint returning expected results'
end
end
end
end
context 'other authorized user' do
let(:current_user) { users.second }
let(:runner) { project_runner }
it "does not return project runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { project_runner }
it "does not return project runner's details" do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'PUT /runners/:id' do
let(:runner_id) { runner.id }
let(:path) { "/runners/#{runner_id}?#{query_path}" }
subject(:perform_request) { put api(path, current_user), params: params }
it_behaves_like 'PUT request permissions for admin mode' do
let(:runner) { project_runner }
let(:params) { { description: 'test' } }
end
context 'admin user', :enable_admin_mode do
let(:current_user) { admin }
# see https://gitlab.com/gitlab-org/gitlab-foss/issues/48625
context 'single parameter update' do
let(:runner) { shared_runner }
context 'when changing description' do
let(:params) { { description: "#{runner.description}_updated" } }
it 'updates runner description' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.description).to eq(params[:description])
end
end
context 'when changing active state' do
let(:params) { { active: !runner.active } }
it 'updates runner active state' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.active).to eq(params[:active])
end
end
context 'when changing paused state' do
let(:params) { { paused: runner.active } }
it 'updates runner paused state' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.active).to eq(!params[:paused])
end
# This test ensures that it is possible to update any attribute on a runner that currently fails the
# validation that ensures that there aren't too many tags associated with a runner
context 'when changing unrelated runner attribute on an existing runner with too many tags' do
let(:params) { { active: !runner.active } }
let(:runner) do
build(:ci_runner, :instance, tag_list: (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" })
.tap { |runner| runner.save!(validate: false) }
end
it 'unrelated runner attribute on an existing runner with too many tags' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.active).to eq(params[:active])
end
end
end
context 'when changing tag list' do
let(:params) { { tag_list: %w[ruby2.1 pgsql mysql] } }
it 'updates runner tag list' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
end
end
context 'when changing untagged flag' do
let(:params) { { tag_list: %w[ruby2.1 pgsql mysql], run_untagged: 'false' } }
it 'updates untagged flag' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.run_untagged?).to be(false)
end
end
context 'when changing locked flag' do
let(:params) { { locked: !runner.locked } }
it 'updates locked flag' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.locked?).to be(params[:locked])
end
end
context 'when changing access level' do
let(:params) { { access_level: 'ref_protected' } }
it 'updates access level' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.ref_protected?).to be_truthy
end
end
context 'when changing maximum timeout' do
let(:params) { { maximum_timeout: 1234 } }
it 'updates maximum timeout' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.maximum_timeout).to eq(1234)
end
end
context 'when changing maintenance note' do
let(:params) { { maintenance_note: "#{runner.maintenance_note}_updated" } }
it 'updates maintenance note' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.maintenance_note).to eq(params[:maintenance_note])
end
end
context 'with no parameters' do
let(:params) { {} }
it 'fails with bad request' do
perform_request
runner.reload
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'when runner is shared' do
let(:runner) { shared_runner }
let(:params) do
{
description: "#{runner.description}_updated",
active: !runner.active,
tag_list: %w[ruby2.1 pgsql mysql],
run_untagged: 'false',
locked: 'true',
access_level: 'ref_protected',
maximum_timeout: 1234
}
end
it 'updates runner' do
active = runner.active
runner_queue_value = runner.ensure_runner_queue_value
perform_request
runner.reload
expect(response).to have_gitlab_http_status(:ok)
expect(runner.description).to eq(params[:description])
expect(runner.active).to eq(!active)
expect(runner.tag_list).to match_array(params[:tag_list])
expect(runner.run_untagged?).to be(false)
expect(runner.locked?).to be(true)
expect(runner.ref_protected?).to be_truthy
expect(runner.ensure_runner_queue_value).not_to eq(runner_queue_value)
expect(runner.maximum_timeout).to eq(1234)
end
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { admin }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it 'updates runner' do
perform_request
runner.reload
expect(response).to have_gitlab_http_status(:ok)
expect(runner.description).to eq(params[:description])
end
end
end
end
context 'when runner is not shared' do
let(:runner) { project_runner }
let(:params) { { description: 'test' } }
it 'updates runner' do
description = runner.description
runner_queue_value = runner.ensure_runner_queue_value
perform_request
runner.reload
expect(response).to have_gitlab_http_status(:ok)
expect(runner.description).to eq(params[:description])
expect(runner.description).not_to eq(description)
expect(runner.ensure_runner_queue_value).not_to eq(runner_queue_value)
end
end
context 'when runner id does not exist' do
let(:runner_id) { non_existing_record_id }
let(:params) { { description: 'test' } }
it 'returns 404' do
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'authorized user' do
let(:current_user) { users.first }
let(:params) { { description: 'test' } }
context 'when runner is shared' do
let(:runner) { shared_runner }
it 'does not update runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[manage_runner create_runner read_api]
end
end
context 'when runner is not shared' do
let(:runner) { project_runner }
it 'updates runner description' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.description).to eq(params[:description])
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it 'updates runner description' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(runner.reload.description).to eq(params[:description])
end
end
end
context 'when user does not have access to runner' do
let(:current_user) { users.second }
it 'does not update runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { project_runner }
let(:params) { { description: 'test' } }
it 'does not update project runner' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
expect(runner.reload.description).not_to eq(params[:description])
end
end
end
describe 'DELETE /runners/:id' do
let(:runner_id) { runner.id }
let(:path) { "/runners/#{runner_id}?#{query_path}" }
subject(:perform_request) { delete api(path, current_user) }
it_behaves_like 'DELETE request permissions for admin mode' do
let(:runner) { shared_runner }
end
context 'admin user', :enable_admin_mode do
let(:current_user) { admin }
context 'when runner is shared' do
let(:runner) { shared_runner }
it 'deletes runner' do
expect_next_instance_of(Ci::Runners::UnregisterRunnerService, runner, current_user) do |service|
expect(service).to receive(:execute).once.and_call_original
end
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.instance_type.count }.by(-1)
end
it_behaves_like '412 response' do
let(:request) { api(path, current_user) }
end
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { admin }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it 'deletes runner' do
expect_next_instance_of(Ci::Runners::UnregisterRunnerService, runner, pat_user) do |service|
expect(service).to receive(:execute).once.and_call_original
end
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.instance_type.count }.by(-1)
end
end
end
end
context 'when runner is not shared' do
let(:runner) { project_runner }
it 'deletes used project runner' do
expect_next_instance_of(Ci::Runners::UnregisterRunnerService, runner, current_user) do |service|
expect(service).to receive(:execute).once.and_call_original
end
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
end
end
context 'when runner does not exist' do
let(:runner_id) { non_existing_record_id }
it 'returns 404' do
allow_next_instance_of(Ci::Runners::UnregisterRunnerService) do |service|
expect(service).not_to receive(:execute)
end
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'authorized user' do
let(:current_user) { users.first }
context 'when runner is shared' do
let(:runner) { shared_runner }
it 'does not delete runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[manage_runner create_runner read_api]
end
end
context 'with a project runner' do
let(:runner) { project_runner }
context 'when user does not have access to runner' do
let(:current_user) { users.second }
it 'does not delete runner without access to it' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when runner is associated with more than one project' do
let(:runner) { two_projects_runner }
it 'does not delete project runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when runner is associated with one owned project' do
let(:runner) { project_runner }
it 'deletes project runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it 'deletes project runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
end
end
end
end
it_behaves_like '412 response' do
let(:request) { api(path, current_user) }
end
end
context 'with group runner' do
let(:runner) { group_runner_a }
context 'when user has guest access' do
let(:current_user) { group_guest }
it 'does not delete runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user has reporter access' do
let(:current_user) { group_reporter }
it 'does not delete runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user has developer access' do
let(:current_user) { group_developer }
it 'does not delete runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user has maintainer access' do
let(:current_user) { group_maintainer }
it 'does not delete runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user has owner access' do
let(:current_user) { users.first }
it 'deletes runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.group_type.count }.by(-1)
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it 'deletes group runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.group_type.count }.by(-1)
end
end
end
end
it_behaves_like '412 response' do
let(:request) { api(path, current_user) }
end
end
context 'with inherited group runner' do
let(:runner) { group_runner_b }
context 'when user has owner access' do
let(:current_user) { users.first }
it 'deletes group runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.group_type.count }.by(-1)
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it 'deletes group runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.group_type.count }.by(-1)
end
end
end
end
it_behaves_like '412 response' do
let(:request) { api(path, current_user) }
end
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { project_runner }
it 'does not delete runner' do
allow_next_instance_of(Ci::Runners::UnregisterRunnerService) do |service|
expect(service).not_to receive(:execute)
end
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'POST /runners/:id/reset_authentication_token' do
let(:runner_id) { runner.id }
let(:path) { "/runners/#{runner_id}/reset_authentication_token?#{query_path}" }
subject(:perform_request) { post api(path, current_user) }
shared_examples 'a runner accepting authentication token reset' do
it 'resets runner authentication token' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq({ 'token' => runner.reload.token, 'token_expires_at' => nil })
end.to change { runner.reload.token }
end
end
it_behaves_like 'POST request permissions for admin mode' do
let(:runner) { project_runner }
let(:params) { {} }
end
context 'admin user', :enable_admin_mode do
let(:current_user) { admin }
context 'when runner is shared' do
let(:runner) { shared_runner }
it_behaves_like 'a runner accepting authentication token reset'
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { admin }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it_behaves_like 'a runner accepting authentication token reset'
end
end
end
context 'when runner does not exist' do
let(:runner_id) { non_existing_record_id }
it 'returns 404' do
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'authorized user' do
context 'with project runner' do
let(:runner) { project_runner }
context 'when user does not have access to runner' do
let(:current_user) { users.second }
it 'does not reset runner' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end.not_to change { runner.reload.token }
end
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { users.second }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[manage_runner create_runner read_api]
end
end
context 'when user has access to runner' do
let(:current_user) { users.first }
it_behaves_like 'a runner accepting authentication token reset'
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it_behaves_like 'a runner accepting authentication token reset'
end
end
end
end
context 'with group runner' do
let(:runner) { group_runner_a }
context 'when user has guest access' do
let(:current_user) { group_guest }
it 'does not reset runner authentication token' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end.not_to change { runner.reload.token }
end
end
context 'when user has reporter access' do
let(:current_user) { group_reporter }
it 'does not reset runner authentication token' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end.not_to change { runner.reload.token }
end
end
context 'when user has developer access' do
let(:current_user) { group_developer }
it 'does not reset runner authentication token' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end.not_to change { runner.reload.token }
end
end
context 'when user has maintainer access' do
let(:current_user) { group_maintainer }
it 'does not reset runner authentication token' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end.not_to change { runner.reload.token }
end
end
context 'when user has owner access' do
let(:current_user) { users.first }
it_behaves_like 'a runner accepting authentication token reset'
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner read_api]
context 'with sufficient scope' do
let(:scope) { :manage_runner }
it_behaves_like 'a runner accepting authentication token reset'
end
end
context 'when runner token has expiration time', :freeze_time do
before do
group.update!(runner_token_expiration_interval: 5.days)
end
it 'resets group runner authentication token with owner access with expiration time' do
expect(runner.reload.token_expires_at).to be_nil
expect do
perform_request
runner.reload
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq({ 'token' => runner.token, 'token_expires_at' => runner.token_expires_at.iso8601(3) })
expect(runner.token_expires_at).to eq(5.days.from_now)
end.to change { runner.reload.token }
end
end
end
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { project_runner }
it 'does not reset authentication token' do
expect do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end.not_to change { runner.reload.token }
end
end
end
describe 'GET /runners/:id/jobs' do
let_it_be(:shared_runner_manager1) { create(:ci_runner_machine, runner: shared_runner, system_xid: 'id2') }
let_it_be(:jobs) do
project_runner_manager1 = create(:ci_runner_machine, runner: project_runner, system_xid: 'id1')
project_runner_manager2 = create(:ci_runner_machine, runner: two_projects_runner, system_xid: 'id1')
pipeline_args = { pipeline: create(:ci_pipeline, project: project) }
pipeline2_args = { pipeline: create(:ci_pipeline, project: project2) }
[
create(:ci_build, pipeline: create(:ci_pipeline)),
create(:ci_build, :running, runner_manager: shared_runner_manager1, **pipeline_args),
create(:ci_build, :failed, runner_manager: shared_runner_manager1, **pipeline_args),
create(:ci_build, :running, runner_manager: project_runner_manager1, **pipeline_args),
create(:ci_build, :failed, runner_manager: project_runner_manager1, **pipeline_args),
create(:ci_build, :running, runner_manager: project_runner_manager2, **pipeline_args),
create(:ci_build, :running, runner_manager: project_runner_manager2, **pipeline2_args)
]
end
let(:runner_id) { runner.id }
let(:path) { "/runners/#{runner_id}/jobs?#{query_path}" }
subject(:perform_request) { get api(path, current_user) }
it_behaves_like 'GET request permissions for admin mode' do
let(:runner) { project_runner }
end
context 'admin user', :enable_admin_mode do
let(:current_user) { admin }
context 'when runner exists' do
context 'when runner is shared' do
let(:runner) { shared_runner }
it 'return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[1].id),
a_hash_including('id' => jobs[2].id)
])
end
it_behaves_like 'an endpoint with keyset pagination', invalid_order: nil do
let(:first_record) { jobs[2] }
let(:second_record) { jobs[1] }
let(:api_call) { api(path, current_user) }
end
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { admin }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner]
context 'with sufficient scope' do # rubocop:disable RSpec/MultipleMemoizedHelpers -- Need helpers for scenarios
let(:scope) { :manage_runner }
it 'return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[1].id),
a_hash_including('id' => jobs[2].id)
])
end
end
end
end
context 'when runner is a project runner' do
let(:runner) { project_runner }
it 'return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[3].id),
a_hash_including('id' => jobs[4].id)
])
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner]
context 'with sufficient scope' do # rubocop:disable RSpec/MultipleMemoizedHelpers -- Need helpers for scenarios
let(:scope) { :manage_runner }
it 'return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[3].id),
a_hash_including('id' => jobs[4].id)
])
end
end
end
context 'when user does not have authorization to see all jobs' do
let(:runner) { two_projects_runner }
let(:current_user) { users.second }
before_all do
project.add_guest(users.second)
project2.add_maintainer(users.second)
end
it 'shows only jobs it has permission to see' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([a_hash_including('id' => jobs[6].id)])
end
context 'with request authorized with access token' do
include_context 'access token setup' do
let(:pat_user) { users.second }
end
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner]
context 'with sufficient scope' do # rubocop:disable RSpec/MultipleMemoizedHelpers -- Need helpers for scenarios
let(:scope) { :manage_runner }
it 'shows only jobs it has permission to see' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([a_hash_including('id' => jobs[6].id)])
end
end
end
end
context 'when valid status is provided' do
let(:query) { { status: :failed } }
it 'return filtered jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([a_hash_including('id' => jobs[4].id)])
end
end
context 'when valid order_by is provided' do
let(:query) { { order_by: :id } }
context 'when sort order is not specified' do
it 'return jobs in descending order' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[4].id),
a_hash_including('id' => jobs[3].id)
])
end
end
context 'when sort order is specified as asc' do
let(:query) { { order_by: :id, sort: :asc } }
it 'return jobs sorted in ascending order' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[3].id),
a_hash_including('id' => jobs[4].id)
])
end
end
end
context 'when invalid status is provided' do
let(:query) { { status: 'non-existing' } }
it 'return 400' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when invalid order_by is provided' do
let(:query) { { order_by: 'non-existing' } }
it 'return 400' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when invalid sort is provided' do
let(:query) { { sort: 'non-existing' } }
it 'return 400' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
describe 'eager loading' do
let!(:runner) { shared_runner }
it 'avoids N+1 DB queries', :use_sql_query_cache, :freeze_time do
another_admin = create(:admin, last_activity_on: Time.current) # Avoid noise from Users::ActivityService
pipeline = create(:ci_pipeline, project: project2, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b')
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api(path, current_user)
end
create(:ci_build, :failed, runner: shared_runner, project: project2, pipeline: pipeline)
expect do
get api(path, another_admin)
end.not_to exceed_all_query_limit(control)
end
it 'batches loading of commits' do
project_with_repo = create(:project, :repository)
shared_runner_manager1 = create(:ci_runner_machine, runner: shared_runner, system_xid: 'id1')
pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b')
create(:ci_build, :running, runner_manager: shared_runner_manager1, project: project_with_repo, pipeline: pipeline)
pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478')
create(:ci_build, :failed, runner_manager: shared_runner_manager1, project: project_with_repo, pipeline: pipeline)
pipeline = create(:ci_pipeline, project: project_with_repo, sha: '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
create(:ci_build, :failed, runner_manager: shared_runner_manager1, project: project_with_repo, pipeline: pipeline)
expect_next_instance_of(Repository) do |repo|
expect(repo).to receive(:commits_by).with(oids:
%w[
1a0b36b3cdad1d2ee32457c102a8c0b7056fa863
c1c67abbaf91f624347bb3ae96eabe3a1b742478
]).once.and_call_original
end
get api(path, current_user), params: { per_page: 2, order_by: 'id', sort: 'desc' }
end
end
context "when runner doesn't exist" do
let(:runner_id) { non_existing_record_id }
it 'returns 404' do
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context "runner project's administrative user" do
let(:current_user) { users.first }
context 'when runner exists' do
context 'when runner is shared' do
let(:runner) { shared_runner }
it 'returns 403' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when runner is a project runner' do
let(:runner) { project_runner }
it 'return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[3].id),
a_hash_including('id' => jobs[4].id)
])
end
context 'when valid status is provided' do
let(:query) { { status: :failed } }
it 'return filtered jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to match([
a_hash_including('id' => jobs[4].id)
])
end
end
context 'when invalid status is provided' do
let(:query) { { status: 'non-existing' } }
it 'return 400' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
context "when runner doesn't exist" do
let(:runner_id) { non_existing_record_id }
it 'returns 404' do
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'other authorized user' do
let(:current_user) { users.second }
let(:runner) { shared_runner }
it 'does not return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { shared_runner }
it 'does not return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
context 'with system_id param' do
let(:extra_query_parts) { { system_id: 'id1' } }
let(:current_user) { users.first }
context 'with project runner' do
let(:runner) { project_runner }
it 'returns jobs from the runner manager' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_limited_pagination_headers
expect(response.headers).not_to include('X-Total', 'X-Total-Pages')
expect(json_response).to match([
a_hash_including('id' => jobs[3].id),
a_hash_including('id' => jobs[4].id)
])
end
end
context 'when system_id does not match runner', :enable_admin_mode do
let(:current_user) { admin }
let(:runner) { shared_runner }
it 'does not return jobs' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
end
end
end
shared_examples_for 'unauthorized access to runners list' do
context 'authorized user without maintainer privileges' do
let(:current_user) { users.second }
it "does not return group's runners" do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
let(:current_user) { nil }
it "does not return project's runners" do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /projects/:id/runners' do
let(:path) { "/projects/#{project.id}/runners?#{query_path}" }
subject(:perform_request) { get api(path, current_user) }
context 'admin user', :enable_admin_mode do
let(:current_user) { admin }
it 'returns response status and headers' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
end
end
context 'authorized user with maintainer privileges' do
let(:current_user) { users.first }
it 'returns all runners' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner', 'active' => true, 'paused' => false),
a_hash_including('description' => 'Two projects runner', 'active' => true, 'paused' => false),
a_hash_including('description' => 'Shared runner', 'active' => true, 'paused' => false)
]
end
context 'when filtering by scope' do
let(:query) { { scope: :specific } }
it 'filters runners by scope' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Project runner'),
a_hash_including('description' => 'Two projects runner')
)
end
context 'and scope is unknown' do
let(:query) { { scope: :unknown } }
it 'avoids filtering' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'when filtering by type' do
let(:query) { { type: :project_type } }
it 'filters runners by type' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Project runner'),
a_hash_including('description' => 'Two projects runner')
)
end
context 'and type is invalid' do
let(:query) { { type: :bogus } }
it 'does not filter' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'with a paused runner' do
let_it_be(:runner) { create(:ci_runner, :project, :paused, description: 'Paused project runner', projects: [project]) }
context 'when filtering by paused status' do
let(:query) { { paused: true } }
it 'filters runners by status' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Paused project runner')
)
end
end
context 'when filtering by status' do
let(:query) { { status: :paused } }
it 'filters runners by status' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Paused project runner')
)
end
context 'and status is invalid' do
let(:query) { { status: :bogus } }
it 'does not filter by invalid status' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
context 'when filtering by tag_list' do
let(:query) { { tag_list: 'tag1,tag2' } }
before_all do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: %w[tag2])
end
it 'filters runners by tag_list' do
perform_request
expect(json_response).to contain_exactly(
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
)
end
end
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner manage_runner]
context 'with sufficient scope' do
let(:scope) { :read_api }
it 'returns all runners' do
perform_request
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner', 'active' => true, 'paused' => false),
a_hash_including('description' => 'Two projects runner', 'active' => true, 'paused' => false),
a_hash_including('description' => 'Shared runner', 'active' => true, 'paused' => false)
]
end
end
end
it_behaves_like 'unauthorized access to runners list'
end
describe 'GET /groups/:id/runners' do
let(:path) { "/groups/#{group.id}/runners?#{query_path}" }
subject(:perform_request) { get api(path, current_user) }
context 'authorized user with maintainer privileges' do
let(:current_user) { users.first }
it 'returns all runners' do
perform_request
expect(json_response).to match_array(
[
a_hash_including('description' => 'Group runner A', 'active' => true, 'paused' => false),
a_hash_including('description' => 'Shared runner', 'active' => true, 'paused' => false)
])
end
context 'filter by type' do
let(:query) { { type: type } }
context 'with type group_type' do
let(:type) { :group_type }
it 'returns group runners' do
perform_request
expect(json_response).to match_array([a_hash_including('description' => 'Group runner A')])
end
end
context 'with type instance_type' do
let(:type) { :instance_type }
it 'returns instance runners' do
perform_request
expect(json_response).to match_array([a_hash_including('description' => 'Shared runner')])
end
end
# TODO: Remove when REST API v5 is implemented (https://gitlab.com/gitlab-org/gitlab/-/issues/351466)
context 'with type project_type' do
let(:type) { :project_type }
it 'returns empty result when type does not match' do
perform_request
expect(json_response).to be_empty
end
end
context 'with invalid type' do
let(:type) { :bogus }
it 'does not filter by invalid type' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
context 'with a paused runner' do
let_it_be(:runner) { create(:ci_runner, :group, :paused, description: 'Paused group runner', groups: [group]) }
context 'when filtering by paused status' do
let(:query) { { paused: true } }
it 'filters runners by status' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Paused group runner'))
end
end
context 'when filtering by status' do
let(:query) { { status: :paused } }
it 'returns runners by valid status' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Paused group runner'))
end
context 'and status is invalid' do
let(:query) { { status: :bogus } }
it 'does not filter by invalid status' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
context 'when filtering by tag_list' do
let(:query) { { tag_list: 'tag1,tag2' } }
before_all do
create(:ci_runner, :group, description: 'Runner tagged with tag1 and tag2', groups: [group], tag_list: %w[tag1 tag2])
create(:ci_runner, :group, description: 'Runner tagged with tag2', groups: [group], tag_list: %w[tag1])
end
it 'filters runners by tag_list' do
perform_request
expect(json_response).to contain_exactly(a_hash_including('description' => 'Runner tagged with tag1 and tag2'))
end
end
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is forbidden', forbidden_scopes: %i[create_runner manage_runner]
context 'with sufficient scope' do
let(:scope) { :read_api }
it 'returns all runners' do
perform_request
expect(json_response).to match_array(
[
a_hash_including('description' => 'Group runner A', 'active' => true, 'paused' => false),
a_hash_including('description' => 'Shared runner', 'active' => true, 'paused' => false)
])
end
end
end
it_behaves_like 'unauthorized access to runners list'
end
describe 'POST /projects/:id/runners' do
let(:params) { { runner_id: runner.id } }
let(:path) { "/projects/#{project.id}/runners" }
subject(:perform_request) { post api(path, current_user), params: params }
it_behaves_like 'POST request permissions for admin mode' do
let!(:new_project_runner) { create(:ci_runner, :project, projects: [project2]) }
let(:params) { { runner_id: new_project_runner.id } }
let(:failed_status_code) { :not_found }
end
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
let(:current_user) { users.first }
let(:runner) { project_runner2 }
it 'assigns project runner' do
expect { perform_request }.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
context 'when assigning already assigned runner' do
let(:runner) { project_runner }
it 'avoids changes' do
expect { perform_request }.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when assigning locked runner' do
let(:runner) { project_runner2 }
before_all do
project_runner2.update!(locked: true)
end
it 'does not assign runner' do
expect { perform_request }.not_to change { project.runners.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when assigning shared runner' do
let(:runner) { shared_runner }
it 'does not assign runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when assigning group runner' do
let(:runner) { group_runner_a }
it 'does not assign runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'user is admin', :enable_admin_mode do
let(:current_user) { admin }
context 'when project runner is used' do
let!(:new_project_runner) { create(:ci_runner, :project, projects: [project2]) }
let(:runner) { new_project_runner }
it 'assigns any project runner' do
expect { perform_request }.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
context 'when it exceeds the application limits' do
before do
create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
end
it 'does not assign runner' do
expect { perform_request }.not_to change { project.runners.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
context 'with request authorized with access token' do
include_context 'access token setup'
where(:scope) { %i[create_runner manage_runner] }
it_behaves_like 'when scope is not allowed', scopes: %i[create_runner manage_runner]
end
context 'when no runner_id param is provided' do
let(:params) { {} }
it 'raises an error' do
perform_request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when user does not have permissions' do
let(:current_user) { users.second }
let(:runner) { project_runner }
it 'does not assign runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'user is not admin and does not have access to project runner' do
let_it_be(:new_project_runner) { create(:ci_runner, :project, projects: [project]) }
let(:runner) { new_project_runner }
let(:current_user) { create(:user, guest_of: project) }
it 'does not assign runner' do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
let(:current_user) { nil }
let(:runner) { project_runner }
it 'does not assign runner' do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'DELETE /projects/:id/runners/:runner_id' do
let(:runner_id) { two_projects_runner.id }
let(:project_to_delete_from) { project2 }
let(:path) { "/projects/#{project_to_delete_from.id}/runners/#{runner_id}" }
subject(:perform_request) { delete api(path, current_user) }
context 'authorized user' do
let(:current_user) { users.first }
context 'when runner have more than one associated project' do
let(:runner_id) { two_projects_runner.id }
it "unassigns project's runner", :aggregate_failures do
expect { perform_request }.to change { project_to_delete_from.runners.count }.by(-1)
expect(response).to have_gitlab_http_status(:no_content)
end
context 'when runner is unassigned from project owner' do
let(:project_to_delete_from) { project }
it "does not unassign project's runner" do
expect { perform_request }.not_to change { project_to_delete_from.runners.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
it_behaves_like '412 response' do
let(:request) { api(path, current_user) }
end
context 'with request authorized with access token' do
include_context 'access token setup'
it_behaves_like 'when scope is not allowed', scopes: %i[create_runner manage_runner]
end
end
context 'when runner have a single associated project' do
let(:runner_id) { project_runner.id }
let(:project_to_delete_from) { project }
it "does not unassign project's runner" do
expect { perform_request }.not_to change { project_to_delete_from.runners.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when runner is not found' do
let(:runner_id) { non_existing_record_id }
it 'returns 404' do
perform_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'authorized user without permissions' do
let(:current_user) { create(:user, developer_of: project_to_delete_from) }
it "does not unassign project's runner" do
perform_request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
let(:current_user) { nil }
it "does not unassign project's runner" do
perform_request
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end