summaryrefslogtreecommitdiff
path: root/app/models/ci/build_dependencies.rb
blob: 8ae921f1416aea76111332824a86bc0deb921e36 (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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# frozen_string_literal: true

module Ci
  class BuildDependencies
    include ::Gitlab::Utils::StrongMemoize

    attr_reader :processable

    def initialize(processable)
      @processable = processable
    end

    def all
      (local + cross_pipeline + cross_project).uniq
    end

    # Dependencies local to the given pipeline
    def local
      return [] if no_local_dependencies_specified?

      deps = model_class.where(pipeline_id: processable.pipeline_id).latest
      deps = from_previous_stages(deps)
      deps = from_needs(deps)
      from_dependencies(deps)
    end

    # Dependencies from the same parent-pipeline hierarchy excluding
    # the current job's pipeline
    def cross_pipeline
      strong_memoize(:cross_pipeline) do
        fetch_dependencies_in_hierarchy
      end
    end

    # Dependencies that are defined by project and ref
    def cross_project
      []
    end

    def invalid_local
      local.reject(&:valid_dependency?)
    end

    def valid?
      valid_local? && valid_cross_pipeline? && valid_cross_project?
    end

    private

    # Dependencies can only be of Ci::Build type because only builds
    # can create artifacts
    def model_class
      ::Ci::Build
    end

    def fetch_dependencies_in_hierarchy
      deps_specifications = specified_cross_pipeline_dependencies
      return [] if deps_specifications.empty?

      deps_specifications = expand_variables_and_validate(deps_specifications)
      jobs_in_pipeline_hierarchy(deps_specifications)
    end

    def jobs_in_pipeline_hierarchy(deps_specifications)
      all_pipeline_ids = []
      all_job_names = []

      deps_specifications.each do |spec|
        all_pipeline_ids << spec[:pipeline]
        all_job_names << spec[:job]
      end

      model_class.latest.success
        .in_pipelines(processable.pipeline.same_family_pipeline_ids)
        .in_pipelines(all_pipeline_ids.uniq)
        .by_name(all_job_names.uniq)
        .select do |dependency|
          # the query may not return exact matches pipeline-job, so we filter
          # them separately.
          deps_specifications.find do |spec|
            spec[:pipeline] == dependency.pipeline_id &&
              spec[:job] == dependency.name
          end
        end
    end

    def expand_variables_and_validate(specifications)
      specifications.map do |spec|
        pipeline = ExpandVariables.expand(spec[:pipeline].to_s, processable_variables).to_i
        # current pipeline is not allowed because local dependencies
        # should be used instead.
        next if pipeline == processable.pipeline_id

        job = ExpandVariables.expand(spec[:job], processable_variables)

        { job: job, pipeline: pipeline }
      end.compact
    end

    def valid_cross_pipeline?
      cross_pipeline.size == specified_cross_pipeline_dependencies.size
    end

    def valid_local?
      return true unless Gitlab::Ci::Features.validate_build_dependencies?(project)

      local.all?(&:valid_dependency?)
    end

    def valid_cross_project?
      true
    end

    def project
      processable.project
    end

    def no_local_dependencies_specified?
      processable.options[:dependencies]&.empty?
    end

    def from_previous_stages(scope)
      scope.before_stage(processable.stage_idx)
    end

    def from_needs(scope)
      return scope unless processable.scheduling_type_dag?

      needs_names = processable.needs.artifacts.select(:name)
      scope.where(name: needs_names)
    end

    def from_dependencies(scope)
      return scope unless processable.options[:dependencies].present?

      scope.where(name: processable.options[:dependencies])
    end

    def processable_variables
      -> { processable.simple_variables_without_dependencies }
    end

    def specified_cross_pipeline_dependencies
      strong_memoize(:specified_cross_pipeline_dependencies) do
        next [] unless Feature.enabled?(:ci_cross_pipeline_artifacts_download, processable.project, default_enabled: true)

        specified_cross_dependencies.select { |dep| dep[:pipeline] && dep[:artifacts] }
      end
    end

    def specified_cross_dependencies
      Array(processable.options[:cross_dependencies])
    end
  end
end

Ci::BuildDependencies.prepend_if_ee('EE::Ci::BuildDependencies')