1# frozen_string_literal: true
2
3module Ci
4  class BuildDependencies
5    include ::Gitlab::Utils::StrongMemoize
6
7    attr_reader :processable
8
9    def initialize(processable)
10      @processable = processable
11    end
12
13    def all
14      (local + cross_pipeline + cross_project).uniq
15    end
16
17    def invalid_local
18      local.reject(&:valid_dependency?)
19    end
20
21    def valid?
22      valid_local? && valid_cross_pipeline? && valid_cross_project?
23    end
24
25    private
26
27    # Dependencies can only be of Ci::Build type because only builds
28    # can create artifacts
29    def model_class
30      ::Ci::Build
31    end
32
33    # Dependencies local to the given pipeline
34    def local
35      strong_memoize(:local) do
36        next [] if no_local_dependencies_specified?
37        next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline
38
39        deps = model_class.where(pipeline_id: processable.pipeline_id).latest
40        deps = find_dependencies(processable, deps)
41
42        from_dependencies(deps).to_a
43      end
44    end
45
46    def find_dependencies(processable, deps)
47      if processable.scheduling_type_dag?
48        from_needs(deps)
49      else
50        from_previous_stages(deps)
51      end
52    end
53
54    # Dependencies from the same parent-pipeline hierarchy excluding
55    # the current job's pipeline
56    def cross_pipeline
57      strong_memoize(:cross_pipeline) do
58        fetch_dependencies_in_hierarchy
59      end
60    end
61
62    # Dependencies that are defined by project and ref
63    def cross_project
64      []
65    end
66
67    def fetch_dependencies_in_hierarchy
68      deps_specifications = specified_cross_pipeline_dependencies
69      return [] if deps_specifications.empty?
70
71      deps_specifications = expand_variables_and_validate(deps_specifications)
72      jobs_in_pipeline_hierarchy(deps_specifications)
73    end
74
75    def jobs_in_pipeline_hierarchy(deps_specifications)
76      all_pipeline_ids = []
77      all_job_names = []
78
79      deps_specifications.each do |spec|
80        all_pipeline_ids << spec[:pipeline]
81        all_job_names << spec[:job]
82      end
83
84      model_class.latest.success
85        .in_pipelines(processable.pipeline.same_family_pipeline_ids)
86        .in_pipelines(all_pipeline_ids.uniq)
87        .by_name(all_job_names.uniq)
88        .select do |dependency|
89          # the query may not return exact matches pipeline-job, so we filter
90          # them separately.
91          deps_specifications.find do |spec|
92            spec[:pipeline] == dependency.pipeline_id &&
93              spec[:job] == dependency.name
94          end
95        end
96    end
97
98    def expand_variables_and_validate(specifications)
99      specifications.map do |spec|
100        pipeline = ExpandVariables.expand(spec[:pipeline].to_s, processable_variables).to_i
101        # current pipeline is not allowed because local dependencies
102        # should be used instead.
103        next if pipeline == processable.pipeline_id
104
105        job = ExpandVariables.expand(spec[:job], processable_variables)
106
107        { job: job, pipeline: pipeline }
108      end.compact
109    end
110
111    def valid_cross_pipeline?
112      cross_pipeline.size == specified_cross_pipeline_dependencies.size
113    end
114
115    def valid_local?
116      local.all?(&:valid_dependency?)
117    end
118
119    def valid_cross_project?
120      true
121    end
122
123    def project
124      processable.project
125    end
126
127    def no_local_dependencies_specified?
128      processable.options[:dependencies]&.empty?
129    end
130
131    def from_previous_stages(scope)
132      scope.before_stage(processable.stage_idx)
133    end
134
135    def from_needs(scope)
136      needs_names = processable.needs.artifacts.select(:name)
137      scope.where(name: needs_names)
138    end
139
140    def from_dependencies(scope)
141      return scope unless processable.options[:dependencies].present?
142
143      scope.where(name: processable.options[:dependencies])
144    end
145
146    def processable_variables
147      -> { processable.simple_variables_without_dependencies }
148    end
149
150    def specified_cross_pipeline_dependencies
151      strong_memoize(:specified_cross_pipeline_dependencies) do
152        specified_cross_dependencies.select { |dep| dep[:pipeline] && dep[:artifacts] }
153      end
154    end
155
156    def specified_cross_dependencies
157      Array(processable.options[:cross_dependencies])
158    end
159  end
160end
161
162Ci::BuildDependencies.prepend_mod_with('Ci::BuildDependencies')
163