Commit 7c9a5f2d authored by Aishwarya Subramanian's avatar Aishwarya Subramanian

Api to list runners for a group

Adds ability for users to retrieve runners for a group.
It retrieves runners for the group as well
as its ancestor groups.

It includes filters same as the ones for project
runners list api.
parent 537bbde3
......@@ -70,6 +70,16 @@ module Ci
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
}
scope :belonging_to_group, -> (group_id, include_ancestors: false) {
groups = ::Group.where(id: group_id)
if include_ancestors
groups = Gitlab::ObjectHierarchy.new(groups).base_and_ancestors
end
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
}
scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors
......
---
title: Add API endpoint to list runners for a group
merge_request: 26328
author:
type: added
......@@ -471,6 +471,66 @@ DELETE /projects/:id/runners/:runner_id
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/runners/9"
```
## List group's runners
List all runners (specific and shared) available in the group as well it's ancestor groups.
Shared runners are listed if at least one shared runner is defined.
```plaintext
GET /groups/:id/runners
GET /groups/:id/runners?type=group_type
GET /groups/:id/runners?status=active
GET /groups/:id/runners?tag_list=tag1,tag2
```
| Attribute | Type | Required | Description |
|------------|----------------|----------|---------------------|
| `id` | integer | yes | The ID of the group owned by the authenticated user |
| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` |
| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` |
| `tag_list` | string array | no | List of of the runner's tags |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/9/runners"
```
Example response:
```json
[
{
"id": 3,
"description": "Shared",
"ip_address": "127.0.0.1",
"active": true,
"is_shared": true,
"name": "gitlab-runner",
"online": null,
"status": "not_connected"
},
{
"id": 6,
"description": "Test",
"ip_address": "127.0.0.1",
"active": true,
"is_shared": true,
"name": "gitlab-runner",
"online": false,
"status": "offline"
},
{
"id": 8,
"description": "Test 2",
"ip_address": "127.0.0.1",
"active": true,
"is_shared": false,
"name": "gitlab-runner",
"online": null,
"status": "not_connected"
}
]
```
## Register a new Runner
Register a new Runner for the instance.
......
......@@ -249,6 +249,10 @@ module API
authorize! :admin_project, user_project
end
def authorize_admin_group
authorize! :admin_group, user_group
end
def authorize_read_builds!
authorize! :read_build, user_project
end
......
......@@ -150,10 +150,10 @@ module API
end
get ':id/runners' do
runners = Ci::Runner.owned_or_instance_wide(user_project.id)
# scope is deprecated (for project runners), however api documentation still supports it.
# Not including them in `apply_filter` method as it's not supported for group runners
runners = filter_runners(runners, params[:scope])
runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES)
runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
runners = apply_filter(runners, params)
present paginate(runners), with: Entities::Runner
end
......@@ -194,6 +194,31 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authorize_admin_group }
desc 'Get runners available for group' do
success Entities::Runner
end
params do
optional :type, type: String, values: Ci::Runner::AVAILABLE_TYPES,
desc: 'The type of the runners to show'
optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
desc: 'The status of the runners to show'
optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
use :pagination
end
get ':id/runners' do
runners = Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true)
runners = apply_filter(runners, params)
present paginate(runners), with: Entities::Runner
end
end
helpers do
def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES)
return runners unless scope.present?
......@@ -210,6 +235,14 @@ module API
runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
end
def apply_filter(runners, params)
runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES)
runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
runners
end
def get_runner(id)
runner = Ci::Runner.find(id)
not_found!('Runner') unless runner
......
......@@ -838,4 +838,33 @@ describe Ci::Runner do
it { is_expected.to eq(contacted_at_stored) }
end
describe '.belonging_to_group' do
it 'returns the specific group runner' do
group = create(:group)
runner = create(:ci_runner, :group, groups: [group])
unrelated_group = create(:group)
create(:ci_runner, :group, groups: [unrelated_group])
expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner)
end
context 'runner belonging to parent group' do
let_it_be(:parent_group) { create(:group) }
let_it_be(:parent_runner) { create(:ci_runner, :group, groups: [parent_group]) }
let_it_be(:group) { create(:group, parent: parent_group) }
context 'when include_parent option is passed' do
it 'returns the group runner from the parent group' do
expect(described_class.belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner)
end
end
context 'when include_parent option is not passed' do
it 'does not return the group runner from the parent group' do
expect(described_class.belonging_to_group(group.id)).to be_empty
end
end
end
end
end
......@@ -12,7 +12,6 @@ describe API::Runners do
let(:project2) { create(:project, creator_id: user.id) }
let(:group) { create(:group).tap { |group| group.add_owner(user) } }
let(:group2) { create(:group).tap { |group| group.add_owner(user) } }
let!(:shared_runner) { create(:ci_runner, :instance, description: 'Shared runner') }
let!(:project_runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
......@@ -734,6 +733,24 @@ describe API::Runners do
end
end
shared_examples_for 'unauthorized access to runners list' do
context 'authorized user without maintainer privileges' do
it "does not return group's runners" do
get api("/#{entity_type}/#{entity.id}/runners", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'unauthorized user' do
it "does not return project's runners" do
get api("/#{entity_type}/#{entity.id}/runners")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe 'GET /projects/:id/runners' do
context 'authorized user with maintainer privileges' do
it 'returns response status and headers' do
......@@ -813,21 +830,78 @@ describe API::Runners do
end
end
context 'authorized user without maintainer privileges' do
it "does not return project's runners" do
get api("/projects/#{project.id}/runners", user2)
it_behaves_like 'unauthorized access to runners list' do
let(:entity_type) { 'projects' }
let(:entity) { project }
end
end
expect(response).to have_gitlab_http_status(:forbidden)
describe 'GET /groups/:id/runners' do
context 'authorized user with maintainer privileges' do
it 'returns all runners' do
get api("/groups/#{group.id}/runners", user)
expect(json_response).to match_array([
a_hash_including('description' => 'Group runner')
])
end
context 'filter by type' do
it 'returns record when valid and present' do
get api("/groups/#{group.id}/runners?type=group_type", user)
expect(json_response).to match_array([
a_hash_including('description' => 'Group runner')
])
end
context 'unauthorized user' do
it "does not return project's runners" do
get api("/projects/#{project.id}/runners")
it 'returns empty result when type does not match' do
get api("/groups/#{group.id}/runners?type=project_type", user)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response).to be_empty
end
it 'does not filter by invalid type' do
get api("/groups/#{group.id}/runners?type=bogus", user)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'filter runners by status' do
it 'returns runners by valid status' do
create(:ci_runner, :group, :inactive, description: 'Inactive group runner', groups: [group])
get api("/groups/#{group.id}/runners?status=paused", user)
expect(json_response).to match_array([
a_hash_including('description' => 'Inactive group runner')
])
end
it 'does not filter by invalid status' do
get api("/groups/#{group.id}/runners?status=bogus", user)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
it 'filters runners by tag_list' 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])
get api("/groups/#{group.id}/runners?tag_list=tag1,tag2", user)
expect(json_response).to match_array([
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
])
end
end
it_behaves_like 'unauthorized access to runners list' do
let(:entity_type) { 'groups' }
let(:entity) { group }
end
end
describe 'POST /projects/:id/runners' 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