Commit a9dbd394 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'refactor/ci-config-add-entry-error' into 'master'

Improve validations and error handling in new CI config entries

## What does this MR do?

This MR improves validation in new CI config.

## Why was this MR needed?

With that it will be easier to handle errors during validation and post-processing.

## What are the relevant issue numbers?

This is a continuation of #15060

See merge request !4560
parents 030db5c3 9510d31b
...@@ -2,7 +2,7 @@ module Ci ...@@ -2,7 +2,7 @@ module Ci
class GitlabCiYamlProcessor class GitlabCiYamlProcessor
class ValidationError < StandardError; end class ValidationError < StandardError; end
include Gitlab::Ci::Config::Node::ValidationHelpers include Gitlab::Ci::Config::Node::LegacyValidationHelpers
DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test' DEFAULT_STAGE = 'test'
......
...@@ -4,8 +4,6 @@ module Gitlab ...@@ -4,8 +4,6 @@ module Gitlab
# Base GitLab CI Configuration facade # Base GitLab CI Configuration facade
# #
class Config class Config
delegate :valid?, :errors, to: :@global
## ##
# Temporary delegations that should be removed after refactoring # Temporary delegations that should be removed after refactoring
# #
...@@ -18,6 +16,14 @@ module Gitlab ...@@ -18,6 +16,14 @@ module Gitlab
@global.process! @global.process!
end end
def valid?
@global.valid?
end
def errors
@global.errors
end
def to_hash def to_hash
@config @config
end end
......
...@@ -15,27 +15,24 @@ module Gitlab ...@@ -15,27 +15,24 @@ module Gitlab
# #
module Configurable module Configurable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Validatable
def allowed_nodes included do
self.class.allowed_nodes || {} validations do
validates :config, hash: true
end
end end
private private
def prevalidate!
unless @value.is_a?(Hash)
@errors << 'should be a configuration entry with hash value'
end
end
def create_node(key, factory) def create_node(key, factory)
factory.with(value: @value[key]) factory.with(value: @config[key], key: key)
factory.nullify! unless @value.has_key?(key) factory.nullify! unless @config.has_key?(key)
factory.create! factory.create!
end end
class_methods do class_methods do
def allowed_nodes def nodes
Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }] Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }]
end end
...@@ -47,7 +44,6 @@ module Gitlab ...@@ -47,7 +44,6 @@ module Gitlab
define_method(symbol) do define_method(symbol) do
raise Entry::InvalidError unless valid? raise Entry::InvalidError unless valid?
@nodes[symbol].try(:value) @nodes[symbol].try(:value)
end end
......
...@@ -8,14 +8,14 @@ module Gitlab ...@@ -8,14 +8,14 @@ module Gitlab
class Entry class Entry
class InvalidError < StandardError; end class InvalidError < StandardError; end
attr_accessor :description attr_reader :config
attr_accessor :key, :description
def initialize(value) def initialize(config)
@value = value @config = config
@nodes = {} @nodes = {}
@errors = [] @validator = self.class.validator.new(self)
@validator.validate
prevalidate!
end end
def process! def process!
...@@ -23,50 +23,54 @@ module Gitlab ...@@ -23,50 +23,54 @@ module Gitlab
return unless valid? return unless valid?
compose! compose!
process_nodes!
nodes.each(&:process!)
nodes.each(&:validate!)
end end
def nodes def nodes
@nodes.values @nodes.values
end end
def valid?
errors.none?
end
def leaf? def leaf?
allowed_nodes.none? self.class.nodes.none?
end end
def errors def key
@errors + nodes.map(&:errors).flatten @key || self.class.name.demodulize.underscore
end end
def allowed_nodes def valid?
{} errors.none?
end end
def validate! def errors
raise NotImplementedError @validator.full_errors +
nodes.map(&:errors).flatten
end end
def value def value
raise NotImplementedError raise NotImplementedError
end end
private def self.nodes
{}
end
def prevalidate! def self.validator
Validator
end end
private
def compose! def compose!
allowed_nodes.each do |key, essence| self.class.nodes.each do |key, essence|
@nodes[key] = create_node(key, essence) @nodes[key] = create_node(key, essence)
end end
end end
def process_nodes!
nodes.each(&:process!)
end
def create_node(key, essence) def create_node(key, essence)
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -30,6 +30,7 @@ module Gitlab ...@@ -30,6 +30,7 @@ module Gitlab
@entry_class.new(@attributes[:value]).tap do |entry| @entry_class.new(@attributes[:value]).tap do |entry|
entry.description = @attributes[:description] entry.description = @attributes[:description]
entry.key = @attributes[:key]
end end
end end
end end
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module Ci module Ci
class Config class Config
module Node module Node
module ValidationHelpers module LegacyValidationHelpers
private private
def validate_duration(value) def validate_duration(value)
......
...@@ -11,16 +11,14 @@ module Gitlab ...@@ -11,16 +11,14 @@ module Gitlab
# implementation in Runner. # implementation in Runner.
# #
class Script < Entry class Script < Entry
include ValidationHelpers include Validatable
def value validations do
@value.join("\n") validates :config, array_of_strings: true
end end
def validate! def value
unless validate_array_of_strings(@value) @config.join("\n")
@errors << 'before_script should be an array of strings'
end
end end
end end
end end
......
module Gitlab
module Ci
class Config
module Node
module Validatable
extend ActiveSupport::Concern
class_methods do
def validator
validator = Class.new(Node::Validator)
if defined?(@validations)
@validations.each { |rules| validator.class_eval(&rules) }
end
validator
end
private
def validations(&block)
(@validations ||= []).append(block)
end
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
class Validator < SimpleDelegator
include ActiveModel::Validations
include Node::Validators
def initialize(node)
super(node)
@node = node
end
def full_errors
errors.full_messages.map do |error|
"#{@node.key} #{error}".humanize
end
end
def self.name
'Validator'
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
module Validators
class ArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
def validate_each(record, attribute, value)
unless validate_array_of_strings(value)
record.errors.add(attribute, 'should be an array of strings')
end
end
end
class HashValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.is_a?(Hash)
record.errors.add(attribute, 'should be a configuration entry hash')
end
end
end
end
end
end
end
end
...@@ -951,7 +951,7 @@ EOT ...@@ -951,7 +951,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 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
......
...@@ -7,26 +7,26 @@ describe Gitlab::Ci::Config::Node::Configurable do ...@@ -7,26 +7,26 @@ describe Gitlab::Ci::Config::Node::Configurable do
node.include(described_class) node.include(described_class)
end end
describe 'allowed 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' allow_node :object, Object, description: 'test object'
end end
end end
describe '#allowed_nodes' do describe '.nodes' do
it 'has valid allowed nodes' do it 'has valid nodes' do
expect(node.allowed_nodes).to include :object expect(node.nodes).to include :object
end end
it 'creates a node factory' do it 'creates a node factory' do
expect(node.allowed_nodes[:object]) expect(node.nodes[:object])
.to be_an_instance_of Gitlab::Ci::Config::Node::Factory .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
end end
it 'returns a duplicated factory object' do it 'returns a duplicated factory object' do
first_factory = node.allowed_nodes[:object] first_factory = node.nodes[:object]
second_factory = node.allowed_nodes[:object] second_factory = node.nodes[:object]
expect(first_factory).not_to be_equal(second_factory) expect(first_factory).not_to be_equal(second_factory)
end end
......
...@@ -25,6 +25,16 @@ describe Gitlab::Ci::Config::Node::Factory do ...@@ -25,6 +25,16 @@ describe Gitlab::Ci::Config::Node::Factory do
expect(entry.description).to eq 'test description' expect(entry.description).to eq 'test description'
end end
end end
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
.with(value: ['ls', 'pwd'], key: 'test key')
.create!
expect(entry.key).to eq 'test key'
end
end
end end
context 'when not setting value' do context 'when not setting value' do
......
...@@ -3,13 +3,19 @@ require 'spec_helper' ...@@ -3,13 +3,19 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Global do describe Gitlab::Ci::Config::Node::Global do
let(:global) { described_class.new(hash) } let(:global) { described_class.new(hash) }
describe '#allowed_nodes' do describe '.nodes' do
it 'can contain global config keys' do it 'can contain global config keys' do
expect(global.allowed_nodes).to include :before_script expect(described_class.nodes).to include :before_script
end end
it 'returns a hash' do it 'returns a hash' do
expect(global.allowed_nodes).to be_a Hash expect(described_class.nodes).to be_a Hash
end
end
describe '#key' do
it 'returns underscored class name' do
expect(global.key).to eq 'global'
end end
end end
...@@ -79,7 +85,7 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -79,7 +85,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 should be an array of strings' .to include 'Before script config should be an array of strings'
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Node::Script do describe Gitlab::Ci::Config::Node::Script do
let(:entry) { described_class.new(value) } let(:entry) { described_class.new(config) }
describe '#validate!' do describe '#process!' do
before { entry.validate! } before { entry.process! }
context 'when entry value is correct' do context 'when entry config value is correct' do
let(:value) { ['ls', 'pwd'] } let(:config) { ['ls', 'pwd'] }
describe '#value' do describe '#value' do
it 'returns concatenated command' do it 'returns concatenated command' do
...@@ -29,12 +29,12 @@ describe Gitlab::Ci::Config::Node::Script do ...@@ -29,12 +29,12 @@ describe Gitlab::Ci::Config::Node::Script do
end end
context 'when entry value is not correct' do context 'when entry value is not correct' do
let(:value) { 'ls' } let(:config) { 'ls' }
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors)
.to include /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::Validatable do
let(:node) { Class.new }
before do
node.include(described_class)
end
describe '.validator' do
before do
node.class_eval do
attr_accessor :test_attribute
validations do
validates :test_attribute, presence: true
end
end
end
it 'returns validator' do
expect(node.validator.superclass)
.to be Gitlab::Ci::Config::Node::Validator
end
context 'when validating node instance' do
let(:node_instance) { node.new }
context 'when attribute is valid' do
before do
node_instance.test_attribute = 'valid'
end
it 'instance of validator is valid' do
expect(node.validator.new(node_instance)).to be_valid
end
end
context 'when attribute is not valid' do
before do
node_instance.test_attribute = nil
end
it 'instance of validator is invalid' do
expect(node.validator.new(node_instance)).to be_invalid
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Validator do
let(:validator) { Class.new(described_class) }
let(:validator_instance) { validator.new(node) }
let(:node) { spy('node') }
shared_examples 'delegated validator' do
context 'when node is valid' do
before do
allow(node).to receive(:test_attribute).and_return('valid value')
end
it 'validates attribute in node' do
expect(node).to receive(:test_attribute)
expect(validator_instance).to be_valid
end
it 'returns no errors' do
validator_instance.validate
expect(validator_instance.full_errors).to be_empty
end
end
context 'when node is invalid' do
before do
allow(node).to receive(:test_attribute).and_return(nil)
end
it 'validates attribute in node' do
expect(node).to receive(:test_attribute)
expect(validator_instance).to be_invalid
end
it 'returns errors' do
validator_instance.validate
expect(validator_instance.full_errors).not_to be_empty
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
...@@ -67,6 +67,12 @@ describe Gitlab::Ci::Config do ...@@ -67,6 +67,12 @@ describe Gitlab::Ci::Config do
expect(config.errors).not_to be_empty expect(config.errors).not_to be_empty
end end
end end
describe '#errors' do
it 'returns an array of strings' do
expect(config.errors).to all(be_an_instance_of(String))
end
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