# frozen_string_literal: true module Gitlab module Ci class YamlProcessor ValidationError = Class.new(StandardError) include Gitlab::Config::Entry::LegacyValidationHelpers attr_reader :stages, :jobs def initialize(config, opts = {}) @ci_config = Gitlab::Ci::Config.new(config, **opts) @config = @ci_config.to_hash unless @ci_config.valid? raise ValidationError, @ci_config.errors.first end initial_parsing rescue Gitlab::Ci::Config::ConfigError => e raise ValidationError, e.message end def builds @jobs.map do |name, _| build_attributes(name) end end def build_attributes(name) job = @jobs.fetch(name.to_sym, {}) { stage_idx: @stages.index(job[:stage]), stage: job[:stage], tag_list: job[:tags], name: job[:name].to_s, allow_failure: job[:ignore], when: job[:when] || 'on_success', environment: job[:environment_name], coverage_regex: job[:coverage], yaml_variables: transform_to_yaml_variables(job_variables(name)), needs_attributes: job.dig(:needs, :job), interruptible: job[:interruptible], rules: job[:rules], cache: job[:cache], options: { image: job[:image], services: job[:services], artifacts: job[:artifacts], dependencies: job[:dependencies], job_timeout: job[:timeout], before_script: job[:before_script], script: job[:script], after_script: job[:after_script], environment: job[:environment], retry: job[:retry], parallel: job[:parallel], instance: job[:instance], start_in: job[:start_in], trigger: job[:trigger], bridge_needs: job.dig(:needs, :bridge)&.first }.compact }.compact end def stage_builds_attributes(stage) @jobs.values .select { |job| job[:stage] == stage } .map { |job| build_attributes(job[:name]) } end def stages_attributes @stages.uniq.map do |stage| seeds = stage_builds_attributes(stage).map do |attributes| job = @jobs.fetch(attributes[:name].to_sym) attributes .merge(only: job.fetch(:only, {})) .merge(except: job.fetch(:except, {})) end { name: stage, index: @stages.index(stage), builds: seeds } end end def workflow_attributes { rules: @config.dig(:workflow, :rules), yaml_variables: transform_to_yaml_variables(@variables) } end def self.validation_message(content, opts = {}) return 'Please provide content of .gitlab-ci.yml' if content.blank? begin Gitlab::Ci::YamlProcessor.new(content, opts) nil rescue ValidationError => e e.message end end private def initial_parsing ## # Global config # @variables = @ci_config.variables @stages = @ci_config.stages ## # Jobs # @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs @jobs.each do |name, job| # logical validation for job validate_job_stage!(name, job) validate_job_dependencies!(name, job) validate_job_needs!(name, job) validate_job_environment!(name, job) end end def job_variables(name) job_variables = @jobs.dig(name.to_sym, :variables) @variables.to_h .merge(job_variables.to_h) end def transform_to_yaml_variables(variables) variables.to_h.map do |key, value| { key: key.to_s, value: value, public: true } end end def validate_job_stage!(name, job) return unless job[:stage] unless job[:stage].is_a?(String) && job[:stage].in?(@stages) raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" end end def validate_job_dependencies!(name, job) return unless job[:dependencies] stage_index = @stages.index(job[:stage]) job[:dependencies].each do |dependency| raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] dependency_stage_index = @stages.index(@jobs[dependency.to_sym][:stage]) unless dependency_stage_index.present? && dependency_stage_index < stage_index raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" end end end def validate_job_needs!(name, job) return unless job.dig(:needs, :job) stage_index = @stages.index(job[:stage]) job.dig(:needs, :job).each do |need| need_job_name = need[:name] raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym] needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage]) unless needs_stage_index.present? && needs_stage_index < stage_index raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages" end end end def validate_job_environment!(name, job) return unless job[:environment] return unless job[:environment].is_a?(Hash) environment = job[:environment] validate_on_stop_job!(name, environment, environment[:on_stop]) end def validate_on_stop_job!(name, environment, on_stop) return unless on_stop on_stop_job = @jobs[on_stop.to_sym] unless on_stop_job raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined" end unless on_stop_job[:environment] raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined" end unless on_stop_job[:environment][:name] == environment[:name] raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name" end unless on_stop_job[:environment][:action] == 'stop' raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" end end end end end