Commit 763ea26d authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'tm/feature/list-runners-jobs-api' into 'master'

Add new API endpoint - list jobs of a specified runner

Closes #39699

See merge request gitlab-org/gitlab-ce!15432
parents 6b0f594c 13a902a9
class RunnerJobsFinder
attr_reader :runner, :params
def initialize(runner, params = {})
@runner = runner
@params = params
end
def execute
items = @runner.builds
items = by_status(items)
items
end
private
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
end
---
title: New API endpoint - list jobs for a specified runner
merge_request: 15432
author:
type: added
......@@ -215,6 +215,91 @@ DELETE /runners/:id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
```
## List runner's jobs
List jobs that are being processed or were processed by specified Runner.
```
GET /runners/:id/jobs
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a runner |
| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running"
```
Example response:
```json
[
{
"id": 2,
"status": "running",
"stage": "test",
"name": "test",
"ref": "master",
"tag": false,
"coverage": null,
"created_at": "2017-11-16T08:50:29.000Z",
"started_at": "2017-11-16T08:51:29.000Z",
"finished_at": "2017-11-16T08:53:29.000Z",
"duration": 120,
"user": {
"id": 1,
"name": "John Doe2",
"username": "user2",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user2",
"created_at": "2017-11-16T18:38:46.000Z",
"bio": null,
"location": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": null
},
"commit": {
"id": "97de212e80737a608d939f648d959671fb0a0142",
"short_id": "97de212e",
"title": "Update configuration\r",
"created_at": "2017-11-16T08:50:28.000Z",
"parent_ids": [
"1b12f15a11fc6e62177bef08f47bc7b5ce50b141",
"498214de67004b1da3d820901307bed2a68a8ef6"
],
"message": "See merge request !123",
"author_name": "John Doe2",
"author_email": "user2@example.org",
"authored_date": "2017-11-16T08:50:27.000Z",
"committer_name": "John Doe2",
"committer_email": "user2@example.org",
"committed_date": "2017-11-16T08:50:27.000Z"
},
"pipeline": {
"id": 2,
"sha": "97de212e80737a608d939f648d959671fb0a0142",
"ref": "master",
"status": "running"
},
"project": {
"id": 1,
"description": null,
"name": "project1",
"name_with_namespace": "John Doe2 / project1",
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2017-11-16T18:38:46.620Z"
}
}
]
```
## List project's runners
List all runners (specific and shared) available in the project. Shared runners
......
......@@ -80,16 +80,21 @@ module API
expose :group_access, as: :group_access_level
end
class BasicProjectDetails < Grape::Entity
expose :id, :description, :default_branch, :tag_list
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
class ProjectIdentity < Grape::Entity
expose :id, :description
expose :name, :name_with_namespace
expose :path, :path_with_namespace
expose :created_at
end
class BasicProjectDetails < ProjectIdentity
expose :default_branch, :tag_list
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
expose :star_count, :forks_count
expose :created_at, :last_activity_at
expose :last_activity_at
end
class Project < BasicProjectDetails
......@@ -827,17 +832,24 @@ module API
expose :id, :sha, :ref, :status
end
class Job < Grape::Entity
class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :duration
expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: Commit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
class Job < JobBasic
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :runner, with: Runner
end
class JobBasicWithProject < JobBasic
expose :project, with: ProjectIdentity
end
class Trigger < Grape::Entity
expose :id
expose :token, :description
......
......@@ -84,6 +84,23 @@ module API
destroy_conditionally!(runner)
end
desc 'List jobs running on a runner' do
success Entities::JobBasicWithProject
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES
use :pagination
end
get ':id/jobs' do
runner = get_runner(params[:id])
authenticate_list_runners_jobs!(runner)
jobs = RunnerJobsFinder.new(runner, params).execute
present paginate(jobs), with: Entities::JobBasicWithProject
end
end
params do
......@@ -192,6 +209,12 @@ module API
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def user_can_access_runner?(runner)
current_user.ci_authorized_runners.exists?(runner.id)
end
......
require 'spec_helper'
describe RunnerJobsFinder do
let(:project) { create(:project) }
let(:runner) { create(:ci_runner, :shared) }
subject { described_class.new(runner, params).execute }
describe '#execute' do
context 'when params is empty' do
let(:params) { {} }
let!(:job) { create(:ci_build, runner: runner, project: project) }
let!(:job1) { create(:ci_build, project: project) }
it 'returns all jobs assigned to Runner' do
is_expected.to match_array(job)
is_expected.not_to match_array(job1)
end
end
context 'when params contains status' do
HasStatus::AVAILABLE_STATUSES.each do |target_status|
context "when status is #{target_status}" do
let(:params) { { status: target_status } }
let!(:job) { create(:ci_build, runner: runner, project: project, status: target_status) }
before do
exception_status = HasStatus::AVAILABLE_STATUSES - [target_status]
create(:ci_build, runner: runner, project: project, status: exception_status.first)
end
it 'returns matched job' do
is_expected.to eq([job])
end
end
end
end
end
end
......@@ -354,6 +354,140 @@ describe API::Runners do
end
end
describe 'GET /runners/:id/jobs' do
set(:job_1) { create(:ci_build) }
let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
let!(:job_4) { create(:ci_build, :running, runner: specific_runner, project: project) }
let!(:job_5) { create(:ci_build, :failed, runner: specific_runner, project: project) }
context 'admin user' do
context 'when runner exists' do
context 'when runner is shared' do
it 'return jobs' do
get api("/runners/#{shared_runner.id}/jobs", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
end
end
context 'when runner is specific' do
it 'return jobs' do
get api("/runners/#{specific_runner.id}/jobs", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
end
end
context 'when valid status is provided' do
it 'return filtered jobs' do
get api("/runners/#{specific_runner.id}/jobs?status=failed", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first).to include('id' => job_5.id)
end
end
context 'when invalid status is provided' do
it 'return 400' do
get api("/runners/#{specific_runner.id}/jobs?status=non-existing", admin)
expect(response).to have_gitlab_http_status(400)
end
end
end
context "when runner doesn't exist" do
it 'returns 404' do
get api('/runners/9999/jobs', admin)
expect(response).to have_gitlab_http_status(404)
end
end
end
context "runner project's administrative user" do
context 'when runner exists' do
context 'when runner is shared' do
it 'returns 403' do
get api("/runners/#{shared_runner.id}/jobs", user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'when runner is specific' do
it 'return jobs' do
get api("/runners/#{specific_runner.id}/jobs", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
end
end
context 'when valid status is provided' do
it 'return filtered jobs' do
get api("/runners/#{specific_runner.id}/jobs?status=failed", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first).to include('id' => job_5.id)
end
end
context 'when invalid status is provided' do
it 'return 400' do
get api("/runners/#{specific_runner.id}/jobs?status=non-existing", user)
expect(response).to have_gitlab_http_status(400)
end
end
end
context "when runner doesn't exist" do
it 'returns 404' do
get api('/runners/9999/jobs', user)
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'other authorized user' do
it 'does not return jobs' do
get api("/runners/#{specific_runner.id}/jobs", user2)
expect(response).to have_gitlab_http_status(403)
end
end
context 'unauthorized user' do
it 'does not return jobs' do
get api("/runners/#{specific_runner.id}/jobs")
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'GET /projects/:id/runners' do
context 'authorized user with master privileges' do
it "returns project's 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