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