diff options
Diffstat (limited to 'app/services/ci/create_downstream_pipeline_service.rb')
-rw-r--r-- | app/services/ci/create_downstream_pipeline_service.rb | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb new file mode 100644 index 00000000000..0394cfb6119 --- /dev/null +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +module Ci + # 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 + include Gitlab::Utils::StrongMemoize + + DuplicateDownstreamPipelineError = Class.new(StandardError) + + MAX_DESCENDANTS_DEPTH = 2 + + def execute(bridge) + @bridge = bridge + + 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 + target_ref = pipeline_params.dig(:target_revision, :ref) + + return unless ensure_preconditions!(target_ref) + + service = ::Ci::CreatePipelineService.new( + pipeline_params.fetch(:project), + current_user, + pipeline_params.fetch(:target_revision)) + + downstream_pipeline = service.execute( + pipeline_params.fetch(:source), pipeline_params[:execute_params]) do |pipeline| + pipeline.variables.build(@bridge.downstream_variables) + end + + downstream_pipeline.tap do |pipeline| + update_bridge_status!(@bridge, pipeline) + end + end + + 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.options[:downstream_errors] = pipeline.errors.full_messages + 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 + + 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 + if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project) + if has_max_descendants_depth? + @bridge.drop!(:reached_max_descendant_pipelines_depth) + return false + end + else + if @bridge.triggers_child_pipeline? && @bridge.pipeline.parent_pipeline.present? + @bridge.drop!(:bridge_pipeline_is_child_pipeline) + return false + end + end + + unless can_create_downstream_pipeline?(target_ref) + @bridge.drop!(:insufficient_bridge_permissions) + return false + end + + 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) + ::Gitlab::UserAccess.new(current_user, container: downstream_project).can_update_branch?(target_ref) + end + + def downstream_project + strong_memoize(:downstream_project) do + @bridge.downstream_project + 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 + end +end |