Commit 06c7d6f3 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'refactor/ci-config-move-global-entries' into 'master'

Move global ci entries handling from legacy to new config

## What does this MR do?

This MR moves responsibility of handling global CI config entries (like `image`, `services`), from legacy `GitlabCiYamlProcessor` to new CI Config

## Why was this MR needed?

This is the next iteration of CI configuration refactoring

## What are the relevant issue numbers?

#15060

## Does this MR meet the acceptance criteria?

- Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)

See merge request !4820
parents ba9ef7f3 bfad4c61
...@@ -4,7 +4,6 @@ module Ci ...@@ -4,7 +4,6 @@ module Ci
include Gitlab::Ci::Config::Node::LegacyValidationHelpers include Gitlab::Ci::Config::Node::LegacyValidationHelpers
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test' DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache] ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
...@@ -14,7 +13,7 @@ module Ci ...@@ -14,7 +13,7 @@ module Ci
ALLOWED_CACHE_KEYS = [:key, :untracked, :paths] ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in] ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in]
attr_reader :after_script, :image, :services, :path, :cache attr_reader :path, :cache, :stages
def initialize(config, path = nil) def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config) @ci_config = Gitlab::Ci::Config.new(config)
...@@ -22,8 +21,11 @@ module Ci ...@@ -22,8 +21,11 @@ module Ci
@path = path @path = path
initial_parsing unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first
end
initial_parsing
validate! validate!
rescue Gitlab::Ci::Config::Loader::FormatError => e rescue Gitlab::Ci::Config::Loader::FormatError => e
raise ValidationError, e.message raise ValidationError, e.message
...@@ -42,10 +44,6 @@ module Ci ...@@ -42,10 +44,6 @@ module Ci
end end
end end
def stages
@stages || DEFAULT_STAGES
end
def global_variables def global_variables
@variables @variables
end end
...@@ -60,12 +58,14 @@ module Ci ...@@ -60,12 +58,14 @@ module Ci
private private
def initial_parsing def initial_parsing
@after_script = @config[:after_script] @before_script = @ci_config.before_script
@image = @config[:image] @image = @ci_config.image
@services = @config[:services] @after_script = @ci_config.after_script
@stages = @config[:stages] || @config[:types] @services = @ci_config.services
@variables = @config[:variables] || {} @variables = @ci_config.variables
@cache = @config[:cache] @stages = @ci_config.stages
@cache = @ci_config.cache
@jobs = {} @jobs = {}
@config.except!(*ALLOWED_YAML_KEYS) @config.except!(*ALLOWED_YAML_KEYS)
...@@ -85,9 +85,14 @@ module Ci ...@@ -85,9 +85,14 @@ module Ci
def build_job(name, job) def build_job(name, job)
{ {
stage_idx: stages.index(job[:stage]), stage_idx: @stages.index(job[:stage]),
stage: job[:stage], stage: job[:stage],
commands: [job[:before_script] || [@ci_config.before_script], job[:script]].flatten.compact.join("\n"), ##
# Refactoring note:
# - before script behaves differently than after script
# - after script returns an array of commands
# - before script should be a concatenated command
commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
tag_list: job[:tags] || [], tag_list: job[:tags] || [],
name: name, name: name,
only: job[:only], only: job[:only],
...@@ -107,12 +112,6 @@ module Ci ...@@ -107,12 +112,6 @@ module Ci
end end
def validate! def validate!
unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first
end
validate_global!
@jobs.each do |name, job| @jobs.each do |name, job|
validate_job!(name, job) validate_job!(name, job)
end end
...@@ -120,50 +119,6 @@ module Ci ...@@ -120,50 +119,6 @@ module Ci
true true
end end
def validate_global!
unless @after_script.nil? || validate_array_of_strings(@after_script)
raise ValidationError, "after_script should be an array of strings"
end
unless @image.nil? || @image.is_a?(String)
raise ValidationError, "image should be a string"
end
unless @services.nil? || validate_array_of_strings(@services)
raise ValidationError, "services should be an array of strings"
end
unless @stages.nil? || validate_array_of_strings(@stages)
raise ValidationError, "stages should be an array of strings"
end
unless @variables.nil? || validate_variables(@variables)
raise ValidationError, "variables should be a map of key-value strings"
end
validate_global_cache! if @cache
end
def validate_global_cache!
@cache.keys.each do |key|
unless ALLOWED_CACHE_KEYS.include? key
raise ValidationError, "#{name} cache unknown parameter #{key}"
end
end
if @cache[:key] && !validate_string(@cache[:key])
raise ValidationError, "cache:key parameter should be a string"
end
if @cache[:untracked] && !validate_boolean(@cache[:untracked])
raise ValidationError, "cache:untracked parameter should be an boolean"
end
if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
raise ValidationError, "cache:paths parameter should be an array of strings"
end
end
def validate_job!(name, job) def validate_job!(name, job)
validate_job_name!(name) validate_job_name!(name)
validate_job_keys!(name, job) validate_job_keys!(name, job)
...@@ -240,8 +195,8 @@ module Ci ...@@ -240,8 +195,8 @@ module Ci
end end
def validate_job_stage!(name, job) def validate_job_stage!(name, job)
unless job[:stage].is_a?(String) && job[:stage].in?(stages) unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}" raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
end end
end end
...@@ -305,12 +260,12 @@ module Ci ...@@ -305,12 +260,12 @@ module Ci
raise ValidationError, "#{name} job: dependencies parameter should be an array of strings" raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
end end
stage_index = stages.index(job[:stage]) stage_index = @stages.index(job[:stage])
job[:dependencies].each do |dependency| job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
unless stages.index(@jobs[dependency.to_sym][:stage]) < stage_index unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end end
end end
......
...@@ -7,7 +7,8 @@ module Gitlab ...@@ -7,7 +7,8 @@ module Gitlab
## ##
# Temporary delegations that should be removed after refactoring # Temporary delegations that should be removed after refactoring
# #
delegate :before_script, to: :@global delegate :before_script, :image, :services, :after_script, :variables,
:stages, :cache, to: :@global
def initialize(config) def initialize(config)
@config = Loader.new(config).load! @config = Loader.new(config).load!
......
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a boolean value.
#
class Boolean < Entry
include Validatable
validations do
validates :config, boolean: true
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a cache configuration
#
class Cache < Entry
include Configurable
node :key, Node::Key,
description: 'Cache key used to define a cache affinity.'
node :untracked, Node::Boolean,
description: 'Cache all untracked files.'
node :paths, Node::Paths,
description: 'Specify which paths should be cached across builds.'
validations do
validates :config, allowed_keys: true
end
end
end
end
end
end
...@@ -19,35 +19,45 @@ module Gitlab ...@@ -19,35 +19,45 @@ module Gitlab
included do included do
validations do validations do
validates :config, hash: true validates :config, type: Hash
end end
end end
private private
def create_node(key, factory) def create_node(key, factory)
factory.with(value: @config[key], key: key) factory.with(value: @config[key], key: key, parent: self)
factory.nullify! unless @config.has_key?(key)
factory.create! factory.create!
end end
class_methods do class_methods do
def nodes def nodes
Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }] Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
end end
private private
def allow_node(symbol, entry_class, metadata) def node(symbol, entry_class, metadata)
factory = Node::Factory.new(entry_class) factory = Node::Factory.new(entry_class)
.with(description: metadata[:description]) .with(description: metadata[:description])
define_method(symbol) do (@nodes ||= {}).merge!(symbol.to_sym => factory)
raise Entry::InvalidError unless valid? end
@nodes[symbol].try(:value)
end
(@allowed_nodes ||= {}).merge!(symbol => factory) def helpers(*nodes)
nodes.each do |symbol|
define_method("#{symbol}_defined?") do
@nodes[symbol].try(:defined?)
end
define_method("#{symbol}_value") do
raise Entry::InvalidError unless valid?
@nodes[symbol].try(:value)
end
alias_method symbol.to_sym, "#{symbol}_value".to_sym
end
end end
end end
end end
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
class InvalidError < StandardError; end class InvalidError < StandardError; end
attr_reader :config attr_reader :config
attr_accessor :key, :description attr_accessor :key, :parent, :description
def initialize(config) def initialize(config)
@config = config @config = config
...@@ -34,8 +34,8 @@ module Gitlab ...@@ -34,8 +34,8 @@ module Gitlab
self.class.nodes.none? self.class.nodes.none?
end end
def key def ancestors
@key || self.class.name.demodulize.underscore @parent ? @parent.ancestors + [@parent] : []
end end
def valid? def valid?
...@@ -43,12 +43,23 @@ module Gitlab ...@@ -43,12 +43,23 @@ module Gitlab
end end
def errors def errors
@validator.full_errors + @validator.messages + nodes.flat_map(&:errors)
nodes.map(&:errors).flatten
end end
def value def value
raise NotImplementedError if leaf?
@config
else
defined = @nodes.select { |_key, value| value.defined? }
Hash[defined.map { |key, node| [key, node.value] }]
end
end
def defined?
true
end
def self.default
end end
def self.nodes def self.nodes
......
...@@ -5,13 +5,11 @@ module Gitlab ...@@ -5,13 +5,11 @@ module Gitlab
## ##
# Factory class responsible for fabricating node entry objects. # Factory class responsible for fabricating node entry objects.
# #
# It uses Fluent Interface pattern to set all necessary attributes.
#
class Factory class Factory
class InvalidFactory < StandardError; end class InvalidFactory < StandardError; end
def initialize(entry_class) def initialize(node)
@entry_class = entry_class @node = node
@attributes = {} @attributes = {}
end end
...@@ -20,17 +18,27 @@ module Gitlab ...@@ -20,17 +18,27 @@ module Gitlab
self self
end end
def nullify!
@entry_class = Node::Null
self
end
def create! def create!
raise InvalidFactory unless @attributes.has_key?(:value) raise InvalidFactory unless @attributes.has_key?(:value)
@entry_class.new(@attributes[:value]).tap do |entry| fabricate.tap do |entry|
entry.description = @attributes[:description]
entry.key = @attributes[:key] entry.key = @attributes[:key]
entry.parent = @attributes[:parent]
entry.description = @attributes[:description]
end
end
private
def fabricate
##
# We assume that unspecified entry is undefined.
# See issue #18775.
#
if @attributes[:value].nil?
Node::Undefined.new(@node)
else
@node.new(@attributes[:value])
end end
end end
end end
......
...@@ -9,8 +9,36 @@ module Gitlab ...@@ -9,8 +9,36 @@ module Gitlab
class Global < Entry class Global < Entry
include Configurable include Configurable
allow_node :before_script, Script, node :before_script, Node::Script,
description: 'Script that will be executed before each job.' description: 'Script that will be executed before each job.'
node :image, Node::Image,
description: 'Docker image that will be used to execute jobs.'
node :services, Node::Services,
description: 'Docker images that will be linked to the container.'
node :after_script, Node::Script,
description: 'Script that will be executed after each job.'
node :variables, Node::Variables,
description: 'Environment variables that will be used.'
node :stages, Node::Stages,
description: 'Configuration of stages for this pipeline.'
node :types, Node::Stages,
description: 'Deprecated: stages for this pipeline.'
node :cache, Node::Cache,
description: 'Configure caching between build jobs.'
helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache
def stages
stages_defined? ? stages_value : types_value
end
end end
end end
end end
......
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a Docker image.
#
class Image < Entry
include Validatable
validations do
validates :config, type: String
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a key.
#
class Key < Entry
include Validatable
validations do
validates :config, key: true
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents an array of paths.
#
class Paths < Entry
include Validatable
validations do
validates :config, array_of_strings: true
end
end
end
end
end
end
...@@ -5,21 +5,12 @@ module Gitlab ...@@ -5,21 +5,12 @@ module Gitlab
## ##
# Entry that represents a script. # Entry that represents a script.
# #
# Each element in the value array is a command that will be executed
# by GitLab Runner. Currently we concatenate these commands with
# new line character as a separator, what is compatible with
# implementation in Runner.
#
class Script < Entry class Script < Entry
include Validatable include Validatable
validations do validations do
validates :config, array_of_strings: true validates :config, array_of_strings: true
end end
def value
@config.join("\n")
end
end end
end end
end end
......
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a configuration of Docker services.
#
class Services < Entry
include Validatable
validations do
validates :config, array_of_strings: true
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a configuration for pipeline stages.
#
class Stages < Entry
include Validatable
validations do
validates :config, array_of_strings: true
end
def self.default
%w[build test deploy]
end
end
end
end
end
end
...@@ -3,22 +3,25 @@ module Gitlab ...@@ -3,22 +3,25 @@ module Gitlab
class Config class Config
module Node module Node
## ##
# This class represents a configuration entry that is not being used # This class represents an undefined entry node.
# in configuration file.
# #
# This implements Null Object pattern. # It takes original entry class as configuration and returns default
# value of original entry as self value.
# #
class Null < Entry #
def value class Undefined < Entry
nil include Validatable
validations do
validates :config, type: Class
end end
def validate! def value
nil @config.default
end end
def method_missing(*) def defined?
nil false
end end
end end
end end
......
...@@ -11,15 +11,29 @@ module Gitlab ...@@ -11,15 +11,29 @@ module Gitlab
@node = node @node = node
end end
def full_errors def messages
errors.full_messages.map do |error| errors.full_messages.map do |error|
"#{@node.key} #{error}".humanize "#{location} #{error}".downcase
end end
end end
def self.name def self.name
'Validator' 'Validator'
end end
def unknown_keys
return [] unless config.is_a?(Hash)
config.keys - @node.class.nodes.keys
end
private
def location
predecessors = ancestors.map(&:key).compact
current = key || @node.class.name.demodulize.underscore
predecessors.append(current).join(':')
end
end end
end end
end end
......
...@@ -3,6 +3,16 @@ module Gitlab ...@@ -3,6 +3,16 @@ module Gitlab
class Config class Config
module Node module Node
module Validators module Validators
class AllowedKeysValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.unknown_keys.any?
unknown_list = record.unknown_keys.join(', ')
record.errors.add(:config,
"contains unknown keys: #{unknown_list}")
end
end
end
class ArrayOfStringsValidator < ActiveModel::EachValidator class ArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers include LegacyValidationHelpers
...@@ -13,10 +23,43 @@ module Gitlab ...@@ -13,10 +23,43 @@ module Gitlab
end end
end end
class HashValidator < ActiveModel::EachValidator 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 KeyValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
def validate_each(record, attribute, value)
unless validate_string(value)
record.errors.add(attribute, 'should be a string or symbol')
end
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)
record.errors.add(attribute, "should be a #{type.name}")
end
end
end
class VariablesValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless value.is_a?(Hash) unless validate_variables(value)
record.errors.add(attribute, 'should be a configuration entry hash') record.errors.add(attribute, 'should be a hash of key value pairs')
end end
end end
end end
......
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents environment variables.
#
class Variables < Entry
include Validatable
validations do
validates :config, variables: true
end
def self.default
{}
end
end
end
end
end
end
...@@ -550,8 +550,8 @@ module Ci ...@@ -550,8 +550,8 @@ module Ci
config_processor = GitlabCiYamlProcessor.new(config, path) config_processor = GitlabCiYamlProcessor.new(config, path)
## ##
# TODO, in next version of CI configuration processor this # When variables config is empty, we assume this is a valid
# should be invalid configuration, see #18775 and #15060 # configuration, see issue #18775
# #
expect(config_processor.job_variables(:rspec)) expect(config_processor.job_variables(:rspec))
.to be_an_instance_of(Array).and be_empty .to be_an_instance_of(Array).and be_empty
...@@ -590,7 +590,20 @@ module Ci ...@@ -590,7 +590,20 @@ module Ci
end end
end end
describe "Caches" do describe 'cache' do
context 'when cache definition has unknown keys' do
it 'raises relevant validation error' do
config = YAML.dump(
{ cache: { untracked: true, invalid: 'key' },
rspec: { script: 'rspec' } })
expect { GitlabCiYamlProcessor.new(config) }.to raise_error(
GitlabCiYamlProcessor::ValidationError,
'cache config contains unknown keys: invalid'
)
end
end
it "returns cache when defined globally" do it "returns cache when defined globally" do
config = YAML.dump({ config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
...@@ -950,7 +963,7 @@ EOT ...@@ -950,7 +963,7 @@ EOT
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Before script config should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings")
end end
it "returns errors if job before_script parameter is not an array of strings" do it "returns errors if job before_script parameter is not an array of strings" do
...@@ -964,7 +977,7 @@ EOT ...@@ -964,7 +977,7 @@ EOT
config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings")
end end
it "returns errors if job after_script parameter is not an array of strings" do it "returns errors if job after_script parameter is not an array of strings" do
...@@ -978,7 +991,7 @@ EOT ...@@ -978,7 +991,7 @@ EOT
config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a string")
end end
it "returns errors if job name is blank" do it "returns errors if job name is blank" do
...@@ -1006,14 +1019,14 @@ EOT ...@@ -1006,14 +1019,14 @@ EOT
config = YAML.dump({ services: "test", rspec: { script: "test" } }) config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings")
end end
it "returns errors if services parameter is not an array of strings" do it "returns errors if services parameter is not an array of strings" do
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings")
end end
it "returns errors if job services parameter is not an array" do it "returns errors if job services parameter is not an array" do
...@@ -1080,31 +1093,31 @@ EOT ...@@ -1080,31 +1093,31 @@ EOT
end end
it "returns errors if stages is not an array" do it "returns errors if stages is not an array" do
config = YAML.dump({ types: "test", rspec: { script: "test" } }) config = YAML.dump({ stages: "test", rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
end end
it "returns errors if stages is not an array of strings" do it "returns errors if stages is not an array of strings" do
config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } }) config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
end end
it "returns errors if variables is not a map" do it "returns errors if variables is not a map" do
config = YAML.dump({ variables: "test", rspec: { script: "test" } }) config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end end
it "returns errors if variables is not a map of key-value strings" do it "returns errors if variables is not a map of key-value strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end end
it "returns errors if job when is not on_success, on_failure or always" do it "returns errors if job when is not on_success, on_failure or always" do
...@@ -1160,21 +1173,21 @@ EOT ...@@ -1160,21 +1173,21 @@ EOT
config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config) GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked parameter should be an boolean") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
end end
it "returns errors if cache:paths is not an array of strings" do it "returns errors if cache:paths is not an array of strings" do
config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config) GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings")
end end
it "returns errors if cache:key is not a string" do it "returns errors if cache:key is not a string" do
config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
expect do expect do
GitlabCiYamlProcessor.new(config) GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key parameter should be a string") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol")
end end
it "returns errors if job cache:key is not an a string" do it "returns errors if job cache:key is not an a string" do
......
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Boolean do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is valid' do
let(:config) { false }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq false
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not valid' do
let(:config) { ['incorrect'] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'boolean config should be a boolean value'
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Cache do
let(:entry) { described_class.new(config) }
describe 'validations' do
before { entry.process! }
context 'when entry config value is correct' do
let(:config) do
{ key: 'some key',
untracked: true,
paths: ['some/path/'] }
end
describe '#value' do
it 'returns hash value' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when is not a hash' do
let(:config) { 'ls' }
it 'reports errors with config value' do
expect(entry.errors)
.to include 'cache config should be a hash'
end
end
context 'when descendants are invalid' do
let(:config) { { key: 1 } }
it 'reports error with descendants' do
expect(entry.errors)
.to include 'key config should be a string or symbol'
end
end
context 'when there is an unknown key present' do
let(:config) { { invalid: true } }
it 'reports error with descendants' do
expect(entry.errors)
.to include 'cache config contains unknown keys: invalid'
end
end
end
end
end
end
...@@ -7,10 +7,42 @@ describe Gitlab::Ci::Config::Node::Configurable do ...@@ -7,10 +7,42 @@ describe Gitlab::Ci::Config::Node::Configurable do
node.include(described_class) node.include(described_class)
end end
describe 'validations' do
let(:validator) { node.validator.new(instance) }
before do
node.class_eval do
attr_reader :config
def initialize(config)
@config = config
end
end
validator.validate
end
context 'when node validator is invalid' do
let(:instance) { node.new('ls') }
it 'returns invalid validator' do
expect(validator).to be_invalid
end
end
context 'when node instance is valid' do
let(:instance) { node.new(key: 'value') }
it 'returns valid validator' do
expect(validator).to be_valid
end
end
end
describe 'configured nodes' do describe 'configured nodes' do
before do before do
node.class_eval do node.class_eval do
allow_node :object, Object, description: 'test object' node :object, Object, description: 'test object'
end end
end end
......
...@@ -5,13 +5,13 @@ describe Gitlab::Ci::Config::Node::Factory do ...@@ -5,13 +5,13 @@ describe Gitlab::Ci::Config::Node::Factory do
let(:factory) { described_class.new(entry_class) } let(:factory) { described_class.new(entry_class) }
let(:entry_class) { Gitlab::Ci::Config::Node::Script } let(:entry_class) { Gitlab::Ci::Config::Node::Script }
context 'when value setting value' do context 'when setting up a value' do
it 'creates entry with valid value' do it 'creates entry with valid value' do
entry = factory entry = factory
.with(value: ['ls', 'pwd']) .with(value: ['ls', 'pwd'])
.create! .create!
expect(entry.value).to eq "ls\npwd" expect(entry.value).to eq ['ls', 'pwd']
end end
context 'when setting description' do context 'when setting description' do
...@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Node::Factory do ...@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Node::Factory do
.with(description: 'test description') .with(description: 'test description')
.create! .create!
expect(entry.value).to eq "ls\npwd" expect(entry.value).to eq ['ls', 'pwd']
expect(entry.description).to eq 'test description' expect(entry.description).to eq 'test description'
end end
end end
...@@ -35,9 +35,21 @@ describe Gitlab::Ci::Config::Node::Factory do ...@@ -35,9 +35,21 @@ describe Gitlab::Ci::Config::Node::Factory do
expect(entry.key).to eq 'test key' expect(entry.key).to eq 'test key'
end end
end end
context 'when setting a parent' do
let(:parent) { Object.new }
it 'creates entry with valid parent' do
entry = factory
.with(value: 'ls', parent: parent)
.create!
expect(entry.parent).to eq parent
end
end
end end
context 'when not setting value' do context 'when not setting up 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::Node::Factory::InvalidFactory Gitlab::Ci::Config::Node::Factory::InvalidFactory
...@@ -45,14 +57,13 @@ describe Gitlab::Ci::Config::Node::Factory do ...@@ -45,14 +57,13 @@ describe Gitlab::Ci::Config::Node::Factory do
end end
end end
context 'when creating a null entry' do context 'when creating entry with nil value' do
it 'creates a null entry' do it 'creates an undefined entry' do
entry = factory entry = factory
.with(value: nil) .with(value: nil)
.nullify!
.create! .create!
expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Null expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
end end
end end
end end
......
...@@ -13,57 +13,163 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -13,57 +13,163 @@ describe Gitlab::Ci::Config::Node::Global do
end end
end end
describe '#key' do
it 'returns underscored class name' do
expect(global.key).to eq 'global'
end
end
context 'when hash is valid' do context 'when hash is valid' do
let(:hash) do context 'when all entries defined' do
{ before_script: ['ls', 'pwd'] } let(:hash) do
end { before_script: ['ls', 'pwd'],
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'value' },
after_script: ['make clean'],
stages: ['build', 'pages'],
cache: { key: 'k', untracked: true, paths: ['public/'] } }
end
describe '#process!' do describe '#process!' do
before { global.process! } before { global.process! }
it 'creates nodes hash' do it 'creates nodes hash' do
expect(global.nodes).to be_an Array expect(global.nodes).to be_an Array
end
it 'creates node object for each entry' do
expect(global.nodes.count).to eq 8
end
it 'creates node object using valid class' do
expect(global.nodes.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Script
expect(global.nodes.second)
.to be_an_instance_of Gitlab::Ci::Config::Node::Image
end
it 'sets correct description for nodes' do
expect(global.nodes.first.description)
.to eq 'Script that will be executed before each job.'
expect(global.nodes.second.description)
.to eq 'Docker image that will be used to execute jobs.'
end
end end
it 'creates node object for each entry' do describe '#leaf?' do
expect(global.nodes.count).to eq 1 it 'is not leaf' do
expect(global).not_to be_leaf
end
end end
it 'creates node object using valid class' do context 'when not processed' do
expect(global.nodes.first) describe '#before_script' do
.to be_an_instance_of Gitlab::Ci::Config::Node::Script it 'returns nil' do
expect(global.before_script).to be nil
end
end
end end
it 'sets correct description for nodes' do context 'when processed' do
expect(global.nodes.first.description) before { global.process! }
.to eq 'Script that will be executed before each job.'
describe '#before_script' do
it 'returns correct script' do
expect(global.before_script).to eq ['ls', 'pwd']
end
end
describe '#image' do
it 'returns valid image' do
expect(global.image).to eq 'ruby:2.2'
end
end
describe '#services' do
it 'returns array of services' do
expect(global.services).to eq ['postgres:9.1', 'mysql:5.5']
end
end
describe '#after_script' do
it 'returns after script' do
expect(global.after_script).to eq ['make clean']
end
end
describe '#variables' do
it 'returns variables' do
expect(global.variables).to eq(VAR: 'value')
end
end
describe '#stages' do
context 'when stages key defined' do
it 'returns array of stages' do
expect(global.stages).to eq %w[build pages]
end
end
context 'when deprecated types key defined' do
let(:hash) { { types: ['test', 'deploy'] } }
it 'returns array of types as stages' do
expect(global.stages).to eq %w[test deploy]
end
end
end
describe '#cache' do
it 'returns cache configuration' do
expect(global.cache)
.to eq(key: 'k', untracked: true, paths: ['public/'])
end
end
end end
end end
describe '#leaf?' do context 'when most of entires not defined' do
it 'is not leaf' do let(:hash) { { cache: { key: 'a' }, rspec: {} } }
expect(global).not_to be_leaf before { global.process! }
describe '#nodes' do
it 'instantizes all nodes' do
expect(global.nodes.count).to eq 8
end
it 'contains undefined nodes' do
expect(global.nodes.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
end
end end
end
describe '#before_script' do describe '#variables' do
context 'when processed' do it 'returns default value for variables' do
before { global.process! } expect(global.variables).to eq({})
end
end
it 'returns correct script' do describe '#stages' do
expect(global.before_script).to eq "ls\npwd" it 'returns an array of default stages' do
expect(global.stages).to eq %w[build test deploy]
end end
end end
context 'when not processed' do describe '#cache' do
it 'returns nil' do it 'returns correct cache definition' do
expect(global.before_script).to be nil expect(global.cache).to eq(key: 'a')
end
end
end
##
# When nodes are specified but not defined, we assume that
# configuration is valid, and we asume that entry is simply undefined,
# despite the fact, that key is present. See issue #18775 for more
# details.
#
context 'when entires specified but not defined' do
let(:hash) { { variables: nil } }
before { global.process! }
describe '#variables' do
it 'undefined entry returns a default value' do
expect(global.variables).to eq({})
end end
end end
end end
...@@ -85,7 +191,7 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -85,7 +191,7 @@ describe Gitlab::Ci::Config::Node::Global do
describe '#errors' do describe '#errors' do
it 'reports errors from child nodes' do it 'reports errors from child nodes' do
expect(global.errors) expect(global.errors)
.to include 'Before script config should be an array of strings' .to include 'before_script config should be an array of strings'
end end
end end
...@@ -106,5 +212,17 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -106,5 +212,17 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global).not_to be_valid expect(global).not_to be_valid
end end
end end
describe '#errors' do
it 'returns error about invalid type' do
expect(global.errors.first).to match /should be a hash/
end
end
end
describe '#defined?' do
it 'is concrete entry that is defined' do
expect(global.defined?).to be true
end
end end
end end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Image do
let(:entry) { described_class.new(config) }
describe 'validation' do
context 'when entry config value is correct' do
let(:config) { 'ruby:2.2' }
describe '#value' do
it 'returns image string' do
expect(entry.value).to eq 'ruby:2.2'
end
end
describe '#errors' do
it 'does not append errors' do
expect(entry.errors).to be_empty
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:config) { ['ruby:2.2'] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'image config should be a string'
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Key do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is correct' do
let(:config) { 'test' }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq 'test'
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:config) { [ 'incorrect' ] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'key config should be a string or symbol'
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Null do
let(:entry) { described_class.new(nil) }
describe '#leaf?' do
it 'is leaf node' do
expect(entry).to be_leaf
end
end
describe '#any_method' do
it 'responds with nil' do
expect(entry.any_method).to be nil
end
end
describe '#value' do
it 'returns nil' do
expect(entry.value).to be nil
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Paths do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is valid' do
let(:config) { ['some/file', 'some/path/'] }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not valid' do
let(:config) { [ 1 ] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'paths config should be an array of strings'
end
end
end
end
end
...@@ -10,8 +10,8 @@ describe Gitlab::Ci::Config::Node::Script do ...@@ -10,8 +10,8 @@ describe Gitlab::Ci::Config::Node::Script do
let(:config) { ['ls', 'pwd'] } let(:config) { ['ls', 'pwd'] }
describe '#value' do describe '#value' do
it 'returns concatenated command' do it 'returns array of strings' do
expect(entry.value).to eq "ls\npwd" expect(entry.value).to eq config
end end
end end
...@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::Node::Script do ...@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::Node::Script do
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors)
.to include 'Script config should be an array of strings' .to include 'script config should be an array of strings'
end end
end end
......
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Services do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is correct' do
let(:config) { ['postgres:9.1', 'mysql:5.5'] }
describe '#value' do
it 'returns array of services as is' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:config) { 'ls' }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'services config should be an array of strings'
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Stages do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is correct' do
let(:config) { [:stage1, :stage2] }
describe '#value' do
it 'returns array of stages' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:config) { { test: true } }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'stages config should be an array of strings'
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
describe '.default' do
it 'returns default stages' do
expect(described_class.default).to eq %w[build test deploy]
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do
let(:undefined) { described_class.new(entry) }
let(:entry) { Class.new }
describe '#leaf?' do
it 'is leaf node' do
expect(undefined).to be_leaf
end
end
describe '#valid?' do
it 'is always valid' do
expect(undefined).to be_valid
end
end
describe '#errors' do
it 'is does not contain errors' do
expect(undefined.errors).to be_empty
end
end
describe '#value' do
before do
allow(entry).to receive(:default).and_return('some value')
end
it 'returns default value for entry' do
expect(undefined.value).to eq 'some value'
end
end
describe '#undefined?' do
it 'is not a defined entry' do
expect(undefined.defined?).to be false
end
end
end
...@@ -5,7 +5,18 @@ describe Gitlab::Ci::Config::Node::Validator do ...@@ -5,7 +5,18 @@ describe Gitlab::Ci::Config::Node::Validator do
let(:validator_instance) { validator.new(node) } let(:validator_instance) { validator.new(node) }
let(:node) { spy('node') } let(:node) { spy('node') }
shared_examples 'delegated validator' do before do
allow(node).to receive(:key).and_return('node')
allow(node).to receive(:ancestors).and_return([])
end
describe 'delegated validator' do
before do
validator.class_eval do
validates :test_attribute, presence: true
end
end
context 'when node is valid' do context 'when node is valid' do
before do before do
allow(node).to receive(:test_attribute).and_return('valid value') allow(node).to receive(:test_attribute).and_return('valid value')
...@@ -19,7 +30,7 @@ describe Gitlab::Ci::Config::Node::Validator do ...@@ -19,7 +30,7 @@ describe Gitlab::Ci::Config::Node::Validator do
it 'returns no errors' do it 'returns no errors' do
validator_instance.validate validator_instance.validate
expect(validator_instance.full_errors).to be_empty expect(validator_instance.messages).to be_empty
end end
end end
...@@ -36,32 +47,9 @@ describe Gitlab::Ci::Config::Node::Validator do ...@@ -36,32 +47,9 @@ describe Gitlab::Ci::Config::Node::Validator do
it 'returns errors' do it 'returns errors' do
validator_instance.validate validator_instance.validate
expect(validator_instance.full_errors).not_to be_empty expect(validator_instance.messages)
.to include "node test attribute can't be blank"
end end
end end
end end
describe 'attributes validations' do
before do
validator.class_eval do
validates :test_attribute, presence: true
end
end
it_behaves_like 'delegated validator'
end
describe 'interface validations' do
before do
validator.class_eval do
validate do
unless @node.test_attribute == 'valid value'
errors.add(:test_attribute, 'invalid value')
end
end
end
end
it_behaves_like 'delegated validator'
end
end end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Variables do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is correct' do
let(:config) do
{ 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
end
describe '#value' do
it 'returns hash with key value strings' do
expect(entry.value).to eq config
end
end
describe '#errors' do
it 'does not append errors' do
expect(entry.errors).to be_empty
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:config) { [ :VAR, 'test' ] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include /should be a hash of key value pairs/
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
end
...@@ -40,38 +40,38 @@ describe Gitlab::Ci::Config do ...@@ -40,38 +40,38 @@ describe Gitlab::Ci::Config do
end end
end end
end end
end
context 'when config is invalid' do context 'when config is invalid' do
context 'when yml is incorrect' do context 'when yml is incorrect' do
let(:yml) { '// invalid' } let(:yml) { '// invalid' }
describe '.new' do describe '.new' do
it 'raises error' do it 'raises error' do
expect { config }.to raise_error( expect { config }.to raise_error(
Gitlab::Ci::Config::Loader::FormatError, Gitlab::Ci::Config::Loader::FormatError,
/Invalid configuration format/ /Invalid configuration format/
) )
end
end end
end end
end
context 'when config logic is incorrect' do context 'when config logic is incorrect' do
let(:yml) { 'before_script: "ls"' } let(:yml) { 'before_script: "ls"' }
describe '#valid?' do describe '#valid?' do
it 'is not valid' do it 'is not valid' do
expect(config).not_to be_valid expect(config).not_to be_valid
end end
it 'has errors' do it 'has errors' do
expect(config.errors).not_to be_empty expect(config.errors).not_to be_empty
end
end end
end
describe '#errors' do describe '#errors' do
it 'returns an array of strings' do it 'returns an array of strings' do
expect(config.errors).to all(be_an_instance_of(String)) expect(config.errors).to all(be_an_instance_of(String))
end
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