summaryrefslogtreecommitdiff
path: root/app/services/ci/create_downstream_pipeline_service.rb
blob: 93f0338fcbaead100a6c40768b9a5b657e5fd242 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# 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])

      downstream_pipeline.tap do |pipeline|
        update_bridge_status!(@bridge, pipeline)
      end
    end

    private

    def update_bridge_status!(bridge, pipeline)
      Gitlab::OptimisticLocking.retry_lock(bridge, name: 'create_downstream_pipeline_update_bridge_status') 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 has_max_descendants_depth?
        @bridge.drop!(:reached_max_descendant_pipelines_depth)
        return false
      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