1# frozen_string_literal: true
2
3module Gitlab
4  module Ci
5    module Pipeline
6      module Seed
7        class Build < Seed::Base
8          include Gitlab::Utils::StrongMemoize
9
10          delegate :dig, to: :@seed_attributes
11
12          def initialize(context, attributes, stages_for_needs_lookup = [])
13            @context = context
14            @pipeline = context.pipeline
15            @seed_attributes = attributes
16            @stages_for_needs_lookup = stages_for_needs_lookup.compact
17            @needs_attributes = dig(:needs_attributes)
18            @resource_group_key = attributes.delete(:resource_group_key)
19            @job_variables = @seed_attributes.delete(:job_variables)
20            @root_variables_inheritance = @seed_attributes.delete(:root_variables_inheritance) { true }
21
22            @using_rules  = attributes.key?(:rules)
23            @using_only   = attributes.key?(:only)
24            @using_except = attributes.key?(:except)
25
26            @only = Gitlab::Ci::Build::Policy
27              .fabricate(attributes.delete(:only))
28            @except = Gitlab::Ci::Build::Policy
29              .fabricate(attributes.delete(:except))
30            @rules = Gitlab::Ci::Build::Rules
31              .new(attributes.delete(:rules), default_when: attributes[:when])
32            @cache = Gitlab::Ci::Build::Cache
33              .new(attributes.delete(:cache), @pipeline)
34
35            calculate_yaml_variables!
36          end
37
38          def name
39            dig(:name)
40          end
41
42          def included?
43            strong_memoize(:inclusion) do
44              if @using_rules
45                rules_result.pass?
46              elsif @using_only || @using_except
47                all_of_only? && none_of_except?
48              else
49                true
50              end
51            end
52          end
53
54          def errors
55            return unless included?
56
57            strong_memoize(:errors) do
58              [needs_errors, variable_expansion_errors].compact.flatten
59            end
60          end
61
62          def attributes
63            @seed_attributes
64              .deep_merge(pipeline_attributes)
65              .deep_merge(rules_attributes)
66              .deep_merge(allow_failure_criteria_attributes)
67              .deep_merge(@cache.cache_attributes)
68              .deep_merge(runner_tags)
69          end
70
71          def bridge?
72            attributes_hash = @seed_attributes.to_h
73            attributes_hash.dig(:options, :trigger).present? ||
74              (attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) &&
75               attributes_hash.dig(:options, :bridge_needs, :pipeline).present?)
76          end
77
78          def to_resource
79            strong_memoize(:resource) do
80              processable = initialize_processable
81              assign_resource_group(processable) unless @pipeline.create_deployment_in_separate_transaction?
82              processable
83            end
84          end
85
86          def initialize_processable
87            if bridge?
88              ::Ci::Bridge.new(attributes)
89            else
90              ::Ci::Build.new(attributes).tap do |build|
91                unless @pipeline.create_deployment_in_separate_transaction?
92                  build.assign_attributes(self.class.deployment_attributes_for(build))
93                end
94              end
95            end
96          end
97
98          def assign_resource_group(processable)
99            processable.resource_group =
100              Seed::Processable::ResourceGroup.new(processable, @resource_group_key)
101                                              .to_resource
102          end
103
104          def self.deployment_attributes_for(build, environment = nil)
105            return {} unless build.has_environment?
106
107            environment = Seed::Environment.new(build).to_resource if environment.nil?
108
109            unless environment.persisted?
110              return { status: :failed, failure_reason: :environment_creation_failure }
111            end
112
113            build.persisted_environment = environment
114
115            {
116              deployment: Seed::Deployment.new(build, environment).to_resource,
117              metadata_attributes: {
118                expanded_environment_name: environment.name
119              }
120            }
121          end
122
123          private
124
125          def all_of_only?
126            @only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
127          end
128
129          def none_of_except?
130            @except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
131          end
132
133          def needs_errors
134            return if @needs_attributes.nil?
135
136            if @needs_attributes.size > max_needs_allowed
137              return [
138                "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \
139                  "See needs keyword documentation for more details"
140              ]
141            end
142
143            @needs_attributes.flat_map do |need|
144              next if need[:optional]
145
146              result = need_present?(need)
147
148              "'#{name}' job needs '#{need[:name]}' job, but '#{need[:name]}' is not in any previous stage" unless result
149            end.compact
150          end
151
152          def need_present?(need)
153            @stages_for_needs_lookup.any? do |stage|
154              stage.seeds_names.include?(need[:name])
155            end
156          end
157
158          def max_needs_allowed
159            @pipeline.project.actual_limits.ci_needs_size_limit
160          end
161
162          def variable_expansion_errors
163            expanded_collection = evaluate_context.variables.sort_and_expand_all
164            errors = expanded_collection.errors
165            ["#{name}: #{errors}"] if errors
166          end
167
168          def pipeline_attributes
169            {
170              pipeline: @pipeline,
171              project: @pipeline.project,
172              user: @pipeline.user,
173              ref: @pipeline.ref,
174              tag: @pipeline.tag,
175              trigger_request: @pipeline.legacy_trigger,
176              protected: @pipeline.protected_ref?
177            }
178          end
179
180          def rules_attributes
181            strong_memoize(:rules_attributes) do
182              next {} unless @using_rules
183
184              rules_variables_result = ::Gitlab::Ci::Variables::Helpers.merge_variables(
185                @seed_attributes[:yaml_variables], rules_result.variables
186              )
187
188              rules_result.build_attributes.merge(yaml_variables: rules_variables_result)
189            end
190          end
191
192          def rules_result
193            strong_memoize(:rules_result) do
194              @rules.evaluate(@pipeline, evaluate_context)
195            end
196          end
197
198          def evaluate_context
199            strong_memoize(:evaluate_context) do
200              Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes)
201            end
202          end
203
204          def runner_tags
205            strong_memoize(:runner_tags) do
206              { tag_list: evaluate_runner_tags }.compact
207            end
208          end
209
210          def evaluate_runner_tags
211            @seed_attributes.delete(:tag_list)&.map do |tag|
212              ExpandVariables.expand_existing(tag, -> { evaluate_context.variables_hash })
213            end
214          end
215
216          # If a job uses `allow_failure:exit_codes` and `rules:allow_failure`
217          # we need to prevent the exit codes from being persisted because they
218          # would break the behavior defined by `rules:allow_failure`.
219          def allow_failure_criteria_attributes
220            return {} if rules_attributes[:allow_failure].nil?
221            return {} unless @seed_attributes.dig(:options, :allow_failure_criteria)
222
223            { options: { allow_failure_criteria: nil } }
224          end
225
226          def calculate_yaml_variables!
227            @seed_attributes[:yaml_variables] = Gitlab::Ci::Variables::Helpers.inherit_yaml_variables(
228              from: @context.root_variables, to: @job_variables, inheritance: @root_variables_inheritance
229            )
230          end
231        end
232      end
233    end
234  end
235end
236