debian-mirror-gitlab/app/services/ci/create_downstream_pipeline_service.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

170 lines
5.7 KiB
Ruby
Raw Normal View History

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
2022-07-16 23:28:13 +05:30
include Ci::DownstreamPipelineHelpers
2020-03-13 15:44:24 +05:30
2020-04-08 14:13:33 +05:30
DuplicateDownstreamPipelineError = Class.new(StandardError)
2022-06-21 17:19:12 +05:30
MAX_NESTED_CHILDREN = 2
2020-11-24 15:15:51 +05:30
2020-03-13 15:44:24 +05:30
def execute(bridge)
@bridge = bridge
2023-03-04 22:38:38 +05:30
if @bridge.has_downstream_pipeline?
2020-04-08 14:13:33 +05:30
Gitlab::ErrorTracking.track_exception(
DuplicateDownstreamPipelineError.new,
bridge_id: @bridge.id, project_id: @bridge.project_id
)
2021-09-04 01:27:46 +05:30
2023-03-04 22:38:38 +05:30
return ServiceResponse.error(message: '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)
2023-03-04 22:38:38 +05:30
return ServiceResponse.error(message: 'Pre-conditions not met') unless ensure_preconditions!(target_ref)
return ServiceResponse.error(message: 'Can not run the bridge') unless @bridge.run
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
2022-07-16 23:28:13 +05:30
log_downstream_pipeline_creation(downstream_pipeline)
2023-03-04 22:38:38 +05:30
update_bridge_status!(@bridge, downstream_pipeline)
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?
2023-03-04 22:38:38 +05:30
ServiceResponse.success(payload: pipeline)
2020-04-08 14:13:33 +05:30
else
2023-03-04 22:38:38 +05:30
message = pipeline.errors.full_messages
subject.options[:downstream_errors] = message
2020-04-08 14:13:33 +05:30
subject.drop!(:downstream_pipeline_creation_failed)
2023-03-04 22:38:38 +05:30
ServiceResponse.error(payload: pipeline, message: message)
2020-04-08 14:13:33 +05:30
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)
2023-03-04 22:38:38 +05:30
ServiceResponse.error(payload: pipeline, message: e.message)
2020-04-08 14:13:33 +05:30
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
2022-06-21 17:19:12 +05:30
# only applies to parent-child pipelines not multi-project
if has_max_nested_children?
2021-01-03 14:25:43 +05:30
@bridge.drop!(:reached_max_descendant_pipelines_depth)
return false
2020-03-13 15:44:24 +05:30
end
2023-04-23 21:23:45 +05:30
if pipeline_tree_too_large?
2022-10-11 01:57:18 +05:30
@bridge.drop!(:reached_max_pipeline_hierarchy_size)
return false
end
2020-03-13 15:44:24 +05:30
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
2022-06-21 17:19:12 +05:30
def has_max_nested_children?
2020-11-24 15:15:51 +05:30
return false unless @bridge.triggers_child_pipeline?
2022-06-21 17:19:12 +05:30
# only applies to parent-child pipelines not multi-project
2022-10-11 01:57:18 +05:30
ancestors_of_new_child = @bridge.pipeline.self_and_project_ancestors
2022-06-21 17:19:12 +05:30
ancestors_of_new_child.count > MAX_NESTED_CHILDREN
2020-11-24 15:15:51 +05:30
end
2021-06-08 01:23:25 +05:30
2022-10-11 01:57:18 +05:30
def pipeline_tree_too_large?
return false unless @bridge.triggers_downstream_pipeline?
# Applies to the entire pipeline tree across all projects
2023-03-04 22:38:38 +05:30
# A pipeline tree can be shared between multiple namespaces (customers), the limit that is used here
# is the limit of the namespace that has added a downstream pipeline to a pipeline tree.
@bridge.project.actual_limits.exceeded?(:pipeline_hierarchy_size, complete_hierarchy_count)
end
def complete_hierarchy_count
@bridge.pipeline.complete_hierarchy_count
2022-10-11 01:57:18 +05:30
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