Commit 99a15b8f authored by Allison Browne's avatar Allison Browne

Block cyclical multi-project pipelines

Block cyclical pipelines in the create downsteam
pipeline service
parent 8ee3d85f
......@@ -23,6 +23,7 @@ module Enums
user_blocked: 14,
project_deleted: 15,
ci_quota_exceeded: 16,
pipeline_loop_detected: 17,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
......
......@@ -15,6 +15,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
scheduler_failure: 'The scheduler failed to assign job to the runner, please try again or contact system administrator',
data_integrity_failure: 'There has been a structural integrity problem detected, please contact system administrator',
forward_deployment_failure: 'The deployment job is older than the previously succeeded deployment job, and therefore cannot be run',
pipeline_loop_detected: 'This job could not be executed because it would create infinitely looping pipelines',
invalid_bridge_trigger: 'This job could not be executed because downstream pipeline trigger definition is invalid',
downstream_bridge_project_not_found: 'This job could not be executed because downstream bridge project could not be found',
insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
......
......@@ -85,6 +85,12 @@ module Ci
return false
end
if has_cyclic_dependency?
@bridge.drop!(:pipeline_loop_detected)
return false
end
true
end
......@@ -109,11 +115,24 @@ module Ci
end
end
def has_cyclic_dependency?
return false if @bridge.triggers_child_pipeline?
if Feature.enabled?(:ci_drop_cyclical_triggered_pipelines, @bridge.project, default_enabled: :yaml)
checksums = @bridge.pipeline.base_and_ancestors.map { |pipeline| config_checksum(pipeline) }
checksums.uniq.length != checksums.length
end
end
def has_max_descendants_depth?
return false unless @bridge.triggers_child_pipeline?
ancestors_of_new_child = @bridge.pipeline.base_and_ancestors(same_project: true)
ancestors_of_new_child.count > MAX_DESCENDANTS_DEPTH
end
def config_checksum(pipeline)
[pipeline.project_id, pipeline.ref].hash
end
end
end
---
name: ci_drop_cyclical_triggered_pipelines
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1195
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329390
milestone: '13.12'
type: development
group: group::continuous integration
default_enabled: true
......@@ -20,6 +20,7 @@ module Gitlab
scheduler_failure: 'scheduler failure',
data_integrity_failure: 'data integrity failure',
forward_deployment_failure: 'forward deployment failure',
pipeline_loop_detected: 'job would create infinitely looping pipelines',
invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
downstream_bridge_project_not_found: 'downstream project could not be found',
insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
include Ci::SourcePipelineHelpers
let_it_be(:user) { create(:user) }
let(:upstream_project) { create(:project, :repository) }
let_it_be(:downstream_project, refind: true) { create(:project, :repository) }
......@@ -394,6 +396,47 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
end
context 'when relationship between pipelines is cyclical' do
before do
pipeline_a = create(:ci_pipeline, project: upstream_project)
pipeline_b = create(:ci_pipeline, project: downstream_project)
pipeline_c = create(:ci_pipeline, project: upstream_project)
create_source_pipeline(pipeline_a, pipeline_b)
create_source_pipeline(pipeline_b, pipeline_c)
create_source_pipeline(pipeline_c, upstream_pipeline)
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 'pipeline_loop_detected'
end
context 'when ci_drop_cyclical_triggered_pipelines is not enabled' do
before do
stub_feature_flags(ci_drop_cyclical_triggered_pipelines: false)
end
it 'creates a new pipeline' do
expect { service.execute(bridge) }
.to change { Ci::Pipeline.count }
end
it 'expect bridge build not to be failed' do
service.execute(bridge)
expect(bridge.reload).not_to be_failed
end
end
end
context 'when downstream pipeline creation errors out' do
let(:stub_config) { false }
......
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