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
|