Commit 505d71ec authored by Kamil Trzciński's avatar Kamil Trzciński Committed by Grzegorz Bizon

Introduce default: for gitlab-ci.yml

This moves all existing `image/services/before_script/variables`
into `default:`. This allows us to easily add a default and
top-level entries. `default`: is keep backward compatible: to
be considered to be job if `default:script:` is specified. This
behavior should be removed.

All existing `image/services/before_script/variables` are properly
handled in root context.
parent c167cc58
---
title: 'Introduce default: for gitlab-ci.yml'
merge_request:
author:
type: added
...@@ -14,23 +14,25 @@ module Gitlab ...@@ -14,23 +14,25 @@ module Gitlab
External::Processor::IncludeError External::Processor::IncludeError
].freeze ].freeze
attr_reader :root
def initialize(config, project: nil, sha: nil, user: nil) def initialize(config, project: nil, sha: nil, user: nil)
@config = Config::Extendable @config = Config::Extendable
.new(build_config(config, project: project, sha: sha, user: user)) .new(build_config(config, project: project, sha: sha, user: user))
.to_hash .to_hash
@global = Entry::Global.new(@config) @root = Entry::Root.new(@config)
@global.compose! @root.compose!
rescue *rescue_errors => e rescue *rescue_errors => e
raise Config::ConfigError, e.message raise Config::ConfigError, e.message
end end
def valid? def valid?
@global.valid? @root.valid?
end end
def errors def errors
@global.errors @root.errors
end end
def to_hash def to_hash
...@@ -40,36 +42,16 @@ module Gitlab ...@@ -40,36 +42,16 @@ module Gitlab
## ##
# Temporary method that should be removed after refactoring # Temporary method that should be removed after refactoring
# #
def before_script
@global.before_script_value
end
def image
@global.image_value
end
def services
@global.services_value
end
def after_script
@global.after_script_value
end
def variables def variables
@global.variables_value root.variables_value
end end
def stages def stages
@global.stages_value root.stages_value
end
def cache
@global.cache_value
end end
def jobs def jobs
@global.jobs_value root.jobs_value
end end
private private
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# This class represents a default entry
# Entry containing default values for all jobs
# defined in configuration file.
#
class Default < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
DuplicateError = Class.new(Gitlab::Config::Loader::FormatError)
ALLOWED_KEYS = %i[before_script image services
after_script cache].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
end
entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.',
inherit: true
entry :image, Entry::Image,
description: 'Docker image that will be used to execute jobs.',
inherit: true
entry :services, Entry::Services,
description: 'Docker images that will be linked to the container.',
inherit: true
entry :after_script, Entry::Script,
description: 'Script that will be executed after each job.',
inherit: true
entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.',
inherit: true
helpers :before_script, :image, :services, :after_script, :cache
def compose!(deps = nil)
super(self)
inherit!(deps)
end
private
def inherit!(deps)
return unless deps
self.class.nodes.each do |key, factory|
next unless factory.inheritable?
root_entry = deps[key]
next unless root_entry.specified?
if self[key].specified?
raise DuplicateError, "#{key} is defined in top-level and `default:` entry"
end
@entries[key] = root_entry
end
end
end
end
end
end
end
...@@ -14,6 +14,14 @@ module Gitlab ...@@ -14,6 +14,14 @@ module Gitlab
validates :config, presence: true validates :config, presence: true
end end
def self.matching?(name, config)
name.to_s.start_with?('.')
end
def self.visible?
false
end
def relevant? def relevant?
false false
end end
......
...@@ -42,7 +42,8 @@ module Gitlab ...@@ -42,7 +42,8 @@ module Gitlab
end end
entry :before_script, Entry::Script, entry :before_script, Entry::Script,
description: 'Global before script overridden in this job.' description: 'Global before script overridden in this job.',
inherit: true
entry :script, Entry::Commands, entry :script, Entry::Commands,
description: 'Commands that will be executed in this job.' description: 'Commands that will be executed in this job.'
...@@ -54,16 +55,20 @@ module Gitlab ...@@ -54,16 +55,20 @@ module Gitlab
description: 'Deprecated: stage this job will be executed into.' description: 'Deprecated: stage this job will be executed into.'
entry :after_script, Entry::Script, entry :after_script, Entry::Script,
description: 'Commands that will be executed when finishing job.' description: 'Commands that will be executed when finishing job.',
inherit: true
entry :cache, Entry::Cache, entry :cache, Entry::Cache,
description: 'Cache definition for this job.' description: 'Cache definition for this job.',
inherit: true
entry :image, Entry::Image, entry :image, Entry::Image,
description: 'Image that will be used to execute this job.' description: 'Image that will be used to execute this job.',
inherit: true
entry :services, Entry::Services, entry :services, Entry::Services,
description: 'Services that will be used to execute this job.' description: 'Services that will be used to execute this job.',
inherit: true
entry :only, Entry::Policy, entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.', description: 'Refs policy this job will be executed for.',
...@@ -95,6 +100,15 @@ module Gitlab ...@@ -95,6 +100,15 @@ module Gitlab
attributes :script, :tags, :allow_failure, :when, :dependencies, attributes :script, :tags, :allow_failure, :when, :dependencies,
:retry, :parallel, :extends, :start_in :retry, :parallel, :extends, :start_in
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
config.is_a?(Hash) && config.key?(:script)
end
def self.visible?
true
end
def compose!(deps = nil) def compose!(deps = nil)
super do super do
if type_defined? && !stage_defined? if type_defined? && !stage_defined?
...@@ -129,15 +143,19 @@ module Gitlab ...@@ -129,15 +143,19 @@ module Gitlab
private private
# We inherit config entries from `default:`
# if the entry has the `inherit: true` flag set
def inherit!(deps) def inherit!(deps)
return unless deps return unless deps
self.class.nodes.each_key do |key| self.class.nodes.each do |key, factory|
global_entry = deps[key] next unless factory.inheritable?
default_entry = deps.default[key]
job_entry = self[key] job_entry = self[key]
if global_entry.specified? && !job_entry.specified? if default_entry.specified? && !job_entry.specified?
@entries[key] = global_entry @entries[key] = default_entry
end end
end end
end end
...@@ -152,7 +170,7 @@ module Gitlab ...@@ -152,7 +170,7 @@ module Gitlab
cache: cache_value, cache: cache_value,
only: only_value, only: only_value,
except: except_value, except: except_value,
variables: variables_defined? ? variables_value : nil, variables: variables_defined? ? variables_value : {},
environment: environment_defined? ? environment_value : nil, environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil, environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil, coverage: coverage_defined? ? coverage_value : nil,
......
...@@ -14,29 +14,48 @@ module Gitlab ...@@ -14,29 +14,48 @@ module Gitlab
validates :config, type: Hash validates :config, type: Hash
validate do validate do
unless has_valid_jobs?
errors.add(:config, 'should contain valid jobs')
end
unless has_visible_job? unless has_visible_job?
errors.add(:config, 'should contain at least one visible job') errors.add(:config, 'should contain at least one visible job')
end end
end end
def has_valid_jobs?
config.all? do |name, value|
Jobs.find_type(name, value)
end
end
def has_visible_job? def has_visible_job?
config.any? { |name, _| !hidden?(name) } config.any? do |name, value|
Jobs.find_type(name, value)&.visible?
end end
end end
end
TYPES = [Entry::Hidden, Entry::Job].freeze
def hidden?(name) private_constant :TYPES
name.to_s.start_with?('.')
def self.all_types
TYPES
end end
def node_type(name) def self.find_type(name, config)
hidden?(name) ? Entry::Hidden : Entry::Job self.all_types.find do |type|
type.matching?(name, config)
end
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def compose!(deps = nil) def compose!(deps = nil)
super do super do
@config.each do |name, config| @config.each do |name, config|
node = node_type(name) node = self.class.find_type(name, config)
next unless node
factory = ::Gitlab::Config::Entry::Factory.new(node) factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {}) .value(config || {})
......
...@@ -8,52 +8,107 @@ module Gitlab ...@@ -8,52 +8,107 @@ 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 < ::Gitlab::Config::Entry::Node class Root < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Configurable
entry :before_script, Entry::Script, ALLOWED_KEYS = %i[default include before_script image services
description: 'Script that will be executed before each job.' after_script variables stages types cache].freeze
entry :image, Entry::Image, validations do
description: 'Docker image that will be used to execute jobs.' validates :config, allowed_keys: ALLOWED_KEYS
end
# reserved:
# defines whether the node name is reserved
# the reserved name cannot be used a job name
# reserved should not be used as it will make
# breaking change to `.gitlab-ci.yml`
entry :default, Entry::Default,
description: 'Default configuration for all jobs.',
default: {}
entry :include, Entry::Includes, entry :include, Entry::Includes,
description: 'List of external YAML files to include.' description: 'List of external YAML files to include.',
reserved: true
entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.',
reserved: true
entry :image, Entry::Image,
description: 'Docker image that will be used to execute jobs.',
reserved: true
entry :services, Entry::Services, entry :services, Entry::Services,
description: 'Docker images that will be linked to the container.' description: 'Docker images that will be linked to the container.',
reserved: true
entry :after_script, Entry::Script, entry :after_script, Entry::Script,
description: 'Script that will be executed after each job.' description: 'Script that will be executed after each job.',
reserved: true
entry :variables, Entry::Variables, entry :variables, Entry::Variables,
description: 'Environment variables that will be used.' description: 'Environment variables that will be used.',
reserved: true
entry :stages, Entry::Stages, entry :stages, Entry::Stages,
description: 'Configuration of stages for this pipeline.' description: 'Configuration of stages for this pipeline.',
reserved: true
entry :types, Entry::Stages, entry :types, Entry::Stages,
description: 'Deprecated: stages for this pipeline.' description: 'Deprecated: stages for this pipeline.',
reserved: true
entry :cache, Entry::Cache, entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.' description: 'Configure caching between build jobs.',
reserved: true
helpers :default, :jobs, :stages, :types, :variables
delegate :before_script_value,
:image_value,
:services_value,
:after_script_value,
:cache_value, to: :default
attr_reader :jobs_config
class << self
include ::Gitlab::Utils::StrongMemoize
def reserved_nodes_names
strong_memoize(:reserved_nodes_names) do
self.nodes.select do |_, node|
node.reserved?
end.keys
end
end
end
helpers :before_script, :image, :services, :after_script, def initialize(config, **metadata)
:variables, :stages, :types, :cache, :jobs super do
filter_jobs!
end
end
def compose!(_deps = nil) def compose!(_deps = nil)
super(self) do super(self) do
compose_jobs!
compose_deprecated_entries! compose_deprecated_entries!
compose_jobs!
end end
end end
def default
self[:default]
end
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def compose_jobs! def compose_jobs!
factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs) factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys)) .value(jobs_config)
.with(key: :jobs, parent: self, .with(key: :jobs, parent: self,
description: 'Jobs definition for this pipeline') description: 'Jobs definition for this pipeline')
...@@ -72,6 +127,18 @@ module Gitlab ...@@ -72,6 +127,18 @@ module Gitlab
@entries.delete(:types) @entries.delete(:types)
end end
def filter_jobs!
return unless @config.is_a?(Hash)
@jobs_config = @config
.except(*self.class.reserved_nodes_names) # rubocop: disable CodeReuse/ActiveRecord
.select do |name, config|
Entry::Jobs.find_type(name, config).present?
end
@config = @config.except(*@jobs_config.keys) # rubocop: disable CodeReuse/ActiveRecord
end
end end
end end
end end
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
include Gitlab::Config::Entry::LegacyValidationHelpers include Gitlab::Config::Entry::LegacyValidationHelpers
attr_reader :cache, :stages, :jobs attr_reader :stages, :jobs
def initialize(config, opts = {}) def initialize(config, opts = {})
@ci_config = Gitlab::Ci::Config.new(config, **opts) @ci_config = Gitlab::Ci::Config.new(config, **opts)
...@@ -95,13 +95,8 @@ module Gitlab ...@@ -95,13 +95,8 @@ module Gitlab
## ##
# Global config # Global config
# #
@before_script = @ci_config.before_script
@image = @ci_config.image
@after_script = @ci_config.after_script
@services = @ci_config.services
@variables = @ci_config.variables @variables = @ci_config.variables
@stages = @ci_config.stages @stages = @ci_config.stages
@cache = @ci_config.cache
## ##
# Jobs # Jobs
......
...@@ -58,13 +58,21 @@ module Gitlab ...@@ -58,13 +58,21 @@ module Gitlab
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
end end
def reserved_node_names
self.nodes.select do |_, node|
node.reserved?
end.keys
end
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def entry(key, entry, metadata) def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil)
factory = ::Gitlab::Config::Entry::Factory.new(entry) factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: metadata[:description]) .with(description: description)
.with(default: metadata[:default]) .with(default: default)
.with(inherit: inherit)
.with(reserved: reserved)
(@nodes ||= {}).merge!(key.to_sym => factory) (@nodes ||= {}).merge!(key.to_sym => factory)
end end
......
...@@ -30,6 +30,18 @@ module Gitlab ...@@ -30,6 +30,18 @@ module Gitlab
self self
end end
def description
@attributes[:description]
end
def inheritable?
@attributes[:inherit]
end
def reserved?
@attributes[:reserved]
end
def create! def create!
raise InvalidFactory unless defined?(@value) raise InvalidFactory unless defined?(@value)
......
...@@ -107,7 +107,7 @@ describe Projects::Ci::LintsController do ...@@ -107,7 +107,7 @@ describe Projects::Ci::LintsController do
end end
it 'assigns errors' do it 'assigns errors' do
expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt') expect(assigns[:error]).to eq('root config contains unknown keys: rubocop')
end end
end end
......
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Default do
let(:entry) { described_class.new(config) }
describe '.nodes' do
it 'returns a hash' do
expect(described_class.nodes).to be_a(Hash)
end
context 'when filtering all the entry/node names' do
it 'contains the expected node names' do
expect(described_class.nodes.keys)
.to match_array(%i[before_script image services
after_script cache])
end
end
end
describe 'validations' do
before do
entry.compose!
end
context 'when default entry value is correct' do
let(:config) { { before_script: ['rspec'] } }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when default entry is empty' do
let(:config) { {} }
describe '#valid' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when default entry is not correct' do
context 'incorrect config value type' do
let(:config) { ['incorrect'] }
describe '#errors' do
it 'reports error about a config type' do
expect(entry.errors)
.to include 'default config should be a hash'
end
end
end
context 'when unknown keys detected' do
let(:config) { { unknown: true } }
describe '#valid' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
end
describe '#compose!' do
let(:specified) do
double('specified', 'specified?' => true, value: 'specified')
end
let(:unspecified) { double('unspecified', 'specified?' => false) }
let(:deps) { double('deps', '[]' => unspecified) }
context 'when default entry inherits configuration from root' do
let(:config) do
{ image: 'some_image' }
end
before do
allow(deps).to receive('[]').with(:image).and_return(specified)
end
it 'raises error' do
expect { entry.compose!(deps) }.to raise_error(
Gitlab::Ci::Config::Entry::Default::DuplicateError)
end
end
context 'when default entry inherits a non-defined configuration from root' do
let(:config) do
{ image: 'some_image' }
end
before do
allow(deps).to receive('[]').with(:after_script).and_return(specified)
entry.compose!(deps)
end
it 'inherits non-defined configuration entries' do
expect(entry[:image].value).to eq(name: 'some_image')
expect(entry[:after_script].value).to eq('specified')
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Hidden do describe Gitlab::Ci::Config::Entry::Hidden do
describe '.matching?' do
subject { described_class.matching?(name, {}) }
context 'when name starts with dot' do
let(:name) { '.hidden_job' }
it { is_expected.to be_truthy }
end
context 'when name does not start with dot' do
let(:name) { 'rspec' }
it { is_expected.to be_falsey }
end
end
describe '.new' do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe 'validations' do describe 'validations' do
...@@ -44,4 +61,5 @@ describe Gitlab::Ci::Config::Entry::Hidden do ...@@ -44,4 +61,5 @@ describe Gitlab::Ci::Config::Entry::Hidden do
expect(entry).not_to be_relevant expect(entry).not_to be_relevant
end end
end end
end
end end
...@@ -17,6 +17,44 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -17,6 +17,44 @@ describe Gitlab::Ci::Config::Entry::Job do
end end
end end
describe '.matching?' do
subject { described_class.matching?(name, config) }
context 'when config is not a hash' do
let(:name) { :rspec }
let(:config) { 'string' }
it { is_expected.to be_falsey }
end
context 'when config is a regular job' do
let(:name) { :rspec }
let(:config) do
{ script: 'ls -al' }
end
it { is_expected.to be_truthy }
end
context 'when config is a bridge job' do
let(:name) { :rspec }
let(:config) do
{ trigger: 'other-project' }
end
it { is_expected.to be_falsey }
end
context 'when config is a hidden job' do
let(:name) { '.rspec' }
let(:config) do
{ script: 'ls -al' }
end
it { is_expected.to be_falsey }
end
end
describe 'validations' do describe 'validations' do
before do before do
entry.compose! entry.compose!
...@@ -195,15 +233,15 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -195,15 +233,15 @@ describe Gitlab::Ci::Config::Entry::Job do
end end
describe '#compose!' do describe '#compose!' do
let(:unspecified) { double('unspecified', 'specified?' => false) }
let(:specified) do let(:specified) do
double('specified', 'specified?' => true, value: 'specified') double('specified', 'specified?' => true, value: 'specified')
end end
let(:deps) { double('deps', '[]' => unspecified) } let(:unspecified) { double('unspecified', 'specified?' => false) }
let(:default) { double('default', '[]' => unspecified) }
let(:deps) { double('deps', 'default' => default, '[]' => unspecified) }
context 'when job config overrides global config' do context 'when job config overrides default config' do
before do before do
entry.compose!(deps) entry.compose!(deps)
end end
...@@ -212,21 +250,22 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -212,21 +250,22 @@ describe Gitlab::Ci::Config::Entry::Job do
{ script: 'rspec', image: 'some_image', cache: { key: 'test' } } { script: 'rspec', image: 'some_image', cache: { key: 'test' } }
end end
it 'overrides global config' do it 'overrides default config' do
expect(entry[:image].value).to eq(name: 'some_image') expect(entry[:image].value).to eq(name: 'some_image')
expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push') expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push')
end end
end end
context 'when job config does not override global config' do context 'when job config does not override default config' do
before do before do
allow(deps).to receive('[]').with(:image).and_return(specified) allow(default).to receive('[]').with(:image).and_return(specified)
entry.compose!(deps) entry.compose!(deps)
end end
let(:config) { { script: 'ls', cache: { key: 'test' } } } let(:config) { { script: 'ls', cache: { key: 'test' } } }
it 'uses config from global entry' do it 'uses config from default entry' do
expect(entry[:image].value).to eq 'specified' expect(entry[:image].value).to eq 'specified'
expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push') expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push')
end end
...@@ -258,7 +297,8 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -258,7 +297,8 @@ describe Gitlab::Ci::Config::Entry::Job do
stage: 'test', stage: 'test',
ignore: false, ignore: false,
after_script: %w[cleanup], after_script: %w[cleanup],
only: { refs: %w[branches tags] }) only: { refs: %w[branches tags] },
variables: {})
end end
end end
end end
......
...@@ -3,6 +3,37 @@ require 'spec_helper' ...@@ -3,6 +3,37 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Jobs do describe Gitlab::Ci::Config::Entry::Jobs do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe '.all_types' do
subject { described_class.all_types }
it { is_expected.to include(::Gitlab::Ci::Config::Entry::Hidden) }
it { is_expected.to include(::Gitlab::Ci::Config::Entry::Job) }
end
describe '.find_type' do
using RSpec::Parameterized::TableSyntax
let(:config) do
{
'.hidden_job'.to_sym => { script: 'something' },
regular_job: { script: 'something' },
invalid_job: 'text'
}
end
where(:name, :type) do
:'.hidden_job' | ::Gitlab::Ci::Config::Entry::Hidden
:regular_job | ::Gitlab::Ci::Config::Entry::Job
:invalid_job | nil
end
subject { described_class.find_type(name, config[name]) }
with_them do
it { is_expected.to eq(type) }
end
end
describe 'validations' do describe 'validations' do
before do before do
entry.compose! entry.compose!
...@@ -29,11 +60,11 @@ describe Gitlab::Ci::Config::Entry::Jobs do ...@@ -29,11 +60,11 @@ describe Gitlab::Ci::Config::Entry::Jobs do
end end
end end
context 'when job is unspecified' do context 'when job is invalid' do
let(:config) { { rspec: nil } } let(:config) { { rspec: nil } }
it 'reports error' do it 'reports error' do
expect(entry.errors).to include "rspec config can't be blank" expect(entry.errors).to include "jobs config should contain valid jobs"
end end
end end
...@@ -49,6 +80,7 @@ describe Gitlab::Ci::Config::Entry::Jobs do ...@@ -49,6 +80,7 @@ describe Gitlab::Ci::Config::Entry::Jobs do
end end
end end
describe '.compose!' do
context 'when valid job entries composed' do context 'when valid job entries composed' do
before do before do
entry.compose! entry.compose!
...@@ -67,12 +99,14 @@ describe Gitlab::Ci::Config::Entry::Jobs do ...@@ -67,12 +99,14 @@ describe Gitlab::Ci::Config::Entry::Jobs do
script: %w[rspec], script: %w[rspec],
ignore: false, ignore: false,
stage: 'test', stage: 'test',
only: { refs: %w[branches tags] } }, only: { refs: %w[branches tags] },
variables: {} },
spinach: { name: :spinach, spinach: { name: :spinach,
script: %w[spinach], script: %w[spinach],
ignore: false, ignore: false,
stage: 'test', stage: 'test',
only: { refs: %w[branches tags] } }) only: { refs: %w[branches tags] },
variables: {} })
end end
end end
...@@ -92,4 +126,5 @@ describe Gitlab::Ci::Config::Entry::Jobs do ...@@ -92,4 +126,5 @@ describe Gitlab::Ci::Config::Entry::Jobs do
end end
end end
end end
end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Global do describe Gitlab::Ci::Config::Entry::Root do
let(:global) { described_class.new(hash) } let(:root) { described_class.new(hash) }
describe '.nodes' do describe '.nodes' do
it 'returns a hash' do it 'returns a hash' do
...@@ -12,17 +12,18 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -12,17 +12,18 @@ describe Gitlab::Ci::Config::Entry::Global do
it 'contains the expected node names' do it 'contains the expected node names' do
expect(described_class.nodes.keys) expect(described_class.nodes.keys)
.to match_array(%i[before_script image services .to match_array(%i[before_script image services
after_script variables stages after_script variables cache
types cache include]) stages types include default])
end end
end end
end end
context 'when configuration is valid' do context 'when configuration is valid' do
context 'when some entries defined' do context 'when top-level entries are defined' do
let(:hash) do let(:hash) do
{ before_script: %w(ls pwd), { before_script: %w(ls pwd),
image: 'ruby:2.2', image: 'ruby:2.2',
default: {},
services: ['postgres:9.1', 'mysql:5.5'], services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'value' }, variables: { VAR: 'value' },
after_script: ['make clean'], after_script: ['make clean'],
...@@ -34,97 +35,53 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -34,97 +35,53 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#compose!' do describe '#compose!' do
before do before do
global.compose! root.compose!
end end
it 'creates nodes hash' do it 'creates nodes hash' do
expect(global.descendants).to be_an Array expect(root.descendants).to be_an Array
end end
it 'creates node object for each entry' do it 'creates node object for each entry' do
expect(global.descendants.count).to eq 9 expect(root.descendants.count).to eq 10
end end
it 'creates node object using valid class' do it 'creates node object using valid class' do
expect(global.descendants.first) expect(root.descendants.first)
.to be_an_instance_of Gitlab::Ci::Config::Entry::Script .to be_an_instance_of Gitlab::Ci::Config::Entry::Default
expect(global.descendants.second) expect(root.descendants.second)
.to be_an_instance_of Gitlab::Ci::Config::Entry::Image .to be_an_instance_of Gitlab::Config::Entry::Unspecified
end end
it 'sets correct description for nodes' do it 'sets correct description for nodes' do
expect(global.descendants.first.description) expect(root.descendants.first.description)
.to eq 'Script that will be executed before each job.' .to eq 'Default configuration for all jobs.'
expect(global.descendants.second.description) expect(root.descendants.second.description)
.to eq 'Docker image that will be used to execute jobs.' .to eq 'List of external YAML files to include.'
end end
describe '#leaf?' do describe '#leaf?' do
it 'is not leaf' do it 'is not leaf' do
expect(global).not_to be_leaf expect(root).not_to be_leaf
end
end
end
context 'when not composed' do
describe '#before_script_value' do
it 'returns nil' do
expect(global.before_script_value).to be nil
end
end
describe '#leaf?' do
it 'is leaf' do
expect(global).to be_leaf
end end
end end
end end
context 'when composed' do context 'when composed' do
before do before do
global.compose! root.compose!
end end
describe '#errors' do describe '#errors' do
it 'has no errors' do it 'has no errors' do
expect(global.errors).to be_empty expect(root.errors).to be_empty
end
end
describe '#before_script_value' do
it 'returns correct script' do
expect(global.before_script_value).to eq %w(ls pwd)
end
end
describe '#image_value' do
it 'returns valid image' do
expect(global.image_value).to eq(name: 'ruby:2.2')
end
end
describe '#services_value' do
it 'returns array of services' do
expect(global.services_value).to eq [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }]
end
end
describe '#after_script_value' do
it 'returns after script' do
expect(global.after_script_value).to eq ['make clean']
end
end
describe '#variables_value' do
it 'returns variables' do
expect(global.variables_value).to eq('VAR' => 'value')
end end
end end
describe '#stages_value' do describe '#stages_value' do
context 'when stages key defined' do context 'when stages key defined' do
it 'returns array of stages' do it 'returns array of stages' do
expect(global.stages_value).to eq %w[build pages] expect(root.stages_value).to eq %w[build pages]
end end
end end
...@@ -135,21 +92,14 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -135,21 +92,14 @@ describe Gitlab::Ci::Config::Entry::Global do
end end
it 'returns array of types as stages' do it 'returns array of types as stages' do
expect(global.stages_value).to eq %w[test deploy] expect(root.stages_value).to eq %w[test deploy]
end
end
end end
describe '#cache_value' do
it 'returns cache configuration' do
expect(global.cache_value)
.to eq(key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push')
end end
end end
describe '#jobs_value' do describe '#jobs_value' do
it 'returns jobs configuration' do it 'returns jobs configuration' do
expect(global.jobs_value).to eq( expect(root.jobs_value).to eq(
rspec: { name: :rspec, rspec: { name: :rspec,
script: %w[rspec ls], script: %w[rspec ls],
before_script: %w(ls pwd), before_script: %w(ls pwd),
...@@ -157,7 +107,7 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -157,7 +107,7 @@ describe Gitlab::Ci::Config::Entry::Global do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test', stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
variables: { 'VAR' => 'value' }, variables: {},
ignore: false, ignore: false,
after_script: ['make clean'], after_script: ['make clean'],
only: { refs: %w[branches tags] } }, only: { refs: %w[branches tags] } },
...@@ -178,9 +128,66 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -178,9 +128,66 @@ describe Gitlab::Ci::Config::Entry::Global do
end end
end end
context 'when a mix of top-level and default entries is used' do
let(:hash) do
{ before_script: %w(ls pwd),
after_script: ['make clean'],
default: {
image: 'ruby:2.1',
services: ['postgres:9.1', 'mysql:5.5']
},
variables: { VAR: 'value' },
stages: %w(build pages),
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: { VAR: 'AA' }, script: 'spinach' } }
end
context 'when composed' do
before do
root.compose!
end
describe '#errors' do
it 'has no errors' do
expect(root.errors).to be_empty
end
end
describe '#jobs_value' do
it 'returns jobs configuration' do
expect(root.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
before_script: %w(ls pwd),
image: { name: 'ruby:2.1' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
variables: {},
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] } },
spinach: { name: :spinach,
before_script: [],
script: %w[spinach],
image: { name: 'ruby:2.1' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
variables: { 'VAR' => 'AA' },
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] } }
)
end
end
end
end
context 'when most of entires not defined' do context 'when most of entires not defined' do
before do before do
global.compose! root.compose!
end end
let(:hash) do let(:hash) do
...@@ -189,30 +196,55 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -189,30 +196,55 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#nodes' do describe '#nodes' do
it 'instantizes all nodes' do it 'instantizes all nodes' do
expect(global.descendants.count).to eq 9 expect(root.descendants.count).to eq 10
end end
it 'contains unspecified nodes' do it 'contains unspecified nodes' do
expect(global.descendants.first) expect(root.descendants.first)
.not_to be_specified .not_to be_specified
end end
end end
describe '#variables_value' do describe '#variables_value' do
it 'returns default value for variables' do it 'returns root value for variables' do
expect(global.variables_value).to eq({}) expect(root.variables_value).to eq({})
end end
end end
describe '#stages_value' do describe '#stages_value' do
it 'returns an array of default stages' do it 'returns an array of root stages' do
expect(global.stages_value).to eq %w[build test deploy] expect(root.stages_value).to eq %w[build test deploy]
end end
end end
describe '#cache_value' do describe '#cache_value' do
it 'returns correct cache definition' do it 'returns correct cache definition' do
expect(global.cache_value).to eq(key: 'a', policy: 'pull-push') expect(root.cache_value).to eq(key: 'a', policy: 'pull-push')
end
end
end
context 'when variables resembles script-type job' do
before do
root.compose!
end
let(:hash) do
{
variables: { script: "ENV_VALUE" },
rspec: { script: "echo Hello World" }
}
end
describe '#variables_value' do
it 'returns root value for variables' do
expect(root.variables_value).to eq("script" => "ENV_VALUE")
end
end
describe '#jobs_value' do
it 'returns one job' do
expect(root.jobs_value.keys).to contain_exactly(:rspec)
end end
end end
end end
...@@ -225,7 +257,7 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -225,7 +257,7 @@ describe Gitlab::Ci::Config::Entry::Global do
# #
context 'when entires specified but not defined' do context 'when entires specified but not defined' do
before do before do
global.compose! root.compose!
end end
let(:hash) do let(:hash) do
...@@ -233,8 +265,8 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -233,8 +265,8 @@ describe Gitlab::Ci::Config::Entry::Global do
end end
describe '#variables_value' do describe '#variables_value' do
it 'undefined entry returns a default value' do it 'undefined entry returns a root value' do
expect(global.variables_value).to eq({}) expect(root.variables_value).to eq({})
end end
end end
end end
...@@ -242,7 +274,7 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -242,7 +274,7 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when configuration is not valid' do context 'when configuration is not valid' do
before do before do
global.compose! root.compose!
end end
context 'when before script is not an array' do context 'when before script is not an array' do
...@@ -252,22 +284,16 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -252,22 +284,16 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#valid?' do describe '#valid?' do
it 'is not valid' do it 'is not valid' do
expect(global).not_to be_valid expect(root).not_to be_valid
end end
end end
describe '#errors' do describe '#errors' do
it 'reports errors from child nodes' do it 'reports errors from child nodes' do
expect(global.errors) expect(root.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
describe '#before_script_value' do
it 'returns nil' do
expect(global.before_script_value).to be_nil
end
end
end end
context 'when job does not have commands' do context 'when job does not have commands' do
...@@ -277,8 +303,8 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -277,8 +303,8 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#errors' do describe '#errors' do
it 'reports errors about missing script' do it 'reports errors about missing script' do
expect(global.errors) expect(root.errors)
.to include "jobs:rspec script can't be blank" .to include "root config contains unknown keys: rspec"
end end
end end
end end
...@@ -289,26 +315,26 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -289,26 +315,26 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#valid?' do describe '#valid?' do
it 'is not valid' do it 'is not valid' do
expect(global).not_to be_valid expect(root).not_to be_valid
end end
end end
describe '#errors' do describe '#errors' do
it 'returns error about invalid type' do it 'returns error about invalid type' do
expect(global.errors.first).to match /should be a hash/ expect(root.errors.first).to match /should be a hash/
end end
end end
end end
describe '#specified?' do describe '#specified?' do
it 'is concrete entry that is defined' do it 'is concrete entry that is defined' do
expect(global.specified?).to be true expect(root.specified?).to be true
end end
end end
describe '#[]' do describe '#[]' do
before do before do
global.compose! root.compose!
end end
let(:hash) do let(:hash) do
...@@ -317,15 +343,15 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -317,15 +343,15 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when entry exists' do context 'when entry exists' do
it 'returns correct entry' do it 'returns correct entry' do
expect(global[:cache]) expect(root[:cache])
.to be_an_instance_of Gitlab::Ci::Config::Entry::Cache .to be_an_instance_of Gitlab::Ci::Config::Entry::Cache
expect(global[:jobs][:rspec][:script].value).to eq ['ls'] expect(root[:jobs][:rspec][:script].value).to eq ['ls']
end end
end end
context 'when entry does not exist' do context 'when entry does not exist' do
it 'always return unspecified node' do it 'always return unspecified node' do
expect(global[:some][:unknown][:node]) expect(root[:some][:unknown][:node])
.not_to be_specified .not_to be_specified
end end
end end
......
...@@ -77,7 +77,14 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do ...@@ -77,7 +77,14 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
end end
context 'when pipeline contains configuration validation errors' do context 'when pipeline contains configuration validation errors' do
let(:config) { { rspec: {} } } let(:config) do
{
rspec: {
before_script: 10,
script: 'ls -al'
}
}
end
let(:pipeline) do let(:pipeline) do
build(:ci_pipeline, project: project, config: config) build(:ci_pipeline, project: project, config: config)
...@@ -85,7 +92,7 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do ...@@ -85,7 +92,7 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
it 'appends configuration validation errors to pipeline errors' do it 'appends configuration validation errors to pipeline errors' do
expect(pipeline.errors.to_a) expect(pipeline.errors.to_a)
.to include "jobs:rspec config can't be blank" .to include "jobs:rspec:before_script config should be an array of strings"
end end
it 'breaks the chain' do it 'breaks the chain' do
......
...@@ -265,6 +265,19 @@ module Gitlab ...@@ -265,6 +265,19 @@ module Gitlab
end end
end end
context "in default context" do
let(:config) do
{
default: { before_script: ["global script"] },
test: { script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:options][:before_script]).to eq(["global script"])
end
end
context "overwritten in local context" do context "overwritten in local context" do
let(:config) do let(:config) do
{ {
...@@ -305,6 +318,19 @@ module Gitlab ...@@ -305,6 +318,19 @@ module Gitlab
end end
end end
context "in default context" do
let(:config) do
{
after_script: ["after_script"],
test: { script: ["script"] }
}
end
it "return after_script in options" do
expect(subject[:options][:after_script]).to eq(["after_script"])
end
end
context "overwritten in local context" do context "overwritten in local context" do
let(:config) do let(:config) do
{ {
...@@ -774,6 +800,28 @@ module Gitlab ...@@ -774,6 +800,28 @@ module Gitlab
) )
end end
it "returns cache when defined in default context" do
config = YAML.dump(
{
default: {
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }
},
rspec: {
script: "rspec"
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
policy: 'pull-push'
)
end
it "returns cache when defined in a job" do it "returns cache when defined in a job" do
config = YAML.dump({ config = YAML.dump({
rspec: { rspec: {
...@@ -1271,7 +1319,7 @@ module Gitlab ...@@ -1271,7 +1319,7 @@ module Gitlab
config = YAML.dump({ extra: "bundle update" }) config = YAML.dump({ extra: "bundle update" })
expect do expect do
Gitlab::Ci::YamlProcessor.new(config) Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "root config contains unknown keys: extra")
end end
it "returns errors if services configuration is not correct" do it "returns errors if services configuration is not correct" do
......
...@@ -34,18 +34,25 @@ describe Gitlab::Config::Entry::Configurable do ...@@ -34,18 +34,25 @@ describe Gitlab::Config::Entry::Configurable do
before do before do
entry.class_exec(entry_class) do |entry_class| entry.class_exec(entry_class) do |entry_class|
entry :object, entry_class, description: 'test object' entry :object, entry_class,
description: 'test object',
inherit: true,
reserved: true
end end
end end
describe '.nodes' do describe '.nodes' do
it 'has valid nodes' do it 'has valid nodes' do
expect(entry.nodes).to include :object expect(entry.nodes).to include(:object)
end end
it 'creates a node factory' do it 'creates a node factory' do
expect(entry.nodes[:object]) factory = entry.nodes[:object]
.to be_an_instance_of Gitlab::Config::Entry::Factory
expect(factory).to be_an_instance_of(Gitlab::Config::Entry::Factory)
expect(factory.description).to eq('test object')
expect(factory.inheritable?).to eq(true)
expect(factory.reserved?).to eq(true)
end end
it 'returns a duplicated factory object' do it 'returns a duplicated factory object' do
...@@ -55,5 +62,17 @@ describe Gitlab::Config::Entry::Configurable do ...@@ -55,5 +62,17 @@ describe Gitlab::Config::Entry::Configurable do
expect(first_factory).not_to be_equal(second_factory) expect(first_factory).not_to be_equal(second_factory)
end end
end end
describe '.reserved_node_names' do
before do
entry.class_exec(entry_class) do |entry_class|
entry :not_reserved, entry_class
end
end
it 'returns all nodes with reserved: true' do
expect(entry.reserved_node_names).to contain_exactly(:object)
end
end
end end
end end
...@@ -23,17 +23,36 @@ describe Gitlab::Config::Entry::Factory do ...@@ -23,17 +23,36 @@ describe Gitlab::Config::Entry::Factory do
end end
context 'when setting description' do context 'when setting description' do
it 'creates entry with description' do before do
entry = factory factory
.value(%w(ls pwd)) .value(%w(ls pwd))
.with(description: 'test description') .with(description: 'test description')
.create! end
it 'configures description' do
expect(factory.description).to eq 'test description'
end
it 'creates entry with description' do
entry = factory.create!
expect(entry.value).to eq %w(ls pwd) expect(entry.value).to eq %w(ls pwd)
expect(entry.description).to eq 'test description' expect(entry.description).to eq 'test description'
end end
end end
context 'when setting inherit' do
before do
factory
.value(%w(ls pwd))
.with(inherit: true)
end
it 'makes object inheritable' do
expect(factory.inheritable?).to eq true
end
end
context 'when setting key' do context 'when setting key' do
it 'creates entry with custom key' do it 'creates entry with custom key' do
entry = factory entry = factory
......
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