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
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/gitlab/config
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/gitlab/config')
-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.rb27
14 files changed, 768 insertions, 0 deletions
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/config/loader/yaml.rb b/lib/gitlab/config/loader/yaml.rb
new file mode 100644
index 00000000000..8159f8b8026
--- /dev/null
+++ b/lib/gitlab/config/loader/yaml.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Loader
+ class Yaml
+ def initialize(config)
+ @config = YAML.safe_load(config, [Symbol], [], true)
+ rescue Psych::Exception => e
+ raise Loader::FormatError, e.message
+ end
+
+ def valid?
+ @config.is_a?(Hash)
+ end
+
+ def load!
+ unless valid?
+ raise Loader::FormatError, 'Invalid configuration format'
+ end
+
+ @config.deep_symbolize_keys
+ end
+ end
+ end
+ end
+end