Commit ad899ebd authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'feature/api/add-bridge-api-endpoint' into 'master'

Implemented #bridge API endpoint

Closes #207996

See merge request gitlab-org/gitlab!31370
parents 2071b181 7926c8d0
......@@ -16,6 +16,9 @@ module Ci
has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
foreign_key: :source_job_id
has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id
has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
validates :ref, presence: true
# rubocop:disable Cop/ActiveRecordSerialize
......
......@@ -41,6 +41,7 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :job_artifacts, through: :builds
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
......
---
title: Add API endpoint for listing bridge jobs.
merge_request: 31370
author: Abhijith Sivarajan
type: added
......@@ -269,6 +269,90 @@ Example of response
]
```
## List pipeline bridges
Get a list of bridge jobs for a pipeline.
```plaintext
GET /projects/:id/pipelines/:pipeline_id/bridges
```
| Attribute | Type | Required | Description |
|---------------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `pipeline_id` | integer | yes | ID of a pipeline. |
| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/bridges?scope[]=pending&scope[]=running'
```
Example of response
```json
[
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.802Z",
"started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:58:27.895Z",
"duration": 240,
"id": 7,
"name": "teaspoon",
"pipeline": {
"id": 6,
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending",
"created_at": "2015-12-24T15:50:16.123Z",
"updated_at": "2015-12-24T18:00:44.432Z",
"web_url": "https://example.com/foo/bar/pipelines/6"
},
"ref": "master",
"stage": "test",
"status": "pending",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/7",
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.dev/root",
"created_at": "2015-12-21T13:14:24.077Z",
"bio": null,
"location": null,
"public_email": "",
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": ""
},
"downstream_pipeline": {
"id": 5,
"sha": "f62a4b2fb89754372a346f24659212eb8da13601",
"ref": "master",
"status": "pending",
"created_at": "2015-12-24T17:54:27.722Z",
"updated_at": "2015-12-24T17:58:27.896Z",
"web_url": "https://example.com/diaspora/diaspora-client/pipelines/5"
}
}
]
```
## Get a single job
Get a single job of a project
......
# frozen_string_literal: true
module API
module Entities
class Bridge < Entities::JobBasic
expose :downstream_pipeline, with: Entities::PipelineBasic
end
end
end
......@@ -70,6 +70,32 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get pipeline bridge jobs' do
success Entities::Bridge
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
use :optional_scope
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/pipelines/:pipeline_id/bridges' do
authorize!(:read_build, user_project)
pipeline = user_project.ci_pipelines.find(params[:pipeline_id])
authorize!(:read_pipeline, pipeline)
bridges = pipeline.bridges
bridges = filter_builds(bridges, params[:scope])
bridges = bridges.preload(
:metadata,
downstream_pipeline: [project: [:route, { namespace: :route }]],
project: [:namespace]
)
present paginate(bridges), with: Entities::Bridge
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get a specific job of a project' do
success Entities::Job
end
......
......@@ -184,6 +184,7 @@ ci_pipelines:
- statuses
- latest_statuses_ordered_by_stage
- builds
- bridges
- processables
- trigger_requests
- variables
......
......@@ -21,6 +21,11 @@ describe Ci::Bridge do
expect(bridge).to have_many(:sourced_pipelines)
end
it 'has one downstream pipeline' do
expect(bridge).to have_one(:sourced_pipeline)
expect(bridge).to have_one(:downstream_pipeline)
end
describe '#tags' do
it 'only has a bridge tag' do
expect(bridge.tags).to eq [:bridge]
......
......@@ -26,6 +26,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to have_many(:trigger_requests) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:bridges) }
it { is_expected.to have_many(:job_artifacts).through(:builds) }
it { is_expected.to have_many(:auto_canceled_pipelines) }
it { is_expected.to have_many(:auto_canceled_jobs) }
......
......@@ -271,6 +271,178 @@ describe API::Jobs do
end
end
describe 'GET /projects/:id/pipelines/:pipeline_id/bridges' do
let!(:bridge) { create(:ci_bridge, pipeline: pipeline) }
let(:downstream_pipeline) { create(:ci_pipeline) }
let!(:pipeline_source) do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: project,
source_job: bridge,
pipeline: downstream_pipeline,
project: downstream_pipeline.project)
end
let(:query) { Hash.new }
before do |example|
unless example.metadata[:skip_before_request]
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
end
end
context 'authorized user' do
it 'returns pipeline bridges' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
it 'returns correct values' do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(json_response.first['id']).to eq bridge.id
expect(json_response.first['name']).to eq bridge.name
expect(json_response.first['stage']).to eq bridge.stage
end
it 'returns pipeline data' do
json_bridge = json_response.first
expect(json_bridge['pipeline']).not_to be_empty
expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
end
it 'returns downstream pipeline data' do
json_bridge = json_response.first
expect(json_bridge['downstream_pipeline']).not_to be_empty
expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
end
context 'filter bridges' do
before do
create_bridge(pipeline, :pending)
create_bridge(pipeline, :running)
end
context 'with one scope element' do
let(:query) { { 'scope' => 'pending' } }
it :skip_before_request do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.count).to eq 1
expect(json_response.first["status"]).to eq "pending"
end
end
context 'with array of scope elements' do
let(:query) { { scope: %w(pending running) } }
it :skip_before_request do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.count).to eq 2
json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
end
end
end
context 'respond 400 when scope contains invalid state' do
let(:query) { { scope: %w(unknown running) } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
context 'bridges in different pipelines' do
let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
it 'excludes bridges from other pipelines' do
json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
end
end
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
end.count
3.times { create_bridge(pipeline) }
expect do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
end.not_to exceed_all_query_limit(control_count)
end
end
context 'unauthorized user' do
context 'when user is not logged in' do
let(:api_user) { nil }
it 'does not return bridges' do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when user is guest' do
let(:api_user) { guest }
it 'does not return bridges' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user has no read access for pipeline' do
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(api_user, :read_pipeline, pipeline).and_return(false)
end
it 'does not return bridges' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user has no read_build access for project' do
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(api_user, :read_build, project).and_return(false)
end
it 'does not return bridges' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
def create_bridge(pipeline, status = :created)
create(:ci_bridge, status: status, pipeline: pipeline).tap do |bridge|
downstream_pipeline = create(:ci_pipeline)
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
source_job: bridge,
pipeline: downstream_pipeline,
project: downstream_pipeline.project)
end
end
end
describe 'GET /projects/:id/jobs/:job_id' do
before do |example|
unless example.metadata[:skip_before_request]
......
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