# frozen_string_literal: true module Gitlab module Ci module Pipeline module Seed class Build < Seed::Base include Gitlab::Utils::StrongMemoize delegate :dig, to: :@seed_attributes def initialize(context, attributes, stages_for_needs_lookup, stage) @context = context @pipeline = context.pipeline @seed_attributes = attributes @stages_for_needs_lookup = stages_for_needs_lookup.compact @needs_attributes = dig(:needs_attributes) @stage = stage @resource_group_key = attributes.delete(:resource_group_key) @job_variables = @seed_attributes.delete(:job_variables) @root_variables_inheritance = @seed_attributes.delete(:root_variables_inheritance) { true } @using_rules = attributes.key?(:rules) @using_only = attributes.key?(:only) @using_except = attributes.key?(:except) @only = Gitlab::Ci::Build::Policy .fabricate(attributes.delete(:only)) @except = Gitlab::Ci::Build::Policy .fabricate(attributes.delete(:except)) @rules = Gitlab::Ci::Build::Rules .new(attributes.delete(:rules), default_when: attributes[:when]) @cache = Gitlab::Ci::Build::Cache .new(attributes.delete(:cache), @pipeline) calculate_yaml_variables! @processable = initialize_processable end def name dig(:name) end def included? logger.instrument(:pipeline_seed_build_inclusion) do if @using_rules rules_result.pass? elsif @using_only || @using_except all_of_only? && none_of_except? else true end end end strong_memoize_attr :included?, :inclusion def errors logger.instrument(:pipeline_seed_build_errors) do # We check rules errors before checking "included?" because rules affects its inclusion status. next rules_errors if rules_errors next unless included? [needs_errors, variable_expansion_errors].compact.flatten end end strong_memoize_attr :errors # TODO: Method used only in specs. Replace with `to_resource.attributes` when # the feature flag ci_reuse_build_in_seed_context is removed. # Then remove this method. def attributes if reuse_build_in_seed_context? initial_attributes.deep_merge(evaluated_attributes) else @seed_attributes .deep_merge(pipeline_attributes) .deep_merge(rules_attributes) .deep_merge(allow_failure_criteria_attributes) .deep_merge(@cache.cache_attributes) .deep_merge(runner_tags) end end def bridge? attributes_hash = @seed_attributes.to_h attributes_hash.dig(:options, :trigger).present? || (attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) && attributes_hash.dig(:options, :bridge_needs, :pipeline).present?) end def to_resource logger.instrument(:pipeline_seed_build_to_resource) do if reuse_build_in_seed_context? # The `options` attribute need to be entirely reassigned because they may # be overridden by evaluated_attributes. # We also don't want to reassign all the `initial_attributes` since those # can affect performance. We only want to assign what's changed. assignable_attributes = initial_attributes.slice(:options) .deep_merge(evaluated_attributes) processable.assign_attributes(assignable_attributes) processable else legacy_initialize_processable end end end strong_memoize_attr :to_resource private attr_reader :processable delegate :logger, to: :@context def legacy_initialize_processable if bridge? ::Ci::Bridge.new(attributes) else ::Ci::Build.new(attributes) end end def initialize_processable return unless reuse_build_in_seed_context? if bridge? ::Ci::Bridge.new(initial_attributes) else ::Ci::Build.new(initial_attributes) end end def initial_attributes @seed_attributes .deep_merge(pipeline_attributes) .deep_merge(ci_stage: @stage) .deep_merge(@cache.cache_attributes) end def evaluated_attributes rules_attributes .deep_merge(allow_failure_criteria_attributes) .deep_merge(runner_tags) end def all_of_only? @only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) } end def none_of_except? @except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) } end def needs_errors return if @needs_attributes.nil? if @needs_attributes.size > max_needs_allowed return [ "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \ "See needs keyword documentation for more details" ] end @needs_attributes.flat_map do |need| next if need[:optional] result = need_present?(need) "'#{name}' job needs '#{need[:name]}' job, but '#{need[:name]}' is not in any previous stage" unless result end.compact end def need_present?(need) @stages_for_needs_lookup.any? do |stage| stage.seeds_names.include?(need[:name]) end end def max_needs_allowed @pipeline.project.actual_limits.ci_needs_size_limit end def variable_expansion_errors expanded_collection = evaluate_context.variables.sort_and_expand_all errors = expanded_collection.errors ["#{name}: #{errors}"] if errors end def pipeline_attributes { pipeline: @pipeline, project: @pipeline.project, user: @pipeline.user, ref: @pipeline.ref, tag: @pipeline.tag, trigger_request: @pipeline.legacy_trigger, protected: @pipeline.protected_ref?, partition_id: @pipeline.partition_id, metadata_attributes: { partition_id: @pipeline.partition_id } } end def rules_attributes return {} unless @using_rules rules_variables_result = ::Gitlab::Ci::Variables::Helpers.merge_variables( @seed_attributes[:yaml_variables], rules_result.variables ) rules_result.build_attributes.merge(yaml_variables: rules_variables_result) end strong_memoize_attr :rules_attributes def rules_result @rules.evaluate(@pipeline, evaluate_context) end strong_memoize_attr :rules_result def rules_errors ["Failed to parse rule for #{name}: #{rules_result.errors.join(', ')}"] if rules_result.errors.present? end strong_memoize_attr :rules_errors def evaluate_context if reuse_build_in_seed_context? Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes, processable) else Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes) end end strong_memoize_attr :evaluate_context def runner_tags { tag_list: evaluate_runner_tags }.compact end strong_memoize_attr :runner_tags def evaluate_runner_tags @seed_attributes.delete(:tag_list)&.map do |tag| ExpandVariables.expand_existing(tag, -> { evaluate_context.variables_hash }) end end # If a job uses `allow_failure:exit_codes` and `rules:allow_failure` # we need to prevent the exit codes from being persisted because they # would break the behavior defined by `rules:allow_failure`. def allow_failure_criteria_attributes return {} if rules_attributes[:allow_failure].nil? return {} unless @seed_attributes.dig(:options, :allow_failure_criteria) { options: { allow_failure_criteria: nil } } end def calculate_yaml_variables! @seed_attributes[:yaml_variables] = Gitlab::Ci::Variables::Helpers.inherit_yaml_variables( from: @context.root_variables, to: @job_variables, inheritance: @root_variables_inheritance ) end def reuse_build_in_seed_context? Feature.enabled?(:ci_reuse_build_in_seed_context, @pipeline.project) end strong_memoize_attr :reuse_build_in_seed_context?, :reuse_build_in_seed_context end end end end end