Commit 6bc60b5a authored by Grzegorz Bizon's avatar Grzegorz Bizon

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

Create downstream pipeline in same project (2nd attempt)

See merge request gitlab-org/gitlab!22663
parents 7c15fffd 1b16a450
...@@ -17,7 +17,7 @@ class PipelinesFinder ...@@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none return Ci::Pipeline.none
end end
items = pipelines items = pipelines.no_child
items = by_scope(items) items = by_scope(items)
items = by_status(items) items = by_status(items)
items = by_ref(items) items = by_ref(items)
......
...@@ -53,6 +53,10 @@ module Ci ...@@ -53,6 +53,10 @@ module Ci
def to_partial_path def to_partial_path
'projects/generic_commit_statuses/generic_commit_status' 'projects/generic_commit_statuses/generic_commit_status'
end end
def yaml_for_downstream
nil
end
end end
end end
......
...@@ -60,7 +60,9 @@ module Ci ...@@ -60,7 +60,9 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData' has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline 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 :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 :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
...@@ -212,6 +214,7 @@ module Ci ...@@ -212,6 +214,7 @@ module Ci
end end
scope :internal, -> { where(source: internal_sources) } 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 :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) } scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) } scope :for_sha, -> (sha) { where(sha: sha) }
...@@ -507,10 +510,6 @@ module Ci ...@@ -507,10 +510,6 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process) builds.skipped.after_stage(stage_idx).find_each(&:process)
end end
def child?
false
end
def latest? def latest?
return false unless git_ref && commit.present? return false unless git_ref && commit.present?
...@@ -693,6 +692,24 @@ module Ci ...@@ -693,6 +692,24 @@ module Ci
all_merge_requests.order(id: :desc) all_merge_requests.order(id: :desc)
end 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) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -23,10 +23,13 @@ module Ci ...@@ -23,10 +23,13 @@ module Ci
schedule: 4, schedule: 4,
api: 5, api: 5,
external: 6, external: 6,
# TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
# https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7, pipeline: 7,
chat: 8, chat: 8,
merge_request_event: 10, merge_request_event: 10,
external_pull_request_event: 11 external_pull_request_event: 11,
parent_pipeline: 12
} }
end end
...@@ -38,7 +41,8 @@ module Ci ...@@ -38,7 +41,8 @@ module Ci
repository_source: 1, repository_source: 1,
auto_devops_source: 2, auto_devops_source: 2,
remote_source: 4, remote_source: 4,
external_project_source: 5 external_project_source: 5,
bridge_source: 6
} }
end end
......
...@@ -18,6 +18,8 @@ module Ci ...@@ -18,6 +18,8 @@ module Ci
validates :source_project, presence: true validates :source_project, presence: true
validates :source_job, presence: true validates :source_job, presence: true
validates :source_pipeline, presence: true validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do expose :flags do
expose :latest?, as: :latest expose :latest?, as: :latest
end end
......
...@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer ...@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations def preloaded_relations
[ [
:latest_statuses_ordered_by_stage, :latest_statuses_ordered_by_stage,
:project,
:stages, :stages,
{ {
failed_builds: %i(project metadata) failed_builds: %i(project metadata)
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists # 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 @pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new( command = Gitlab::Ci::Pipeline::Chain::Command.new(
...@@ -46,6 +46,7 @@ module Ci ...@@ -46,6 +46,7 @@ module Ci
current_user: current_user, current_user: current_user,
push_options: params[:push_options] || {}, push_options: params[:push_options] || {},
chat_data: params[:chat_data], chat_data: params[:chat_data],
bridge: bridge,
**extra_options(options)) **extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence sequence = Gitlab::Ci::Pipeline::Chain::Sequence
...@@ -104,14 +105,14 @@ module Ci ...@@ -104,14 +105,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true) if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines project.ci_pipelines
.where(ref: pipeline.ref) .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)) .where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled .alive_or_scheduled
.with_only_interruptible_builds .with_only_interruptible_builds
else else
project.ci_pipelines project.ci_pipelines
.where(ref: pipeline.ref) .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)) .where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending .created_or_pending
end end
......
---
title: Allow an upstream pipeline to create a downstream pipeline in the same project
merge_request: 22663
author:
type: added
...@@ -4,6 +4,7 @@ module EE ...@@ -4,6 +4,7 @@ module EE
module Ci module Ci
module Bridge module Bridge
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
InvalidBridgeTypeError = Class.new(StandardError) InvalidBridgeTypeError = Class.new(StandardError)
...@@ -98,19 +99,42 @@ module EE ...@@ -98,19 +99,42 @@ module EE
self.user self.user
end end
def target_project_path def target_project
downstream_project || upstream_project downstream_project || upstream_project
end 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 def downstream_project
strong_memoize(:downstream_project) do 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
end end
def upstream_project def upstream_project
strong_memoize(:upstream_project) do strong_memoize(:upstream_project) do
options&.dig(:bridge_needs, :pipeline) upstream_project_path && ::Project.find_by_full_path(upstream_project_path)
end end
end end
...@@ -138,6 +162,51 @@ module EE ...@@ -138,6 +162,51 @@ module EE
end end
end 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: :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 end
end end
# frozen_string_literal: true # frozen_string_literal: true
module Ci module Ci
# TODO: rename this (and worker) to CreateDownstreamPipelineService
class CreateCrossProjectPipelineService < ::BaseService class CreateCrossProjectPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def execute(bridge) def execute(bridge)
@bridge = bridge @bridge = bridge
unless target_project_exists? pipeline_params = @bridge.downstream_pipeline_params
return bridge.drop!(:downstream_bridge_project_not_found) target_ref = pipeline_params.dig(:target_revision, :ref)
end
if target_project == project return unless ensure_preconditions!(target_ref)
return bridge.drop!(:invalid_bridge_trigger)
end
unless can_create_cross_pipeline? service = ::Ci::CreatePipelineService.new(
return bridge.drop!(:insufficient_bridge_permissions) pipeline_params.fetch(:project),
end 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 end
private private
def target_project_exists? def ensure_preconditions!(target_ref)
target_project.present? && unless downstream_project_accessible?
can?(current_user, :read_project, target_project) @bridge.drop!(:downstream_bridge_project_not_found)
end return false
end
def can_create_cross_pipeline? # TODO: Remove this condition if favour of model validation
can?(current_user, :update_pipeline, project) && # https://gitlab.com/gitlab-org/gitlab/issues/38338
can?(target_user, :create_pipeline, target_project) && if downstream_project == project && !@bridge.triggers_child_pipeline?
can_update_branch? @bridge.drop!(:invalid_bridge_trigger)
end return false
end
def can_update_branch? unless can_create_downstream_pipeline?(target_ref)
::Gitlab::UserAccess.new(target_user, project: target_project).can_update_branch?(target_ref) @bridge.drop!(:insufficient_bridge_permissions)
end return false
end
def create_pipeline! true
::Ci::CreatePipelineService end
.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)
pipeline.variables.build(@bridge.downstream_variables) def downstream_project_accessible?
end downstream_project.present? &&
can?(current_user, :read_project, downstream_project)
end end
def target_user def can_create_downstream_pipeline?(target_ref)
strong_memoize(:target_user) { @bridge.target_user } can?(current_user, :update_pipeline, project) &&
can?(current_user, :create_pipeline, downstream_project) &&
can_update_branch?(target_ref)
end end
def target_ref def can_update_branch?(target_ref)
strong_memoize(:target_ref) do ::Gitlab::UserAccess.new(current_user, project: downstream_project).can_update_branch?(target_ref)
@bridge.target_ref || target_project.default_branch
end
end end
def target_project def downstream_project
strong_memoize(:target_project) do strong_memoize(:downstream_project) do
Project.find_by_full_path(@bridge.target_project_path) @bridge.downstream_project
end end
end end
end end
......
...@@ -5,7 +5,7 @@ module Ci ...@@ -5,7 +5,7 @@ module Ci
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
def execute(bridge) def execute(bridge)
return unless bridge.upstream_project return unless bridge.upstream_project_path
@bridge = bridge @bridge = bridge
...@@ -29,7 +29,7 @@ module Ci ...@@ -29,7 +29,7 @@ module Ci
def upstream_project def upstream_project
strong_memoize(:upstream_project) do strong_memoize(:upstream_project) do
::Project.find_by_full_path(@bridge.target_project_path) @bridge.upstream_project
end end
end end
......
...@@ -22,19 +22,61 @@ module EE ...@@ -22,19 +22,61 @@ module EE
end end
end end
class ComplexTrigger < ::Gitlab::Config::Entry::Node class ComplexTrigger < ::Gitlab::Config::Entry::Simplifiable
include ::Gitlab::Config::Entry::Validatable strategy :CrossProjectTrigger, if: -> (config) { !config.key?(:include) }
include ::Gitlab::Config::Entry::Attributable
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 validations do
attributes :project, :branch, :strategy 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 class UnknownStrategy < ::Gitlab::Config::Entry::Node
validates :config, presence: true def errors
validates :config, allowed_keys: ALLOWED_KEYS if ::Feature.enabled?(:ci_parent_child_pipeline)
validates :project, presence: true ['config must specify either project or include']
validates :branch, type: String, allow_nil: true else
validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true ['config must specify project']
end
end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'spec_helper'
require_dependency 'active_model'
describe EE::Gitlab::Ci::Config::Entry::Trigger do describe EE::Gitlab::Ci::Config::Entry::Trigger do
subject { described_class.new(config) } subject { described_class.new(config) }
...@@ -83,6 +82,53 @@ describe EE::Gitlab::Ci::Config::Entry::Trigger do ...@@ -83,6 +82,53 @@ describe EE::Gitlab::Ci::Config::Entry::Trigger do
end end
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 context 'when config contains unknown keys' do
let(:config) { { project: 'some/project', unknown: 123 } } let(:config) { { project: 'some/project', unknown: 123 } }
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
describe Ci::Bridge do describe Ci::Bridge do
set(:project) { create(:project) } set(:project) { create(:project) }
set(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) }
set(:pipeline) { create(:ci_pipeline, project: project) } set(:pipeline) { create(:ci_pipeline, project: project) }
let(:bridge) do let(:bridge) do
...@@ -157,10 +158,10 @@ describe Ci::Bridge do ...@@ -157,10 +158,10 @@ describe Ci::Bridge do
end end
end end
describe '#target_project_path' do describe '#target_project' do
context 'when trigger is defined' do context 'when trigger is defined' do
it 'returns a full path of a project' 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
end end
...@@ -168,7 +169,7 @@ describe Ci::Bridge do ...@@ -168,7 +169,7 @@ describe Ci::Bridge do
let(:options) { { trigger: {} } } let(:options) { { trigger: {} } }
it 'returns nil' do it 'returns nil' do
expect(bridge.target_project_path).to be_nil expect(bridge.target_project).to be_nil
end end
end end
end end
...@@ -289,4 +290,61 @@ describe Ci::Bridge do ...@@ -289,4 +290,61 @@ describe Ci::Bridge do
expect(bridge.metadata.config_options).to be bridge.options expect(bridge.metadata.config_options).to be bridge.options
end end
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 end
...@@ -4,10 +4,10 @@ require 'spec_helper' ...@@ -4,10 +4,10 @@ require 'spec_helper'
describe Ci::CreateCrossProjectPipelineService, '#execute' do describe Ci::CreateCrossProjectPipelineService, '#execute' do
set(:user) { create(:user) } set(:user) { create(:user) }
set(:upstream_project) { create(:project, :repository) } let(:upstream_project) { create(:project, :repository) }
set(:downstream_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) create(:ci_pipeline, :running, project: upstream_project)
end end
...@@ -30,7 +30,6 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -30,7 +30,6 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
let(:service) { described_class.new(upstream_project, user) } let(:service) { described_class.new(upstream_project, user) }
before do before do
stub_ci_pipeline_to_return_yaml_file
upstream_project.add_developer(user) upstream_project.add_developer(user)
end end
...@@ -87,8 +86,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -87,8 +86,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end end
context 'when user can create pipeline in a downstream project' do context 'when user can create pipeline in a downstream project' do
let(:stub_config) { true }
before do before do
downstream_project.add_developer(user) downstream_project.add_developer(user)
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' })) if stub_config
end end
it 'creates only one new pipeline' do it 'creates only one new pipeline' do
...@@ -126,21 +128,95 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -126,21 +128,95 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end end
end end
context 'when circular dependency is defined' do context 'when downstream project is the same as the job project' do
let(:trigger) do let(:trigger) do
{ trigger: { project: upstream_project.full_path } } { trigger: { project: upstream_project.full_path } }
end end
it 'does not create a new pipeline' do context 'detects a circular dependency' do
expect { service.execute(bridge) } it 'does not create a new pipeline' do
.not_to change { Ci::Pipeline.count } 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
it 'changes status of the bridge build' do context 'when "include" is provided' do
service.execute(bridge) 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 it 'creates a child pipeline in the same project' do
expect(bridge.failure_reason).to eq 'invalid_bridge_trigger' 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
end end
......
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
describe Ci::CreatePipelineService do describe Ci::CreatePipelineService do
subject(:execute) { service.execute(:push) } subject(:execute) { service.execute(:push) }
set(:downstream_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'some'))}
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:admin) } let(:user) { create(:admin) }
let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) } let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) }
...@@ -46,6 +47,6 @@ describe Ci::CreatePipelineService do ...@@ -46,6 +47,6 @@ describe Ci::CreatePipelineService do
it 'persists bridge target project' do it 'persists bridge target project' do
bridge = execute.stages.last.bridges.first bridge = execute.stages.last.bridges.first
expect(bridge.downstream_project).to eq('some/project') expect(bridge.downstream_project).to eq downstream_project
end end
end end
...@@ -73,8 +73,6 @@ describe Ci::CreatePipelineService, '#execute' do ...@@ -73,8 +73,6 @@ describe Ci::CreatePipelineService, '#execute' do
describe 'cross-project pipeline triggers' do describe 'cross-project pipeline triggers' do
before do before do
stub_feature_flags(cross_project_pipeline_triggers: true)
stub_ci_pipeline_yaml_file <<~YAML stub_ci_pipeline_yaml_file <<~YAML
test: test:
script: rspec script: rspec
...@@ -144,6 +142,41 @@ describe Ci::CreatePipelineService, '#execute' do ...@@ -144,6 +142,41 @@ describe Ci::CreatePipelineService, '#execute' do
end end
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! def create_pipeline!
service.execute(:push) service.execute(:push)
end end
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request, :trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted, :ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options, :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: # These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds :config_content, :config_processor, :stage_seeds
) do ) do
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers include Chain::Helpers
SOURCES = [ 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::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject, Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote, Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
].freeze ].freeze
LEGACY_SOURCES = [ 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::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze ].freeze
......
...@@ -6,20 +6,15 @@ module Gitlab ...@@ -6,20 +6,15 @@ module Gitlab
module Chain module Chain
module Config module Config
class Content class Content
class Runtime < Source class Bridge < Source
def content def content
@command.config_content return unless @command.bridge
@command.bridge.yaml_for_downstream
end end
def source def source
# The only case when this source is used is when the config content :bridge_source
# 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
end end
end end
end end
......
...@@ -64,6 +64,19 @@ describe PipelinesFinder do ...@@ -64,6 +64,19 @@ describe PipelinesFinder do
end end
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| HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do context "when status is #{target}" do
let(:params) { { status: target } } let(:params) { { status: target } }
......
...@@ -98,6 +98,34 @@ describe Gitlab::Ci::Build::Policy::Refs do ...@@ -98,6 +98,34 @@ describe Gitlab::Ci::Build::Policy::Refs do
.not_to be_satisfied_by(pipeline) .not_to be_satisfied_by(pipeline)
end end
end end
context 'when source is pipeline' do
let(:pipeline) { build_stubbed(:ci_pipeline, source: :pipeline) }
it 'is satisfied with only: pipelines' do
expect(described_class.new(%w[pipelines]))
.to be_satisfied_by(pipeline)
end
it 'is satisfied with only: pipeline' do
expect(described_class.new(%w[pipeline]))
.to be_satisfied_by(pipeline)
end
end
context 'when source is parent_pipeline' do
let(:pipeline) { build_stubbed(:ci_pipeline, source: :parent_pipeline) }
it 'is satisfied with only: parent_pipelines' do
expect(described_class.new(%w[parent_pipelines]))
.to be_satisfied_by(pipeline)
end
it 'is satisfied with only: parent_pipeline' do
expect(described_class.new(%w[parent_pipeline]))
.to be_satisfied_by(pipeline)
end
end
end end
context 'when matching a ref by a regular expression' do context 'when matching a ref by a regular expression' do
......
...@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do ...@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false) stub_feature_flags(ci_root_config_content: false)
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
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 context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' } let(:ci_config_path) { 'path/to/config.yml' }
...@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do ...@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end end
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 context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' } let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do let(:config_content_result) do
......
...@@ -201,6 +201,8 @@ ci_pipelines: ...@@ -201,6 +201,8 @@ ci_pipelines:
- sourced_pipelines - sourced_pipelines
- triggered_by_pipeline - triggered_by_pipeline
- triggered_pipelines - triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges - downstream_bridges
- job_artifacts - job_artifacts
- vulnerabilities_occurrence_pipelines - vulnerabilities_occurrence_pipelines
......
...@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do ...@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
end end
end 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 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