Merge branch 'generalize-ci-config' into 'master'
ci/config: generalize Config validation into Gitlab::Config:: module See merge request gitlab-org/gitlab-ce!23443
This commit is contained in:
commit
bd45beaf20
60 changed files with 841 additions and 850 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
4
lib/gitlab/ci/config/external/file/base.rb
vendored
4
lib/gitlab/ci/config/external/file/base.rb
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
27
lib/gitlab/config/entry/attributable.rb
Normal file
27
lib/gitlab/config/entry/attributable.rb
Normal file
|
@ -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
|
18
lib/gitlab/config/entry/boolean.rb
Normal file
18
lib/gitlab/config/entry/boolean.rb
Normal file
|
@ -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
|
81
lib/gitlab/config/entry/configurable.rb
Normal file
81
lib/gitlab/config/entry/configurable.rb
Normal file
|
@ -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
|
73
lib/gitlab/config/entry/factory.rb
Normal file
73
lib/gitlab/config/entry/factory.rb
Normal file
|
@ -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
|
70
lib/gitlab/config/entry/legacy_validation_helpers.rb
Normal file
70
lib/gitlab/config/entry/legacy_validation_helpers.rb
Normal file
|
@ -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
|
101
lib/gitlab/config/entry/node.rb
Normal file
101
lib/gitlab/config/entry/node.rb
Normal file
|
@ -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
|
43
lib/gitlab/config/entry/simplifiable.rb
Normal file
43
lib/gitlab/config/entry/simplifiable.rb
Normal file
|
@ -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
|
40
lib/gitlab/config/entry/undefined.rb
Normal file
40
lib/gitlab/config/entry/undefined.rb
Normal file
|
@ -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
|
19
lib/gitlab/config/entry/unspecified.rb
Normal file
19
lib/gitlab/config/entry/unspecified.rb
Normal file
|
@ -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
|
38
lib/gitlab/config/entry/validatable.rb
Normal file
38
lib/gitlab/config/entry/validatable.rb
Normal file
|
@ -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
|
26
lib/gitlab/config/entry/validator.rb
Normal file
26
lib/gitlab/config/entry/validator.rb
Normal file
|
@ -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
|
196
lib/gitlab/config/entry/validators.rb
Normal file
196
lib/gitlab/config/entry/validators.rb
Normal file
|
@ -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
|
9
lib/gitlab/config/loader/format_error.rb
Normal file
9
lib/gitlab/config/loader/format_error.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Config
|
||||
module Loader
|
||||
FormatError = Class.new(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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') }
|
||||
|
|
@ -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
|
|
@ -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') }
|
|
@ -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
|
Loading…
Reference in a new issue