Commit 39dac14e authored by Grzegorz Bizon's avatar Grzegorz Bizon

Refactor `include` code and improve error reporting

parent e0830a74
...@@ -6,22 +6,62 @@ module Gitlab ...@@ -6,22 +6,62 @@ module Gitlab
module External module External
module File module File
class Base class Base
include Gitlab::Utils::StrongMemoize
attr_reader :location, :opts, :errors
YAML_WHITELIST_EXTENSION = /(yml|yaml)$/i.freeze YAML_WHITELIST_EXTENSION = /(yml|yaml)$/i.freeze
def initialize(location, opts = {}) def initialize(location, opts = {})
@location = location @location = location
@opts = opts
@errors = []
validate_location!
validate_content!
validate_hash!
end
def invalid_extension?
!location.match(YAML_WHITELIST_EXTENSION)
end end
def valid? def valid?
location.match(YAML_WHITELIST_EXTENSION) && content errors.none?
end
def error_message
errors.first
end end
def content def content
raise NotImplementedError, 'content must be implemented and return a string or nil' raise NotImplementedError, 'subclass must implement fetching raw content'
end end
def error_message def to_hash
raise NotImplementedError, 'error_message must be implemented and return a string' @hash ||= Ci::Config::Loader.new(content).load!
rescue Ci::Config::Loader::FormatError
nil
end
protected
def validate_location!
if invalid_extension?
errors.push("Included file `#{location}` does not have YAML extension!")
end
end
def validate_content!
if errors.none? && content.blank?
errors.push("Included file `#{location}` is empty or does not exist!")
end
end
def validate_hash!
if errors.none? && to_hash.blank?
errors.push("Included file `#{location}` does not have valid YAML syntax!")
end
end end
end end
end end
......
...@@ -6,25 +6,33 @@ module Gitlab ...@@ -6,25 +6,33 @@ module Gitlab
module External module External
module File module File
class Local < Base class Local < Base
attr_reader :location, :project, :sha include Gitlab::Utils::StrongMemoize
def initialize(location, opts = {}) attr_reader :project, :sha
super
def initialize(location, opts = {})
@project = opts.fetch(:project) @project = opts.fetch(:project)
@sha = opts.fetch(:sha) @sha = opts.fetch(:sha)
end
def content super
@content ||= fetch_local_content
end end
def error_message def content
"Local file '#{location}' is not valid." strong_memoize(:content) { fetch_local_content }
end end
private private
def validate_content!
return if errors.any?
if content.nil?
errors.push("Local file `#{location}` does not exist!")
elsif content.blank?
errors.push("Local file `#{location}` is empty!")
end
end
def fetch_local_content def fetch_local_content
project.repository.blob_data_at(sha, location) project.repository.blob_data_at(sha, location)
end end
......
...@@ -7,21 +7,34 @@ module Gitlab ...@@ -7,21 +7,34 @@ module Gitlab
module File module File
class Remote < Base class Remote < Base
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :location
def content def content
strong_memoize(:content) { fetch_remote_content } strong_memoize(:content) { fetch_remote_content }
end end
def error_message
"Remote file '#{location}' is not valid."
end
private private
def validate_location!
super
unless ::Gitlab::UrlSanitizer.valid?(location)
errors.push("Remote file `#{location}` does not have a valid address!")
end
end
def fetch_remote_content def fetch_remote_content
Gitlab::HTTP.get(location) Gitlab::HTTP.get(location)
rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Gitlab::HTTP::BlockedUrlError rescue SocketError
errors.push("Remote file `#{location}` could not be fetched because of a socket error!")
nil
rescue Timeout::Error
errors.push("Remote file `#{location}` could not be fetched because of a timeout error!")
nil
rescue Gitlab::HTTP::Error
errors.push("Remote file `#{location}` could not be fetched because of a HTTP error!")
nil
rescue Gitlab::HTTP::BlockedUrlError
errors.push("Remote file `#{location}` could not be fetched because the URL is blocked!")
nil nil
end end
end end
......
...@@ -14,38 +14,34 @@ module Gitlab ...@@ -14,38 +14,34 @@ module Gitlab
end end
def perform def perform
return values if external_files.empty? return @values if @external_files.empty?
external_files.each do |external_file| validate_external_files!
validate_external_file(external_file) merge_external_files!
@content.deep_merge!(content_of(external_file)) append_inline_content!
end remove_include_keyword!
append_inline_content
remove_include_keyword
end end
private private
attr_reader :values, :external_files, :content def validate_external_files!
@external_files.each do |file|
def validate_external_file(external_file) raise IncludeError, file.error_message unless file.valid?
unless external_file.valid?
raise IncludeError, external_file.error_message
end end
end end
def content_of(external_file) def merge_external_files!
Config::Loader.new(external_file.content).load! @external_files.each do |file|
@content.deep_merge!(file.to_hash)
end
end end
def append_inline_content def append_inline_content!
@content.deep_merge!(@values) @content.deep_merge!(@values)
end end
def remove_include_keyword def remove_include_keyword!
content.delete(:include) @content.tap { @content.delete(:include) }
content
end end
end end
end end
......
...@@ -72,7 +72,7 @@ describe Gitlab::Ci::Config::External::File::Local do ...@@ -72,7 +72,7 @@ describe Gitlab::Ci::Config::External::File::Local do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
it 'should return an error message' do it 'should return an error message' do
expect(local_file.error_message).to eq("Local file '#{location}' is not valid.") expect(local_file.error_message).to eq("Local file `#{location}` does not exist!")
end end
end end
end end
...@@ -108,7 +108,7 @@ describe Gitlab::Ci::Config::External::File::Remote do ...@@ -108,7 +108,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
it 'should return an error message' do it 'should return an error message' do
expect(remote_file.error_message).to eq("Remote file '#{location}' is not valid.") expect(remote_file.error_message).to eq("Remote file `#{location}` does not have a valid address!")
end end
end end
end end
...@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::External::Processor do
it 'should raise an error' do it 'should raise an error' do
expect { processor.perform }.to raise_error( expect { processor.perform }.to raise_error(
described_class::IncludeError, described_class::IncludeError,
"Local file '/lib/gitlab/ci/templates/non-existent-file.yml' is not valid." "Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
) )
end end
end end
...@@ -37,7 +37,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -37,7 +37,7 @@ describe Gitlab::Ci::Config::External::Processor do
it 'should raise an error' do it 'should raise an error' do
expect { processor.perform }.to raise_error( expect { processor.perform }.to raise_error(
described_class::IncludeError, described_class::IncludeError,
"Remote file '#{remote_file}' is not valid." "Remote file `#{remote_file}` could not be fetched because of a socket error!"
) )
end end
end end
...@@ -159,7 +159,10 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -159,7 +159,10 @@ describe Gitlab::Ci::Config::External::Processor do
end end
it 'should raise an error' do it 'should raise an error' do
expect { processor.perform }.to raise_error(Gitlab::Ci::Config::Loader::FormatError) expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
)
end end
end end
......
...@@ -201,7 +201,7 @@ describe Gitlab::Ci::Config do ...@@ -201,7 +201,7 @@ describe Gitlab::Ci::Config do
it 'raises error YamlProcessor validationError' do it 'raises error YamlProcessor validationError' do
expect { config }.to raise_error( expect { config }.to raise_error(
described_class::ConfigError, described_class::ConfigError,
"Local file 'invalid' is not valid." "Included file `invalid` does not have YAML extension!"
) )
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