diff options
-rw-r--r-- | changelogs/unreleased/include-ci-yaml.yml | 5 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/global.rb | 3 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/include.rb | 23 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/includes.rb | 32 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/entry/global_spec.rb | 6 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/yaml_processor_spec.rb | 79 |
6 files changed, 145 insertions, 3 deletions
diff --git a/changelogs/unreleased/include-ci-yaml.yml b/changelogs/unreleased/include-ci-yaml.yml new file mode 100644 index 00000000000..5909950ef0b --- /dev/null +++ b/changelogs/unreleased/include-ci-yaml.yml @@ -0,0 +1,5 @@ +--- +title: Validate 'include' keywords in gitlab-ci.yml configuration files. +merge_request: 24098 +author: Paul Bonaud +type: fixed diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index 09ecb5fdb99..2b5a59c078e 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -17,6 +17,9 @@ module Gitlab entry :image, Entry::Image, description: 'Docker image that will be used to execute jobs.' + entry :include, Entry::Includes, + description: 'List of external YAML files to include.' + entry :services, Entry::Services, description: 'Docker images that will be linked to the container.' diff --git a/lib/gitlab/ci/config/entry/include.rb b/lib/gitlab/ci/config/entry/include.rb new file mode 100644 index 00000000000..f2f3dd84eda --- /dev/null +++ b/lib/gitlab/ci/config/entry/include.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a single include. + # + class Include < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_KEYS = %i[local file remote template].freeze + + validations do + validates :config, hash_or_string: true + validates :config, allowed_keys: ALLOWED_KEYS + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/includes.rb b/lib/gitlab/ci/config/entry/includes.rb new file mode 100644 index 00000000000..82b2b1ccf4b --- /dev/null +++ b/lib/gitlab/ci/config/entry/includes.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a list of include. + # + class Includes < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, type: Array + end + + def self.aspects + super.append -> do + @config = Array.wrap(@config) + + @config.each_with_index do |config, i| + @entries[i] = ::Gitlab::Config::Entry::Factory.new(Entry::Include) + .value(config || {}) + .create! + end + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 7651f594a4c..e23efff18d5 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Global do expect(described_class.nodes.keys) .to match_array(%i[before_script image services after_script variables stages - types cache]) + types cache include]) end end end @@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -189,7 +189,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 91139d421f5..29638ef47c5 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -602,6 +602,85 @@ module Gitlab end end + describe "Include" do + let(:opts) { {} } + + let(:config) do + { + include: include_content, + rspec: { script: "test" } + } + end + + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } + + context "when validating a ci config file with no project context" do + context "when an array is provided" do + let(:include_content) { ["/local.gitlab-ci.yml"] } + + it "does not return any error" do + expect { subject }.not_to raise_error + end + end + + context "when an array of wrong keyed object is provided" do + let(:include_content) { [{ yolo: "/local.gitlab-ci.yml" }] } + + it "returns a validation error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + + context "when an array of mixed typed objects is provided" do + let(:include_content) do + [ + 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml', + '/templates/.after-script-template.yml', + { template: 'Auto-DevOps.gitlab-ci.yml' } + ] + end + + it "does not return any error" do + expect { subject }.not_to raise_error + end + end + + context "when the include type is incorrect" do + let(:include_content) { { name: "/local.gitlab-ci.yml" } } + + it "returns an invalid configuration error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + end + + context "when validating a ci config file within a project" do + let(:include_content) { "/local.gitlab-ci.yml" } + let(:project) { create(:project, :repository) } + let(:opts) { { project: project, sha: project.commit.sha } } + + context "when the included internal file is present" do + before do + expect(project.repository).to receive(:blob_data_at) + .and_return(YAML.dump({ job1: { script: 'hello' } })) + end + + it "does not return an error" do + expect { subject }.not_to raise_error + end + end + + context "when the included internal file is not present" do + it "returns an error with missing file details" do + expect { subject }.to raise_error( + Gitlab::Ci::YamlProcessor::ValidationError, + "Local file `#{include_content}` does not exist!" + ) + end + end + end + end + describe "When" do %w(on_success on_failure always).each do |when_state| it "returns #{when_state} when defined" do |