debian-mirror-gitlab/lib/gitlab/ci/yaml_processor.rb

273 lines
8.1 KiB
Ruby
Raw Normal View History

2018-12-13 13:39:08 +05:30
# frozen_string_literal: true
2018-03-17 18:26:18 +05:30
module Gitlab
module Ci
class YamlProcessor
2020-07-28 23:09:34 +05:30
# ValidationError is treated like a result object in the form of an exception.
# We can return any warnings, raised during the config validation, along with
# the error object until we support multiple messages to be returned.
class ValidationError < StandardError
attr_reader :warnings
def initialize(message, warnings: [])
@warnings = warnings
super(message)
end
end
2018-03-17 18:26:18 +05:30
2019-02-15 15:39:39 +05:30
include Gitlab::Config::Entry::LegacyValidationHelpers
2018-03-17 18:26:18 +05:30
2019-09-30 21:07:59 +05:30
attr_reader :stages, :jobs
2018-03-17 18:26:18 +05:30
2020-07-28 23:09:34 +05:30
class Result
attr_reader :config, :errors, :warnings
def initialize(config: nil, errors: [], warnings: [])
@config = config
@errors = errors
@warnings = warnings
end
2020-01-01 13:55:28 +05:30
def valid?
2020-07-28 23:09:34 +05:30
config.present? && errors.empty?
2020-01-01 13:55:28 +05:30
end
end
2018-05-09 12:01:36 +05:30
def initialize(config, opts = {})
2019-02-15 15:39:39 +05:30
@ci_config = Gitlab::Ci::Config.new(config, **opts)
2018-03-17 18:26:18 +05:30
@config = @ci_config.to_hash
unless @ci_config.valid?
2020-07-28 23:09:34 +05:30
error!(@ci_config.errors.first)
2018-03-17 18:26:18 +05:30
end
initial_parsing
2018-11-20 20:47:30 +05:30
rescue Gitlab::Ci::Config::ConfigError => e
2020-07-28 23:09:34 +05:30
error!(e.message)
2018-03-17 18:26:18 +05:30
end
2020-01-01 13:55:28 +05:30
def self.new_with_validation_errors(content, opts = {})
2020-07-28 23:09:34 +05:30
return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) if content.blank?
2020-01-01 13:55:28 +05:30
config = Gitlab::Ci::Config.new(content, **opts)
2020-07-28 23:09:34 +05:30
return Result.new(errors: config.errors, warnings: config.warnings) unless config.valid?
2020-01-01 13:55:28 +05:30
config = Gitlab::Ci::YamlProcessor.new(content, opts)
2020-07-28 23:09:34 +05:30
Result.new(config: config, warnings: config.warnings)
rescue ValidationError => e
Result.new(errors: [e.message], warnings: e.warnings)
rescue Gitlab::Ci::Config::ConfigError => e
Result.new(errors: [e.message])
end
def warnings
@ci_config&.warnings || []
2020-01-01 13:55:28 +05:30
end
2018-03-17 18:26:18 +05:30
def builds
@jobs.map do |name, _|
build_attributes(name)
end
end
def build_attributes(name)
2018-05-09 12:01:36 +05:30
job = @jobs.fetch(name.to_sym, {})
2018-03-17 18:26:18 +05:30
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
2019-03-02 22:35:43 +05:30
tag_list: job[:tags],
2018-03-17 18:26:18 +05:30
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
2020-04-08 14:13:33 +05:30
yaml_variables: transform_to_yaml_variables(job[:variables]),
2019-12-26 22:10:19 +05:30
needs_attributes: job.dig(:needs, :job),
2019-12-04 20:38:33 +05:30
interruptible: job[:interruptible],
2020-01-01 13:55:28 +05:30
only: job[:only],
except: job[:except],
2019-12-04 20:38:33 +05:30
rules: job[:rules],
2019-12-26 22:10:19 +05:30
cache: job[:cache],
2020-03-13 15:44:24 +05:30
resource_group_key: job[:resource_group],
scheduling_type: job[:scheduling_type],
2020-07-28 23:09:34 +05:30
secrets: job[:secrets],
2018-03-17 18:26:18 +05:30
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
2020-01-01 13:55:28 +05:30
cross_dependencies: job.dig(:needs, :cross_dependency),
2019-12-04 20:38:33 +05:30
job_timeout: job[:timeout],
2018-03-17 18:26:18 +05:30
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
2018-12-05 23:21:45 +05:30
retry: job[:retry],
2018-12-13 13:39:08 +05:30
parallel: job[:parallel],
instance: job[:instance],
2019-03-02 22:35:43 +05:30
start_in: job[:start_in],
2019-10-12 21:52:04 +05:30
trigger: job[:trigger],
2020-03-13 15:44:24 +05:30
bridge_needs: job.dig(:needs, :bridge)&.first,
release: release(job)
2019-03-02 22:35:43 +05:30
}.compact }.compact
2018-03-17 18:26:18 +05:30
end
2020-03-13 15:44:24 +05:30
def release(job)
2020-10-24 23:57:45 +05:30
job[:release]
2020-03-13 15:44:24 +05:30
end
2018-05-09 12:01:36 +05:30
def stage_builds_attributes(stage)
@jobs.values
.select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
2018-03-17 18:26:18 +05:30
end
2018-05-09 12:01:36 +05:30
def stages_attributes
@stages.uniq.map do |stage|
2020-01-01 13:55:28 +05:30
seeds = stage_builds_attributes(stage)
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
{ name: stage, index: @stages.index(stage), builds: seeds }
end
2018-03-17 18:26:18 +05:30
end
2019-12-26 22:10:19 +05:30
def workflow_attributes
{
rules: @config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(@variables)
}
end
2018-05-09 12:01:36 +05:30
def self.validation_message(content, opts = {})
2018-03-17 18:26:18 +05:30
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
2018-05-09 12:01:36 +05:30
Gitlab::Ci::YamlProcessor.new(content, opts)
2018-03-17 18:26:18 +05:30
nil
rescue ValidationError => e
e.message
end
end
private
def initial_parsing
##
# Global config
#
@variables = @ci_config.variables
@stages = @ci_config.stages
##
# Jobs
#
2018-12-13 13:39:08 +05:30
@jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
2018-03-17 18:26:18 +05:30
@jobs.each do |name, job|
# logical validation for job
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
2019-10-12 21:52:04 +05:30
validate_job_needs!(name, job)
2020-04-22 19:07:51 +05:30
validate_dynamic_child_pipeline_dependencies!(name, job)
2018-03-17 18:26:18 +05:30
validate_job_environment!(name, job)
end
end
2019-12-26 22:10:19 +05:30
def transform_to_yaml_variables(variables)
variables.to_h.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
2018-03-17 18:26:18 +05:30
end
def validate_job_stage!(name, job)
return unless job[:stage]
unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
2020-07-28 23:09:34 +05:30
error!("#{name} job: chosen stage does not exist; available stages are #{@stages.join(", ")}")
2018-03-17 18:26:18 +05:30
end
end
2020-07-28 23:09:34 +05:30
def error!(message)
raise ValidationError.new(message, warnings: warnings)
end
2018-03-17 18:26:18 +05:30
def validate_job_dependencies!(name, job)
return unless job[:dependencies]
job[:dependencies].each do |dependency|
2020-04-22 19:07:51 +05:30
validate_job_dependency!(name, dependency)
end
end
2018-03-17 18:26:18 +05:30
2020-04-22 19:07:51 +05:30
def validate_dynamic_child_pipeline_dependencies!(name, job)
return unless includes = job.dig(:trigger, :include)
2019-10-12 21:52:04 +05:30
2020-04-22 19:07:51 +05:30
Array(includes).each do |included|
next unless included.is_a?(Hash)
next unless dependency = included[:job]
validate_job_dependency!(name, dependency)
2018-03-17 18:26:18 +05:30
end
end
2019-10-12 21:52:04 +05:30
def validate_job_needs!(name, job)
2020-04-22 19:07:51 +05:30
return unless needs = job.dig(:needs, :job)
2019-10-12 21:52:04 +05:30
2020-04-22 19:07:51 +05:30
needs.each do |need|
validate_job_dependency!(name, need[:name], 'need')
end
end
2019-12-26 22:10:19 +05:30
2020-04-22 19:07:51 +05:30
def validate_job_dependency!(name, dependency, dependency_type = 'dependency')
unless @jobs[dependency.to_sym]
2020-07-28 23:09:34 +05:30
error!("#{name} job: undefined #{dependency_type}: #{dependency}")
2020-04-22 19:07:51 +05:30
end
2019-10-12 21:52:04 +05:30
2020-04-22 19:07:51 +05:30
job_stage_index = stage_index(name)
dependency_stage_index = stage_index(dependency)
2019-10-12 21:52:04 +05:30
2020-04-22 19:07:51 +05:30
# A dependency might be defined later in the configuration
# with a stage that does not exist
unless dependency_stage_index.present? && dependency_stage_index < job_stage_index
2020-07-28 23:09:34 +05:30
error!("#{name} job: #{dependency_type} #{dependency} is not defined in prior stages")
2019-10-12 21:52:04 +05:30
end
end
2020-04-22 19:07:51 +05:30
def stage_index(name)
stage = @jobs.dig(name.to_sym, :stage)
@stages.index(stage)
end
2018-03-17 18:26:18 +05:30
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
2020-07-28 23:09:34 +05:30
error!("#{name} job: on_stop job #{on_stop} is not defined")
2018-03-17 18:26:18 +05:30
end
unless on_stop_job[:environment]
2020-07-28 23:09:34 +05:30
error!("#{name} job: on_stop job #{on_stop} does not have environment defined")
2018-03-17 18:26:18 +05:30
end
unless on_stop_job[:environment][:name] == environment[:name]
2020-07-28 23:09:34 +05:30
error!("#{name} job: on_stop job #{on_stop} have different environment name")
2018-03-17 18:26:18 +05:30
end
unless on_stop_job[:environment][:action] == 'stop'
2020-07-28 23:09:34 +05:30
error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined")
2018-03-17 18:26:18 +05:30
end
end
end
end
end