Commit 92932aa6 authored by Fabio Pitino's avatar Fabio Pitino

Drop bridge if downstream pipeline has errors

Set this behavior behind a feature flag
Log error if downstream pipeline exists
parent 46b78677
...@@ -227,6 +227,7 @@ module Ci ...@@ -227,6 +227,7 @@ module Ci
end end
after_transition created: :pending do |pipeline| after_transition created: :pending do |pipeline|
next if Feature.enabled?(:ci_drop_bridge_on_downstream_errors, pipeline.project, default_enabled: true)
next unless pipeline.bridge_triggered? next unless pipeline.bridge_triggered?
next if pipeline.bridge_waiting? next if pipeline.bridge_waiting?
...@@ -756,6 +757,8 @@ module Ci ...@@ -756,6 +757,8 @@ module Ci
raise BridgeStatusError unless source_bridge.active? raise BridgeStatusError unless source_bridge.active?
source_bridge.success! source_bridge.success!
rescue => e
Gitlab::ErrorTracking.track_exception(e, pipeline_id: id)
end end
def bridge_triggered? def bridge_triggered?
...@@ -774,6 +777,10 @@ module Ci ...@@ -774,6 +777,10 @@ module Ci
child_pipelines.exists? child_pipelines.exists?
end end
def created_successfully?
persisted? && failure_reason.blank?
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)
......
...@@ -5,9 +5,18 @@ module Ci ...@@ -5,9 +5,18 @@ module Ci
class CreateCrossProjectPipelineService < ::BaseService class CreateCrossProjectPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
DuplicateDownstreamPipelineError = Class.new(StandardError)
def execute(bridge) def execute(bridge)
@bridge = bridge @bridge = bridge
return if bridge.has_downstream_pipeline?
if bridge.has_downstream_pipeline?
Gitlab::ErrorTracking.track_exception(
DuplicateDownstreamPipelineError.new,
bridge_id: @bridge.id, project_id: @bridge.project_id
)
return
end
pipeline_params = @bridge.downstream_pipeline_params pipeline_params = @bridge.downstream_pipeline_params
target_ref = pipeline_params.dig(:target_revision, :ref) target_ref = pipeline_params.dig(:target_revision, :ref)
...@@ -19,14 +28,32 @@ module Ci ...@@ -19,14 +28,32 @@ module Ci
current_user, current_user,
pipeline_params.fetch(:target_revision)) pipeline_params.fetch(:target_revision))
service.execute( downstream_pipeline = service.execute(
pipeline_params.fetch(:source), pipeline_params[:execute_params]) do |pipeline| pipeline_params.fetch(:source), pipeline_params[:execute_params]) do |pipeline|
pipeline.variables.build(@bridge.downstream_variables) pipeline.variables.build(@bridge.downstream_variables)
end end
downstream_pipeline.tap do |pipeline|
next if Feature.disabled?(:ci_drop_bridge_on_downstream_errors, project, default_enabled: true)
update_bridge_status!(@bridge, pipeline)
end
end end
private private
def update_bridge_status!(bridge, pipeline)
Gitlab::OptimisticLocking.retry_lock(bridge) do |subject|
if pipeline.created_successfully?
# If bridge uses `strategy:depend` we leave it running
# and update the status when the downstream pipeline completes.
subject.success! unless subject.dependent?
else
subject.drop!(:downstream_pipeline_creation_failed)
end
end
end
def ensure_preconditions!(target_ref) def ensure_preconditions!(target_ref)
unless downstream_project_accessible? unless downstream_project_accessible?
@bridge.drop!(:downstream_bridge_project_not_found) @bridge.drop!(:downstream_bridge_project_not_found)
......
---
title: Drop bridge if downstream pipeline has errors
merge_request: 25706
author:
type: fixed
...@@ -2813,6 +2813,30 @@ describe Ci::Pipeline, :mailer do ...@@ -2813,6 +2813,30 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#created_successfully?' do
subject { pipeline.created_successfully? }
context 'when pipeline is not persisted' do
let(:pipeline) { build(:ci_pipeline) }
it { is_expected.to be_falsey }
end
context 'when pipeline is persisted' do
context 'when pipeline has failure reasons' do
let(:pipeline) { create(:ci_pipeline, failure_reason: :config_error) }
it { is_expected.to be_falsey }
end
context 'when pipeline has no failure reasons' do
let(:pipeline) { create(:ci_pipeline, failure_reason: nil) }
it { is_expected.to be_truthy }
end
end
end
describe '#parent_pipeline' do describe '#parent_pipeline' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
...@@ -2960,8 +2984,7 @@ describe Ci::Pipeline, :mailer do ...@@ -2960,8 +2984,7 @@ describe Ci::Pipeline, :mailer do
it 'can not update bridge status if is not active' do it 'can not update bridge status if is not active' do
bridge.success! bridge.success!
expect { pipeline.update_bridge_status! } expect { pipeline.update_bridge_status! }.not_to change { bridge.status }
.to raise_error Ci::Pipeline::BridgeStatusError
end end
end end
end end
...@@ -2992,9 +3015,12 @@ describe Ci::Pipeline, :mailer do ...@@ -2992,9 +3015,12 @@ describe Ci::Pipeline, :mailer do
end end
describe '#update_bridge_status!' do describe '#update_bridge_status!' do
it 'can not update upstream job status' do it 'tracks an ArgumentError and does not update upstream job status' do
expect { pipeline.update_bridge_status! } expect(Gitlab::ErrorTracking)
.to raise_error ArgumentError .to receive(:track_exception)
.with(instance_of(ArgumentError), pipeline_id: pipeline.id)
pipeline.update_bridge_status!
end end
end end
end end
......
...@@ -126,7 +126,13 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -126,7 +126,13 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
) )
end end
it 'does nothing' do it 'logs an error and exits' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.with(
instance_of(Ci::CreateCrossProjectPipelineService::DuplicateDownstreamPipelineError),
bridge_id: bridge.id, project_id: bridge.project.id)
.and_call_original
expect(Ci::CreatePipelineService).not_to receive(:new) expect(Ci::CreatePipelineService).not_to receive(:new)
expect(service.execute(bridge)).to be_nil expect(service.execute(bridge)).to be_nil
end end
...@@ -165,13 +171,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -165,13 +171,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
expect(pipeline.source_bridge).to be_a ::Ci::Bridge expect(pipeline.source_bridge).to be_a ::Ci::Bridge
end end
it 'does not update bridge status when downstream pipeline gets processed' do it 'updates the bridge status when downstream pipeline gets processed' do
pipeline = service.execute(bridge) pipeline = service.execute(bridge)
expect(pipeline.reload).to be_failed expect(pipeline.reload).to be_failed
# TODO: This should change to failed once #198354 gets fixed. expect(bridge.reload).to be_failed
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25706
expect(bridge.reload).to be_pending
end end
end end
...@@ -258,6 +262,22 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -258,6 +262,22 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline' it_behaves_like 'creates a child pipeline'
it 'updates the bridge job to success' do
expect { service.execute(bridge) }.to change { bridge.status }.to 'success'
end
context 'when bridge uses "depend" strategy' do
let(:trigger) do
{
trigger: { include: 'child-pipeline.yml', strategy: 'depend' }
}
end
it 'does not update the bridge job status' do
expect { service.execute(bridge) }.not_to change { bridge.status }
end
end
context 'when latest sha for the ref changed in the meantime' do context 'when latest sha for the ref changed in the meantime' do
before do before do
upstream_project.repository.create_file( upstream_project.repository.create_file(
...@@ -314,6 +334,34 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -314,6 +334,34 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end end
end end
context 'when downstream pipeline creation errors out' do
let(:stub_config) { false }
before do
stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
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 the downstream project' do
pipeline = service.execute(bridge)
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
end
it 'drops the bridge' do
pipeline = service.execute(bridge)
expect(pipeline.reload).to be_failed
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
end
end
context 'when bridge job has YAML variables defined' do context 'when bridge job has YAML variables defined' do
before do before do
bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }] bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }]
......
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