diff options
author | Maxim Rydkin <maks.rydkin@gmail.com> | 2017-09-06 14:23:24 +0300 |
---|---|---|
committer | Maxim Rydkin <maks.rydkin@gmail.com> | 2017-09-12 22:32:02 +0300 |
commit | c45ace8972f18af1f232f9074d6e4104fc4f0c14 (patch) | |
tree | 0234b8da9c0eeafb67e1523b1d05d86ef33ca5c0 /lib | |
parent | c295d3362b610945de5c722616b39323a48c377e (diff) |
move `lib/ci/gitlab_ci_yaml_processor.rb` into `lib/gitlab/ci/yaml_processor.rb`
Diffstat (limited to 'lib')
-rw-r--r-- | lib/api/lint.rb | 2 | ||||
-rw-r--r-- | lib/ci/gitlab_ci_yaml_processor.rb | 251 | ||||
-rw-r--r-- | lib/gitlab/ci/yaml_processor.rb | 253 |
3 files changed, 254 insertions, 252 deletions
diff --git a/lib/api/lint.rb b/lib/api/lint.rb index ae43a4a3237..d202eaa4c49 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -6,7 +6,7 @@ module API requires :content, type: String, desc: 'Content of .gitlab-ci.yml' end post '/lint' do - error = Ci::GitlabCiYamlProcessor.validation_message(params[:content]) + error = Gitlab::Ci::YamlProcessor.validation_message(params[:content]) status 200 diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb deleted file mode 100644 index 62b44389b15..00000000000 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ /dev/null @@ -1,251 +0,0 @@ -module Ci - class GitlabCiYamlProcessor - ValidationError = Class.new(StandardError) - - include Gitlab::Ci::Config::Entry::LegacyValidationHelpers - - attr_reader :path, :cache, :stages, :jobs - - def initialize(config, path = nil) - @ci_config = Gitlab::Ci::Config.new(config) - @config = @ci_config.to_hash - @path = path - - unless @ci_config.valid? - raise ValidationError, @ci_config.errors.first - end - - initial_parsing - rescue Gitlab::Ci::Config::Loader::FormatError => e - raise ValidationError, e.message - end - - def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| - build_attributes(name) - end - end - - def builds - @jobs.map do |name, _| - build_attributes(name) - end - end - - def stage_seeds(pipeline) - seeds = @stages.uniq.map do |stage| - builds = pipeline_stage_builds(stage, pipeline) - - Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? - end - - seeds.compact - end - - def build_attributes(name) - job = @jobs[name.to_sym] || {} - - { stage_idx: @stages.index(job[:stage]), - stage: job[:stage], - commands: job[:commands], - 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: yaml_variables(name), - options: { - image: job[:image], - services: job[:services], - artifacts: job[:artifacts], - cache: job[:cache], - dependencies: job[:dependencies], - before_script: job[:before_script], - script: job[:script], - after_script: job[:after_script], - environment: job[:environment], - retry: job[:retry] - }.compact } - end - - def self.validation_message(content) - return 'Please provide content of .gitlab-ci.yml' if content.blank? - - begin - Ci::GitlabCiYamlProcessor.new(content) - nil - rescue ValidationError, Psych::SyntaxError => e - e.message - end - end - - private - - def pipeline_stage_builds(stage, pipeline) - builds = builds_for_stage_and_ref( - stage, pipeline.ref, pipeline.tag?, pipeline.source) - - builds.select do |build| - job = @jobs[build.fetch(:name).to_sym] - has_kubernetes = pipeline.has_kubernetes_active? - only_kubernetes = job.dig(:only, :kubernetes) - except_kubernetes = job.dig(:except, :kubernetes) - - [!only_kubernetes && !except_kubernetes, - only_kubernetes && has_kubernetes, - except_kubernetes && !has_kubernetes].any? - end - end - - def jobs_for_ref(ref, tag = false, source = nil) - @jobs.select do |_, job| - process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) - end - end - - def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_ref(ref, tag, source).select do |_, job| - job[:stage] == stage - end - end - - def initial_parsing - ## - # Global config - # - @before_script = @ci_config.before_script - @image = @ci_config.image - @after_script = @ci_config.after_script - @services = @ci_config.services - @variables = @ci_config.variables - @stages = @ci_config.stages - @cache = @ci_config.cache - - ## - # Jobs - # - @jobs = @ci_config.jobs - - @jobs.each do |name, job| - # logical validation for job - - validate_job_stage!(name, job) - validate_job_dependencies!(name, job) - validate_job_environment!(name, job) - end - end - - def yaml_variables(name) - variables = (@variables || {}) - .merge(job_variables(name)) - - variables.map do |key, value| - { key: key.to_s, value: value, public: true } - end - end - - def job_variables(name) - job = @jobs[name.to_sym] - return {} unless job - - job[:variables] || {} - 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] - - unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index - raise ValidationError, "#{name} job: dependency #{dependency} 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 - - def process?(only_params, except_params, ref, tag, source) - if only_params.present? - return false unless matching?(only_params, ref, tag, source) - end - - if except_params.present? - return false if matching?(except_params, ref, tag, source) - end - - true - end - - def matching?(patterns, ref, tag, source) - patterns.any? do |pattern| - pattern, path = pattern.split('@', 2) - matches_path?(path) && matches_pattern?(pattern, ref, tag, source) - end - end - - def matches_path?(path) - return true unless path - - path == self.path - end - - def matches_pattern?(pattern, ref, tag, source) - return true if tag && pattern == 'tags' - return true if !tag && pattern == 'branches' - return true if source_to_pattern(source) == pattern - - if pattern.first == "/" && pattern.last == "/" - Regexp.new(pattern[1...-1]) =~ ref - else - pattern == ref - end - end - - def source_to_pattern(source) - if %w[api external web].include?(source) - source - else - source&.pluralize - end - end - end -end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb new file mode 100644 index 00000000000..7582964b24e --- /dev/null +++ b/lib/gitlab/ci/yaml_processor.rb @@ -0,0 +1,253 @@ +module Gitlab + module Ci + class YamlProcessor + ValidationError = Class.new(StandardError) + + include Gitlab::Ci::Config::Entry::LegacyValidationHelpers + + attr_reader :path, :cache, :stages, :jobs + + def initialize(config, path = nil) + @ci_config = Gitlab::Ci::Config.new(config) + @config = @ci_config.to_hash + @path = path + + unless @ci_config.valid? + raise ValidationError, @ci_config.errors.first + end + + initial_parsing + rescue Gitlab::Ci::Config::Loader::FormatError => e + raise ValidationError, e.message + end + + def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| + build_attributes(name) + end + end + + def builds + @jobs.map do |name, _| + build_attributes(name) + end + end + + def stage_seeds(pipeline) + seeds = @stages.uniq.map do |stage| + builds = pipeline_stage_builds(stage, pipeline) + + Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? + end + + seeds.compact + end + + def build_attributes(name) + job = @jobs[name.to_sym] || {} + + { stage_idx: @stages.index(job[:stage]), + stage: job[:stage], + commands: job[:commands], + 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: yaml_variables(name), + options: { + image: job[:image], + services: job[:services], + artifacts: job[:artifacts], + cache: job[:cache], + dependencies: job[:dependencies], + before_script: job[:before_script], + script: job[:script], + after_script: job[:after_script], + environment: job[:environment], + retry: job[:retry] + }.compact } + end + + def self.validation_message(content) + return 'Please provide content of .gitlab-ci.yml' if content.blank? + + begin + Gitlab::Ci::YamlProcessor.new(content) + nil + rescue ValidationError, Psych::SyntaxError => e + e.message + end + end + + private + + def pipeline_stage_builds(stage, pipeline) + builds = builds_for_stage_and_ref( + stage, pipeline.ref, pipeline.tag?, pipeline.source) + + builds.select do |build| + job = @jobs[build.fetch(:name).to_sym] + has_kubernetes = pipeline.has_kubernetes_active? + only_kubernetes = job.dig(:only, :kubernetes) + except_kubernetes = job.dig(:except, :kubernetes) + + [!only_kubernetes && !except_kubernetes, + only_kubernetes && has_kubernetes, + except_kubernetes && !has_kubernetes].any? + end + end + + def jobs_for_ref(ref, tag = false, source = nil) + @jobs.select do |_, job| + process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) + end + end + + def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_ref(ref, tag, source).select do |_, job| + job[:stage] == stage + end + end + + def initial_parsing + ## + # Global config + # + @before_script = @ci_config.before_script + @image = @ci_config.image + @after_script = @ci_config.after_script + @services = @ci_config.services + @variables = @ci_config.variables + @stages = @ci_config.stages + @cache = @ci_config.cache + + ## + # Jobs + # + @jobs = @ci_config.jobs + + @jobs.each do |name, job| + # logical validation for job + + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) + validate_job_environment!(name, job) + end + end + + def yaml_variables(name) + variables = (@variables || {}) + .merge(job_variables(name)) + + variables.map do |key, value| + { key: key.to_s, value: value, public: true } + end + end + + def job_variables(name) + job = @jobs[name.to_sym] + return {} unless job + + job[:variables] || {} + 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] + + unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index + raise ValidationError, "#{name} job: dependency #{dependency} 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 + + def process?(only_params, except_params, ref, tag, source) + if only_params.present? + return false unless matching?(only_params, ref, tag, source) + end + + if except_params.present? + return false if matching?(except_params, ref, tag, source) + end + + true + end + + def matching?(patterns, ref, tag, source) + patterns.any? do |pattern| + pattern, path = pattern.split('@', 2) + matches_path?(path) && matches_pattern?(pattern, ref, tag, source) + end + end + + def matches_path?(path) + return true unless path + + path == self.path + end + + def matches_pattern?(pattern, ref, tag, source) + return true if tag && pattern == 'tags' + return true if !tag && pattern == 'branches' + return true if source_to_pattern(source) == pattern + + if pattern.first == "/" && pattern.last == "/" + Regexp.new(pattern[1...-1]) =~ ref + else + pattern == ref + end + end + + def source_to_pattern(source) + if %w[api external web].include?(source) + source + else + source&.pluralize + end + end + end + end +end |