Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2018-11-29 14:44:48 +0300
committerKamil Trzciński <ayufan@ayufan.eu>2018-11-29 18:09:18 +0300
commit64b1044e7ac22d14a9c17ef773dd075b74df00fa (patch)
tree7b3c6d04c96ceb9183c745d67c34c8b9d4c8da1b /lib
parent6775dafa3816239f6fa1b12428df42572be5a158 (diff)
ci/config: generalize Config validation into Gitlab::Config:: module
This decouples Ci::Config to provide a common interface for handling user configuration files.
Diffstat (limited to 'lib')
-rw-r--r--lib/gitlab/ci/config.rb4
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb8
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb29
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb20
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb4
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb83
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb4
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb4
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb75
-rw-r--r--lib/gitlab/ci/config/entry/global.rb6
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb4
-rw-r--r--lib/gitlab/ci/config/entry/image.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb6
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb6
-rw-r--r--lib/gitlab/ci/config/entry/key.rb4
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb72
-rw-r--r--lib/gitlab/ci/config/entry/node.rb103
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb4
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb14
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb6
-rw-r--r--lib/gitlab/ci/config/entry/retry.rb14
-rw-r--r--lib/gitlab/ci/config/entry/script.rb4
-rw-r--r--lib/gitlab/ci/config/entry/service.rb2
-rw-r--r--lib/gitlab/ci/config/entry/services.rb6
-rw-r--r--lib/gitlab/ci/config/entry/simplifiable.rb45
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb4
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb4
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb42
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb21
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb40
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb28
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb198
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/base.rb4
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--lib/gitlab/config/entry/attributable.rb27
-rw-r--r--lib/gitlab/config/entry/boolean.rb18
-rw-r--r--lib/gitlab/config/entry/configurable.rb81
-rw-r--r--lib/gitlab/config/entry/factory.rb73
-rw-r--r--lib/gitlab/config/entry/legacy_validation_helpers.rb70
-rw-r--r--lib/gitlab/config/entry/node.rb101
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb43
-rw-r--r--lib/gitlab/config/entry/undefined.rb40
-rw-r--r--lib/gitlab/config/entry/unspecified.rb19
-rw-r--r--lib/gitlab/config/entry/validatable.rb38
-rw-r--r--lib/gitlab/config/entry/validator.rb26
-rw-r--r--lib/gitlab/config/entry/validators.rb196
-rw-r--r--lib/gitlab/config/loader/format_error.rb9
-rw-r--r--lib/gitlab/config/loader/yaml.rb (renamed from lib/gitlab/ci/config/loader.rb)12
50 files changed, 811 insertions, 828 deletions
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
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