Commit 64b1044e authored by Kamil Trzciński's avatar Kamil Trzciński

ci/config: generalize Config validation into Gitlab::Config:: module

This decouples Ci::Config to provide a common interface for handling
user configuration files.
parent 6775dafa
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
@global = Entry::Global.new(@config) @global = Entry::Global.new(@config)
@global.compose! @global.compose!
rescue Loader::FormatError, rescue Gitlab::Config::Loader::FormatError,
Extendable::ExtensionError, Extendable::ExtensionError,
External::Processor::IncludeError => e External::Processor::IncludeError => e
raise Config::ConfigError, e.message raise Config::ConfigError, e.message
...@@ -71,7 +71,7 @@ module Gitlab ...@@ -71,7 +71,7 @@ module Gitlab
private private
def build_config(config, opts = {}) 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) project = opts.fetch(:project, nil)
if project if project
......
...@@ -7,10 +7,10 @@ module Gitlab ...@@ -7,10 +7,10 @@ module Gitlab
## ##
# Entry that represents a configuration of job artifacts. # Entry that represents a configuration of job artifacts.
# #
class Artifacts < Node class Artifacts < ::Gitlab::Config::Entry::Node
include Configurable include ::Gitlab::Config::Entry::Configurable
include Validatable include ::Gitlab::Config::Entry::Validatable
include Attributable include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze
......
# 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
# 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 ...@@ -7,9 +7,9 @@ module Gitlab
## ##
# Entry that represents a cache configuration # Entry that represents a cache configuration
# #
class Cache < Node class Cache < ::Gitlab::Config::Entry::Node
include Configurable include ::Gitlab::Config::Entry::Configurable
include Attributable include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[key untracked paths policy].freeze ALLOWED_KEYS = %i[key untracked paths policy].freeze
DEFAULT_POLICY = 'pull-push'.freeze DEFAULT_POLICY = 'pull-push'.freeze
...@@ -22,7 +22,7 @@ module Gitlab ...@@ -22,7 +22,7 @@ module Gitlab
entry :key, Entry::Key, entry :key, Entry::Key,
description: 'Cache key used to define a cache affinity.' description: 'Cache key used to define a cache affinity.'
entry :untracked, Entry::Boolean, entry :untracked, ::Gitlab::Config::Entry::Boolean,
description: 'Cache all untracked files.' description: 'Cache all untracked files.'
entry :paths, Entry::Paths, entry :paths, Entry::Paths,
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a job script. # Entry that represents a job script.
# #
class Commands < Node class Commands < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings_or_string: true validates :config, array_of_strings_or_string: true
......
# 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 ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents Coverage settings. # Entry that represents Coverage settings.
# #
class Coverage < Node class Coverage < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, regexp: true validates :config, regexp: true
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents an environment. # Entry that represents an environment.
# #
class Environment < Node class Environment < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name url action on_stop].freeze ALLOWED_KEYS = %i[name url action on_stop].freeze
......
# 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 ...@@ -8,8 +8,8 @@ module Gitlab
# This class represents a global entry - root Entry for entire # This class represents a global entry - root Entry for entire
# GitLab CI Configuration file. # GitLab CI Configuration file.
# #
class Global < Node class Global < ::Gitlab::Config::Entry::Node
include Configurable include ::Gitlab::Config::Entry::Configurable
entry :before_script, Entry::Script, entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.' description: 'Script that will be executed before each job.'
...@@ -49,7 +49,7 @@ module Gitlab ...@@ -49,7 +49,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def compose_jobs! def compose_jobs!
factory = Entry::Factory.new(Entry::Jobs) factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys)) .value(@config.except(*self.class.nodes.keys))
.with(key: :jobs, parent: self, .with(key: :jobs, parent: self,
description: 'Jobs definition for this pipeline') description: 'Jobs definition for this pipeline')
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a hidden CI/CD key. # Entry that represents a hidden CI/CD key.
# #
class Hidden < Node class Hidden < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, presence: true validates :config, presence: true
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a Docker image. # Entry that represents a Docker image.
# #
class Image < Node class Image < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint].freeze ALLOWED_KEYS = %i[name entrypoint].freeze
......
...@@ -7,9 +7,9 @@ module Gitlab ...@@ -7,9 +7,9 @@ module Gitlab
## ##
# Entry that represents a concrete CI/CD job. # Entry that represents a concrete CI/CD job.
# #
class Job < Node class Job < ::Gitlab::Config::Entry::Node
include Configurable include ::Gitlab::Config::Entry::Configurable
include Attributable include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[tags script only except type image services ALLOWED_KEYS = %i[tags script only except type image services
allow_failure type stage when start_in artifacts cache allow_failure type stage when start_in artifacts cache
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a set of jobs. # Entry that represents a set of jobs.
# #
class Jobs < Node class Jobs < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, type: Hash validates :config, type: Hash
...@@ -34,7 +34,7 @@ module Gitlab ...@@ -34,7 +34,7 @@ module Gitlab
@config.each do |name, config| @config.each do |name, config|
node = hidden?(name) ? Entry::Hidden : Entry::Job node = hidden?(name) ? Entry::Hidden : Entry::Job
factory = Entry::Factory.new(node) factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {}) .value(config || {})
.metadata(name: name) .metadata(name: name)
.with(key: name, parent: self, .with(key: name, parent: self,
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a key. # Entry that represents a key.
# #
class Key < Node class Key < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, key: true validates :config, key: true
......
# 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
# 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 ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents an array of paths. # Entry that represents an array of paths.
# #
class Paths < Node class Paths < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings: true validates :config, array_of_strings: true
......
...@@ -7,12 +7,12 @@ module Gitlab ...@@ -7,12 +7,12 @@ module Gitlab
## ##
# Entry that represents an only/except trigger policy for the job. # 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 :RefsPolicy, if: -> (config) { config.is_a?(Array) }
strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) } strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
class RefsPolicy < Entry::Node class RefsPolicy < ::Gitlab::Config::Entry::Node
include Entry::Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings_or_regexps: true validates :config, array_of_strings_or_regexps: true
...@@ -23,9 +23,9 @@ module Gitlab ...@@ -23,9 +23,9 @@ module Gitlab
end end
end end
class ComplexPolicy < Entry::Node class ComplexPolicy < ::Gitlab::Config::Entry::Node
include Entry::Validatable include ::Gitlab::Config::Entry::Validatable
include Entry::Attributable include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze
attributes :refs, :kubernetes, :variables, :changes attributes :refs, :kubernetes, :variables, :changes
...@@ -58,7 +58,7 @@ module Gitlab ...@@ -58,7 +58,7 @@ module Gitlab
end end
end end
class UnknownStrategy < Entry::Node class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors def errors
["#{location} has to be either an array of conditions or a hash"] ["#{location} has to be either an array of conditions or a hash"]
end end
......
...@@ -7,9 +7,9 @@ module Gitlab ...@@ -7,9 +7,9 @@ module Gitlab
## ##
# Entry that represents a configuration of job artifacts. # Entry that represents a configuration of job artifacts.
# #
class Reports < Node class Reports < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
include Attributable include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
......
...@@ -7,12 +7,12 @@ module Gitlab ...@@ -7,12 +7,12 @@ module Gitlab
## ##
# Entry that represents a retry config for a job. # 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 :SimpleRetry, if: -> (config) { config.is_a?(Integer) }
strategy :FullRetry, if: -> (config) { config.is_a?(Hash) } strategy :FullRetry, if: -> (config) { config.is_a?(Hash) }
class SimpleRetry < Entry::Node class SimpleRetry < ::Gitlab::Config::Entry::Node
include Entry::Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, numericality: { only_integer: true, validates :config, numericality: { only_integer: true,
...@@ -31,9 +31,9 @@ module Gitlab ...@@ -31,9 +31,9 @@ module Gitlab
end end
end end
class FullRetry < Entry::Node class FullRetry < ::Gitlab::Config::Entry::Node
include Entry::Validatable include ::Gitlab::Config::Entry::Validatable
include Entry::Attributable include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[max when].freeze ALLOWED_KEYS = %i[max when].freeze
attributes :max, :when attributes :max, :when
...@@ -73,7 +73,7 @@ module Gitlab ...@@ -73,7 +73,7 @@ module Gitlab
end end
end end
class UnknownStrategy < Entry::Node class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors def errors
["#{location} has to be either an integer or a hash"] ["#{location} has to be either an integer or a hash"]
end end
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a script. # Entry that represents a script.
# #
class Script < Node class Script < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings: true validates :config, array_of_strings: true
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
# Entry that represents a configuration of Docker service. # Entry that represents a configuration of Docker service.
# #
class Service < Image class Service < Image
include Validatable include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint command alias].freeze ALLOWED_KEYS = %i[name entrypoint command alias].freeze
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a configuration of Docker services. # Entry that represents a configuration of Docker services.
# #
class Services < Node class Services < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, type: Array validates :config, type: Array
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
super do super do
@entries = [] @entries = []
@config.each do |config| @config.each do |config|
@entries << Entry::Factory.new(Entry::Service) @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
.value(config || {}) .value(config || {})
.create! .create!
end end
......
# 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 ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a stage for a job. # Entry that represents a stage for a job.
# #
class Stage < Node class Stage < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, type: String validates :config, type: String
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents a configuration for pipeline stages. # Entry that represents a configuration for pipeline stages.
# #
class Stages < Node class Stages < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings: true validates :config, array_of_strings: true
......
# 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
# 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
# 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
# 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
# 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 ...@@ -7,8 +7,8 @@ module Gitlab
## ##
# Entry that represents environment variables. # Entry that represents environment variables.
# #
class Variables < Node class Variables < ::Gitlab::Config::Entry::Node
include Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, variables: true validates :config, variables: true
......
...@@ -37,8 +37,8 @@ module Gitlab ...@@ -37,8 +37,8 @@ module Gitlab
end end
def to_hash def to_hash
@hash ||= Ci::Config::Loader.new(content).load! @hash ||= Gitlab::Config::Loader::Yaml.new(content).load!
rescue Ci::Config::Loader::FormatError rescue Gitlab::Config::Loader::FormatError
nil nil
end end
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
class YamlProcessor class YamlProcessor
ValidationError = Class.new(StandardError) ValidationError = Class.new(StandardError)
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers include Gitlab::Config::Entry::LegacyValidationHelpers
attr_reader :cache, :stages, :jobs attr_reader :cache, :stages, :jobs
......
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# frozen_string_literal: true
module Gitlab
module Config
module Loader
FormatError = Class.new(StandardError)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Gitlab module Gitlab
module Ci module Config
class Config module Loader
class Loader class Yaml
FormatError = Class.new(StandardError)
def initialize(config) def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true) @config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e rescue Psych::Exception => e
raise FormatError, e.message raise Loader::FormatError, e.message
end end
def valid? def valid?
...@@ -18,7 +16,7 @@ module Gitlab ...@@ -18,7 +16,7 @@ module Gitlab
def load! def load!
unless valid? unless valid?
raise FormatError, 'Invalid configuration format' raise Loader::FormatError, 'Invalid configuration format'
end end
@config.deep_symbolize_keys @config.deep_symbolize_keys
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Attributable do describe Gitlab::Config::Entry::Attributable do
let(:node) do let(:node) do
Class.new do Class.new do
include Gitlab::Ci::Config::Entry::Attributable include Gitlab::Config::Entry::Attributable
end end
end end
...@@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do ...@@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do
it 'raises an error' do it 'raises an error' do
expectation = expect do expectation = expect do
Class.new(String) do Class.new(String) do
include Gitlab::Ci::Config::Entry::Attributable include Gitlab::Config::Entry::Attributable
attributes :length attributes :length
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Boolean do describe Gitlab::Config::Entry::Boolean do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe 'validations' do describe 'validations' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Configurable do describe Gitlab::Config::Entry::Configurable do
let(:entry) do let(:entry) do
Class.new(Gitlab::Ci::Config::Entry::Node) do Class.new(Gitlab::Config::Entry::Node) do
include Gitlab::Ci::Config::Entry::Configurable include Gitlab::Config::Entry::Configurable
end end
end end
...@@ -39,7 +39,7 @@ describe Gitlab::Ci::Config::Entry::Configurable do ...@@ -39,7 +39,7 @@ describe Gitlab::Ci::Config::Entry::Configurable do
it 'creates a node factory' do it 'creates a node factory' do
expect(entry.nodes[:object]) expect(entry.nodes[:object])
.to be_an_instance_of Gitlab::Ci::Config::Entry::Factory .to be_an_instance_of Gitlab::Config::Entry::Factory
end end
it 'returns a duplicated factory object' do it 'returns a duplicated factory object' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Factory do describe Gitlab::Config::Entry::Factory do
describe '#create!' 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(:factory) { described_class.new(entry) }
let(:entry) { Gitlab::Ci::Config::Entry::Script }
context 'when setting a concrete value' do context 'when setting a concrete value' do
it 'creates entry with valid value' do it 'creates entry with valid value' do
...@@ -54,7 +62,7 @@ describe Gitlab::Ci::Config::Entry::Factory do ...@@ -54,7 +62,7 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when not setting a value' do context 'when not setting a value' do
it 'raises error' do it 'raises error' do
expect { factory.create! }.to raise_error( expect { factory.create! }.to raise_error(
Gitlab::Ci::Config::Entry::Factory::InvalidFactory Gitlab::Config::Entry::Factory::InvalidFactory
) )
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Simplifiable do describe Gitlab::Config::Entry::Simplifiable do
describe '.strategy' do describe '.strategy' do
let(:entry) do let(:entry) do
Class.new(described_class) do Class.new(described_class) do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Undefined do describe Gitlab::Config::Entry::Undefined do
let(:entry) { described_class.new } let(:entry) { described_class.new }
describe '#leaf?' do describe '#leaf?' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Unspecified do describe Gitlab::Config::Entry::Unspecified do
let(:unspecified) { described_class.new(entry) } let(:unspecified) { described_class.new(entry) }
let(:entry) { spy('Entry') } let(:entry) { spy('Entry') }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Validatable do describe Gitlab::Config::Entry::Validatable do
let(:entry) do let(:entry) do
Class.new(Gitlab::Ci::Config::Entry::Node) do Class.new(Gitlab::Config::Entry::Node) do
include Gitlab::Ci::Config::Entry::Validatable include Gitlab::Config::Entry::Validatable
end end
end end
...@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do ...@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do
it 'returns validator' do it 'returns validator' do
expect(entry.validator.superclass) expect(entry.validator.superclass)
.to be Gitlab::Ci::Config::Entry::Validator .to be Gitlab::Config::Entry::Validator
end end
it 'returns only one validator to mitigate leaks' do it 'returns only one validator to mitigate leaks' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Validator do describe Gitlab::Config::Entry::Validator do
let(:validator) { Class.new(described_class) } let(:validator) { Class.new(described_class) }
let(:validator_instance) { validator.new(node) } let(:validator_instance) { validator.new(node) }
let(:node) { spy('node') } let(:node) { spy('node') }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Loader do describe Gitlab::Config::Loader::Yaml do
let(:loader) { described_class.new(yml) } let(:loader) { described_class.new(yml) }
context 'when yaml syntax is correct' do context 'when yaml syntax is correct' do
...@@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Loader do ...@@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Loader do
describe '#load!' do describe '#load!' do
it 'raises error' do it 'raises error' do
expect { loader.load! }.to raise_error( expect { loader.load! }.to raise_error(
Gitlab::Ci::Config::Loader::FormatError, Gitlab::Config::Loader::FormatError,
'Invalid configuration format' 'Invalid configuration format'
) )
end end
...@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Loader do ...@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Loader do
describe '#initialize' do describe '#initialize' do
it 'raises FormatError' 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 end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment