Commit 3cb41e08 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'create-downstream-pipeline-in-same-project' into 'master'

Create downstream pipeline inside same project

See merge request gitlab-org/gitlab!20930
parents 05347706 58a849e8
......@@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none
end
items = pipelines
items = pipelines.no_child
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
......
......@@ -54,6 +54,10 @@ module Ci
def to_partial_path
'projects/generic_commit_statuses/generic_commit_status'
end
def yaml_for_downstream
nil
end
end
end
......
......@@ -61,7 +61,9 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
has_many :child_pipelines, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :sourced_pipelines, source: :pipeline
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
has_one :parent_pipeline, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :source_pipeline, source: :source_pipeline
has_one :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
......@@ -213,6 +215,7 @@ module Ci
end
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
......@@ -508,10 +511,6 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process)
end
def child?
false
end
def latest?
return false unless git_ref && commit.present?
......@@ -694,6 +693,24 @@ module Ci
all_merge_requests.order(id: :desc)
end
# If pipeline is a child of another pipeline, include the parent
# and the siblings, otherwise return only itself.
def same_family_pipeline_ids
if (parent = parent_pipeline)
[parent.id] + parent.child_pipelines.pluck(:id)
else
[self.id]
end
end
def child?
parent_pipeline.present?
end
def parent?
child_pipelines.exists?
end
def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)
......
......@@ -23,10 +23,11 @@ module Ci
schedule: 4,
api: 5,
external: 6,
pipeline: 7,
cross_project_pipeline: 7,
chat: 8,
merge_request_event: 10,
external_pull_request_event: 11
external_pull_request_event: 11,
parent_pipeline: 12
}
end
......@@ -38,7 +39,8 @@ module Ci
repository_source: 1,
auto_devops_source: 2,
remote_source: 4,
external_project_source: 5
external_project_source: 5,
bridge_source: 6
}
end
......
......@@ -18,6 +18,8 @@ module Ci
validates :source_project, presence: true
validates :source_job, presence: true
validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end
end
end
......
# frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do
expose :latest?, as: :latest
end
......
......@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations
[
:latest_statuses_ordered_by_stage,
:project,
:stages,
{
failed_builds: %i(project metadata)
......
......@@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
......@@ -46,6 +46,7 @@ module Ci
current_user: current_user,
push_options: params[:push_options] || {},
chat_data: params[:chat_data],
bridge: bridge,
**extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
......@@ -104,14 +105,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
.where.not(id: pipeline.same_family_pipeline_ids)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled
.with_only_interruptible_builds
else
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
.where.not(id: pipeline.same_family_pipeline_ids)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end
......
......@@ -44,7 +44,7 @@ module Ci
return error("400 Job has to be running", 400) unless job.running?
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
.execute(:cross_project_pipeline, ignore_skip_ci: true) do |pipeline|
source = job.sourced_pipelines.build(
source_pipeline: job.pipeline,
source_project: job.project,
......
---
title: Allow an upstream pipeline to create a downstream pipeline in the same project
merge_request: 20930
author:
type: added
......@@ -4,6 +4,7 @@ module EE
module Ci
module Bridge
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
InvalidBridgeTypeError = Class.new(StandardError)
......@@ -98,19 +99,42 @@ module EE
self.user
end
def target_project_path
def target_project
downstream_project || upstream_project
end
def triggers_child_pipeline?
yaml_for_downstream.present?
end
override :yaml_for_downstream
def yaml_for_downstream
strong_memoize(:yaml_for_downstream) do
includes = options&.dig(:trigger, :include)
YAML.dump('include' => includes) if includes
end
end
def downstream_pipeline_params
return child_params if triggers_child_pipeline?
return cross_project_params if downstream_project.present?
{}
end
def downstream_project
strong_memoize(:downstream_project) do
options&.dig(:trigger, :project)
if downstream_project_path
::Project.find_by_full_path(downstream_project_path)
elsif triggers_child_pipeline?
project
end
end
end
def upstream_project
strong_memoize(:upstream_project) do
options&.dig(:bridge_needs, :pipeline)
upstream_project_path && ::Project.find_by_full_path(upstream_project_path)
end
end
......@@ -138,6 +162,51 @@ module EE
end
end
end
def downstream_project_path
strong_memoize(:downstream_project_path) do
options&.dig(:trigger, :project)
end
end
def upstream_project_path
strong_memoize(:upstream_project_path) do
options&.dig(:bridge_needs, :pipeline)
end
end
private
def cross_project_params
{
project: downstream_project,
source: :cross_project_pipeline,
target_revision: {
ref: target_ref || downstream_project.default_branch
},
execute_params: { ignore_skip_ci: true }
}
end
def child_params
parent_pipeline = pipeline
{
project: project,
source: :parent_pipeline,
target_revision: {
ref: parent_pipeline.ref,
checkout_sha: parent_pipeline.sha,
before: parent_pipeline.before_sha,
source_sha: parent_pipeline.source_sha,
target_sha: parent_pipeline.target_sha
},
execute_params: {
ignore_skip_ci: true,
bridge: self
}
}
end
end
end
end
# frozen_string_literal: true
module Ci
# TODO: rename this (and worker) to CreateDownstreamPipelineService
class CreateCrossProjectPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize
def execute(bridge)
@bridge = bridge
unless target_project_exists?
return bridge.drop!(:downstream_bridge_project_not_found)
end
pipeline_params = @bridge.downstream_pipeline_params
target_ref = pipeline_params.dig(:target_revision, :ref)
if target_project == project
return bridge.drop!(:invalid_bridge_trigger)
end
return unless ensure_preconditions!(target_ref)
unless can_create_cross_pipeline?
return bridge.drop!(:insufficient_bridge_permissions)
end
service = ::Ci::CreatePipelineService.new(
pipeline_params.fetch(:project),
current_user,
pipeline_params.fetch(:target_revision))
create_pipeline!
service.execute(
pipeline_params.fetch(:source), pipeline_params[:execute_params]) do |pipeline|
@bridge.sourced_pipelines.build(
source_pipeline: @bridge.pipeline,
source_project: @bridge.project,
project: @bridge.downstream_project,
pipeline: pipeline)
pipeline.variables.build(@bridge.downstream_variables)
end
end
private
def target_project_exists?
target_project.present? &&
can?(current_user, :read_project, target_project)
end
def ensure_preconditions!(target_ref)
unless downstream_project_accessible?
@bridge.drop!(:downstream_bridge_project_not_found)
return false
end
def can_create_cross_pipeline?
can?(current_user, :update_pipeline, project) &&
can?(target_user, :create_pipeline, target_project) &&
can_update_branch?
end
# TODO: Remove this condition if favour of model validation
# https://gitlab.com/gitlab-org/gitlab/issues/38338
if downstream_project == project && !@bridge.triggers_child_pipeline?
@bridge.drop!(:invalid_bridge_trigger)
return false
end
def can_update_branch?
::Gitlab::UserAccess.new(target_user, project: target_project).can_update_branch?(target_ref)
end
unless can_create_downstream_pipeline?(target_ref)
@bridge.drop!(:insufficient_bridge_permissions)
return false
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)
true
end
pipeline.variables.build(@bridge.downstream_variables)
end
def downstream_project_accessible?
downstream_project.present? &&
can?(current_user, :read_project, downstream_project)
end
def target_user
strong_memoize(:target_user) { @bridge.target_user }
def can_create_downstream_pipeline?(target_ref)
can?(current_user, :update_pipeline, project) &&
can?(current_user, :create_pipeline, downstream_project) &&
can_update_branch?(target_ref)
end
def target_ref
strong_memoize(:target_ref) do
@bridge.target_ref || target_project.default_branch
end
def can_update_branch?(target_ref)
::Gitlab::UserAccess.new(current_user, project: downstream_project).can_update_branch?(target_ref)
end
def target_project
strong_memoize(:target_project) do
Project.find_by_full_path(@bridge.target_project_path)
def downstream_project
strong_memoize(:downstream_project) do
@bridge.downstream_project
end
end
end
......
......@@ -5,7 +5,7 @@ module Ci
include ::Gitlab::Utils::StrongMemoize
def execute(bridge)
return unless bridge.upstream_project
return unless bridge.upstream_project_path
@bridge = bridge
......@@ -29,7 +29,7 @@ module Ci
def upstream_project
strong_memoize(:upstream_project) do
::Project.find_by_full_path(@bridge.target_project_path)
@bridge.upstream_project
end
end
......
......@@ -22,19 +22,61 @@ module EE
end
end
class ComplexTrigger < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
class ComplexTrigger < ::Gitlab::Config::Entry::Simplifiable
strategy :CrossProjectTrigger, if: -> (config) { !config.key?(:include) }
strategy :SameProjectTrigger, if: -> (config) do
::Feature.enabled?(:ci_parent_child_pipeline) &&
config.key?(:include)
end
class CrossProjectTrigger < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[project branch strategy].freeze
attributes :project, :branch, :strategy
validations do
validates :config, presence: true
validates :config, allowed_keys: ALLOWED_KEYS
validates :project, presence: true
validates :branch, type: String, allow_nil: true
validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true
end
end
class SameProjectTrigger < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[strategy include].freeze
attributes :strategy
ALLOWED_KEYS = %i[project branch strategy].freeze
attributes :project, :branch, :strategy
validations do
validates :config, presence: true
validates :config, allowed_keys: ALLOWED_KEYS
validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true
end
entry :include, ::Gitlab::Ci::Config::Entry::Includes,
description: 'List of external YAML files to include.',
reserved: true
def value
@config
end
end
validations do
validates :config, presence: true
validates :config, allowed_keys: ALLOWED_KEYS
validates :project, presence: true
validates :branch, type: String, allow_nil: true
validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true
class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
if ::Feature.enabled?(:ci_parent_child_pipeline)
['config must specify either project or include']
else
['config must specify project']
end
end
end
end
......
# frozen_string_literal: true
require 'fast_spec_helper'
require_dependency 'active_model'
require 'spec_helper'
describe EE::Gitlab::Ci::Config::Entry::Trigger do
subject { described_class.new(config) }
......@@ -83,6 +82,53 @@ describe EE::Gitlab::Ci::Config::Entry::Trigger do
end
end
context '#include' do
context 'with simple include' do
let(:config) { { include: 'path/to/config.yml' } }
it { is_expected.to be_valid }
it 'returns a trigger configuration hash' do
expect(subject.value).to eq(include: 'path/to/config.yml' )
end
end
context 'with project' do
let(:config) { { project: 'some/project', include: 'path/to/config.yml' } }
it { is_expected.not_to be_valid }
it 'is returns an error' do
expect(subject.errors.first)
.to match /config contains unknown keys: project/
end
end
context 'with branch' do
let(:config) { { branch: 'feature', include: 'path/to/config.yml' } }
it { is_expected.not_to be_valid }
it 'is returns an error' do
expect(subject.errors.first)
.to match /config contains unknown keys: branch/
end
end
context 'when feature flag is off' do
before do
stub_feature_flags(ci_parent_child_pipeline: false)
end
let(:config) { { include: 'path/to/config.yml' } }
it 'is returns an error if include is used' do
expect(subject.errors.first)
.to match /config must specify project/
end
end
end
context 'when config contains unknown keys' do
let(:config) { { project: 'some/project', unknown: 123 } }
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
describe Ci::Bridge do
set(:project) { create(:project) }
set(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) }
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:bridge) do
......@@ -157,10 +158,10 @@ describe Ci::Bridge do
end
end
describe '#target_project_path' do
describe '#target_project' 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'
expect(bridge.target_project).to eq target_project
end
end
......@@ -168,7 +169,7 @@ describe Ci::Bridge do
let(:options) { { trigger: {} } }
it 'returns nil' do
expect(bridge.target_project_path).to be_nil
expect(bridge.target_project).to be_nil
end
end
end
......@@ -289,4 +290,61 @@ describe Ci::Bridge do
expect(bridge.metadata.config_options).to be bridge.options
end
end
describe '#triggers_child_pipeline?' do
subject { bridge.triggers_child_pipeline? }
context 'when bridge defines a downstream YAML' do
let(:options) do
{
trigger: {
include: 'path/to/child.yml'
}
}
end
it { is_expected.to be_truthy }
end
context 'when bridge does not define a downstream YAML' do
let(:options) do
{
trigger: {
project: project.full_path
}
}
end
it { is_expected.to be_falsey }
end
end
describe '#yaml_for_downstream' do
subject { bridge.yaml_for_downstream }
context 'when bridge defines a downstream YAML' do
let(:options) do
{
trigger: {
include: 'path/to/child.yml'
}
}
end
let(:yaml) do
<<~EOY
---
include: path/to/child.yml
EOY
end
it { is_expected.to eq yaml }
end
context 'when bridge does not define a downstream YAML' do
let(:options) { {} }
it { is_expected.to be_nil }
end
end
end
......@@ -60,7 +60,7 @@ describe API::Triggers do
expect { subject }.to change(Ci::Pipeline, :count)
expect(response).to have_http_status(201)
expect(Ci::Pipeline.last.source).to eq('pipeline')
expect(Ci::Pipeline.last.source).to eq('cross_project_pipeline')
expect(Ci::Pipeline.last.triggered_by_pipeline).not_to be_nil
expect(Ci::Sources::Pipeline.last).to have_attributes(
pipeline_id: (a_value > 0),
......@@ -94,7 +94,7 @@ describe API::Triggers do
.and change(Ci::PipelineVariable, :count)
expect(response).to have_http_status(201)
expect(Ci::Pipeline.last.source).to eq('pipeline')
expect(Ci::Pipeline.last.source).to eq('cross_project_pipeline')
expect(Ci::Pipeline.last.triggered_by_pipeline).not_to be_nil
expect(Ci::Pipeline.last.variables.map { |v| { v.key => v.value } }.last).to eq(params[:variables])
end
......
......@@ -4,10 +4,10 @@ require 'spec_helper'
describe Ci::CreateCrossProjectPipelineService, '#execute' do
set(:user) { create(:user) }
set(:upstream_project) { create(:project, :repository) }
let(:upstream_project) { create(:project, :repository) }
set(:downstream_project) { create(:project, :repository) }
set(:upstream_pipeline) do
let!(:upstream_pipeline) do
create(:ci_pipeline, :running, project: upstream_project)
end
......@@ -30,7 +30,6 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
let(:service) { described_class.new(upstream_project, user) }
before do
stub_ci_pipeline_to_return_yaml_file
upstream_project.add_developer(user)
end
......@@ -87,8 +86,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end
context 'when user can create pipeline in a downstream project' do
let(:stub_config) { true }
before do
downstream_project.add_developer(user)
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' })) if stub_config
end
it 'creates only one new pipeline' do
......@@ -126,21 +128,95 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end
end
context 'when circular dependency is defined' do
context 'when downstream project is the same as the job project' 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 }
context 'detects a circular dependency' 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
it 'changes status of the bridge build' do
service.execute(bridge)
context 'when "include" is provided' do
shared_examples 'creates a child pipeline' do
it 'creates only one new pipeline' do
expect { service.execute(bridge) }
.to change { Ci::Pipeline.count }.by(1)
end
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq 'invalid_bridge_trigger'
it 'creates a child pipeline in the same project' do
pipeline = service.execute(bridge)
pipeline.reload
expect(pipeline.builds.map(&:name)).to eq %w[rspec echo]
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq bridge.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
it 'propagates parent pipeline settings to the child pipeline' do
pipeline = service.execute(bridge)
pipeline.reload
expect(pipeline.ref).to eq(upstream_pipeline.ref)
expect(pipeline.sha).to eq(upstream_pipeline.sha)
expect(pipeline.source_sha).to eq(upstream_pipeline.source_sha)
expect(pipeline.target_sha).to eq(upstream_pipeline.target_sha)
expect(pipeline.target_sha).to eq(upstream_pipeline.target_sha)
expect(pipeline.trigger_requests.last).to eq(bridge.trigger_request)
end
end
before do
file_content = YAML.dump(
rspec: { script: 'rspec' },
echo: { script: 'echo' })
upstream_project.repository.create_file(
user, 'child-pipeline.yml', file_content, message: 'message', branch_name: 'master')
upstream_pipeline.update!(sha: upstream_project.commit.id)
end
let(:stub_config) { false }
let(:trigger) do
{
trigger: { include: 'child-pipeline.yml' }
}
end
it_behaves_like 'creates a child pipeline'
context 'when latest sha for the ref changed in the meantime' do
before do
upstream_project.repository.create_file(
user, 'another-change', 'test', message: 'message', branch_name: 'master')
end
# it does not auto-cancel pipelines from the same family
it_behaves_like 'creates a child pipeline'
end
end
end
......
......@@ -5,6 +5,7 @@ require 'spec_helper'
describe Ci::CreatePipelineService do
subject(:execute) { service.execute(:push) }
set(:downstream_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'some'))}
let(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) }
......@@ -46,6 +47,6 @@ describe Ci::CreatePipelineService do
it 'persists bridge target project' do
bridge = execute.stages.last.bridges.first
expect(bridge.downstream_project).to eq('some/project')
expect(bridge.downstream_project).to eq downstream_project
end
end
......@@ -73,8 +73,6 @@ describe Ci::CreatePipelineService, '#execute' do
describe 'cross-project pipeline triggers' do
before do
stub_feature_flags(cross_project_pipeline_triggers: true)
stub_ci_pipeline_yaml_file <<~YAML
test:
script: rspec
......@@ -144,6 +142,41 @@ describe Ci::CreatePipelineService, '#execute' do
end
end
describe 'child pipeline triggers' do
before do
stub_ci_pipeline_yaml_file <<~YAML
test:
script: rspec
deploy:
variables:
CROSS: downstream
stage: deploy
trigger:
include:
- local: path/to/child.yml
YAML
end
it 'creates bridge jobs correctly' do
pipeline = create_pipeline!
test = pipeline.statuses.find_by(name: 'test')
bridge = pipeline.statuses.find_by(name: 'deploy')
expect(pipeline).to be_persisted
expect(test).to be_a Ci::Build
expect(bridge).to be_a Ci::Bridge
expect(bridge.stage).to eq 'deploy'
expect(pipeline.statuses).to match_array [test, bridge]
expect(bridge.options).to eq(
'trigger' => { 'include' => [{ 'local' => 'path/to/child.yml' }] }
)
expect(bridge.yaml_variables)
.to include(key: 'CROSS', value: 'downstream', public: true)
end
end
def create_pipeline!
service.execute(:push)
end
......
......@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update,
:chat_data, :allow_mirror_update, :bridge,
# These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds
) do
......
......@@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers
SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
......@@ -17,7 +17,7 @@ module Gitlab
].freeze
LEGACY_SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze
......
......@@ -6,20 +6,15 @@ module Gitlab
module Chain
module Config
class Content
class Runtime < Source
class Bridge < Source
def content
@command.config_content
return unless @command.bridge
@command.bridge.yaml_for_downstream
end
def source
# The only case when this source is used is when the config content
# is passed in as parameter to Ci::CreatePipelineService.
# This would only occur with parent/child pipelines which is being
# implemented.
# TODO: change source to return :runtime_source
# https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
nil
:bridge_source
end
end
end
......
......@@ -64,6 +64,19 @@ describe PipelinesFinder do
end
end
context 'when project has child pipelines' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
let!(:pipeline_source) do
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
end
it 'filters out child pipelines and show only the parents' do
is_expected.to eq([parent_pipeline])
end
end
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
let(:params) { { status: target } }
......
......@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false)
end
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
before do
command.bridge = bridge
end
context 'when bridge job has downstream yaml' do
before do
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
end
it 'returns the content already available in command' do
subject.perform!
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
end
end
context 'when bridge job does not have downstream yaml' do
before do
allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
end
it 'returns the next available source' do
subject.perform!
expect(pipeline.config_source).to eq 'auto_devops_source'
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
expect(command.config_content).to eq(template.content)
end
end
end
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
......@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
before do
command.bridge = bridge
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
end
it 'returns the content already available in command' do
subject.perform!
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
end
end
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do
......
......@@ -201,6 +201,8 @@ ci_pipelines:
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines
......
......@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
end
end
end
describe '#parent_pipeline' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline is triggered by a pipeline from the same project' do
let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
before do
create(:ci_sources_pipeline,
source_pipeline: upstream_pipeline,
source_project: project,
pipeline: pipeline,
project: project)
end
it 'returns the parent pipeline' do
expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
end
it 'is child' do
expect(pipeline).to be_child
end
end
context 'when pipeline is triggered by a pipeline from another project' do
let(:upstream_pipeline) { create(:ci_pipeline) }
before do
create(:ci_sources_pipeline,
source_pipeline: upstream_pipeline,
source_project: upstream_pipeline.project,
pipeline: pipeline,
project: project)
end
it 'returns nil' do
expect(pipeline.parent_pipeline).to be_nil
end
it 'is not child' do
expect(pipeline).not_to be_child
end
end
context 'when pipeline is not triggered by a pipeline' do
it 'returns nil' do
expect(pipeline.parent_pipeline).to be_nil
end
it 'is not child' do
expect(pipeline).not_to be_child
end
end
end
describe '#child_pipelines' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline triggered other pipelines on same project' do
let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
before do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
pipeline: downstream_pipeline,
project: pipeline.project)
end
it 'returns the child pipelines' do
expect(pipeline.child_pipelines).to eq [downstream_pipeline]
end
it 'is parent' do
expect(pipeline).to be_parent
end
end
context 'when pipeline triggered other pipelines on another project' do
let(:downstream_pipeline) { create(:ci_pipeline) }
before do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
pipeline: downstream_pipeline,
project: downstream_pipeline.project)
end
it 'returns empty array' do
expect(pipeline.child_pipelines).to be_empty
end
it 'is not parent' do
expect(pipeline).not_to be_parent
end
end
context 'when pipeline did not trigger any pipelines' do
it 'returns empty array' do
expect(pipeline.child_pipelines).to be_empty
end
it 'is not parent' do
expect(pipeline).not_to be_parent
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
context 'custom config content' do
let(:bridge) do
double(:bridge, yaml_for_downstream: <<~YML
rspec:
script: rspec
custom:
script: custom
YML
)
end
subject { service.execute(:push, bridge: bridge) }
it 'creates a pipeline using the content passed in as param' do
expect(subject).to be_persisted
expect(subject.builds.map(&:name)).to eq %w[rspec custom]
expect(subject.config_source).to eq 'bridge_source'
end
end
end
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