Commit 505d71ec authored by Kamil Trzciński's avatar Kamil Trzciński Committed by Grzegorz Bizon
Browse files

Introduce default: for gitlab-ci.yml

This moves all existing `image/services/before_script/variables`
into `default:`. This allows us to easily add a default and
top-level entries. `default`: is keep backward compatible: to
be considered to be job if `default:script:` is specified. This
behavior should be removed.

All existing `image/services/before_script/variables` are properly
handled in root context.
parent c167cc58
---
title: 'Introduce default: for gitlab-ci.yml'
merge_request:
author:
type: added
...@@ -14,23 +14,25 @@ module Gitlab ...@@ -14,23 +14,25 @@ module Gitlab
External::Processor::IncludeError External::Processor::IncludeError
].freeze ].freeze
   
attr_reader :root
def initialize(config, project: nil, sha: nil, user: nil) def initialize(config, project: nil, sha: nil, user: nil)
@config = Config::Extendable @config = Config::Extendable
.new(build_config(config, project: project, sha: sha, user: user)) .new(build_config(config, project: project, sha: sha, user: user))
.to_hash .to_hash
   
@global = Entry::Global.new(@config) @root = Entry::Root.new(@config)
@global.compose! @root.compose!
rescue *rescue_errors => e rescue *rescue_errors => e
raise Config::ConfigError, e.message raise Config::ConfigError, e.message
end end
   
def valid? def valid?
@global.valid? @root.valid?
end end
   
def errors def errors
@global.errors @root.errors
end end
   
def to_hash def to_hash
...@@ -40,36 +42,16 @@ module Gitlab ...@@ -40,36 +42,16 @@ module Gitlab
## ##
# Temporary method that should be removed after refactoring # Temporary method that should be removed after refactoring
# #
def before_script
@global.before_script_value
end
def image
@global.image_value
end
def services
@global.services_value
end
def after_script
@global.after_script_value
end
def variables def variables
@global.variables_value root.variables_value
end end
   
def stages def stages
@global.stages_value root.stages_value
end
def cache
@global.cache_value
end end
   
def jobs def jobs
@global.jobs_value root.jobs_value
end end
   
private private
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# This class represents a default entry
# Entry containing default values for all jobs
# defined in configuration file.
#
class Default < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
DuplicateError = Class.new(Gitlab::Config::Loader::FormatError)
ALLOWED_KEYS = %i[before_script image services
after_script cache].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
end
entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.',
inherit: true
entry :image, Entry::Image,
description: 'Docker image that will be used to execute jobs.',
inherit: true
entry :services, Entry::Services,
description: 'Docker images that will be linked to the container.',
inherit: true
entry :after_script, Entry::Script,
description: 'Script that will be executed after each job.',
inherit: true
entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.',
inherit: true
helpers :before_script, :image, :services, :after_script, :cache
def compose!(deps = nil)
super(self)
inherit!(deps)
end
private
def inherit!(deps)
return unless deps
self.class.nodes.each do |key, factory|
next unless factory.inheritable?
root_entry = deps[key]
next unless root_entry.specified?
if self[key].specified?
raise DuplicateError, "#{key} is defined in top-level and `default:` entry"
end
@entries[key] = root_entry
end
end
end
end
end
end
end
...@@ -14,6 +14,14 @@ module Gitlab ...@@ -14,6 +14,14 @@ module Gitlab
validates :config, presence: true validates :config, presence: true
end end
   
def self.matching?(name, config)
name.to_s.start_with?('.')
end
def self.visible?
false
end
def relevant? def relevant?
false false
end end
......
...@@ -42,7 +42,8 @@ module Gitlab ...@@ -42,7 +42,8 @@ module Gitlab
end end
   
entry :before_script, Entry::Script, entry :before_script, Entry::Script,
description: 'Global before script overridden in this job.' description: 'Global before script overridden in this job.',
inherit: true
   
entry :script, Entry::Commands, entry :script, Entry::Commands,
description: 'Commands that will be executed in this job.' description: 'Commands that will be executed in this job.'
...@@ -54,16 +55,20 @@ module Gitlab ...@@ -54,16 +55,20 @@ module Gitlab
description: 'Deprecated: stage this job will be executed into.' description: 'Deprecated: stage this job will be executed into.'
   
entry :after_script, Entry::Script, entry :after_script, Entry::Script,
description: 'Commands that will be executed when finishing job.' description: 'Commands that will be executed when finishing job.',
inherit: true
   
entry :cache, Entry::Cache, entry :cache, Entry::Cache,
description: 'Cache definition for this job.' description: 'Cache definition for this job.',
inherit: true
   
entry :image, Entry::Image, entry :image, Entry::Image,
description: 'Image that will be used to execute this job.' description: 'Image that will be used to execute this job.',
inherit: true
   
entry :services, Entry::Services, entry :services, Entry::Services,
description: 'Services that will be used to execute this job.' description: 'Services that will be used to execute this job.',
inherit: true
   
entry :only, Entry::Policy, entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.', description: 'Refs policy this job will be executed for.',
...@@ -95,6 +100,15 @@ module Gitlab ...@@ -95,6 +100,15 @@ module Gitlab
attributes :script, :tags, :allow_failure, :when, :dependencies, attributes :script, :tags, :allow_failure, :when, :dependencies,
:retry, :parallel, :extends, :start_in :retry, :parallel, :extends, :start_in
   
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
config.is_a?(Hash) && config.key?(:script)
end
def self.visible?
true
end
def compose!(deps = nil) def compose!(deps = nil)
super do super do
if type_defined? && !stage_defined? if type_defined? && !stage_defined?
...@@ -129,15 +143,19 @@ module Gitlab ...@@ -129,15 +143,19 @@ module Gitlab
   
private private
   
# We inherit config entries from `default:`
# if the entry has the `inherit: true` flag set
def inherit!(deps) def inherit!(deps)
return unless deps return unless deps
   
self.class.nodes.each_key do |key| self.class.nodes.each do |key, factory|
global_entry = deps[key] next unless factory.inheritable?
default_entry = deps.default[key]
job_entry = self[key] job_entry = self[key]
   
if global_entry.specified? && !job_entry.specified? if default_entry.specified? && !job_entry.specified?
@entries[key] = global_entry @entries[key] = default_entry
end end
end end
end end
...@@ -152,7 +170,7 @@ module Gitlab ...@@ -152,7 +170,7 @@ module Gitlab
cache: cache_value, cache: cache_value,
only: only_value, only: only_value,
except: except_value, except: except_value,
variables: variables_defined? ? variables_value : nil, variables: variables_defined? ? variables_value : {},
environment: environment_defined? ? environment_value : nil, environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil, environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil, coverage: coverage_defined? ? coverage_value : nil,
......
...@@ -14,29 +14,48 @@ module Gitlab ...@@ -14,29 +14,48 @@ module Gitlab
validates :config, type: Hash validates :config, type: Hash
   
validate do validate do
unless has_valid_jobs?
errors.add(:config, 'should contain valid jobs')
end
unless has_visible_job? unless has_visible_job?
errors.add(:config, 'should contain at least one visible job') errors.add(:config, 'should contain at least one visible job')
end end
end end
   
def has_valid_jobs?
config.all? do |name, value|
Jobs.find_type(name, value)
end
end
def has_visible_job? def has_visible_job?
config.any? { |name, _| !hidden?(name) } config.any? do |name, value|
Jobs.find_type(name, value)&.visible?
end
end end
end end
   
def hidden?(name) TYPES = [Entry::Hidden, Entry::Job].freeze
name.to_s.start_with?('.')
private_constant :TYPES
def self.all_types
TYPES
end end
   
def node_type(name) def self.find_type(name, config)
hidden?(name) ? Entry::Hidden : Entry::Job self.all_types.find do |type|
type.matching?(name, config)
end
end end
   
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def compose!(deps = nil) def compose!(deps = nil)
super do super do
@config.each do |name, config| @config.each do |name, config|
node = node_type(name) node = self.class.find_type(name, config)
next unless node
   
factory = ::Gitlab::Config::Entry::Factory.new(node) factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {}) .value(config || {})
......
...@@ -8,52 +8,107 @@ module Gitlab ...@@ -8,52 +8,107 @@ module Gitlab
# This class represents a global entry - root Entry for entire # This class represents a global entry - root Entry for entire
# GitLab CI Configuration file. # GitLab CI Configuration file.
# #
class Global < ::Gitlab::Config::Entry::Node class Root < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Configurable
   
entry :before_script, Entry::Script, ALLOWED_KEYS = %i[default include before_script image services
description: 'Script that will be executed before each job.' after_script variables stages types cache].freeze
   
entry :image, Entry::Image, validations do
description: 'Docker image that will be used to execute jobs.' validates :config, allowed_keys: ALLOWED_KEYS
end
# reserved:
# defines whether the node name is reserved
# the reserved name cannot be used a job name
# reserved should not be used as it will make
# breaking change to `.gitlab-ci.yml`
entry :default, Entry::Default,
description: 'Default configuration for all jobs.',
default: {}
   
entry :include, Entry::Includes, entry :include, Entry::Includes,
description: 'List of external YAML files to include.' description: 'List of external YAML files to include.',
reserved: true
entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.',
reserved: true
entry :image, Entry::Image,
description: 'Docker image that will be used to execute jobs.',
reserved: true
   
entry :services, Entry::Services, entry :services, Entry::Services,
description: 'Docker images that will be linked to the container.' description: 'Docker images that will be linked to the container.',
reserved: true
   
entry :after_script, Entry::Script, entry :after_script, Entry::Script,
description: 'Script that will be executed after each job.' description: 'Script that will be executed after each job.',
reserved: true
   
entry :variables, Entry::Variables, entry :variables, Entry::Variables,
description: 'Environment variables that will be used.' description: 'Environment variables that will be used.',
reserved: true
   
entry :stages, Entry::Stages, entry :stages, Entry::Stages,
description: 'Configuration of stages for this pipeline.' description: 'Configuration of stages for this pipeline.',
reserved: true
   
entry :types, Entry::Stages, entry :types, Entry::Stages,
description: 'Deprecated: stages for this pipeline.' description: 'Deprecated: stages for this pipeline.',
reserved: true
   
entry :cache, Entry::Cache, entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.' description: 'Configure caching between build jobs.',
reserved: true
helpers :default, :jobs, :stages, :types, :variables
delegate :before_script_value,
:image_value,
:services_value,
:after_script_value,
:cache_value, to: :default
   
helpers :before_script, :image, :services, :after_script, attr_reader :jobs_config
:variables, :stages, :types, :cache, :jobs
class << self
include ::Gitlab::Utils::StrongMemoize
def reserved_nodes_names
strong_memoize(:reserved_nodes_names) do
self.nodes.select do |_, node|
node.reserved?
end.keys
end
end
end
def initialize(config, **metadata)
super do
filter_jobs!
end
end
   
def compose!(_deps = nil) def compose!(_deps = nil)
super(self) do super(self) do
compose_jobs!
compose_deprecated_entries! compose_deprecated_entries!
compose_jobs!
end end
end end
   
def default
self[:default]
end
private private
   
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def compose_jobs! def compose_jobs!
factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs) factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys)) .value(jobs_config)
.with(key: :jobs, parent: self, .with(key: :jobs, parent: self,
description: 'Jobs definition for this pipeline') description: 'Jobs definition for this pipeline')
   
...@@ -72,6 +127,18 @@ module Gitlab ...@@ -72,6 +127,18 @@ module Gitlab
   
@entries.delete(:types) @entries.delete(:types)
end end
def filter_jobs!
return unless @config.is_a?(Hash)
@jobs_config = @config
.except(*self.class.reserved_nodes_names) # rubocop: disable CodeReuse/ActiveRecord
.select do |name, config|
Entry::Jobs.find_type(name, config).present?
end
@config = @config.except(*@jobs_config.keys) # rubocop: disable CodeReuse/ActiveRecord
end
end end
end end
end end
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
   
include Gitlab::Config::Entry::LegacyValidationHelpers include Gitlab::Config::Entry::LegacyValidationHelpers
   
attr_reader :cache, :stages, :jobs attr_reader :stages, :jobs
   
def initialize(config, opts = {}) def initialize(config, opts = {})
@ci_config = Gitlab::Ci::Config.new(config, **opts) @ci_config = Gitlab::Ci::Config.new(config, **opts)
...@@ -95,13 +95,8 @@ module Gitlab ...@@ -95,13 +95,8 @@ module Gitlab
## ##
# Global config # Global config
# #
@before_script = @ci_config.before_script
@image = @ci_config.image
@after_script = @ci_config.after_script
@services = @ci_config.services
@variables = @ci_config.variables @variables = @ci_config.variables
@stages = @ci_config.stages @stages = @ci_config.stages
@cache = @ci_config.cache
   
## ##
# Jobs # Jobs
......
...@@ -58,13 +58,21 @@ module Gitlab ...@@ -58,13 +58,21 @@ module Gitlab
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
end end
   
def reserved_node_names
self.nodes.select do |_, node|
node.reserved?
end.keys
end
private private
   
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def entry(key, entry, metadata) def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil)
factory = ::Gitlab::Config::Entry::Factory.new(entry) factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: metadata[:description]) .with(description: description)
.with(default: metadata[:default])