Commit 51ec4902 authored by drew's avatar drew Committed by Kamil Trzciński
Browse files

Created Workflow::Rules configuration

- Added Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules
- Basic E2E spec for skipping Pipelines via workflow:rules
- CI config validation for workflow:rules
parent cb39aebc
......@@ -12,6 +12,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create,
......
# frozen_string_literal: true
module Gitlab
module Ci
module Build
module Context
class Base
attr_reader :pipeline
def initialize(pipeline)
@pipeline = pipeline
end
def variables
raise NotImplementedError
end
protected
def pipeline_attributes
{
pipeline: pipeline,
project: pipeline.project,
user: pipeline.user,
ref: pipeline.ref,
tag: pipeline.tag,
trigger_request: pipeline.legacy_trigger,
protected: pipeline.protected_ref?
}
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Build
module Context
class Build < Base
include Gitlab::Utils::StrongMemoize
attr_reader :attributes
def initialize(pipeline, attributes = {})
super(pipeline)
@attributes = attributes
end
def variables
strong_memoize(:variables) do
# This is a temporary piece of technical debt to allow us access
# to the CI variables to evaluate rules before we persist a Build
# with the result. We should refactor away the extra Build.new,
# but be able to get CI Variables directly from the Seed::Build.
stub_build.scoped_variables_hash
end
end
private
def stub_build
::Ci::Build.new(build_attributes)
end
def build_attributes
attributes.merge(pipeline_attributes)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Build
module Context
class Global < Base
include Gitlab::Utils::StrongMemoize
def initialize(pipeline, yaml_variables:)
super(pipeline)
@yaml_variables = yaml_variables.to_a
end
def variables
strong_memoize(:variables) do
# This is a temporary piece of technical debt to allow us access
# to the CI variables to evaluate workflow:rules
# with the result. We should refactor away the extra Build.new,
# but be able to get CI Variables directly from the Seed::Build.
stub_build.scoped_variables_hash
.reject { |key, _value| key =~ /\ACI_(JOB|BUILD)/ }
end
end
private
def stub_build
::Ci::Build.new(build_attributes)
end
def build_attributes
pipeline_attributes.merge(
yaml_variables: @yaml_variables)
end
end
end
end
end
end
......@@ -9,7 +9,7 @@ module Gitlab
@globs = Array(globs)
end
 
def satisfied_by?(pipeline, seed)
def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
 
pipeline.modified_paths.any? do |path|
......
......@@ -11,7 +11,7 @@ module Gitlab
end
end
 
def satisfied_by?(pipeline, seed = nil)
def satisfied_by?(pipeline, context = nil)
pipeline.has_kubernetes_active?
end
end
......
......@@ -9,7 +9,7 @@ module Gitlab
@patterns = Array(refs)
end
 
def satisfied_by?(pipeline, seed = nil)
def satisfied_by?(pipeline, context = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
 
......
......@@ -17,7 +17,7 @@ module Gitlab
@spec = spec
end
 
def satisfied_by?(pipeline, seed = nil)
def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
......
......@@ -9,8 +9,8 @@ module Gitlab
@expressions = Array(expressions)
end
 
def satisfied_by?(pipeline, seed)
variables = seed.scoped_variables_hash
def satisfied_by?(pipeline, context)
variables = context.variables
 
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
......
......@@ -13,17 +13,21 @@ module Gitlab
options: { start_in: start_in }.compact
}.compact
end
def pass?
self.when != 'never'
end
end
 
def initialize(rule_hashes, default_when = 'on_success')
def initialize(rule_hashes, default_when:)
@rule_list = Rule.fabricate_list(rule_hashes)
@default_when = default_when
end
 
def evaluate(pipeline, build)
def evaluate(pipeline, context)
if @rule_list.nil?
Result.new(@default_when)
elsif matched_rule = match_rule(pipeline, build)
elsif matched_rule = match_rule(pipeline, context)
Result.new(
matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in]
......@@ -35,8 +39,8 @@ module Gitlab
 
private
 
def match_rule(pipeline, build)
@rule_list.find { |rule| rule.matches?(pipeline, build) }
def match_rule(pipeline, context)
@rule_list.find { |rule| rule.matches?(pipeline, context) }
end
end
end
......
......@@ -23,8 +23,8 @@ module Gitlab
end
end
 
def matches?(pipeline, build)
@clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
def matches?(pipeline, context)
@clauses.all? { |clause| clause.satisfied_by?(pipeline, context) }
end
end
end
......
......@@ -20,7 +20,7 @@ module Gitlab
@spec = spec
end
 
def satisfied_by?(pipeline, seed = nil)
def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
......
......@@ -8,7 +8,7 @@ module Gitlab
@globs = Array(globs)
end
 
def satisfied_by?(pipeline, seed)
def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
 
pipeline.modified_paths.any? do |path|
......
......@@ -15,7 +15,7 @@ module Gitlab
@exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?))
end
 
def satisfied_by?(pipeline, seed)
def satisfied_by?(pipeline, context)
paths = worktree_paths(pipeline)
 
exact_matches?(paths) || pattern_matches?(paths)
......
......@@ -8,10 +8,9 @@ module Gitlab
@expression = expression
end
 
def satisfied_by?(pipeline, seed)
variables = seed.scoped_variables_hash
::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
def satisfied_by?(pipeline, context)
::Gitlab::Ci::Pipeline::Expression::Statement.new(
@expression, context.variables).truthful?
end
end
end
......
......@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
 
ALLOWED_KEYS = %i[default include before_script image services
after_script variables stages types cache].freeze
after_script variables stages types cache workflow].freeze
 
validations do
validates :config, allowed_keys: ALLOWED_KEYS
......@@ -64,6 +64,9 @@ module Gitlab
description: 'Configure caching between build jobs.',
reserved: true
 
entry :workflow, Entry::Workflow,
description: 'List of evaluable rules to determine Pipeline status'
helpers :default, :jobs, :stages, :types, :variables
 
delegate :before_script_value,
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
class Workflow < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[rules].freeze
validations do
validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, presence: true
end
entry :rules, Entry::Rules,
description: 'List of evaluable Rules to determine Pipeline status.',
metadata: { allowed_when: %w[always never] }
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
class EvaluateWorkflowRules < Chain::Base
include ::Gitlab::Utils::StrongMemoize
include Chain::Helpers
def perform!
return unless Feature.enabled?(:workflow_rules, @pipeline.project)
unless workflow_passed?
error('Pipeline filtered out by workflow rules.')
end
end
def break?
return false unless Feature.enabled?(:workflow_rules, @pipeline.project)
!workflow_passed?
end
private
def workflow_passed?
strong_memoize(:workflow_passed) do
workflow_rules.evaluate(@pipeline, global_context).pass?
end
end
def workflow_rules
Gitlab::Ci::Build::Rules.new(
workflow_config[:rules], default_when: 'always')
end
def global_context
Gitlab::Ci::Build::Context::Global.new(
@pipeline, yaml_variables: workflow_config[:yaml_variables])
end
def workflow_config
@pipeline.config_processor.workflow_attributes || {}
end
end
end
end
end
end
......@@ -28,7 +28,7 @@ module Gitlab
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules
.new(attributes.delete(:rules))
.new(attributes.delete(:rules), default_when: 'on_success')
@cache = Seed::Build::Cache
.new(pipeline, attributes.delete(:cache))
end
......@@ -40,7 +40,7 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
if @using_rules
included_by_rules?
rules_result.pass?
elsif @using_only || @using_except
all_of_only? && none_of_except?
else
......@@ -83,26 +83,14 @@ module Gitlab
end
end
 
def scoped_variables_hash
strong_memoize(:scoped_variables_hash) do
# This is a temporary piece of technical debt to allow us access
# to the CI variables to evaluate rules before we persist a Build
# with the result. We should refactor away the extra Build.new,
# but be able to get CI Variables directly from the Seed::Build.
::Ci::Build.new(
@seed_attributes.merge(pipeline_attributes)
).scoped_variables_hash
end
end
private
 
def all_of_only?
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
@only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
 
def none_of_except?
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
@except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
 
def needs_errors
......@@ -144,13 +132,21 @@ module Gitlab
}
end
 
def included_by_rules?
rules_attributes[:when] != 'never'
def rules_attributes
return {} unless @using_rules
rules_result.build_attributes
end
 
def rules_attributes
strong_memoize(:rules_attributes) do
@using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
def rules_result
strong_memoize(:rules_result) do
@rules.evaluate(@pipeline, evaluate_context)
end
end
def evaluate_context
strong_memoize(:evaluate_context) do
Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes)
end
end
 
......
......@@ -39,7 +39,7 @@ module Gitlab
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
yaml_variables: transform_to_yaml_variables(job_variables(name)),
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
rules: job[:rules],
......@@ -83,6 +83,13 @@ module Gitlab
end
end
 
def workflow_attributes
{
rules: @config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(@variables)
}
end
def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
 
......@@ -118,20 +125,17 @@ module Gitlab
end
end
 
def yaml_variables(name)
variables = (@variables || {})
.merge(job_variables(name))
def job_variables(name)
job_variables = @jobs.dig(name.to_sym, :variables)
 
variables.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
@variables.to_h
.merge(job_variables.to_h)
end
 
def job_variables(name)
job = @jobs[name.to_sym]
return {} unless job
job[:variables] || {}
def transform_to_yaml_variables(variables)
variables.to_h.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
end
 
def validate_job_stage!(name, job)
......
Supports Markdown
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