Commit b6a91e28 authored by Fabio Pitino's avatar Fabio Pitino Committed by Shinya Maeda

Validate dependency for dynamic child pipeline

Ensure that dependency is valid when using
`include:[artifact,job]`.

Refactor existing validations.
parent 1503495e
---
title: Validate dependency on job generating a CI config when using dynamic child pipelines
merge_request: 27916
author:
type: added
...@@ -136,12 +136,11 @@ your own script to generate a YAML file, which is then [used to trigger a child ...@@ -136,12 +136,11 @@ your own script to generate a YAML file, which is then [used to trigger a child
This technique can be very powerful in generating pipelines targeting content that changed or to This technique can be very powerful in generating pipelines targeting content that changed or to
build a matrix of targets and architectures. build a matrix of targets and architectures.
In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail.
This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/209070).
## Limitations ## Limitations
A parent pipeline can trigger many child pipelines, but a child pipeline cannot trigger A parent pipeline can trigger many child pipelines, but a child pipeline cannot trigger
further child pipelines. See the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/29651) further child pipelines. See the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/29651)
for discussion on possible future improvements. for discussion on possible future improvements.
When triggering dynamic child pipelines, if the job containing the CI config artifact is not a predecessor of the
trigger job, the child pipeline will fail to be created, causing also the parent pipeline to fail.
In the future we want to validate the trigger job's dependencies [at the time the parent pipeline is created](https://gitlab.com/gitlab-org/gitlab/-/issues/209070) rather than when the child pipeline is created.
...@@ -15,6 +15,11 @@ module Gitlab ...@@ -15,6 +15,11 @@ module Gitlab
validations do validations do
validates :config, hash_or_string: true validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
validate do
if config[:artifact] && config[:job].blank?
errors.add(:config, "must specify the job where to fetch the artifact from")
end
end
end end
end end
end end
......
...@@ -142,6 +142,7 @@ module Gitlab ...@@ -142,6 +142,7 @@ module Gitlab
validate_job_stage!(name, job) validate_job_stage!(name, job)
validate_job_dependencies!(name, job) validate_job_dependencies!(name, job)
validate_job_needs!(name, job) validate_job_needs!(name, job)
validate_dynamic_child_pipeline_dependencies!(name, job)
validate_job_environment!(name, job) validate_job_environment!(name, job)
end end
end end
...@@ -163,35 +164,50 @@ module Gitlab ...@@ -163,35 +164,50 @@ module Gitlab
def validate_job_dependencies!(name, job) def validate_job_dependencies!(name, job)
return unless job[:dependencies] return unless job[:dependencies]
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] validate_job_dependency!(name, dependency)
end
end
dependency_stage_index = @stages.index(@jobs[dependency.to_sym][:stage]) def validate_dynamic_child_pipeline_dependencies!(name, job)
return unless includes = job.dig(:trigger, :include)
unless dependency_stage_index.present? && dependency_stage_index < stage_index includes.each do |included|
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" next unless dependency = included[:job]
end
validate_job_dependency!(name, dependency)
end end
end end
def validate_job_needs!(name, job) def validate_job_needs!(name, job)
return unless job.dig(:needs, :job) return unless needs = job.dig(:needs, :job)
stage_index = @stages.index(job[:stage])
job.dig(:needs, :job).each do |need| needs.each do |need|
need_job_name = need[:name] dependency = need[:name]
validate_job_dependency!(name, dependency, 'need')
end
end
raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym] def validate_job_dependency!(name, dependency, dependency_type = 'dependency')
unless @jobs[dependency.to_sym]
raise ValidationError, "#{name} job: undefined #{dependency_type}: #{dependency}"
end
needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage]) job_stage_index = stage_index(name)
dependency_stage_index = stage_index(dependency)
unless needs_stage_index.present? && needs_stage_index < stage_index # A dependency might be defined later in the configuration
raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages" # with a stage that does not exist
unless dependency_stage_index.present? && dependency_stage_index < job_stage_index
raise ValidationError, "#{name} job: #{dependency_type} #{dependency} is not defined in prior stages"
end end
end end
def stage_index(name)
job = @jobs[name.to_sym]
return unless job
@stages.index(job[:stage])
end end
def validate_job_environment!(name, job) def validate_job_environment!(name, job)
......
...@@ -1647,6 +1647,48 @@ module Gitlab ...@@ -1647,6 +1647,48 @@ module Gitlab
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /is not defined in prior stages/) } it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /is not defined in prior stages/) }
end end
context 'when trigger job includes artifact generated by a dependency' do
context 'when dependency is defined in previous stages' do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
test1: { stage: 'test', trigger: {
include: [{ job: 'build1', artifact: 'generated.yml' }]
} }
}
end
it { expect { subject }.not_to raise_error }
end
context 'when dependency is defined in later stages' do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
test1: { stage: 'test', trigger: {
include: [{ job: 'deploy1', artifact: 'generated.yml' }]
} },
deploy1: { stage: 'deploy', script: 'test' }
}
end
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /is not defined in prior stages/) }
end
context 'when dependency is not defined' do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
test1: { stage: 'test', trigger: {
include: [{ job: 'non-existent', artifact: 'generated.yml' }]
} }
}
end
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /undefined dependency: non-existent/) }
end
end
end end
describe "Job Needs" do describe "Job Needs" do
...@@ -2052,6 +2094,34 @@ module Gitlab ...@@ -2052,6 +2094,34 @@ module Gitlab
end end
end end
describe 'with trigger:include' do
context 'when artifact and job are specified' do
let(:config) do
YAML.dump({
build1: { stage: 'build', script: 'test' },
test1: { stage: 'test', trigger: {
include: [{ artifact: 'generated.yml', job: 'build1' }]
} }
})
end
it { expect { subject }.not_to raise_error }
end
context 'when artifact is specified without job' do
let(:config) do
YAML.dump({
build1: { stage: 'build', script: 'test' },
test1: { stage: 'test', trigger: {
include: [{ artifact: 'generated.yml' }]
} }
})
end
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /must specify the job where to fetch the artifact from/) }
end
end
describe "Error handling" do describe "Error handling" do
it "fails to parse YAML" do it "fails to parse YAML" do
expect do expect do
......
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