Commit 35d0b9ff authored by Kamil Trzcinski's avatar Kamil Trzcinski

Allow to use Pipeline Triggers to create Cross-project pipeline relation

parent 23712d20
......@@ -52,7 +52,8 @@ module Ci
trigger: 3,
schedule: 4,
api: 5,
external: 6
external: 6,
dependent_pipeline: 7
}
state_machine :status, initial: :created do
......@@ -376,7 +377,8 @@ module Ci
def predefined_variables
[
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
{ key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
]
end
......
......@@ -16,6 +16,16 @@ module Ci
validates :source_project, presence: true
validates :source_job, presence: true
validates :source_pipeline, presence: true
before_validation :set_dependent_objects
private
def set_dependent_objects
project ||= pipeline.project
source_pipeline ||= source_job.pipeline
source_project ||= source_pipeline.project
end
end
end
end
......@@ -60,6 +60,8 @@ module Ci
Ci::Pipeline.transaction do
update_merge_requests_head_pipeline if pipeline.save
yield(pipeline) if block_given?
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
......
module Ci
class PipelineTriggerService < BaseService
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
elsif job_from_token
create_pipeline_from_job(job_from_token)
end
end
private
def create_pipeline_from_trigger(trigger)
# this check is to not leak the presence of the project if user cannot read it
return unless trigger.project == project
trigger_request = trigger.trigger_requests.create(variables: params[:variables])
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]).
execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
if pipeline.persisted?
success(pipeline: pipeline)
else
error(pipeline.errors.messages, 400)
end
end
def create_pipeline_from_job(job)
# this check is to not leak the presence of the project if user cannot read it
return unless can?(job.user, :read_project, project)
return error("400 Job has to be running", 400) unless job.running?
return error("400 Variables not supported", 400) if params[:variables].any?
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]).
execute(:dependent_pipeline, ignore_skip_ci: true) do |pipeline|
job.sourced_pipelines.create!(pipeline: pipeline)
end
if pipeline.persisted?
success(pipeline: pipeline)
else
error(pipeline.errors.messages, 400)
end
end
def trigger_from_token
return @trigger if defined?(@trigger)
@trigger ||= Ci::Trigger.find_by_token(params[:token].to_s)
end
def job_from_token
return @job if defined?(@job)
@job ||= Ci::Build.find_by_token(params[:token].to_s)
end
end
end
---
title: Allow to Trigger Pipeline using CI Job Token
merge_request:
author:
......@@ -4,7 +4,8 @@
- [Introduced][ci-229] in GitLab CE 7.14.
- GitLab 8.12 has a completely redesigned job permissions system. Read all
about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
- GitLab 9.0 introduced a trigger ownership to solve permission problems.
- GitLab 9.0 introduced a trigger ownership to solve permission problems,
- GitLab 9.3 introduced an ability to use CI Job Token to trigger dependent pipelines,
Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call.
......@@ -161,6 +162,25 @@ probably not the wisest idea, so you might want to use a
[secure variable](../variables/README.md#user-defined-variables-secure-variables)
for that purpose._
---
Since GitLab 9.3 you can trigger a new pipeline using a CI_JOB_TOKEN.
This method currently doesn't support Variables.
The support for them will be included in 9.4 of GitLab.
This way of triggering does create a dependent pipeline relation on Pipeline Graph page.
```yaml
build_docs:
stage: deploy
script:
- "curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
only:
- tags
```
Pipelines triggered that way do expose a special variable: `CI_PIPELINE_SOURCE=dependent_pipeline`.
### Making use of trigger variables
Using trigger variables can be proven useful for a variety of reasons.
......
......@@ -53,6 +53,7 @@ future GitLab releases.**
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_SOURCE** | 9.3 | all | The variable indicates how the pipeline was triggered, possible options are: push, web, trigger, schedule, api, dependent_pipeline |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
......
......@@ -11,28 +11,26 @@ module API
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
requires :token, type: String, desc: 'The unique token of trigger'
requires :token, type: String, desc: 'The unique token of trigger or job token'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
# validate variables
variables = params[:variables].to_h
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400)
end
# create request and trigger builds
trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
if trigger_request
present trigger_request.pipeline, with: Entities::Pipeline
project = find_project(params[:id])
not_found! unless project
result = Ci::PipelineTriggerService.new(project, nil, params).execute
not_found! unless result
if result[:http_status]
render_api_error!(result[:message], result[:http_status])
else
errors = 'No pipeline created'
render_api_error!(errors, 400)
present result[:pipeline], with: Entities::Pipeline
end
end
......
......@@ -36,12 +36,6 @@ describe API::Triggers do
expect(response).to have_http_status(404)
end
it 'returns unauthorized if token is for different project' do
post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master')
expect(response).to have_http_status(401)
end
end
context 'Have a commit' do
......@@ -93,6 +87,12 @@ describe API::Triggers do
end
context 'when triggering a pipeline from a trigger token' do
it 'does not leak the presence of project when using valid token' do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
expect(response).to have_http_status(404)
end
it 'creates builds from the ref given in the URL, not in the body' do
expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
......@@ -113,6 +113,92 @@ describe API::Triggers do
end
end
end
context 'when triggering a pipeline from a job token' do
let(:other_job) { create(:ci_build, :running, user: other_user) }
let(:params) { { ref: 'refs/heads/other-branch' } }
subject do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{other_job.token}"), params
end
context 'without user' do
let(:other_user) { nil }
it 'does not leak the presence of project when using valid token' do
subject
expect(response).to have_http_status(404)
end
end
context 'for unreleated user' do
let(:other_user) { create(:user) }
it 'does not leak the presence of project when using valid token' do
subject
expect(response).to have_http_status(404)
end
end
context 'for related user' do
let(:other_user) { create(:user) }
context 'with reported permissions' do
before do
project.add_reporter(other_user)
end
it 'forbidds pipeline creation' do
subject
expect(response).to have_http_status(400)
expect(json_response['message']).to eq("base" => ["Insufficient permissions to create a new pipeline"])
end
end
context 'with developer permissions' do
before do
project.add_developer(other_user)
end
it 'creates a new pipeline' do
expect { subject }.to change(Ci::Pipeline, :count)
expect(response).to have_http_status(201)
expect(Ci::Pipeline.last.source).to eq('dependent_pipeline')
expect(Ci::Pipeline.last.triggered_by_pipeline).not_to be_nil
end
context 'when build is complete' do
before do
other_job.success
end
it 'creates a new pipeline' do
subject
expect(response).to have_http_status(400)
end
end
context 'when variables are defined' do
let(:params) do
{ ref: 'refs/heads/other-branch',
variables: { 'KEY' => 'VALUE' } }
end
it 'forbidds pipeline creation' do
subject
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 Variables not supported')
end
end
end
end
end
end
describe 'GET /projects/:id/triggers' 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