summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/gitlab/ci/config/entry/global.rb10
-rw-r--r--lib/gitlab/ci/config/entry/includes.rb22
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb32
-rw-r--r--lib/gitlab/ci/external_files/external_file.rb38
-rw-r--r--lib/gitlab/ci/external_files/mapper.rb27
-rw-r--r--lib/gitlab/ci/external_files/processor.rb46
-rw-r--r--spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml10
-rw-r--r--spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-2.yml8
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/includes_spec.rb97
-rw-r--r--spec/lib/gitlab/ci/external_files/external_file_spec.rb78
-rw-r--r--spec/lib/gitlab/ci/external_files/mapper_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/external_files/processor_spec.rb141
13 files changed, 550 insertions, 4 deletions
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
index a4ec8f0ff2f..401e566de77 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -33,11 +33,15 @@ module Gitlab
entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.'
+ entry :includes, Entry::Includes,
+ description: 'External GitlLab Ci files'
+
helpers :before_script, :image, :services, :after_script,
- :variables, :stages, :types, :cache, :jobs
+ :variables, :stages, :types, :cache, :jobs, :includes
def compose!(_deps = nil)
super(self) do
+ append_external_files!
compose_jobs!
compose_deprecated_entries!
end
@@ -45,6 +49,10 @@ module Gitlab
private
+ def append_external_files!
+ return if includes_value.nil?
+ end
+
def compose_jobs!
factory = Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys))
diff --git a/lib/gitlab/ci/config/entry/includes.rb b/lib/gitlab/ci/config/entry/includes.rb
new file mode 100644
index 00000000000..c7a8c7960e8
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/includes.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a Docker image.
+ #
+ class Includes < Node
+ include Validatable
+
+ validations do
+ validates :config, array_or_string: true, external_file: true, allow_nil: true
+ end
+
+ def value
+ Array(@config)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index b3c889ee92f..5b4d5d53821 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -60,6 +60,38 @@ module Gitlab
end
end
+ class ArrayOrStringValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array) || value.is_a?(String)
+ record.errors.add(attribute, 'should be an array or a string')
+ end
+ end
+ end
+
+ class ExternalFileValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if value.is_a?(Array)
+ value.each do |path|
+ validate_external_file(path, record, attribute)
+ end
+ else
+ validate_external_file(value, record, attribute)
+ end
+ end
+
+ private
+
+ def validate_external_file(value, record, attribute)
+ unless valid_url?(value)
+ record.errors.add(attribute, 'should be a valid local or remote file')
+ end
+ end
+
+ def valid_url?(value)
+ Gitlab::UrlSanitizer.valid?(value) || File.exists?("#{Rails.root}/#{value}")
+ end
+ end
+
class KeyValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
diff --git a/lib/gitlab/ci/external_files/external_file.rb b/lib/gitlab/ci/external_files/external_file.rb
new file mode 100644
index 00000000000..9bad8231c42
--- /dev/null
+++ b/lib/gitlab/ci/external_files/external_file.rb
@@ -0,0 +1,38 @@
+require 'open-uri'
+
+module Gitlab
+ module Ci
+ module ExternalFiles
+ class ExternalFile
+
+ def initialize(value)
+ @value = value
+ end
+
+ def content
+ if remote_url?
+ open(value).read
+ else
+ File.read(base_path)
+ end
+ end
+
+ def valid?
+ remote_url? || File.exists?(base_path)
+ end
+
+ private
+
+ attr_reader :value
+
+ def base_path
+ "#{Rails.root}/#{value}"
+ end
+
+ def remote_url?
+ ::Gitlab::UrlSanitizer.valid?(value)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/external_files/mapper.rb b/lib/gitlab/ci/external_files/mapper.rb
new file mode 100644
index 00000000000..5deb2f5a2e5
--- /dev/null
+++ b/lib/gitlab/ci/external_files/mapper.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ module ExternalFiles
+ class Mapper
+
+ def self.fetch_paths(values)
+ paths = values.fetch(:includes, [])
+ normalize_paths(paths)
+ end
+
+ private
+
+ def self.normalize_paths(paths)
+ if paths.is_a?(String)
+ [build_external_file(paths)]
+ else
+ paths.map { |path| build_external_file(path) }
+ end
+ end
+
+ def self.build_external_file(path)
+ ::Gitlab::Ci::ExternalFiles::ExternalFile.new(path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/external_files/processor.rb b/lib/gitlab/ci/external_files/processor.rb
new file mode 100644
index 00000000000..f38bc7633b5
--- /dev/null
+++ b/lib/gitlab/ci/external_files/processor.rb
@@ -0,0 +1,46 @@
+module Gitlab
+ module Ci
+ module ExternalFiles
+ class Processor
+ ExternalFileError = Class.new(StandardError)
+
+ def initialize(values)
+ @values = values
+ @external_files = ::Gitlab::Ci::ExternalFiles::Mapper.fetch_paths(values)
+ end
+
+ def perform
+ return values if external_files.empty?
+
+ external_files.each do |external_file|
+ validate_external_file(external_file)
+ append_external_content(external_file)
+ end
+
+ remove_include_keyword
+ end
+
+ private
+
+ attr_reader :values, :external_files
+
+ def validate_external_file(external_file)
+ unless external_file.valid?
+ raise ExternalFileError, 'External files should be a valid local or remote file'
+ end
+ end
+
+ def append_external_content(external_file)
+ external_values = ::Gitlab::Ci::Config::Loader.new(external_file.content).load!
+ @values.merge!(external_values)
+ end
+
+ def remove_include_keyword
+ values.delete(:includes)
+ values
+ end
+
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml b/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml
new file mode 100644
index 00000000000..0bab94a7c2e
--- /dev/null
+++ b/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml
@@ -0,0 +1,10 @@
+before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+
+rspec:
+ script:
+ - bundle exec rspec
diff --git a/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-2.yml b/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-2.yml
new file mode 100644
index 00000000000..b341cca8946
--- /dev/null
+++ b/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-2.yml
@@ -0,0 +1,8 @@
+variables:
+ # AUTO_DEVOPS_DOMAIN is the application deployment domain and should be set as a variable at the group or project level.
+
+ AUTO_DEVOPS_DOMAIN: domain.example.com
+ POSTGRES_USER: user
+ POSTGRES_PASSWORD: testing-password
+ POSTGRES_ENABLED: "true"
+ POSTGRES_DB: $CI_ENVIRONMENT_SLUG
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 1860ed79bfd..ff623b95be8 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 includes])
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/config/entry/includes_spec.rb b/spec/lib/gitlab/ci/config/entry/includes_spec.rb
new file mode 100644
index 00000000000..d72503535ed
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/includes_spec.rb
@@ -0,0 +1,97 @@
+require 'rails_helper'
+
+describe Gitlab::Ci::Config::Entry::Includes do
+ let(:entry) { described_class.new(config) }
+
+ shared_examples 'valid external file' do
+ it 'should be valid' do
+ expect(entry).to be_valid
+ end
+
+ it 'should not return any error' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ shared_examples 'invalid external file' do
+ it 'should not be valid' do
+ expect(entry).not_to be_valid
+ end
+
+ it 'should return an error' do
+ expect(entry.errors.first).to match(/should be a valid local or remote file/)
+ end
+ end
+
+ describe "#valid?" do
+ context 'with no external file given' do
+ let(:config) { nil }
+
+ it_behaves_like 'valid external file'
+ end
+
+ context 'with multiple external files' do
+ let(:config) { %w(https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-2.yml) }
+
+ it_behaves_like 'valid external file'
+ end
+
+ context 'with just one external file' do
+ let(:config) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ it_behaves_like 'valid external file'
+ end
+
+ context 'when they contain valid URLs' do
+ let(:config) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ it_behaves_like 'valid external file'
+ end
+
+ context 'when they contain valid relative URLs' do
+ let(:config) { '/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml' }
+
+ it_behaves_like 'valid external file'
+ end
+
+ context 'when they not contain valid URLs' do
+ let(:config) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ it_behaves_like 'invalid external file'
+ end
+
+ context 'when they not contain valid relative URLs' do
+ let(:config) { '/vendor/gitlab-ci-yml/non-existent-file.yml' }
+
+ it_behaves_like 'invalid external file'
+ end
+ end
+
+ describe "#value" do
+ context 'with multiple external files' do
+ let(:config) { %w(https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-2.yml) }
+ it 'should return an array' do
+ expect(entry.value).to be_an(Array)
+ expect(entry.value.count).to eq(2)
+ end
+ end
+
+ context 'with just one external file' do
+ let(:config) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ it 'should return an array' do
+ expect(entry.value).to be_an(Array)
+ expect(entry.value.count).to eq(1)
+ end
+ end
+
+ context 'with no external file given' do
+ let(:config) { nil }
+
+ it 'should return an empty array' do
+ expect(entry.value).to be_an(Array)
+ expect(entry.value).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/external_files/external_file_spec.rb b/spec/lib/gitlab/ci/external_files/external_file_spec.rb
new file mode 100644
index 00000000000..80468f63435
--- /dev/null
+++ b/spec/lib/gitlab/ci/external_files/external_file_spec.rb
@@ -0,0 +1,78 @@
+require 'rails_helper'
+
+describe Gitlab::Ci::ExternalFiles::ExternalFile do
+ let(:external_file) { described_class.new(value) }
+
+ describe "#valid?" do
+ context 'when is a valid remote url' do
+ let(:value) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ it 'should return true' do
+ expect(external_file.valid?).to be_truthy
+ end
+ end
+
+ context 'when is not a valid remote url' do
+ let(:value) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ it 'should return false' do
+ expect(external_file.valid?).to be_falsy
+ end
+ end
+
+ context 'when is a valid local path' do
+ let(:value) { '/vendor/gitlab-ci-yml/existent-file.yml' }
+
+ it 'should return true' do
+ allow(File).to receive(:exists?).and_return(true)
+ expect(external_file.valid?).to be_truthy
+ end
+ end
+
+ context 'when is not a valid local path' do
+ let(:value) { '/vendor/gitlab-ci-yml/non-existent-file.yml' }
+
+ it 'should return false' do
+ expect(external_file.valid?).to be_falsy
+ end
+ end
+ end
+
+ describe "#content" do
+ let(:external_file_content) {
+ <<-HEREDOC
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+ HEREDOC
+ }
+
+ context 'with a local file' do
+ let(:value) { '/vendor/gitlab-ci-yml/non-existent-file.yml' }
+
+ before do
+ allow(File).to receive(:exists?).and_return(true)
+ allow(File).to receive(:read).and_return(external_file_content)
+ end
+
+ it 'should return the content of the file' do
+ expect(external_file.content).to eq(external_file_content)
+ end
+ end
+
+ context 'with a valid remote file' do
+ let(:value) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ before do
+ allow_any_instance_of(Kernel).to receive_message_chain(:open, :read).and_return(external_file_content)
+ end
+
+ it 'should return the content of the file' do
+ expect(external_file.content).to eq(external_file_content)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/external_files/mapper_spec.rb b/spec/lib/gitlab/ci/external_files/mapper_spec.rb
new file mode 100644
index 00000000000..dfbf8bfa098
--- /dev/null
+++ b/spec/lib/gitlab/ci/external_files/mapper_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+describe Gitlab::Ci::ExternalFiles::Mapper do
+ describe '.fetch_paths' do
+ context 'when includes is defined as string' do
+ let(:values) { { includes: '/vendor/gitlab-ci-yml/non-existent-file.yml', image: 'ruby:2.2'} }
+
+ it 'returns an array' do
+ expect(described_class.fetch_paths(values)).to be_an(Array)
+ end
+
+ it 'returns ExternalFile instances' do
+ expect(described_class.fetch_paths(values).first).to be_an_instance_of(::Gitlab::Ci::ExternalFiles::ExternalFile)
+ end
+ end
+
+ context 'when includes is defined as an array' do
+ let(:values) { { includes: ['https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml', '/vendor/gitlab-ci-yml/template.yml'], image: 'ruby:2.2'} }
+ it 'returns an array' do
+ expect(described_class.fetch_paths(values)).to be_an(Array)
+ end
+
+ it 'returns ExternalFile instances' do
+ paths = described_class.fetch_paths(values)
+ paths.each do |path|
+ expect(path).to be_an_instance_of(::Gitlab::Ci::ExternalFiles::ExternalFile)
+ end
+ end
+ end
+
+ context 'when includes is not defined' do
+ let(:values) { { image: 'ruby:2.2'} }
+
+ it 'returns an empty array' do
+ expect(described_class.fetch_paths(values)).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/external_files/processor_spec.rb b/spec/lib/gitlab/ci/external_files/processor_spec.rb
new file mode 100644
index 00000000000..51ed14de4cf
--- /dev/null
+++ b/spec/lib/gitlab/ci/external_files/processor_spec.rb
@@ -0,0 +1,141 @@
+require 'rails_helper'
+
+describe Gitlab::Ci::ExternalFiles::Processor do
+ let(:processor) { described_class.new(values) }
+
+ describe "#perform" do
+ context 'when no external files defined' do
+ let(:values) { { image: 'ruby:2.2' } }
+
+ it 'should return the same values' do
+ expect(processor.perform).to eq(values)
+ end
+ end
+
+ context 'when an invalid local file is defined' do
+ let(:values) { { includes: '/vendor/gitlab-ci-yml/non-existent-file.yml', image: 'ruby:2.2'} }
+
+ it 'should raise an error' do
+ expect { processor.perform }.to raise_error(described_class::ExternalFileError)
+ end
+ end
+
+ context 'when an invalid remote file is defined' do
+ let(:values) { { includes: 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml', image: 'ruby:2.2'} }
+
+ it 'should raise an error' do
+ expect { processor.perform }.to raise_error(described_class::ExternalFileError)
+ end
+ end
+
+ context 'with a valid remote external file is defined' do
+ let(:values) { { includes: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml', image: 'ruby:2.2' } }
+ let(:external_file_content) {
+ <<-HEREDOC
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+
+ rspec:
+ script:
+ - bundle exec rspec
+
+ rubocop:
+ script:
+ - bundle exec rubocop
+ HEREDOC
+ }
+
+ before do
+ allow_any_instance_of(Kernel).to receive_message_chain(:open, :read).and_return(external_file_content)
+ end
+
+ it 'should append the file to the values' do
+ output = processor.perform
+ expect(output.keys).to match_array([:image, :before_script, :rspec, :rubocop])
+ end
+
+ it "should remove the 'includes' keyword" do
+ expect(processor.perform[:includes]).to be_nil
+ end
+ end
+
+ context 'with a valid local external file is defined' do
+ let(:values) { { includes: '/vendor/gitlab-ci-yml/template.yml' , image: 'ruby:2.2'} }
+ let(:external_file_content) {
+ <<-HEREDOC
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+ HEREDOC
+ }
+
+ before do
+ allow(File).to receive(:exists?).and_return(true)
+ allow(File).to receive(:read).and_return(external_file_content)
+ end
+
+ it 'should append the file to the values' do
+ output = processor.perform
+ expect(output.keys).to match_array([:image, :before_script])
+ end
+
+ it "should remove the 'includes' keyword" do
+ expect(processor.perform[:includes]).to be_nil
+ end
+ end
+
+ context 'with multiple external files are defined' do
+ let(:external_files) {
+ [
+ "/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml",
+ "/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-2.yml",
+ 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml'
+ ]
+ }
+ let(:values) { { includes: external_files, image: 'ruby:2.2'} }
+
+ let(:remote_file_content) {
+ <<-HEREDOC
+ stages:
+ - build
+ - review
+ - cleanup
+ HEREDOC
+ }
+
+ before do
+ allow_any_instance_of(Kernel).to receive_message_chain(:open, :read).and_return(remote_file_content)
+ end
+
+ it 'should append the files to the values' do
+ expect(processor.perform.keys).to match_array([:image, :variables, :stages, :before_script, :rspec])
+ end
+
+ it "should remove the 'includes' keyword" do
+ expect(processor.perform[:includes]).to be_nil
+ end
+ end
+
+ context 'when external files are defined but not valid' do
+ let(:values) { { includes: '/vendor/gitlab-ci-yml/template.yml', image: 'ruby:2.2'} }
+
+ let(:external_file_content) { 'invalid content file ////' }
+
+ before do
+ allow(File).to receive(:exists?).and_return(true)
+ allow(File).to receive(:read).and_return(external_file_content)
+ end
+
+ it 'should raise an error' do
+ expect { processor.perform }.to raise_error(Gitlab::Ci::Config::Loader::FormatError)
+ end
+ end
+ end
+end