Commit c1cb7907 authored by Kamil Trzciński's avatar Kamil Trzciński Committed by Matija Čupić

Add post compose validation

Use post compose validation:
- ensure that bridge has a valid number of entries
- use simplifiable to construct need type
- use aspect with opt to indicate allowed opts
parent c456c179
...@@ -23,34 +23,26 @@ module EE ...@@ -23,34 +23,26 @@ module EE
validates :name, presence: true validates :name, presence: true
validates :name, type: Symbol validates :name, type: Symbol
validate do with_options allow_nil: true do
unless trigger.present? || needs.present? validates :when,
errors.add(:config, 'should contain either a trigger or a needs:pipeline') inclusion: { in: %w[on_success on_failure always],
end message: 'should be on_success, on_failure or always' }
validates :extends, type: String
end end
validate do validate on: :composed do
next unless needs.present? unless trigger.present? || bridge_needs.present?
errors.add(:config, 'should contain either a trigger or a needs:pipeline')
if [needs].flatten.any? { |need| !(Entry::Need::Bridge.matching?(need) || ::Gitlab::Ci::Config::Entry::Need::Pipeline.matching?(need)) }
errors.add(:needs, 'can only have bridge or pipeline type needs')
end end
end end
validate do validate on: :composed do
next unless needs.present? next unless bridge_needs.present?
if [needs].flatten.count { |need| Entry::Need::Bridge.matching?(need) } > 1 unless bridge_needs.one?
errors.add(:needs, 'can only have one bridge type needs') errors.add(:config, 'should contain exactly one bridge need')
end end
end end
with_options allow_nil: true do
validates :when,
inclusion: { in: %w[on_success on_failure always],
message: 'should be on_success, on_failure or always' }
validates :extends, type: String
end
end end
entry :trigger, ::EE::Gitlab::Ci::Config::Entry::Trigger, entry :trigger, ::EE::Gitlab::Ci::Config::Entry::Trigger,
...@@ -59,7 +51,8 @@ module EE ...@@ -59,7 +51,8 @@ module EE
entry :needs, ::Gitlab::Ci::Config::Entry::Needs, entry :needs, ::Gitlab::Ci::Config::Entry::Needs,
description: 'CI/CD Bridge needs dependency definition.', description: 'CI/CD Bridge needs dependency definition.',
inherit: false inherit: false,
metadata: { allowed_needs: %i[bridge job] }
entry :stage, ::Gitlab::Ci::Config::Entry::Stage, entry :stage, ::Gitlab::Ci::Config::Entry::Stage,
description: 'Pipeline stage this job will be executed into.', description: 'Pipeline stage this job will be executed into.',
...@@ -114,6 +107,10 @@ module EE ...@@ -114,6 +107,10 @@ module EE
def overwrite_entry(deps, key, current_entry) def overwrite_entry(deps, key, current_entry)
deps.default[key] unless current_entry.specified? deps.default[key] unless current_entry.specified?
end end
def bridge_needs
needs_value[:bridge] if needs_value
end
end end
end end
end end
......
...@@ -25,10 +25,6 @@ module EE ...@@ -25,10 +25,6 @@ module EE
validates :pipeline, type: String, presence: true validates :pipeline, type: String, presence: true
end end
def self.matching?(config)
config.is_a?(Hash)
end
def type def type
:bridge :bridge
end end
......
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Config
module Entry
module Needs
extend ActiveSupport::Concern
class_methods do
extend ::Gitlab::Utils::Override
override :all_types
def all_types
super + [Entry::Need::Bridge]
end
end
end
end
end
end
end
end
...@@ -160,7 +160,19 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do ...@@ -160,7 +160,19 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do
end end
end end
context 'when bridge has bridge and pipeline needs' do context 'when bridge has only job needs' do
let(:config) do
{
needs: ['some_job']
}
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
end
context 'when bridge has bridge and job needs' do
let(:config) do let(:config) do
{ {
trigger: 'other-project', trigger: 'other-project',
...@@ -187,7 +199,7 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do ...@@ -187,7 +199,7 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do
describe '#errors' do describe '#errors' do
it 'returns an error about too many bridge needs' do it 'returns an error about too many bridge needs' do
expect(subject.errors).to contain_exactly('bridge needs can only have one bridge type needs') expect(subject.errors).to contain_exactly('bridge config should contain exactly one bridge need')
end end
end end
end end
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Needs do describe ::Gitlab::Ci::Config::Entry::Needs do
subject(:needs) { described_class.new(config) } subject(:needs) { described_class.new(config) }
before do
needs.metadata[:allowed_needs] = %i[job bridge]
end
describe 'validations' do describe 'validations' do
before do before do
needs.compose! needs.compose!
...@@ -27,8 +31,8 @@ describe ::Gitlab::Ci::Config::Entry::Needs do ...@@ -27,8 +31,8 @@ describe ::Gitlab::Ci::Config::Entry::Needs do
describe '#errors' do describe '#errors' do
it 'returns error about incorrect type' do it 'returns error about incorrect type' do
expect(needs.errors) expect(needs.errors).to contain_exactly(
.to contain_exactly('needs need has an unsupported type') 'need has an unsupported type')
end end
end end
end end
...@@ -50,9 +54,16 @@ describe ::Gitlab::Ci::Config::Entry::Needs do ...@@ -50,9 +54,16 @@ describe ::Gitlab::Ci::Config::Entry::Needs do
needs.compose! needs.compose!
end end
it 'is valid' do
expect(needs).to be_valid
end
describe '#value' do describe '#value' do
it 'returns key value' do it 'returns key value' do
expect(needs.value).to eq(pipeline: [{ name: 'first_job_name' }], bridge: [{ pipeline: 'some/project' }]) expect(needs.value).to eq(
job: [{ name: 'first_job_name' }],
bridge: [{ pipeline: 'some/project' }]
)
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
describe 'validations' do
before do
entry.compose!
end
context 'when entry value is not correct' do
context 'when has needs' do
context 'when needs is bridge type' do
let(:config) do
{
script: 'echo',
stage: 'test',
needs: { pipeline: 'some/project' }
}
end
it 'returns error about invalid needs type' do
expect(entry).not_to be_valid
expect(entry.errors).to contain_exactly('needs config uses bridge type(s)')
end
end
end
end
end
end
...@@ -57,14 +57,6 @@ module Gitlab ...@@ -57,14 +57,6 @@ module Gitlab
validates :start_in, duration: { limit: '1 day' }, if: :delayed? validates :start_in, duration: { limit: '1 day' }, if: :delayed?
validates :start_in, absence: true, if: -> { has_rules? || !delayed? } validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
validate do
next unless needs.present?
if [needs].flatten.any? { |need| !Entry::Need::Pipeline.matching?(need) }
errors.add(:needs, 'can only have pipeline type needs')
end
end
validate do validate do
next unless dependencies.present? next unless dependencies.present?
next unless needs.present? next unless needs.present?
...@@ -122,7 +114,8 @@ module Gitlab ...@@ -122,7 +114,8 @@ module Gitlab
inherit: false inherit: false
entry :needs, Entry::Needs, entry :needs, Entry::Needs,
description: 'Needs configuration for this job.' description: 'Needs configuration for this job.',
metadata: { allowed_needs: %i[job] }
entry :variables, Entry::Variables, entry :variables, Entry::Variables,
description: 'Environment variables available for this job.', description: 'Environment variables available for this job.',
......
...@@ -5,9 +5,9 @@ module Gitlab ...@@ -5,9 +5,9 @@ module Gitlab
class Config class Config
module Entry module Entry
class Need < ::Gitlab::Config::Entry::Simplifiable class Need < ::Gitlab::Config::Entry::Simplifiable
strategy :Pipeline, if: -> (config) { config.is_a?(String) } strategy :Job, if: -> (config) { config.is_a?(String) }
class Pipeline < ::Gitlab::Config::Entry::Node class Job < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
...@@ -15,12 +15,8 @@ module Gitlab ...@@ -15,12 +15,8 @@ module Gitlab
validates :config, type: String validates :config, type: String
end end
def self.matching?(config)
config.is_a?(String)
end
def type def type
:pipeline :job
end end
def value def value
...@@ -29,6 +25,15 @@ module Gitlab ...@@ -29,6 +25,15 @@ module Gitlab
end end
class UnknownStrategy < ::Gitlab::Config::Entry::Node class UnknownStrategy < ::Gitlab::Config::Entry::Node
def type
end
def value
end
def errors
["#{location} has an unsupported type"]
end
end end
end end
end end
......
...@@ -15,33 +15,18 @@ module Gitlab ...@@ -15,33 +15,18 @@ module Gitlab
validate do validate do
unless config.is_a?(Hash) || config.is_a?(Array) unless config.is_a?(Hash) || config.is_a?(Array)
errors.add(:config, 'can only be a hash or an array') errors.add(:config, 'can only be a Hash or an Array')
end end
end end
validate do validate on: :composed do
[config].flatten.each do |need| extra_keys = value.keys - opt(:allowed_needs)
if Needs.find_type(need).nil? if extra_keys.any?
errors.add(:need, 'has an unsupported type') errors.add(:config, "uses #{extra_keys.join(', ')} type(s)")
end
end end
end end
end end
TYPES = [Entry::Need::Pipeline].freeze
private_constant :TYPES
def self.all_types
TYPES
end
def self.find_type(config)
self.all_types.find do |type|
type.matching?(config)
end
end
def compose!(deps = nil) def compose!(deps = nil)
super(deps) do super(deps) do
[@config].flatten.each_with_index do |need, index| [@config].flatten.each_with_index do |need, index|
...@@ -58,7 +43,8 @@ module Gitlab ...@@ -58,7 +43,8 @@ module Gitlab
end end
def value def value
@entries.values.group_by(&:type).transform_values do |values| values = @entries.values.select(&:type)
values.group_by(&:type).transform_values do |values|
values.map(&:value) values.map(&:value)
end end
end end
...@@ -67,5 +53,3 @@ module Gitlab ...@@ -67,5 +53,3 @@ module Gitlab
end end
end end
end end
::Gitlab::Ci::Config::Entry::Needs.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Needs')
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
environment: job[:environment_name], environment: job[:environment_name],
coverage_regex: job[:coverage], coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name), yaml_variables: yaml_variables(name),
needs_attributes: job.dig(:needs, :pipeline), needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible], interruptible: job[:interruptible],
rules: job[:rules], rules: job[:rules],
options: { options: {
...@@ -159,11 +159,11 @@ module Gitlab ...@@ -159,11 +159,11 @@ module Gitlab
end end
def validate_job_needs!(name, job) def validate_job_needs!(name, job)
return unless job.dig(:needs, :pipeline) return unless job.dig(:needs, :job)
stage_index = @stages.index(job[:stage]) stage_index = @stages.index(job[:stage])
job.dig(:needs, :pipeline).each do |need| job.dig(:needs, :job).each do |need|
need_job_name = need[:name] need_job_name = need[:name]
raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym] raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym]
......
...@@ -29,22 +29,24 @@ module Gitlab ...@@ -29,22 +29,24 @@ module Gitlab
def compose!(deps = nil) def compose!(deps = nil)
return unless valid? return unless valid?
self.class.nodes.each do |key, factory| super do
# If we override the config type validation self.class.nodes.each do |key, factory|
# we can end with different config types like String # If we override the config type validation
next unless config.is_a?(Hash) # we can end with different config types like String
next unless config.is_a?(Hash)
factory factory
.value(config[key]) .value(config[key])
.with(key: key, parent: self) .with(key: key, parent: self)
entries[key] = factory.create! entries[key] = factory.create!
end end
yield if block_given? yield if block_given?
entries.each_value do |entry| entries.each_value do |entry|
entry.compose!(deps) entry.compose!(deps)
end
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -67,12 +69,13 @@ module Gitlab ...@@ -67,12 +69,13 @@ module Gitlab
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil) def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, metadata: {})
factory = ::Gitlab::Config::Entry::Factory.new(entry) factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: description) .with(description: description)
.with(default: default) .with(default: default)
.with(inherit: inherit) .with(inherit: inherit)
.with(reserved: reserved) .with(reserved: reserved)
.metadata(metadata)
(@nodes ||= {}).merge!(key.to_sym => factory) (@nodes ||= {}).merge!(key.to_sym => factory)
end end
......
...@@ -112,6 +112,10 @@ module Gitlab ...@@ -112,6 +112,10 @@ module Gitlab
@aspects ||= [] @aspects ||= []
end end
def self.with_aspect(blk)
self.aspects.append(blk)
end
private private
attr_reader :entries attr_reader :entries
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
attr_reader :subject attr_reader :subject
def initialize(config, **metadata) def initialize(config, **metadata, &blk)
unless self.class.const_defined?(:UnknownStrategy) unless self.class.const_defined?(:UnknownStrategy)
raise ArgumentError, 'UndefinedStrategy not available!' raise ArgumentError, 'UndefinedStrategy not available!'
end end
...@@ -19,9 +19,8 @@ module Gitlab ...@@ -19,9 +19,8 @@ module Gitlab
entry = self.class.entry_class(strategy) entry = self.class.entry_class(strategy)
@subject = entry.new(config, metadata) @subject = entry.new(config, metadata, &blk)
yield(@subject) if block_given?
super(@subject) super(@subject)
end end
......
...@@ -7,14 +7,27 @@ module Gitlab ...@@ -7,14 +7,27 @@ module Gitlab
extend ActiveSupport::Concern extend ActiveSupport::Concern
def self.included(node) def self.included(node)
node.aspects.append -> do node.with_aspect -> do
@validator = self.class.validator.new(self) validate(:new)
@validator.validate(:new)
end end
end end
def validator
@validator ||= self.class.validator.new(self)
end
def validate(context = nil)
validator.validate(context)
end
def compose!(deps = nil, &blk)
super(deps, &blk)
validate(:composed)
end
def errors def errors
@validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables validator.messages + descendants.flat_map(&:errors)
end end
class_methods do class_methods do
......
...@@ -413,21 +413,6 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -413,21 +413,6 @@ describe Gitlab::Ci::Config::Entry::Job do
expect(entry.errors).to include 'job config missing required keys: stage' expect(entry.errors).to include 'job config missing required keys: stage'
end end
end end
context 'when needs is bridge type' do
let(:config) do
{
script: 'echo',
stage: 'test',
needs: { pipeline: 'some/project' }
}
end
it 'returns error about invalid needs type' do
expect(entry).not_to be_valid
expect(entry.errors).to contain_exactly('job needs can only have pipeline type needs')
end
end
end end
context 'when timeout value is not correct' do context 'when timeout value is not correct' do
......
...@@ -29,7 +29,7 @@ describe ::Gitlab::Ci::Config::Entry::Need do ...@@ -29,7 +29,7 @@ describe ::Gitlab::Ci::Config::Entry::Need do
describe '#errors' do describe '#errors' do
it 'is returns an error about an empty config' do it 'is returns an error about an empty config' do
expect(need.errors) expect(need.errors)
.to contain_exactly("pipeline config can't be blank") .to contain_exactly("job config can't be blank")
end end
end end
end end
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Needs do describe ::Gitlab::Ci::Config::Entry::Needs do
subject(:needs) { described_class.new(config) } subject(:needs) { described_class.new(config) }
before do
needs.metadata[:allowed_needs] = %i[job]
end
describe 'validations' do describe 'validations' do
before do before do
needs.compose! needs.compose!
...@@ -42,8 +46,8 @@ describe ::Gitlab::Ci::Config::Entry::Needs do ...@@ -42,8 +46,8 @@ describe ::Gitlab::Ci::Config::Entry::Needs do
describe '#errors' do describe '#errors' do
it 'returns error about incorrect type' do it 'returns error about incorrect type' do
expect(needs.errors) expect(needs.errors).to contain_exactly(
.to contain_exactly('needs need has an unsupported type') 'need has an unsupported type')
end end
end end
end end
...@@ -59,7 +63,12 @@ describe ::Gitlab::Ci::Config::Entry::Needs do ...@@ -59,7 +63,12 @@ describe ::Gitlab::Ci::Config::Entry::Needs do
describe '#value' do describe '#value' do
it 'returns key value' do it 'returns key value' do
expect(needs.value).to eq(pipeline: [{ name: 'first_job_name' }, { name: 'second_job_name' }]) expect(needs.value).to eq(
job: [
{ name: 'first_job_name' },
{ name: 'second_job_name' }
]
)
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