mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-09 04:36:11 +10:00
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
This commit is contained in:
@@ -2,7 +2,7 @@ module Ci
|
||||
class GitlabCiYamlProcessor
|
||||
class ValidationError < StandardError; end
|
||||
|
||||
include Gitlab::Ci::Config::Node::ValidationHelpers
|
||||
include Gitlab::Ci::Config::Node::LegacyValidationHelpers
|
||||
|
||||
DEFAULT_STAGES = %w(build test deploy)
|
||||
DEFAULT_STAGE = 'test'
|
||||
|
||||
@@ -4,8 +4,6 @@ module Gitlab
|
||||
# Base GitLab CI Configuration facade
|
||||
#
|
||||
class Config
|
||||
delegate :valid?, :errors, to: :@global
|
||||
|
||||
##
|
||||
# Temporary delegations that should be removed after refactoring
|
||||
#
|
||||
@@ -18,6 +16,14 @@ module Gitlab
|
||||
@global.process!
|
||||
end
|
||||
|
||||
def valid?
|
||||
@global.valid?
|
||||
end
|
||||
|
||||
def errors
|
||||
@global.errors
|
||||
end
|
||||
|
||||
def to_hash
|
||||
@config
|
||||
end
|
||||
|
||||
@@ -15,27 +15,24 @@ module Gitlab
|
||||
#
|
||||
module Configurable
|
||||
extend ActiveSupport::Concern
|
||||
include Validatable
|
||||
|
||||
def allowed_nodes
|
||||
self.class.allowed_nodes || {}
|
||||
included do
|
||||
validations do
|
||||
validates :config, hash: true
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prevalidate!
|
||||
unless @value.is_a?(Hash)
|
||||
@errors << 'should be a configuration entry with hash value'
|
||||
end
|
||||
end
|
||||
|
||||
def create_node(key, factory)
|
||||
factory.with(value: @value[key])
|
||||
factory.nullify! unless @value.has_key?(key)
|
||||
factory.with(value: @config[key], key: key)
|
||||
factory.nullify! unless @config.has_key?(key)
|
||||
factory.create!
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def allowed_nodes
|
||||
def nodes
|
||||
Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }]
|
||||
end
|
||||
|
||||
@@ -47,7 +44,6 @@ module Gitlab
|
||||
|
||||
define_method(symbol) do
|
||||
raise Entry::InvalidError unless valid?
|
||||
|
||||
@nodes[symbol].try(:value)
|
||||
end
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ module Gitlab
|
||||
class Entry
|
||||
class InvalidError < StandardError; end
|
||||
|
||||
attr_accessor :description
|
||||
attr_reader :config
|
||||
attr_accessor :key, :description
|
||||
|
||||
def initialize(value)
|
||||
@value = value
|
||||
def initialize(config)
|
||||
@config = config
|
||||
@nodes = {}
|
||||
@errors = []
|
||||
|
||||
prevalidate!
|
||||
@validator = self.class.validator.new(self)
|
||||
@validator.validate
|
||||
end
|
||||
|
||||
def process!
|
||||
@@ -23,50 +23,54 @@ module Gitlab
|
||||
return unless valid?
|
||||
|
||||
compose!
|
||||
|
||||
nodes.each(&:process!)
|
||||
nodes.each(&:validate!)
|
||||
process_nodes!
|
||||
end
|
||||
|
||||
def nodes
|
||||
@nodes.values
|
||||
end
|
||||
|
||||
def leaf?
|
||||
self.class.nodes.none?
|
||||
end
|
||||
|
||||
def key
|
||||
@key || self.class.name.demodulize.underscore
|
||||
end
|
||||
|
||||
def valid?
|
||||
errors.none?
|
||||
end
|
||||
|
||||
def leaf?
|
||||
allowed_nodes.none?
|
||||
end
|
||||
|
||||
def errors
|
||||
@errors + nodes.map(&:errors).flatten
|
||||
end
|
||||
|
||||
def allowed_nodes
|
||||
{}
|
||||
end
|
||||
|
||||
def validate!
|
||||
raise NotImplementedError
|
||||
@validator.full_errors +
|
||||
nodes.map(&:errors).flatten
|
||||
end
|
||||
|
||||
def value
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prevalidate!
|
||||
def self.nodes
|
||||
{}
|
||||
end
|
||||
|
||||
def self.validator
|
||||
Validator
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compose!
|
||||
allowed_nodes.each do |key, essence|
|
||||
self.class.nodes.each do |key, essence|
|
||||
@nodes[key] = create_node(key, essence)
|
||||
end
|
||||
end
|
||||
|
||||
def process_nodes!
|
||||
nodes.each(&:process!)
|
||||
end
|
||||
|
||||
def create_node(key, essence)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@ module Gitlab
|
||||
|
||||
@entry_class.new(@attributes[:value]).tap do |entry|
|
||||
entry.description = @attributes[:description]
|
||||
entry.key = @attributes[:key]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Node
|
||||
module ValidationHelpers
|
||||
module LegacyValidationHelpers
|
||||
private
|
||||
|
||||
def validate_duration(value)
|
||||
@@ -11,16 +11,14 @@ module Gitlab
|
||||
# implementation in Runner.
|
||||
#
|
||||
class Script < Entry
|
||||
include ValidationHelpers
|
||||
include Validatable
|
||||
|
||||
def value
|
||||
@value.join("\n")
|
||||
validations do
|
||||
validates :config, array_of_strings: true
|
||||
end
|
||||
|
||||
def validate!
|
||||
unless validate_array_of_strings(@value)
|
||||
@errors << 'before_script should be an array of strings'
|
||||
end
|
||||
def value
|
||||
@config.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
|
||||
expect do
|
||||
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
|
||||
|
||||
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
|
||||
node.include(described_class)
|
||||
end
|
||||
|
||||
describe 'allowed nodes' do
|
||||
describe 'configured nodes' do
|
||||
before do
|
||||
node.class_eval do
|
||||
allow_node :object, Object, description: 'test object'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#allowed_nodes' do
|
||||
it 'has valid allowed nodes' do
|
||||
expect(node.allowed_nodes).to include :object
|
||||
describe '.nodes' do
|
||||
it 'has valid nodes' do
|
||||
expect(node.nodes).to include :object
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
it 'returns a duplicated factory object' do
|
||||
first_factory = node.allowed_nodes[:object]
|
||||
second_factory = node.allowed_nodes[:object]
|
||||
first_factory = node.nodes[:object]
|
||||
second_factory = node.nodes[:object]
|
||||
|
||||
expect(first_factory).not_to be_equal(second_factory)
|
||||
end
|
||||
|
||||
@@ -25,6 +25,16 @@ describe Gitlab::Ci::Config::Node::Factory do
|
||||
expect(entry.description).to eq 'test description'
|
||||
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
|
||||
|
||||
context 'when not setting value' do
|
||||
|
||||
@@ -3,13 +3,19 @@ require 'spec_helper'
|
||||
describe Gitlab::Ci::Config::Node::Global do
|
||||
let(:global) { described_class.new(hash) }
|
||||
|
||||
describe '#allowed_nodes' do
|
||||
describe '.nodes' 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
|
||||
|
||||
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
|
||||
|
||||
@@ -79,7 +85,7 @@ describe Gitlab::Ci::Config::Node::Global do
|
||||
describe '#errors' do
|
||||
it 'reports errors from child nodes' do
|
||||
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
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Ci::Config::Node::Script do
|
||||
let(:entry) { described_class.new(value) }
|
||||
let(:entry) { described_class.new(config) }
|
||||
|
||||
describe '#validate!' do
|
||||
before { entry.validate! }
|
||||
describe '#process!' do
|
||||
before { entry.process! }
|
||||
|
||||
context 'when entry value is correct' do
|
||||
let(:value) { ['ls', 'pwd'] }
|
||||
context 'when entry config value is correct' do
|
||||
let(:config) { ['ls', 'pwd'] }
|
||||
|
||||
describe '#value' do
|
||||
it 'returns concatenated command' do
|
||||
@@ -29,12 +29,12 @@ describe Gitlab::Ci::Config::Node::Script do
|
||||
end
|
||||
|
||||
context 'when entry value is not correct' do
|
||||
let(:value) { 'ls' }
|
||||
let(:config) { 'ls' }
|
||||
|
||||
describe '#errors' do
|
||||
it 'saves errors' do
|
||||
expect(entry.errors)
|
||||
.to include /should be an array of strings/
|
||||
.to include 'Script config should be an array of strings'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
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
|
||||
expect(config.errors).not_to be_empty
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user