From 64b1044e7ac22d14a9c17ef773dd075b74df00fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 29 Nov 2018 12:44:48 +0100 Subject: [PATCH] ci/config: generalize Config validation into Gitlab::Config:: module This decouples Ci::Config to provide a common interface for handling user configuration files. --- lib/gitlab/ci/config.rb | 4 +- lib/gitlab/ci/config/entry/artifacts.rb | 8 +- lib/gitlab/ci/config/entry/attributable.rb | 29 --- lib/gitlab/ci/config/entry/boolean.rb | 20 -- lib/gitlab/ci/config/entry/cache.rb | 8 +- lib/gitlab/ci/config/entry/commands.rb | 4 +- lib/gitlab/ci/config/entry/configurable.rb | 83 -------- lib/gitlab/ci/config/entry/coverage.rb | 4 +- lib/gitlab/ci/config/entry/environment.rb | 4 +- lib/gitlab/ci/config/entry/factory.rb | 75 ------- lib/gitlab/ci/config/entry/global.rb | 6 +- lib/gitlab/ci/config/entry/hidden.rb | 4 +- lib/gitlab/ci/config/entry/image.rb | 4 +- lib/gitlab/ci/config/entry/job.rb | 6 +- lib/gitlab/ci/config/entry/jobs.rb | 6 +- lib/gitlab/ci/config/entry/key.rb | 4 +- .../config/entry/legacy_validation_helpers.rb | 72 ------- lib/gitlab/ci/config/entry/node.rb | 103 --------- lib/gitlab/ci/config/entry/paths.rb | 4 +- lib/gitlab/ci/config/entry/policy.rb | 14 +- lib/gitlab/ci/config/entry/reports.rb | 6 +- lib/gitlab/ci/config/entry/retry.rb | 14 +- lib/gitlab/ci/config/entry/script.rb | 4 +- lib/gitlab/ci/config/entry/service.rb | 2 +- lib/gitlab/ci/config/entry/services.rb | 6 +- lib/gitlab/ci/config/entry/simplifiable.rb | 45 ---- lib/gitlab/ci/config/entry/stage.rb | 4 +- lib/gitlab/ci/config/entry/stages.rb | 4 +- lib/gitlab/ci/config/entry/undefined.rb | 42 ---- lib/gitlab/ci/config/entry/unspecified.rb | 21 -- lib/gitlab/ci/config/entry/validatable.rb | 40 ---- lib/gitlab/ci/config/entry/validator.rb | 28 --- lib/gitlab/ci/config/entry/validators.rb | 198 ------------------ lib/gitlab/ci/config/entry/variables.rb | 4 +- lib/gitlab/ci/config/external/file/base.rb | 4 +- lib/gitlab/ci/yaml_processor.rb | 2 +- lib/gitlab/config/entry/attributable.rb | 27 +++ lib/gitlab/config/entry/boolean.rb | 18 ++ lib/gitlab/config/entry/configurable.rb | 81 +++++++ lib/gitlab/config/entry/factory.rb | 73 +++++++ .../config/entry/legacy_validation_helpers.rb | 70 +++++++ lib/gitlab/config/entry/node.rb | 101 +++++++++ lib/gitlab/config/entry/simplifiable.rb | 43 ++++ lib/gitlab/config/entry/undefined.rb | 40 ++++ lib/gitlab/config/entry/unspecified.rb | 19 ++ lib/gitlab/config/entry/validatable.rb | 38 ++++ lib/gitlab/config/entry/validator.rb | 26 +++ lib/gitlab/config/entry/validators.rb | 196 +++++++++++++++++ lib/gitlab/config/loader/format_error.rb | 9 + .../loader.rb => config/loader/yaml.rb} | 12 +- .../config/entry/attributable_spec.rb | 6 +- .../{ci => }/config/entry/boolean_spec.rb | 2 +- .../config/entry/configurable_spec.rb | 8 +- .../{ci => }/config/entry/factory_spec.rb | 14 +- .../config/entry/simplifiable_spec.rb | 2 +- .../{ci => }/config/entry/undefined_spec.rb | 2 +- .../{ci => }/config/entry/unspecified_spec.rb | 2 +- .../{ci => }/config/entry/validatable_spec.rb | 8 +- .../{ci => }/config/entry/validator_spec.rb | 2 +- .../loader/yaml_spec.rb} | 6 +- 60 files changed, 841 insertions(+), 850 deletions(-) delete mode 100644 lib/gitlab/ci/config/entry/attributable.rb delete mode 100644 lib/gitlab/ci/config/entry/boolean.rb delete mode 100644 lib/gitlab/ci/config/entry/configurable.rb delete mode 100644 lib/gitlab/ci/config/entry/factory.rb delete mode 100644 lib/gitlab/ci/config/entry/legacy_validation_helpers.rb delete mode 100644 lib/gitlab/ci/config/entry/node.rb delete mode 100644 lib/gitlab/ci/config/entry/simplifiable.rb delete mode 100644 lib/gitlab/ci/config/entry/undefined.rb delete mode 100644 lib/gitlab/ci/config/entry/unspecified.rb delete mode 100644 lib/gitlab/ci/config/entry/validatable.rb delete mode 100644 lib/gitlab/ci/config/entry/validator.rb delete mode 100644 lib/gitlab/ci/config/entry/validators.rb create mode 100644 lib/gitlab/config/entry/attributable.rb create mode 100644 lib/gitlab/config/entry/boolean.rb create mode 100644 lib/gitlab/config/entry/configurable.rb create mode 100644 lib/gitlab/config/entry/factory.rb create mode 100644 lib/gitlab/config/entry/legacy_validation_helpers.rb create mode 100644 lib/gitlab/config/entry/node.rb create mode 100644 lib/gitlab/config/entry/simplifiable.rb create mode 100644 lib/gitlab/config/entry/undefined.rb create mode 100644 lib/gitlab/config/entry/unspecified.rb create mode 100644 lib/gitlab/config/entry/validatable.rb create mode 100644 lib/gitlab/config/entry/validator.rb create mode 100644 lib/gitlab/config/entry/validators.rb create mode 100644 lib/gitlab/config/loader/format_error.rb rename lib/gitlab/{ci/config/loader.rb => config/loader/yaml.rb} (66%) rename spec/lib/gitlab/{ci => }/config/entry/attributable_spec.rb (87%) rename spec/lib/gitlab/{ci => }/config/entry/boolean_spec.rb (93%) rename spec/lib/gitlab/{ci => }/config/entry/configurable_spec.rb (82%) rename spec/lib/gitlab/{ci => }/config/entry/factory_spec.rb (86%) rename spec/lib/gitlab/{ci => }/config/entry/simplifiable_spec.rb (97%) rename spec/lib/gitlab/{ci => }/config/entry/undefined_spec.rb (93%) rename spec/lib/gitlab/{ci => }/config/entry/unspecified_spec.rb (92%) rename spec/lib/gitlab/{ci => }/config/entry/validatable_spec.rb (84%) rename spec/lib/gitlab/{ci => }/config/entry/validator_spec.rb (96%) rename spec/lib/gitlab/{ci/config/loader_spec.rb => config/loader/yaml_spec.rb} (84%) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 2fb3c4582e7..6333799a491 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -15,7 +15,7 @@ module Gitlab @global = Entry::Global.new(@config) @global.compose! - rescue Loader::FormatError, + rescue Gitlab::Config::Loader::FormatError, Extendable::ExtensionError, External::Processor::IncludeError => e raise Config::ConfigError, e.message @@ -71,7 +71,7 @@ module Gitlab private def build_config(config, opts = {}) - initial_config = Loader.new(config).load! + initial_config = Gitlab::Config::Loader::Yaml.new(config).load! project = opts.fetch(:project, nil) if project diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb index ef5f25b42c0..41613369ca2 100644 --- a/lib/gitlab/ci/config/entry/artifacts.rb +++ b/lib/gitlab/ci/config/entry/artifacts.rb @@ -7,10 +7,10 @@ module Gitlab ## # Entry that represents a configuration of job artifacts. # - class Artifacts < Node - include Configurable - include Validatable - include Attributable + class Artifacts < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb deleted file mode 100644 index 3c2e1df9b83..00000000000 --- a/lib/gitlab/ci/config/entry/attributable.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module Attributable - extend ActiveSupport::Concern - - class_methods do - def attributes(*attributes) - attributes.flatten.each do |attribute| - if method_defined?(attribute) - raise ArgumentError, 'Method already defined!' - end - - define_method(attribute) do - return unless config.is_a?(Hash) - - config[attribute] - end - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb deleted file mode 100644 index b9639c83075..00000000000 --- a/lib/gitlab/ci/config/entry/boolean.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # Entry that represents a boolean value. - # - class Boolean < Node - include Validatable - - validations do - validates :config, boolean: true - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb index 0a25057f482..7b94af24c09 100644 --- a/lib/gitlab/ci/config/entry/cache.rb +++ b/lib/gitlab/ci/config/entry/cache.rb @@ -7,9 +7,9 @@ module Gitlab ## # Entry that represents a cache configuration # - class Cache < Node - include Configurable - include Attributable + class Cache < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[key untracked paths policy].freeze DEFAULT_POLICY = 'pull-push'.freeze @@ -22,7 +22,7 @@ module Gitlab entry :key, Entry::Key, description: 'Cache key used to define a cache affinity.' - entry :untracked, Entry::Boolean, + entry :untracked, ::Gitlab::Config::Entry::Boolean, description: 'Cache all untracked files.' entry :paths, Entry::Paths, diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb index d9658291ebe..02e368c1813 100644 --- a/lib/gitlab/ci/config/entry/commands.rb +++ b/lib/gitlab/ci/config/entry/commands.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a job script. # - class Commands < Node - include Validatable + class Commands < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings_or_string: true diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb deleted file mode 100644 index 4aabf0cfa31..00000000000 --- a/lib/gitlab/ci/config/entry/configurable.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # This mixin is responsible for adding DSL, which purpose is to - # simplifly process of adding child nodes. - # - # This can be used only if parent node is a configuration entry that - # holds a hash as a configuration value, for example: - # - # job: - # script: ... - # artifacts: ... - # - module Configurable - extend ActiveSupport::Concern - - included do - include Validatable - - validations do - validates :config, type: Hash - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def compose!(deps = nil) - return unless valid? - - self.class.nodes.each do |key, factory| - factory - .value(config[key]) - .with(key: key, parent: self) - - entries[key] = factory.create! - end - - yield if block_given? - - entries.each_value do |entry| - entry.compose!(deps) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - class_methods do - def nodes - Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def entry(key, entry, metadata) - factory = Entry::Factory.new(entry) - .with(description: metadata[:description]) - - (@nodes ||= {}).merge!(key.to_sym => factory) - end - # rubocop: enable CodeReuse/ActiveRecord - - def helpers(*nodes) - nodes.each do |symbol| - define_method("#{symbol}_defined?") do - entries[symbol]&.specified? - end - - define_method("#{symbol}_value") do - return unless entries[symbol] && entries[symbol].valid? - - entries[symbol].value - end - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 690409ccf77..89545158bed 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents Coverage settings. # - class Coverage < Node - include Validatable + class Coverage < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, regexp: true diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index 07e9e1d3f67..69a3a1aedef 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents an environment. # - class Environment < Node - include Validatable + class Environment < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable ALLOWED_KEYS = %i[name url action on_stop].freeze diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb deleted file mode 100644 index 85c9c3511a4..00000000000 --- a/lib/gitlab/ci/config/entry/factory.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # Factory class responsible for fabricating entry objects. - # - class Factory - InvalidFactory = Class.new(StandardError) - - def initialize(entry) - @entry = entry - @metadata = {} - @attributes = {} - end - - def value(value) - @value = value - self - end - - def metadata(metadata) - @metadata.merge!(metadata) - self - end - - def with(attributes) - @attributes.merge!(attributes) - self - end - - def create! - raise InvalidFactory unless defined?(@value) - - ## - # We assume that unspecified entry is undefined. - # See issue #18775. - # - if @value.nil? - Entry::Unspecified.new( - fabricate_unspecified - ) - else - fabricate(@entry, @value) - end - end - - private - - def fabricate_unspecified - ## - # If entry has a default value we fabricate concrete node - # with default value. - # - if @entry.default.nil? - fabricate(Entry::Undefined) - else - fabricate(@entry, @entry.default) - end - end - - def fabricate(entry, value = nil) - entry.new(value, @metadata).tap do |node| - node.key = @attributes[:key] - node.parent = @attributes[:parent] - node.description = @attributes[:description] - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index eba203d9d06..09ecb5fdb99 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -8,8 +8,8 @@ module Gitlab # This class represents a global entry - root Entry for entire # GitLab CI Configuration file. # - class Global < Node - include Configurable + class Global < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable entry :before_script, Entry::Script, description: 'Script that will be executed before each job.' @@ -49,7 +49,7 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def compose_jobs! - factory = Entry::Factory.new(Entry::Jobs) + factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs) .value(@config.except(*self.class.nodes.keys)) .with(key: :jobs, parent: self, description: 'Jobs definition for this pipeline') diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb index dc0ede2a25f..76e5d05639f 100644 --- a/lib/gitlab/ci/config/entry/hidden.rb +++ b/lib/gitlab/ci/config/entry/hidden.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a hidden CI/CD key. # - class Hidden < Node - include Validatable + class Hidden < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, presence: true diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index fc453b72fa5..a13a0625e90 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a Docker image. # - class Image < Node - include Validatable + class Image < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable ALLOWED_KEYS = %i[name entrypoint].freeze diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index c8cb3248fa7..4f2ac94b6c3 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -7,9 +7,9 @@ module Gitlab ## # Entry that represents a concrete CI/CD job. # - class Job < Node - include Configurable - include Attributable + class Job < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when start_in artifacts cache diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb index 1535b108000..82b72e40404 100644 --- a/lib/gitlab/ci/config/entry/jobs.rb +++ b/lib/gitlab/ci/config/entry/jobs.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a set of jobs. # - class Jobs < Node - include Validatable + class Jobs < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, type: Hash @@ -34,7 +34,7 @@ module Gitlab @config.each do |name, config| node = hidden?(name) ? Entry::Hidden : Entry::Job - factory = Entry::Factory.new(node) + factory = ::Gitlab::Config::Entry::Factory.new(node) .value(config || {}) .metadata(name: name) .with(key: name, parent: self, diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb index 963b200c7bb..0c10967e629 100644 --- a/lib/gitlab/ci/config/entry/key.rb +++ b/lib/gitlab/ci/config/entry/key.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a key. # - class Key < Node - include Validatable + class Key < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, key: true diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb deleted file mode 100644 index 4043629dea9..00000000000 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module LegacyValidationHelpers - private - - def validate_duration(value) - value.is_a?(String) && ChronicDuration.parse(value) - rescue ChronicDuration::DurationParseError - false - end - - def validate_duration_limit(value, limit) - return false unless value.is_a?(String) - - ChronicDuration.parse(value).second.from_now < - ChronicDuration.parse(limit).second.from_now - rescue ChronicDuration::DurationParseError - false - end - - def validate_array_of_strings(values) - values.is_a?(Array) && values.all? { |value| validate_string(value) } - end - - def validate_array_of_strings_or_regexps(values) - values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) } - end - - def validate_variables(variables) - variables.is_a?(Hash) && - variables.flatten.all? do |value| - validate_string(value) || validate_integer(value) - end - end - - def validate_integer(value) - value.is_a?(Integer) - end - - def validate_string(value) - value.is_a?(String) || value.is_a?(Symbol) - end - - def validate_regexp(value) - !value.nil? && Regexp.new(value.to_s) && true - rescue RegexpError, TypeError - false - end - - def validate_string_or_regexp(value) - return true if value.is_a?(Symbol) - return false unless value.is_a?(String) - - if value.first == '/' && value.last == '/' - validate_regexp(value[1...-1]) - else - true - end - end - - def validate_boolean(value) - value.in?([true, false]) - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb deleted file mode 100644 index 347089722e4..00000000000 --- a/lib/gitlab/ci/config/entry/node.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # Base abstract class for each configuration entry node. - # - class Node - InvalidError = Class.new(StandardError) - - attr_reader :config, :metadata - attr_accessor :key, :parent, :description - - def initialize(config, **metadata) - @config = config - @metadata = metadata - @entries = {} - - self.class.aspects.to_a.each do |aspect| - instance_exec(&aspect) - end - end - - def [](key) - @entries[key] || Entry::Undefined.new - end - - def compose!(deps = nil) - return unless valid? - - yield if block_given? - end - - def leaf? - @entries.none? - end - - def descendants - @entries.values - end - - def ancestors - @parent ? @parent.ancestors + [@parent] : [] - end - - def valid? - errors.none? - end - - def errors - [] - end - - def value - if leaf? - @config - else - meaningful = @entries.select do |_key, value| - value.specified? && value.relevant? - end - - Hash[meaningful.map { |key, entry| [key, entry.value] }] - end - end - - def specified? - true - end - - def relevant? - true - end - - def location - name = @key.presence || self.class.name.to_s.demodulize - .underscore.humanize.downcase - - ancestors.map(&:key).append(name).compact.join(':') - end - - def inspect - val = leaf? ? config : descendants - unspecified = specified? ? '' : '(unspecified) ' - "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" - end - - def self.default - end - - def self.aspects - @aspects ||= [] - end - - private - - attr_reader :entries - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb index 9580b5e2e7f..d6f287c6552 100644 --- a/lib/gitlab/ci/config/entry/paths.rb +++ b/lib/gitlab/ci/config/entry/paths.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents an array of paths. # - class Paths < Node - include Validatable + class Paths < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings: true diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 0535d7c1a1a..998da1f6837 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -7,12 +7,12 @@ module Gitlab ## # Entry that represents an only/except trigger policy for the job. # - class Policy < Simplifiable + class Policy < ::Gitlab::Config::Entry::Simplifiable strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) } strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) } - class RefsPolicy < Entry::Node - include Entry::Validatable + class RefsPolicy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings_or_regexps: true @@ -23,9 +23,9 @@ module Gitlab end end - class ComplexPolicy < Entry::Node - include Entry::Validatable - include Entry::Attributable + class ComplexPolicy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze attributes :refs, :kubernetes, :variables, :changes @@ -58,7 +58,7 @@ module Gitlab end end - class UnknownStrategy < Entry::Node + class UnknownStrategy < ::Gitlab::Config::Entry::Node def errors ["#{location} has to be either an array of conditions or a hash"] end diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index 3ac2a6fa777..a3f6cc31321 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -7,9 +7,9 @@ module Gitlab ## # Entry that represents a configuration of job artifacts. # - class Reports < Node - include Validatable - include Attributable + class Reports < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb index ee82ab10f9c..eaf8b38aa3c 100644 --- a/lib/gitlab/ci/config/entry/retry.rb +++ b/lib/gitlab/ci/config/entry/retry.rb @@ -7,12 +7,12 @@ module Gitlab ## # Entry that represents a retry config for a job. # - class Retry < Simplifiable + class Retry < ::Gitlab::Config::Entry::Simplifiable strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) } strategy :FullRetry, if: -> (config) { config.is_a?(Hash) } - class SimpleRetry < Entry::Node - include Entry::Validatable + class SimpleRetry < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, numericality: { only_integer: true, @@ -31,9 +31,9 @@ module Gitlab end end - class FullRetry < Entry::Node - include Entry::Validatable - include Entry::Attributable + class FullRetry < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[max when].freeze attributes :max, :when @@ -73,7 +73,7 @@ module Gitlab end end - class UnknownStrategy < Entry::Node + class UnknownStrategy < ::Gitlab::Config::Entry::Node def errors ["#{location} has to be either an integer or a hash"] end diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb index f7d39e5cf55..9d25a82b521 100644 --- a/lib/gitlab/ci/config/entry/script.rb +++ b/lib/gitlab/ci/config/entry/script.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a script. # - class Script < Node - include Validatable + class Script < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings: true diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index 47bf9205147..6df67083310 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -8,7 +8,7 @@ module Gitlab # Entry that represents a configuration of Docker service. # class Service < Image - include Validatable + include ::Gitlab::Config::Entry::Validatable ALLOWED_KEYS = %i[name entrypoint command alias].freeze diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index bdf7f80f382..71475f69218 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a configuration of Docker services. # - class Services < Node - include Validatable + class Services < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, type: Array @@ -18,7 +18,7 @@ module Gitlab super do @entries = [] @config.each do |config| - @entries << Entry::Factory.new(Entry::Service) + @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service) .value(config || {}) .create! end diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb deleted file mode 100644 index 9961bbfaa40..00000000000 --- a/lib/gitlab/ci/config/entry/simplifiable.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - class Simplifiable < SimpleDelegator - EntryStrategy = Struct.new(:name, :condition) - - def initialize(config, **metadata) - unless self.class.const_defined?(:UnknownStrategy) - raise ArgumentError, 'UndefinedStrategy not available!' - end - - strategy = self.class.strategies.find do |variant| - variant.condition.call(config) - end - - entry = self.class.entry_class(strategy) - - super(entry.new(config, metadata)) - end - - def self.strategy(name, **opts) - EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy| - strategies.append(strategy) - end - end - - def self.strategies - @strategies ||= [] - end - - def self.entry_class(strategy) - if strategy.present? - self.const_get(strategy.name) - else - self::UnknownStrategy - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb index 65ab5953131..d6d576a3139 100644 --- a/lib/gitlab/ci/config/entry/stage.rb +++ b/lib/gitlab/ci/config/entry/stage.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a stage for a job. # - class Stage < Node - include Validatable + class Stage < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, type: String diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb index ab184246d29..2d715cbc6bb 100644 --- a/lib/gitlab/ci/config/entry/stages.rb +++ b/lib/gitlab/ci/config/entry/stages.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a configuration for pipeline stages. # - class Stages < Node - include Validatable + class Stages < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings: true diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb deleted file mode 100644 index 77dcfa88170..00000000000 --- a/lib/gitlab/ci/config/entry/undefined.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # This class represents an undefined entry. - # - class Undefined < Node - def initialize(*) - super(nil) - end - - def value - nil - end - - def valid? - true - end - - def errors - [] - end - - def specified? - false - end - - def relevant? - false - end - - def inspect - "#<#{self.class.name}>" - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb deleted file mode 100644 index bab32489d2f..00000000000 --- a/lib/gitlab/ci/config/entry/unspecified.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # This class represents an unspecified entry. - # - # It decorates original entry adding method that indicates it is - # unspecified. - # - class Unspecified < SimpleDelegator - def specified? - false - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb deleted file mode 100644 index 08a6593c980..00000000000 --- a/lib/gitlab/ci/config/entry/validatable.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module Validatable - extend ActiveSupport::Concern - - def self.included(node) - node.aspects.append -> do - @validator = self.class.validator.new(self) - @validator.validate(:new) - end - end - - def errors - @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables - end - - class_methods do - def validator - @validator ||= Class.new(Entry::Validator).tap do |validator| - if defined?(@validations) - @validations.each { |rules| validator.class_eval(&rules) } - end - end - end - - private - - def validations(&block) - (@validations ||= []).append(block) - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb deleted file mode 100644 index 33ffdd3a95d..00000000000 --- a/lib/gitlab/ci/config/entry/validator.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - class Validator < SimpleDelegator - include ActiveModel::Validations - include Entry::Validators - - def initialize(entry) - super(entry) - end - - def messages - errors.full_messages.map do |error| - "#{location} #{error}".downcase - end - end - - def self.name - 'Validator' - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb deleted file mode 100644 index a1d552fb2e5..00000000000 --- a/lib/gitlab/ci/config/entry/validators.rb +++ /dev/null @@ -1,198 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module Validators - class AllowedKeysValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unknown_keys = value.try(:keys).to_a - options[:in] - - if unknown_keys.any? - record.errors.add(attribute, "contains unknown keys: " + - unknown_keys.join(', ')) - end - end - end - - class AllowedValuesValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless options[:in].include?(value.to_s) - record.errors.add(attribute, "unknown value: #{value}") - end - end - end - - class AllowedArrayValuesValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unkown_values = value - options[:in] - unless unkown_values.empty? - record.errors.add(attribute, "contains unknown values: " + - unkown_values.join(', ')) - end - end - end - - class ArrayOfStringsValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_array_of_strings(value) - record.errors.add(attribute, 'should be an array of strings') - end - end - end - - class BooleanValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_boolean(value) - record.errors.add(attribute, 'should be a boolean value') - end - end - end - - class DurationValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_duration(value) - record.errors.add(attribute, 'should be a duration') - end - - if options[:limit] - unless validate_duration_limit(value, options[:limit]) - record.errors.add(attribute, 'should not exceed the limit') - end - end - end - end - - class HashOrStringValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless value.is_a?(Hash) || value.is_a?(String) - record.errors.add(attribute, 'should be a hash or a string') - end - end - end - - class HashOrIntegerValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless value.is_a?(Hash) || value.is_a?(Integer) - record.errors.add(attribute, 'should be a hash or an integer') - end - end - end - - class KeyValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - if validate_string(value) - validate_path(record, attribute, value) - else - record.errors.add(attribute, 'should be a string or symbol') - end - end - - private - - def validate_path(record, attribute, value) - path = CGI.unescape(value.to_s) - - if path.include?('/') - record.errors.add(attribute, 'cannot contain the "/" character') - elsif path == '.' || path == '..' - record.errors.add(attribute, 'cannot be "." or ".."') - end - end - end - - class RegexpValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_regexp(value) - record.errors.add(attribute, 'must be a regular expression') - end - end - - private - - def look_like_regexp?(value) - value.is_a?(String) && value.start_with?('/') && - value.end_with?('/') - end - - def validate_regexp(value) - look_like_regexp?(value) && - Regexp.new(value.to_s[1...-1]) && - true - rescue RegexpError - false - end - end - - class ArrayOfStringsOrRegexpsValidator < RegexpValidator - def validate_each(record, attribute, value) - unless validate_array_of_strings_or_regexps(value) - record.errors.add(attribute, 'should be an array of strings or regexps') - end - end - - private - - def validate_array_of_strings_or_regexps(values) - values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) - end - - def validate_string_or_regexp(value) - return false unless value.is_a?(String) - return validate_regexp(value) if look_like_regexp?(value) - - true - end - end - - class ArrayOfStringsOrStringValidator < RegexpValidator - def validate_each(record, attribute, value) - unless validate_array_of_strings_or_string(value) - record.errors.add(attribute, 'should be an array of strings or a string') - end - end - - private - - def validate_array_of_strings_or_string(values) - validate_array_of_strings(values) || validate_string(values) - end - end - - class TypeValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - type = options[:with] - raise unless type.is_a?(Class) - - unless value.is_a?(type) - message = options[:message] || "should be a #{type.name}" - record.errors.add(attribute, message) - end - end - end - - class VariablesValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_variables(value) - record.errors.add(attribute, 'should be a hash of key value pairs') - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index 6fd3cec2f5f..89d790ebfa6 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents environment variables. # - class Variables < Node - include Validatable + class Variables < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, variables: true diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb index 15ca47ef60e..ee4ea9bbb1d 100644 --- a/lib/gitlab/ci/config/external/file/base.rb +++ b/lib/gitlab/ci/config/external/file/base.rb @@ -37,8 +37,8 @@ module Gitlab end def to_hash - @hash ||= Ci::Config::Loader.new(content).load! - rescue Ci::Config::Loader::FormatError + @hash ||= Gitlab::Config::Loader::Yaml.new(content).load! + rescue Gitlab::Config::Loader::FormatError nil end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index e6ec400e476..172926b8ab0 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -5,7 +5,7 @@ module Gitlab class YamlProcessor ValidationError = Class.new(StandardError) - include Gitlab::Ci::Config::Entry::LegacyValidationHelpers + include Gitlab::Config::Entry::LegacyValidationHelpers attr_reader :cache, :stages, :jobs diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb new file mode 100644 index 00000000000..560fe63df0e --- /dev/null +++ b/lib/gitlab/config/entry/attributable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module Attributable + extend ActiveSupport::Concern + + class_methods do + def attributes(*attributes) + attributes.flatten.each do |attribute| + if method_defined?(attribute) + raise ArgumentError, 'Method already defined!' + end + + define_method(attribute) do + return unless config.is_a?(Hash) + + config[attribute] + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/boolean.rb b/lib/gitlab/config/entry/boolean.rb new file mode 100644 index 00000000000..1e8a57356e3 --- /dev/null +++ b/lib/gitlab/config/entry/boolean.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Entry that represents a boolean value. + # + class Boolean < Node + include Validatable + + validations do + validates :config, boolean: true + end + end + end + end +end diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb new file mode 100644 index 00000000000..afdb60b2cd5 --- /dev/null +++ b/lib/gitlab/config/entry/configurable.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # This mixin is responsible for adding DSL, which purpose is to + # simplifly process of adding child nodes. + # + # This can be used only if parent node is a configuration entry that + # holds a hash as a configuration value, for example: + # + # job: + # script: ... + # artifacts: ... + # + module Configurable + extend ActiveSupport::Concern + + included do + include Validatable + + validations do + validates :config, type: Hash + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def compose!(deps = nil) + return unless valid? + + self.class.nodes.each do |key, factory| + factory + .value(config[key]) + .with(key: key, parent: self) + + entries[key] = factory.create! + end + + yield if block_given? + + entries.each_value do |entry| + entry.compose!(deps) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + class_methods do + def nodes + Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def entry(key, entry, metadata) + factory = ::Gitlab::Config::Entry::Factory.new(entry) + .with(description: metadata[:description]) + + (@nodes ||= {}).merge!(key.to_sym => factory) + end + # rubocop: enable CodeReuse/ActiveRecord + + def helpers(*nodes) + nodes.each do |symbol| + define_method("#{symbol}_defined?") do + entries[symbol]&.specified? + end + + define_method("#{symbol}_value") do + return unless entries[symbol] && entries[symbol].valid? + + entries[symbol].value + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb new file mode 100644 index 00000000000..30d43c9f9a1 --- /dev/null +++ b/lib/gitlab/config/entry/factory.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Factory class responsible for fabricating entry objects. + # + class Factory + InvalidFactory = Class.new(StandardError) + + def initialize(entry) + @entry = entry + @metadata = {} + @attributes = {} + end + + def value(value) + @value = value + self + end + + def metadata(metadata) + @metadata.merge!(metadata) + self + end + + def with(attributes) + @attributes.merge!(attributes) + self + end + + def create! + raise InvalidFactory unless defined?(@value) + + ## + # We assume that unspecified entry is undefined. + # See issue #18775. + # + if @value.nil? + Entry::Unspecified.new( + fabricate_unspecified + ) + else + fabricate(@entry, @value) + end + end + + private + + def fabricate_unspecified + ## + # If entry has a default value we fabricate concrete node + # with default value. + # + if @entry.default.nil? + fabricate(Entry::Undefined) + else + fabricate(@entry, @entry.default) + end + end + + def fabricate(entry, value = nil) + entry.new(value, @metadata).tap do |node| + node.key = @attributes[:key] + node.parent = @attributes[:parent] + node.description = @attributes[:description] + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb new file mode 100644 index 00000000000..d3ab5625743 --- /dev/null +++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module LegacyValidationHelpers + private + + def validate_duration(value) + value.is_a?(String) && ChronicDuration.parse(value) + rescue ChronicDuration::DurationParseError + false + end + + def validate_duration_limit(value, limit) + return false unless value.is_a?(String) + + ChronicDuration.parse(value).second.from_now < + ChronicDuration.parse(limit).second.from_now + rescue ChronicDuration::DurationParseError + false + end + + def validate_array_of_strings(values) + values.is_a?(Array) && values.all? { |value| validate_string(value) } + end + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) } + end + + def validate_variables(variables) + variables.is_a?(Hash) && + variables.flatten.all? do |value| + validate_string(value) || validate_integer(value) + end + end + + def validate_integer(value) + value.is_a?(Integer) + end + + def validate_string(value) + value.is_a?(String) || value.is_a?(Symbol) + end + + def validate_regexp(value) + !value.nil? && Regexp.new(value.to_s) && true + rescue RegexpError, TypeError + false + end + + def validate_string_or_regexp(value) + return true if value.is_a?(Symbol) + return false unless value.is_a?(String) + + if value.first == '/' && value.last == '/' + validate_regexp(value[1...-1]) + else + true + end + end + + def validate_boolean(value) + value.in?([true, false]) + end + end + end + end +end diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb new file mode 100644 index 00000000000..30357b2c95b --- /dev/null +++ b/lib/gitlab/config/entry/node.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Base abstract class for each configuration entry node. + # + class Node + InvalidError = Class.new(StandardError) + + attr_reader :config, :metadata + attr_accessor :key, :parent, :description + + def initialize(config, **metadata) + @config = config + @metadata = metadata + @entries = {} + + self.class.aspects.to_a.each do |aspect| + instance_exec(&aspect) + end + end + + def [](key) + @entries[key] || Entry::Undefined.new + end + + def compose!(deps = nil) + return unless valid? + + yield if block_given? + end + + def leaf? + @entries.none? + end + + def descendants + @entries.values + end + + def ancestors + @parent ? @parent.ancestors + [@parent] : [] + end + + def valid? + errors.none? + end + + def errors + [] + end + + def value + if leaf? + @config + else + meaningful = @entries.select do |_key, value| + value.specified? && value.relevant? + end + + Hash[meaningful.map { |key, entry| [key, entry.value] }] + end + end + + def specified? + true + end + + def relevant? + true + end + + def location + name = @key.presence || self.class.name.to_s.demodulize + .underscore.humanize.downcase + + ancestors.map(&:key).append(name).compact.join(':') + end + + def inspect + val = leaf? ? config : descendants + unspecified = specified? ? '' : '(unspecified) ' + "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" + end + + def self.default + end + + def self.aspects + @aspects ||= [] + end + + private + + attr_reader :entries + end + end + end +end diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb new file mode 100644 index 00000000000..3e148fe2e91 --- /dev/null +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + class Simplifiable < SimpleDelegator + EntryStrategy = Struct.new(:name, :condition) + + def initialize(config, **metadata) + unless self.class.const_defined?(:UnknownStrategy) + raise ArgumentError, 'UndefinedStrategy not available!' + end + + strategy = self.class.strategies.find do |variant| + variant.condition.call(config) + end + + entry = self.class.entry_class(strategy) + + super(entry.new(config, metadata)) + end + + def self.strategy(name, **opts) + EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy| + strategies.append(strategy) + end + end + + def self.strategies + @strategies ||= [] + end + + def self.entry_class(strategy) + if strategy.present? + self.const_get(strategy.name) + else + self::UnknownStrategy + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/undefined.rb b/lib/gitlab/config/entry/undefined.rb new file mode 100644 index 00000000000..5f708abc80c --- /dev/null +++ b/lib/gitlab/config/entry/undefined.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # This class represents an undefined entry. + # + class Undefined < Node + def initialize(*) + super(nil) + end + + def value + nil + end + + def valid? + true + end + + def errors + [] + end + + def specified? + false + end + + def relevant? + false + end + + def inspect + "#<#{self.class.name}>" + end + end + end + end +end diff --git a/lib/gitlab/config/entry/unspecified.rb b/lib/gitlab/config/entry/unspecified.rb new file mode 100644 index 00000000000..c096180d0f8 --- /dev/null +++ b/lib/gitlab/config/entry/unspecified.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # This class represents an unspecified entry. + # + # It decorates original entry adding method that indicates it is + # unspecified. + # + class Unspecified < SimpleDelegator + def specified? + false + end + end + end + end +end diff --git a/lib/gitlab/config/entry/validatable.rb b/lib/gitlab/config/entry/validatable.rb new file mode 100644 index 00000000000..1c88c68c11c --- /dev/null +++ b/lib/gitlab/config/entry/validatable.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module Validatable + extend ActiveSupport::Concern + + def self.included(node) + node.aspects.append -> do + @validator = self.class.validator.new(self) + @validator.validate(:new) + end + end + + def errors + @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + class_methods do + def validator + @validator ||= Class.new(Entry::Validator).tap do |validator| + if defined?(@validations) + @validations.each { |rules| validator.class_eval(&rules) } + end + end + end + + private + + def validations(&block) + (@validations ||= []).append(block) + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/validator.rb b/lib/gitlab/config/entry/validator.rb new file mode 100644 index 00000000000..e5efd4a7b0a --- /dev/null +++ b/lib/gitlab/config/entry/validator.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + class Validator < SimpleDelegator + include ActiveModel::Validations + include Entry::Validators + + def initialize(entry) + super(entry) + end + + def messages + errors.full_messages.map do |error| + "#{location} #{error}".downcase + end + end + + def self.name + 'Validator' + end + end + end + end +end diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb new file mode 100644 index 00000000000..25bfa50f829 --- /dev/null +++ b/lib/gitlab/config/entry/validators.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module Validators + class AllowedKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unknown_keys = value.try(:keys).to_a - options[:in] + + if unknown_keys.any? + record.errors.add(attribute, "contains unknown keys: " + + unknown_keys.join(', ')) + end + end + end + + class AllowedValuesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless options[:in].include?(value.to_s) + record.errors.add(attribute, "unknown value: #{value}") + end + end + end + + class AllowedArrayValuesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unkown_values = value - options[:in] + unless unkown_values.empty? + record.errors.add(attribute, "contains unknown values: " + + unkown_values.join(', ')) + end + end + end + + class ArrayOfStringsValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_array_of_strings(value) + record.errors.add(attribute, 'should be an array of strings') + end + end + end + + class BooleanValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_boolean(value) + record.errors.add(attribute, 'should be a boolean value') + end + end + end + + class DurationValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_duration(value) + record.errors.add(attribute, 'should be a duration') + end + + if options[:limit] + unless validate_duration_limit(value, options[:limit]) + record.errors.add(attribute, 'should not exceed the limit') + end + end + end + end + + class HashOrStringValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(String) + record.errors.add(attribute, 'should be a hash or a string') + end + end + end + + class HashOrIntegerValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(Integer) + record.errors.add(attribute, 'should be a hash or an integer') + end + end + end + + class KeyValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + if validate_string(value) + validate_path(record, attribute, value) + else + record.errors.add(attribute, 'should be a string or symbol') + end + end + + private + + def validate_path(record, attribute, value) + path = CGI.unescape(value.to_s) + + if path.include?('/') + record.errors.add(attribute, 'cannot contain the "/" character') + elsif path == '.' || path == '..' + record.errors.add(attribute, 'cannot be "." or ".."') + end + end + end + + class RegexpValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_regexp(value) + record.errors.add(attribute, 'must be a regular expression') + end + end + + private + + def look_like_regexp?(value) + value.is_a?(String) && value.start_with?('/') && + value.end_with?('/') + end + + def validate_regexp(value) + look_like_regexp?(value) && + Regexp.new(value.to_s[1...-1]) && + true + rescue RegexpError + false + end + end + + class ArrayOfStringsOrRegexpsValidator < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_regexps(value) + record.errors.add(attribute, 'should be an array of strings or regexps') + end + end + + private + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) + end + + def validate_string_or_regexp(value) + return false unless value.is_a?(String) + return validate_regexp(value) if look_like_regexp?(value) + + true + end + end + + class ArrayOfStringsOrStringValidator < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_string(value) + record.errors.add(attribute, 'should be an array of strings or a string') + end + end + + private + + def validate_array_of_strings_or_string(values) + validate_array_of_strings(values) || validate_string(values) + end + end + + class TypeValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + type = options[:with] + raise unless type.is_a?(Class) + + unless value.is_a?(type) + message = options[:message] || "should be a #{type.name}" + record.errors.add(attribute, message) + end + end + end + + class VariablesValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_variables(value) + record.errors.add(attribute, 'should be a hash of key value pairs') + end + end + end + end + end + end +end diff --git a/lib/gitlab/config/loader/format_error.rb b/lib/gitlab/config/loader/format_error.rb new file mode 100644 index 00000000000..848ff96d201 --- /dev/null +++ b/lib/gitlab/config/loader/format_error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Loader + FormatError = Class.new(StandardError) + end + end +end diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/config/loader/yaml.rb similarity index 66% rename from lib/gitlab/ci/config/loader.rb rename to lib/gitlab/config/loader/yaml.rb index b4c491e84a6..8159f8b8026 100644 --- a/lib/gitlab/ci/config/loader.rb +++ b/lib/gitlab/config/loader/yaml.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true module Gitlab - module Ci - class Config - class Loader - FormatError = Class.new(StandardError) - + module Config + module Loader + class Yaml def initialize(config) @config = YAML.safe_load(config, [Symbol], [], true) rescue Psych::Exception => e - raise FormatError, e.message + raise Loader::FormatError, e.message end def valid? @@ -18,7 +16,7 @@ module Gitlab def load! unless valid? - raise FormatError, 'Invalid configuration format' + raise Loader::FormatError, 'Invalid configuration format' end @config.deep_symbolize_keys diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb similarity index 87% rename from spec/lib/gitlab/ci/config/entry/attributable_spec.rb rename to spec/lib/gitlab/config/entry/attributable_spec.rb index b028b771375..abb4fff3ad7 100644 --- a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Attributable do +describe Gitlab::Config::Entry::Attributable do let(:node) do Class.new do - include Gitlab::Ci::Config::Entry::Attributable + include Gitlab::Config::Entry::Attributable end end @@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do it 'raises an error' do expectation = expect do Class.new(String) do - include Gitlab::Ci::Config::Entry::Attributable + include Gitlab::Config::Entry::Attributable attributes :length end diff --git a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb b/spec/lib/gitlab/config/entry/boolean_spec.rb similarity index 93% rename from spec/lib/gitlab/ci/config/entry/boolean_spec.rb rename to spec/lib/gitlab/config/entry/boolean_spec.rb index 5f067cad93c..1b7a3f850ec 100644 --- a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb +++ b/spec/lib/gitlab/config/entry/boolean_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Boolean do +describe Gitlab::Config::Entry::Boolean do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb similarity index 82% rename from spec/lib/gitlab/ci/config/entry/configurable_spec.rb rename to spec/lib/gitlab/config/entry/configurable_spec.rb index 088d4b472da..85a7cf1d241 100644 --- a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/config/entry/configurable_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Configurable do +describe Gitlab::Config::Entry::Configurable do let(:entry) do - Class.new(Gitlab::Ci::Config::Entry::Node) do - include Gitlab::Ci::Config::Entry::Configurable + Class.new(Gitlab::Config::Entry::Node) do + include Gitlab::Config::Entry::Configurable end end @@ -39,7 +39,7 @@ describe Gitlab::Ci::Config::Entry::Configurable do it 'creates a node factory' do expect(entry.nodes[:object]) - .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory + .to be_an_instance_of Gitlab::Config::Entry::Factory end it 'returns a duplicated factory object' do diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb similarity index 86% rename from spec/lib/gitlab/ci/config/entry/factory_spec.rb rename to spec/lib/gitlab/config/entry/factory_spec.rb index 8dd48e4efae..c29d17eaee3 100644 --- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb +++ b/spec/lib/gitlab/config/entry/factory_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Factory do +describe Gitlab::Config::Entry::Factory do describe '#create!' do + class Script < Gitlab::Config::Entry::Node + include Gitlab::Config::Entry::Validatable + + validations do + validates :config, array_of_strings: true + end + end + + let(:entry) { Script } let(:factory) { described_class.new(entry) } - let(:entry) { Gitlab::Ci::Config::Entry::Script } context 'when setting a concrete value' do it 'creates entry with valid value' do @@ -54,7 +62,7 @@ describe Gitlab::Ci::Config::Entry::Factory do context 'when not setting a value' do it 'raises error' do expect { factory.create! }.to raise_error( - Gitlab::Ci::Config::Entry::Factory::InvalidFactory + Gitlab::Config::Entry::Factory::InvalidFactory ) end end diff --git a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb similarity index 97% rename from spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb rename to spec/lib/gitlab/config/entry/simplifiable_spec.rb index 395062207a3..bc8387ada67 100644 --- a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb +++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Simplifiable do +describe Gitlab::Config::Entry::Simplifiable do describe '.strategy' do let(:entry) do Class.new(described_class) do diff --git a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb b/spec/lib/gitlab/config/entry/undefined_spec.rb similarity index 93% rename from spec/lib/gitlab/ci/config/entry/undefined_spec.rb rename to spec/lib/gitlab/config/entry/undefined_spec.rb index fdf48d84192..48f9d276c95 100644 --- a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb +++ b/spec/lib/gitlab/config/entry/undefined_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Undefined do +describe Gitlab::Config::Entry::Undefined do let(:entry) { described_class.new } describe '#leaf?' do diff --git a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb b/spec/lib/gitlab/config/entry/unspecified_spec.rb similarity index 92% rename from spec/lib/gitlab/ci/config/entry/unspecified_spec.rb rename to spec/lib/gitlab/config/entry/unspecified_spec.rb index 66f88fa35b6..64421824a12 100644 --- a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb +++ b/spec/lib/gitlab/config/entry/unspecified_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Unspecified do +describe Gitlab::Config::Entry::Unspecified do let(:unspecified) { described_class.new(entry) } let(:entry) { spy('Entry') } diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/config/entry/validatable_spec.rb similarity index 84% rename from spec/lib/gitlab/ci/config/entry/validatable_spec.rb rename to spec/lib/gitlab/config/entry/validatable_spec.rb index ae2a7a51ba6..5a8f9766d23 100644 --- a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb +++ b/spec/lib/gitlab/config/entry/validatable_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Validatable do +describe Gitlab::Config::Entry::Validatable do let(:entry) do - Class.new(Gitlab::Ci::Config::Entry::Node) do - include Gitlab::Ci::Config::Entry::Validatable + Class.new(Gitlab::Config::Entry::Node) do + include Gitlab::Config::Entry::Validatable end end @@ -20,7 +20,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do it 'returns validator' do expect(entry.validator.superclass) - .to be Gitlab::Ci::Config::Entry::Validator + .to be Gitlab::Config::Entry::Validator end it 'returns only one validator to mitigate leaks' do diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/config/entry/validator_spec.rb similarity index 96% rename from spec/lib/gitlab/ci/config/entry/validator_spec.rb rename to spec/lib/gitlab/config/entry/validator_spec.rb index 172b6b47a4f..efa16c4265c 100644 --- a/spec/lib/gitlab/ci/config/entry/validator_spec.rb +++ b/spec/lib/gitlab/config/entry/validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Validator do +describe Gitlab::Config::Entry::Validator do let(:validator) { Class.new(described_class) } let(:validator_instance) { validator.new(node) } let(:node) { spy('node') } diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb similarity index 84% rename from spec/lib/gitlab/ci/config/loader_spec.rb rename to spec/lib/gitlab/config/loader/yaml_spec.rb index 590fc8904c1..44c9a3896a8 100644 --- a/spec/lib/gitlab/ci/config/loader_spec.rb +++ b/spec/lib/gitlab/config/loader/yaml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Loader do +describe Gitlab::Config::Loader::Yaml do let(:loader) { described_class.new(yml) } context 'when yaml syntax is correct' do @@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Loader do describe '#load!' do it 'raises error' do expect { loader.load! }.to raise_error( - Gitlab::Ci::Config::Loader::FormatError, + Gitlab::Config::Loader::FormatError, 'Invalid configuration format' ) end @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Loader do describe '#initialize' do it 'raises FormatError' do - expect { loader }.to raise_error(Gitlab::Ci::Config::Loader::FormatError, 'Unknown alias: bad_alias') + expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias') end end end -- GitLab