1# frozen_string_literal: true
2
3module Ci
4  class Bridge < Ci::Processable
5    include Ci::Contextable
6    include Ci::Metadatable
7    include Importable
8    include AfterCommitQueue
9    include Ci::HasRef
10
11    InvalidBridgeTypeError = Class.new(StandardError)
12    InvalidTransitionError = Class.new(StandardError)
13
14    belongs_to :project
15    belongs_to :trigger_request
16    has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
17                                  foreign_key: :source_job_id
18
19    has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id
20    has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
21
22    validates :ref, presence: true
23
24    # rubocop:disable Cop/ActiveRecordSerialize
25    serialize :options
26    serialize :yaml_variables, ::Gitlab::Serializer::Ci::Variables
27    # rubocop:enable Cop/ActiveRecordSerialize
28
29    state_machine :status do
30      after_transition [:created, :manual, :waiting_for_resource] => :pending do |bridge|
31        next unless bridge.triggers_downstream_pipeline?
32
33        bridge.run_after_commit do
34          ::Ci::CreateDownstreamPipelineWorker.perform_async(bridge.id)
35        end
36      end
37
38      event :pending do
39        transition all => :pending
40      end
41
42      event :manual do
43        transition all => :manual
44      end
45
46      event :scheduled do
47        transition all => :scheduled
48      end
49
50      event :actionize do
51        transition created: :manual
52      end
53    end
54
55    def self.retry(bridge, current_user)
56      raise NotImplementedError
57    end
58
59    def self.with_preloads
60      preload(
61        :metadata,
62        downstream_pipeline: [project: [:route, { namespace: :route }]],
63        project: [:namespace]
64      )
65    end
66
67    def inherit_status_from_downstream!(pipeline)
68      case pipeline.status
69      when 'success'
70        self.success!
71      when 'failed', 'canceled', 'skipped'
72        self.drop!
73      else
74        false
75      end
76    end
77
78    def has_downstream_pipeline?
79      sourced_pipelines.exists?
80    end
81
82    def downstream_pipeline_params
83      return child_params if triggers_child_pipeline?
84      return cross_project_params if downstream_project.present?
85
86      {}
87    end
88
89    def downstream_project
90      strong_memoize(:downstream_project) do
91        if downstream_project_path
92          ::Project.find_by_full_path(downstream_project_path)
93        elsif triggers_child_pipeline?
94          project
95        end
96      end
97    end
98
99    def downstream_project_path
100      strong_memoize(:downstream_project_path) do
101        options&.dig(:trigger, :project)
102      end
103    end
104
105    def parent_pipeline
106      pipeline if triggers_child_pipeline?
107    end
108
109    def triggers_downstream_pipeline?
110      triggers_child_pipeline? || triggers_cross_project_pipeline?
111    end
112
113    def triggers_child_pipeline?
114      yaml_for_downstream.present?
115    end
116
117    def triggers_cross_project_pipeline?
118      downstream_project_path.present?
119    end
120
121    def tags
122      [:bridge]
123    end
124
125    def detailed_status(current_user)
126      Gitlab::Ci::Status::Bridge::Factory
127        .new(self, current_user)
128        .fabricate!
129    end
130
131    def schedulable?
132      false
133    end
134
135    def playable?
136      action? && !archived? && manual?
137    end
138
139    def action?
140      %w[manual].include?(self.when)
141    end
142
143    # rubocop: disable CodeReuse/ServiceClass
144    # We don't need it but we are taking `job_variables_attributes` parameter
145    # to make it consistent with `Ci::Build#play` method.
146    def play(current_user, job_variables_attributes = nil)
147      Ci::PlayBridgeService
148        .new(project, current_user)
149        .execute(self)
150    end
151    # rubocop: enable CodeReuse/ServiceClass
152
153    def artifacts?
154      false
155    end
156
157    def runnable?
158      false
159    end
160
161    def any_unmet_prerequisites?
162      false
163    end
164
165    def expanded_environment_name
166    end
167
168    def persisted_environment
169    end
170
171    def execute_hooks
172      raise NotImplementedError
173    end
174
175    def to_partial_path
176      'projects/generic_commit_statuses/generic_commit_status'
177    end
178
179    def yaml_for_downstream
180      strong_memoize(:yaml_for_downstream) do
181        includes = options&.dig(:trigger, :include)
182        YAML.dump('include' => includes) if includes
183      end
184    end
185
186    def target_ref
187      branch = options&.dig(:trigger, :branch)
188      return unless branch
189
190      scoped_variables.to_runner_variables.yield_self do |all_variables|
191        ::ExpandVariables.expand(branch, all_variables)
192      end
193    end
194
195    def dependent?
196      strong_memoize(:dependent) do
197        options&.dig(:trigger, :strategy) == 'depend'
198      end
199    end
200
201    def downstream_variables
202      variables = scoped_variables.concat(pipeline.persisted_variables)
203
204      variables.to_runner_variables.yield_self do |all_variables|
205        yaml_variables.to_a.map do |hash|
206          { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], all_variables) }
207        end
208      end
209    end
210
211    def target_revision_ref
212      downstream_pipeline_params.dig(:target_revision, :ref)
213    end
214
215    private
216
217    def cross_project_params
218      {
219        project: downstream_project,
220        source: :pipeline,
221        target_revision: {
222          ref: target_ref || downstream_project.default_branch,
223          variables_attributes: downstream_variables
224        },
225        execute_params: {
226          ignore_skip_ci: true,
227          bridge: self
228        }
229      }
230    end
231
232    def child_params
233      parent_pipeline = pipeline
234
235      {
236        project: project,
237        source: :parent_pipeline,
238        target_revision: {
239          ref: parent_pipeline.ref,
240          checkout_sha: parent_pipeline.sha,
241          before: parent_pipeline.before_sha,
242          source_sha: parent_pipeline.source_sha,
243          target_sha: parent_pipeline.target_sha,
244          variables_attributes: downstream_variables
245        },
246        execute_params: {
247          ignore_skip_ci: true,
248          bridge: self,
249          merge_request: parent_pipeline.merge_request
250        }
251      }
252    end
253  end
254end
255
256::Ci::Bridge.prepend_mod_with('Ci::Bridge')
257