Commit 494dfe89 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'feature/gb/pipeline-bridge-transitions' into 'master'

Implement CI/CD bridge transitions

See merge request gitlab-org/gitlab-ee!8813
parents 73310aec 18aa2fb6
......@@ -32,3 +32,5 @@ module Ci
end
end
end
::Ci::Bridge.prepend(::EE::Ci::Bridge)
......@@ -6,10 +6,11 @@ module Ci
self.table_name = "ci_sources_pipelines"
belongs_to :project, class_name: Project
belongs_to :pipeline, class_name: Ci::Pipeline
belongs_to :pipeline, class_name: Ci::Pipeline, inverse_of: :source_pipeline
belongs_to :source_project, class_name: Project, foreign_key: :source_project_id
belongs_to :source_job, class_name: Ci::Build, foreign_key: :source_job_id
belongs_to :source_job, class_name: CommitStatus, foreign_key: :source_job_id
belongs_to :source_bridge, class_name: Ci::Bridge, foreign_key: :source_job_id
belongs_to :source_pipeline, class_name: Ci::Pipeline, foreign_key: :source_pipeline_id
validates :project, presence: true
......
# frozen_string_literal: true
module EE
module Ci
module Bridge
extend ActiveSupport::Concern
prepended do
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
has_many :sourced_pipelines, class_name: ::Ci::Sources::Pipeline,
foreign_key: :source_job_id
state_machine :status do
after_transition created: :pending do |bridge|
bridge.run_after_commit do
bridge.schedule_downstream_pipeline!
end
end
end
end
def schedule_downstream_pipeline!
::Ci::CreateCrossProjectPipelineWorker.perform_async(self.id)
end
def target_user
self.user
end
def target_project_path
options&.dig(:trigger, :project)
end
def target_ref
options&.dig(:trigger, :branch)
end
end
end
end
......@@ -5,6 +5,8 @@ module EE
module Pipeline
extend ActiveSupport::Concern
BridgeStatusError = Class.new(StandardError)
EE_FAILURE_REASONS = {
activity_limit_exceeded: 20,
size_limit_exceeded: 21
......@@ -17,10 +19,12 @@ module EE
has_many :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::OccurrencePipeline'
has_many :vulnerabilities, source: :occurrence, through: :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::Occurrence'
has_one :source_pipeline, class_name: ::Ci::Sources::Pipeline
has_one :source_pipeline, class_name: ::Ci::Sources::Pipeline, inverse_of: :pipeline
has_many :sourced_pipelines, class_name: ::Ci::Sources::Pipeline, foreign_key: :source_pipeline_id
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
has_one :source_job, through: :source_pipeline, source: :source_job
has_one :source_bridge, through: :source_pipeline, source: :source_bridge
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
......@@ -95,9 +99,26 @@ module EE
StoreSecurityReportsWorker.perform_async(pipeline.id)
end
end
after_transition created: :pending do |pipeline|
next unless pipeline.bridge_triggered?
pipeline.update_bridge_status!
end
end
end
def bridge_triggered?
source_bridge.present?
end
def update_bridge_status!
raise ArgumentError unless bridge_triggered?
raise BridgeStatusError unless source_bridge.active?
source_bridge.success!
end
def any_report_artifact_for_type(file_type)
report_artifact_for_file_type(file_type) || legacy_report_artifact_for_file_type(file_type)
end
......
......@@ -9,7 +9,9 @@ module EE
override :failure_reasons
def failure_reasons
super.merge(protected_environment_failure: 1_000)
super.merge(protected_environment_failure: 1_000,
insufficient_bridge_permissions: 1_001,
invalid_bridge_trigger: 1_002)
end
end
end
......
......@@ -5,7 +5,9 @@ module EE
prepended do
EE_CALLOUT_FAILURE_MESSAGES = const_get(:CALLOUT_FAILURE_MESSAGES).merge(
protected_environment_failure: 'The environment this job is deploying to is protected. Only users with permission may successfully run this job'
protected_environment_failure: 'The environment this job is deploying to is protected. Only users with permission may successfully run this job.',
insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline.',
invalid_bridge_trigger: 'This job could not be executed because downstream pipeline trigger definition is invalid.'
).freeze
EE::CommitStatusPresenter.private_constant :EE_CALLOUT_FAILURE_MESSAGES
......
# frozen_string_literal: true
module Ci
class CreateCrossProjectPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize
def execute(bridge)
@bridge = bridge
unless target_project_exists?
return bridge.drop!(:invalid_bridge_trigger)
end
if target_project == project
return bridge.drop!(:invalid_bridge_trigger)
end
unless can_create_cross_pipeline?
return bridge.drop!(:insufficient_bridge_permissions)
end
create_pipeline!
end
private
def target_project_exists?
target_project.present? &&
can?(current_user, :read_project, target_project)
end
def can_create_cross_pipeline?
can?(current_user, :update_pipeline, project) &&
can?(target_user, :create_pipeline, target_project)
end
def create_pipeline!
::Ci::CreatePipelineService
.new(target_project, target_user, ref: target_ref)
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
@bridge.sourced_pipelines.build(
source_pipeline: @bridge.pipeline,
source_project: @bridge.project,
project: target_project,
pipeline: pipeline)
end
end
def target_user
strong_memoize(:target_user) { @bridge.target_user }
end
def target_ref
strong_memoize(:target_ref) do
@bridge.target_ref || target_project.default_branch
end
end
def target_project
strong_memoize(:target_project) do
Project.find_by_full_path(@bridge.target_project_path)
end
end
end
end
......@@ -42,6 +42,7 @@
- geo:geo_scheduler_secondary_scheduler
- pipeline_default:store_security_reports
- pipeline_default:ci_create_cross_project_pipeline
- admin_emails
- chat_notification
......
# frozen_string_literal: true
module Ci
class CreateCrossProjectPipelineWorker
include ::ApplicationWorker
include ::PipelineQueue
def perform(bridge_id)
::Ci::Bridge.find_by_id(bridge_id).try do |bridge|
::Ci::CreateCrossProjectPipelineService
.new(bridge.project, bridge.user)
.execute(bridge)
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Ci
......@@ -9,7 +10,9 @@ module EE
prepended do
EE_REASONS = const_get(:REASONS).merge(
protected_environment_failure: 'protected environment failure'
protected_environment_failure: 'protected environment failure',
invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline'
).freeze
EE::Gitlab::Ci::Status::Build::Failed.private_constant :EE_REASONS
......
......@@ -13,6 +13,8 @@ merge_requests:
- draft_notes
ci_pipelines:
- source_pipeline
- source_bridge
- source_job
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
......
require 'spec_helper'
describe Ci::Bridge do
set(:project) { create(:project) }
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:bridge) do
create(:ci_bridge, status: :created, options: options, pipeline: pipeline)
end
let(:options) do
{ trigger: { project: 'my/project', branch: 'master' } }
end
it 'has many sourced pipelines' do
expect(bridge).to have_many(:sourced_pipelines)
end
describe 'state machine transitions' do
context 'when it changes status from created to pending' do
it 'schedules downstream pipeline creation' do
expect(bridge).to receive(:schedule_downstream_pipeline!)
bridge.enqueue!
end
end
end
describe '#target_user' do
it 'is the same as a user who created a pipeline' do
expect(bridge.target_user).to eq bridge.user
end
end
describe '#target_project_path' do
context 'when trigger is defined' do
it 'returns a full path of a project' do
expect(bridge.target_project_path).to eq 'my/project'
end
end
context 'when trigger does not have project defined' do
let(:options) { { trigger: {} } }
it 'returns nil' do
expect(bridge.target_project_path).to be_nil
end
end
end
describe '#target_ref' do
context 'when trigger is defined' do
it 'returns a ref name' do
expect(bridge.target_ref).to eq 'master'
end
end
context 'when trigger does not have project defined' do
let(:options) { nil }
it 'returns nil' do
expect(bridge.target_ref).to be_nil
end
end
end
end
......@@ -393,4 +393,82 @@ describe Ci::Pipeline do
end
end
end
describe 'upstream status interactions' do
context 'when a pipeline has an upstream status' do
context 'when an upstream status is a bridge' do
let(:bridge) { create(:ci_bridge, status: :pending) }
before do
create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge)
end
describe '#bridge_triggered?' do
it 'is a pipeline triggered by a bridge' do
expect(pipeline).to be_bridge_triggered
end
end
describe '#source_job' do
it 'has a correct source job' do
expect(pipeline.source_job).to eq bridge
end
end
describe '#source_bridge' do
it 'has a correct bridge source' do
expect(pipeline.source_bridge).to eq bridge
end
end
describe '#update_bridge_status!' do
it 'can update bridge status if it is running' do
pipeline.update_bridge_status!
expect(bridge.reload).to be_success
end
it 'can not update bridge status if is not active' do
bridge.success!
expect { pipeline.update_bridge_status! }
.to raise_error EE::Ci::Pipeline::BridgeStatusError
end
end
end
context 'when an upstream status is a build' do
let(:build) { create(:ci_build) }
before do
create(:ci_sources_pipeline, pipeline: pipeline, source_job: build)
end
describe '#bridge_triggered?' do
it 'is a pipeline that has not been triggered by a bridge' do
expect(pipeline).not_to be_bridge_triggered
end
end
describe '#source_job' do
it 'has a correct source job' do
expect(pipeline.source_job).to eq build
end
end
describe '#source_bridge' do
it 'does not have a bridge source' do
expect(pipeline.source_bridge).to be_nil
end
end
describe '#update_bridge_status!' do
it 'can not update upstream job status' do
expect { pipeline.update_bridge_status! }
.to raise_error ArgumentError
end
end
end
end
end
end
......@@ -9,7 +9,9 @@ describe Ci::BuildPresenter do
it 'returns a verbose failure reason' do
description = presenter.callout_failure_message
expect(description).to eq('The environment this job is deploying to is protected. Only users with permission may successfully run this job')
expect(description).to eq 'The environment this job is deploying to is protected. ' \
'Only users with permission may successfully run this job.'
end
end
end
require 'spec_helper'
describe Ci::CreateCrossProjectPipelineService, '#execute' do
set(:user) { create(:user) }
set(:upstream_project) { create(:project, :repository) }
set(:downstream_project) { create(:project, :repository) }
set(:upstream_pipeline) do
create(:ci_pipeline, :running, project: upstream_project)
end
let(:trigger) do
{
trigger: {
project: downstream_project.full_path,
branch: 'feature'
}
}
end
let(:bridge) do
create(:ci_bridge, status: :pending,
user: user,
options: trigger,
pipeline: upstream_pipeline)
end
let(:service) { described_class.new(upstream_project, user) }
before do
stub_ci_pipeline_to_return_yaml_file
upstream_project.add_developer(user)
end
context 'when downstream project has not been found' do
let(:trigger) do
{ trigger: { project: 'unknown/project' } }
end
it 'does not create a pipeline' do
expect { service.execute(bridge) }
.not_to change { Ci::Pipeline.count }
end
it 'changes pipeline bridge job status to failed' do
service.execute(bridge)
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq 'invalid_bridge_trigger'
end
end
context 'when user can not access downstream project' do
it 'does not create a new pipeline' do
expect { service.execute(bridge) }
.not_to change { Ci::Pipeline.count }
end
it 'changes status of the bridge build' do
service.execute(bridge)
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq 'invalid_bridge_trigger'
end
end
context 'when user does not have access to create pipeline' do
before do
downstream_project.add_guest(user)
end
it 'does not create a new pipeline' do
expect { service.execute(bridge) }
.not_to change { Ci::Pipeline.count }
end
it 'changes status of the bridge build' do
service.execute(bridge)
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq 'insufficient_bridge_permissions'
end
end
context 'when user can create pipeline in a downstream project' do
before do
downstream_project.add_developer(user)
end
it 'creates only one new pipeline' do
expect { service.execute(bridge) }
.to change { Ci::Pipeline.count }.by(1)
end
it 'creates a new pipeline in a downstream project' do
pipeline = service.execute(bridge)
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
expect(pipeline.source_bridge).to eq bridge
expect(pipeline.source_bridge).to be_a ::Ci::Bridge
end
it 'updates bridge status when downstream pipeline gets proceesed' do
pipeline = service.execute(bridge)
expect(pipeline.reload).to be_pending
expect(bridge.reload).to be_success
end
context 'when target ref is not specified' do
let(:trigger) do
{ trigger: { project: downstream_project.full_path } }
end
it 'is using default branch name' do
pipeline = service.execute(bridge)
expect(pipeline.ref).to eq 'master'
end
end
context 'when circular dependency is defined' do
let(:trigger) do
{ trigger: { project: upstream_project.full_path } }
end
it 'does not create a new pipeline' do
expect { service.execute(bridge) }
.not_to change { Ci::Pipeline.count }
end
it 'changes status of the bridge build' do
service.execute(bridge)
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq 'invalid_bridge_trigger'
end
end
end
end
require 'spec_helper'
describe Ci::CreateCrossProjectPipelineWorker do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:pipeline) { create(:ci_pipeline, project: project) }
set(:bridge) { create(:ci_bridge, user: user, pipeline: pipeline) }
let(:service) { double('pipeline creation service') }
describe '#perform' do
context 'when bridge exists' do
it 'calls cross project pipeline creation service' do
expect(Ci::CreateCrossProjectPipelineService)
.to receive(:new)
.with(project, user)
.and_return(service)
expect(service).to receive(:execute).with(bridge)
described_class.new.perform(bridge.id)
end
end
context 'when bridge does not exist' do
it 'does nothing' do
expect(Ci::CreateCrossProjectPipelineService)
.not_to receive(:new)
described_class.new.perform(123)
end
end
end
end
......@@ -6,7 +6,7 @@ FactoryBot.define do
ref 'master'
tag false
created_at 'Di 29. Okt 09:50:00 CET 2013'
status :success
status :created
pipeline factory: :ci_pipeline
......
......@@ -5,7 +5,7 @@ describe Ci::Bridge do
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:bridge) do
create(:ci_bridge, pipeline: pipeline)
create(:ci_bridge, status: :success, pipeline: pipeline)
end
describe '#tags' 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