2020-03-13 15:44:24 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Ci
|
2020-11-24 15:15:51 +05:30
|
|
|
# Takes in input a Ci::Bridge job and creates a downstream pipeline
|
|
|
|
# (either multi-project or child pipeline) according to the Ci::Bridge
|
|
|
|
# specifications.
|
|
|
|
class CreateDownstreamPipelineService < ::BaseService
|
2020-03-13 15:44:24 +05:30
|
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
|
2020-04-08 14:13:33 +05:30
|
|
|
DuplicateDownstreamPipelineError = Class.new(StandardError)
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
MAX_DESCENDANTS_DEPTH = 2
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
def execute(bridge)
|
|
|
|
@bridge = bridge
|
|
|
|
|
2020-04-08 14:13:33 +05:30
|
|
|
if bridge.has_downstream_pipeline?
|
|
|
|
Gitlab::ErrorTracking.track_exception(
|
|
|
|
DuplicateDownstreamPipelineError.new,
|
|
|
|
bridge_id: @bridge.id, project_id: @bridge.project_id
|
|
|
|
)
|
2021-09-04 01:27:46 +05:30
|
|
|
|
|
|
|
return error('Already has a downstream pipeline')
|
2020-04-08 14:13:33 +05:30
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
pipeline_params = @bridge.downstream_pipeline_params
|
|
|
|
target_ref = pipeline_params.dig(:target_revision, :ref)
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
return error('Pre-conditions not met') unless ensure_preconditions!(target_ref)
|
2020-03-13 15:44:24 +05:30
|
|
|
|
|
|
|
service = ::Ci::CreatePipelineService.new(
|
|
|
|
pipeline_params.fetch(:project),
|
|
|
|
current_user,
|
|
|
|
pipeline_params.fetch(:target_revision))
|
|
|
|
|
2021-10-27 15:23:28 +05:30
|
|
|
downstream_pipeline = service
|
|
|
|
.execute(pipeline_params.fetch(:source), **pipeline_params[:execute_params])
|
|
|
|
.payload
|
2020-04-08 14:13:33 +05:30
|
|
|
|
|
|
|
downstream_pipeline.tap do |pipeline|
|
|
|
|
update_bridge_status!(@bridge, pipeline)
|
|
|
|
end
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2020-04-08 14:13:33 +05:30
|
|
|
def update_bridge_status!(bridge, pipeline)
|
2021-04-17 20:07:23 +05:30
|
|
|
Gitlab::OptimisticLocking.retry_lock(bridge, name: 'create_downstream_pipeline_update_bridge_status') do |subject|
|
2020-04-08 14:13:33 +05:30
|
|
|
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
|
2020-06-23 00:09:42 +05:30
|
|
|
subject.options[:downstream_errors] = pipeline.errors.full_messages
|
2020-04-08 14:13:33 +05:30
|
|
|
subject.drop!(:downstream_pipeline_creation_failed)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue StateMachines::InvalidTransition => e
|
|
|
|
Gitlab::ErrorTracking.track_exception(
|
|
|
|
Ci::Bridge::InvalidTransitionError.new(e.message),
|
|
|
|
bridge_id: bridge.id,
|
|
|
|
downstream_pipeline_id: pipeline.id)
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
def ensure_preconditions!(target_ref)
|
|
|
|
unless downstream_project_accessible?
|
|
|
|
@bridge.drop!(:downstream_bridge_project_not_found)
|
|
|
|
return false
|
|
|
|
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
|
|
|
|
|
|
|
|
# TODO: Remove this condition if favour of model validation
|
|
|
|
# https://gitlab.com/gitlab-org/gitlab/issues/38338
|
2021-01-03 14:25:43 +05:30
|
|
|
if has_max_descendants_depth?
|
|
|
|
@bridge.drop!(:reached_max_descendant_pipelines_depth)
|
|
|
|
return false
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
unless can_create_downstream_pipeline?(target_ref)
|
|
|
|
@bridge.drop!(:insufficient_bridge_permissions)
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
if has_cyclic_dependency?
|
|
|
|
@bridge.drop!(:pipeline_loop_detected)
|
|
|
|
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def downstream_project_accessible?
|
|
|
|
downstream_project.present? &&
|
|
|
|
can?(current_user, :read_project, downstream_project)
|
|
|
|
end
|
|
|
|
|
|
|
|
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 can_update_branch?(target_ref)
|
2020-10-24 23:57:45 +05:30
|
|
|
::Gitlab::UserAccess.new(current_user, container: downstream_project).can_update_branch?(target_ref)
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def downstream_project
|
|
|
|
strong_memoize(:downstream_project) do
|
|
|
|
@bridge.downstream_project
|
|
|
|
end
|
|
|
|
end
|
2020-11-24 15:15:51 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
def has_cyclic_dependency?
|
|
|
|
return false if @bridge.triggers_child_pipeline?
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
pipeline_checksums = @bridge.pipeline.self_and_upstreams.filter_map do |pipeline|
|
|
|
|
config_checksum(pipeline) unless pipeline.child?
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2022-05-07 20:08:51 +05:30
|
|
|
|
|
|
|
# To avoid false positives we allow 1 cycle in the ancestry and
|
|
|
|
# fail when 2 cycles are detected: A -> B -> A -> B -> A
|
|
|
|
pipeline_checksums.tally.any? { |_checksum, occurrences| occurrences > 2 }
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
def has_max_descendants_depth?
|
|
|
|
return false unless @bridge.triggers_child_pipeline?
|
|
|
|
|
2021-09-30 23:02:18 +05:30
|
|
|
ancestors_of_new_child = @bridge.pipeline.self_and_ancestors
|
2020-11-24 15:15:51 +05:30
|
|
|
ancestors_of_new_child.count > MAX_DESCENDANTS_DEPTH
|
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
|
|
|
|
def config_checksum(pipeline)
|
2022-04-04 11:22:00 +05:30
|
|
|
[pipeline.project_id, pipeline.ref, pipeline.source].hash
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
end
|