Commit e8ce2068 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'feature/gb/variables-expressions-in-only-except-ee' into 'master'

Implement pipeline variables expressions / EE version

See merge request gitlab-org/gitlab-ee!5127
parents 512a1241 4698453e
......@@ -6,6 +6,8 @@ module Ci
include ObjectStorage::BackgroundMove
include Presentable
include Importable
include Gitlab::Utils::StrongMemoize
prepend EE::Ci::Build
MissingDependenciesError = Class.new(StandardError)
......@@ -28,15 +30,17 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
# The "environment" field for builds is a String, and is the unexpanded name
##
# The "environment" field for builds is a String, and is the unexpanded name!
#
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
project: project
)
return unless has_environment?
strong_memoize(:persisted_environment) do
Environment.find_by(name: expanded_environment_name, project: project)
end
end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
......@@ -215,7 +219,11 @@ module Ci
end
def expanded_environment_name
ExpandVariables.expand(environment, simple_variables) if environment
return unless has_environment?
strong_memoize(:expanded_environment_name) do
ExpandVariables.expand(environment, simple_variables)
end
end
def has_environment?
......@@ -261,31 +269,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s)
end
# Variables whose value does not depend on environment
def simple_variables
variables(environment: nil)
end
# All variables, including those dependent on environment, which could
# contain unexpanded variables.
def variables(environment: persisted_environment)
collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
##
# Variables in the environment name scope.
#
def scoped_variables(environment: expanded_environment_name)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
variables.concat(project.deployment_variables(environment: environment)) if has_environment?
variables.concat(project.deployment_variables(environment: environment)) if environment
variables.concat(yaml_variables)
variables.concat(user_variables)
variables.concat(project.group.secret_variables_for(ref, project)) if project.group
variables.concat(secret_variables(environment: environment))
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables.concat(persisted_environment_variables) if environment
end
end
collection.to_runner_variables
##
# Variables that do not depend on the environment name.
#
def simple_variables
strong_memoize(:simple_variables) do
scoped_variables(environment: nil).to_runner_variables
end
end
##
# All variables, including persisted environment variables.
#
def variables
Gitlab::Ci::Variables::Collection.new
.concat(persisted_variables)
.concat(scoped_variables)
.concat(persisted_environment_variables)
.to_runner_variables
end
##
# Regular Ruby hash of scoped variables, without duplicates that are
# possible to be present in an array of hashes returned from `variables`.
#
def scoped_variables_hash
scoped_variables.to_hash
end
def features
......@@ -462,9 +491,14 @@ module Ci
end
end
def secret_variables(environment: persisted_environment)
def secret_group_variables
return [] unless project.group
project.group.secret_variables_for(ref, project)
end
def secret_project_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
.map(&:to_runner_variable)
end
def steps
......@@ -561,6 +595,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted?
variables
.append(key: 'CI_JOB_ID', value: id.to_s)
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
end
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
......@@ -569,16 +618,11 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
......@@ -586,23 +630,8 @@ module Ci
end
end
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted_environment
variables.concat(persisted_environment.predefined_variables)
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
end
end
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_ID', value: id.to_s)
variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
......@@ -615,6 +644,19 @@ module Ci
end
end
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
end
end
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
......
---
title: Add support for pipeline variables expressions in only/except
merge_request: 17316
author:
type: added
......@@ -6,7 +6,8 @@ module EE
def find_cluster_platform_kubernetes(environment: nil)
return super unless environment && feature_available?(:multiple_clusters)
clusters.enabled.on_environment(environment.name)
clusters.enabled
.on_environment(environment)
.last&.platform_kubernetes
end
end
......
......@@ -286,7 +286,7 @@ module EE
return super.where(environment_scope: '*') unless
environment && feature_available?(:variable_environment_scope)
super.on_environment(environment.name)
super.on_environment(environment)
end
def execute_hooks(data, hooks_scope = :push_hooks)
......
require 'spec_helper'
describe Gitlab::Ci::Build::Policy::Variables do
let(:project) { create(:project) }
let(:pipeline) do
build(:ci_empty_pipeline, project: project, ref: 'master')
end
let(:ci_build) do
build(:ci_build, pipeline: pipeline,
project: project,
ref: 'master',
stage: 'review',
environment: 'test/$CI_JOB_STAGE/1')
end
let(:seed) { double('build seed', to_resource: ci_build) }
describe '#satisfied_by?' do
context 'when using project secret variables in environment scope' do
before do
create(:ci_variable, project: project,
key: 'SCOPED_VARIABLE',
value: 'my-value-1')
create(:ci_variable, project: project,
key: 'SCOPED_VARIABLE',
value: 'my-value-2',
environment_scope: 'test/review/*')
end
context 'when environment scope variables feature is enabled' do
before do
stub_licensed_features(variable_environment_scope: true)
end
it 'is satisfied by scoped variable match' do
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-2"'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'is not satisfied when matching against overridden variable' do
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-1"'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
end
context 'when environment scope variables feature is disabled' do
before do
stub_licensed_features(variable_environment_scope: false)
end
it 'is not satisfied by scoped variable match' do
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-2"'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'is satisfied when matching against unscoped variable' do
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-1"'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
end
end
end
end
......@@ -51,7 +51,8 @@ describe EE::DeploymentPlatform do
context 'when environment is specified' do
let!(:default_cluster) { create(:cluster, :provided_by_user, projects: [project], environment_scope: '*') }
let!(:cluster) { create(:cluster, :provided_by_user, environment_scope: 'review/*', projects: [project]) }
let(:environment) { create(:environment, project: project, name: 'review/name') }
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
......@@ -90,11 +91,14 @@ describe EE::DeploymentPlatform do
is_expected.to eq(default_cluster.platform_kubernetes)
end
it 'matches literally for _' do
cluster.update!(environment_scope: 'foo_bar/*')
environment.update!(name: 'foo_bar/test')
context 'when environment name contains an underscore' do
let(:environment) { 'foo_bar/test' }
is_expected.to eq(cluster.platform_kubernetes)
it 'matches literally for _' do
cluster.update!(environment_scope: 'foo_bar/*')
is_expected.to eq(cluster.platform_kubernetes)
end
end
end
......@@ -113,11 +117,14 @@ describe EE::DeploymentPlatform do
is_expected.to eq(default_cluster.platform_kubernetes)
end
it 'matches literally for %' do
cluster.update_attribute(:environment_scope, 'foo%bar/*')
environment.update_attribute(:name, 'foo%bar/test')
context 'when environment name contains a percent char' do
let(:environment) { 'foo%bar/test' }
is_expected.to eq(cluster.platform_kubernetes)
it 'matches literally for %' do
cluster.update_attribute(:environment_scope, 'foo%bar/*')
is_expected.to eq(cluster.platform_kubernetes)
end
end
end
......@@ -137,8 +144,9 @@ describe EE::DeploymentPlatform do
context 'with multiple clusters and multiple environments' do
let!(:cluster_1) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'staging/*') }
let!(:cluster_2) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'test/*') }
let(:environment_1) { create(:environment, project: project, name: 'staging/name') }
let(:environment_2) { create(:environment, project: project, name: 'test/name') }
let(:environment_1) { 'staging/name' }
let(:environment_2) { 'test/name' }
before do
stub_licensed_features(multiple_clusters: true)
......
......@@ -852,8 +852,8 @@ describe Project do
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
context 'when environment is specified' do
let(:environment) { create(:environment, name: 'review/name') }
context 'when environment name is specified' do
let(:environment) { 'review/name' }
subject do
project.secret_variables_for(ref: 'ref', environment: environment)
......@@ -938,11 +938,14 @@ describe Project do
is_expected.not_to contain_exactly(secret_variable)
end
it 'matches literally for _' do
secret_variable.update(environment_scope: 'foo_bar/*')
environment.update(name: 'foo_bar/test')
context 'when environment name contains underscore' do
let(:environment) { 'foo_bar/test' }
is_expected.to contain_exactly(secret_variable)
it 'matches literally for _' do
secret_variable.update(environment_scope: 'foo_bar/*')
is_expected.to contain_exactly(secret_variable)
end
end
end
......@@ -961,11 +964,14 @@ describe Project do
is_expected.not_to contain_exactly(secret_variable)
end
it 'matches literally for _' do
secret_variable.update(environment_scope: 'foo%bar/*')
environment.update_attribute(:name, 'foo%bar/test')
context 'when environment name contains a percent' do
let(:environment) { 'foo%bar/test' }
is_expected.to contain_exactly(secret_variable)
it 'matches literally for _' do
secret_variable.update(environment_scope: 'foo%bar/*')
is_expected.to contain_exactly(secret_variable)
end
end
end
......
......@@ -9,7 +9,7 @@ module Gitlab
end
end
def satisfied_by?(pipeline)
def satisfied_by?(pipeline, seed = nil)
pipeline.has_kubernetes_active?
end
end
......
......@@ -7,7 +7,7 @@ module Gitlab
@patterns = Array(refs)
end
def satisfied_by?(pipeline)
def satisfied_by?(pipeline, seed = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
......
......@@ -15,7 +15,7 @@ module Gitlab
@spec = spec
end
def satisfied_by?(pipeline)
def satisfied_by?(pipeline, seed = nil)
raise NotImplementedError
end
end
......
module Gitlab
module Ci
module Build
module Policy
class Variables < Policy::Specification
def initialize(expressions)
@expressions = Array(expressions)
end
def satisfied_by?(pipeline, seed)
variables = seed.to_resource.scoped_variables_hash
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
.new(statement, variables)
end
statements.any?(&:truthful?)
end
end
end
end
end
end
......@@ -25,15 +25,31 @@ module Gitlab
include Entry::Validatable
include Entry::Attributable
attributes :refs, :kubernetes
attributes :refs, :kubernetes, :variables
validations do
validates :config, presence: true
validates :config, allowed_keys: %i[refs kubernetes]
validates :config, allowed_keys: %i[refs kubernetes variables]
validate :variables_expressions_syntax
with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true
validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true
end
def variables_expressions_syntax
return unless variables.is_a?(Array)
statements = variables.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement.new(statement)
end
statements.each do |statement|
unless statement.valid?
errors.add(:variables, "Invalid expression syntax")
end
end
end
end
end
......
......@@ -17,8 +17,6 @@ module Gitlab
# Populate pipeline with all stages and builds from pipeline seeds.
#
pipeline.stage_seeds.each do |stage|
stage.user = current_user
pipeline.stages << stage.to_resource
stage.seeds.each do |build|
......
......@@ -4,7 +4,7 @@ module Gitlab
module Expression
module Lexeme
class String < Lexeme::Value
PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
def initialize(value)
@value = value
......
......@@ -11,7 +11,7 @@ module Gitlab
end
def evaluate(variables = {})
HashWithIndifferentAccess.new(variables).fetch(@name, nil)
variables.with_indifferent_access.fetch(@name, nil)
end
def self.build(string)
......
......@@ -14,12 +14,9 @@ module Gitlab
%w[variable]
].freeze
def initialize(statement, pipeline)
def initialize(statement, variables = {})
@lexer = Expression::Lexer.new(statement)
@variables = pipeline.variables.map do |variable|
[variable.key, variable.value]
end
@variables = variables.with_indifferent_access
end
def parse_tree
......@@ -35,6 +32,16 @@ module Gitlab
def evaluate
parse_tree.evaluate(@variables.to_h)
end
def truthful?
evaluate.present?
end
def valid?
parse_tree.is_a?(Lexeme::Base)
rescue StatementError
false
end
end
end
end
......
......@@ -11,21 +11,16 @@ module Gitlab
@pipeline = pipeline
@attributes = attributes
@only = attributes.delete(:only)
@except = attributes.delete(:except)
end
def user=(current_user)
@attributes.merge!(user: current_user)
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
end
def included?
strong_memoize(:inclusion) do
only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) } &&
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
end
end
......@@ -33,6 +28,7 @@ module Gitlab
@attributes.merge(
pipeline: @pipeline,
project: @pipeline.project,
user: @pipeline.user,
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger,
......
......@@ -17,10 +17,6 @@ module Gitlab
end
end
def user=(current_user)
@builds.each { |seed| seed.user = current_user }
end
def attributes
{ name: @attributes.fetch(:name),
pipeline: @pipeline,
......
......@@ -30,7 +30,13 @@ module Gitlab
end
def to_runner_variables
self.map(&:to_hash)
self.map(&:to_runner_variable)
end
def to_hash
self.to_runner_variables
.map { |env| [env.fetch(:key), env.fetch(:value)] }
.to_h.with_indifferent_access
end
end
end
......
......@@ -17,7 +17,7 @@ module Gitlab
end
def ==(other)
to_hash == self.class.fabricate(other).to_hash
to_runner_variable == self.class.fabricate(other).to_runner_variable
end
##
......@@ -25,7 +25,7 @@ module Gitlab
# don't expose `file` attribute at all (stems from what the runner
# expects).
#
def to_hash
def to_runner_variable
@variable.reject do |hash_key, hash_value|
hash_key == :file && hash_value == false
end
......
require 'spec_helper'
describe Gitlab::Ci::Build::Policy::Variables do
set(:project) { create(:project) }
let(:pipeline) do
build(:ci_empty_pipeline, project: project, ref: 'master', source: :push)
end
let(:ci_build) do
build(:ci_build, pipeline: pipeline, project: project, ref: 'master')
end
let(:seed) { double('build seed', to_resource: ci_build) }
before do
pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
end
describe '#satisfied_by?' do
it 'is satisfied by at least one matching statement' do
policy = described_class.new(['$CI_PROJECT_ID', '$UNDEFINED'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'is not satisfied by an overriden empty variable' do
policy = described_class.new(['$CI_PROJECT_NAME'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'is satisfied by a truthy pipeline expression' do
policy = described_class.new([%($CI_PIPELINE_SOURCE == "push")])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'is not satisfied by a falsy pipeline expression' do
policy = described_class.new([%($CI_PIPELINE_SOURCE == "invalid source")])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'is satisfied by a truthy expression using undefined variable' do
policy = described_class.new(['$UNDEFINED == null'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'is not satisfied by a falsy expression using undefined variable' do
policy = described_class.new(['$UNDEFINED'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'allows to evaluate regular secret variables' do
create(:ci_variable, project: project, key: 'SECRET', value: 'my secret')
policy = described_class.new(["$SECRET == 'my secret'"])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'does not persist neither pipeline nor build' do
described_class.new('$VAR').satisfied_by?(pipeline, seed)
expect(pipeline).not_to be_persisted
expect(seed.to_resource).not_to be_persisted
end
end
end
......@@ -83,6 +83,39 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
context 'when specifying valid variables expressions policy' do
let(:config) { { variables: ['$VAR == null'] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(config)
end
end
context 'when specifying variables expressions in invalid format' do
let(:config) { { variables: '$MY_VAR' } }
it 'reports an error about invalid format' do
expect(entry.errors).to include /should be an array of strings/
end
end
context 'when specifying invalid variables expressions statement' do
let(:config) { { variables: ['$MY_VAR =='] } }
it 'reports an error about invalid statement' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
it 'reports an error about invalid statement' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } }
......
......@@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project,
ref: 'master')
ref: 'master',
user: user)
end
let(:command) do
......@@ -42,6 +43,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
it 'correctly assigns user' do
expect(pipeline.builds).to all(have_attributes(user: user))
end
end
context 'when pipeline is empty' do
......
......@@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some " string'
end
it 'allows to use an empty string inside single quotes' do
scanner = StringScanner.new(%(''))
token = described_class.scan(scanner)
expect(token.build.evaluate).to eq ''
end
it 'allow to use an empty string inside double quotes' do
scanner = StringScanner.new(%(""))
token = described_class.scan(scanner)
expect(token.build.evaluate).to eq ''
end
end
end
......
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:pipeline) { build(:ci_pipeline) }
subject do
described_class.new(text, pipeline)
described_class.new(text, variables)
end
let(:variables) do
{ 'PRESENT_VARIABLE' => 'my variable',
EMPTY_VARIABLE: '' }
end
before do
pipeline.variables.build([key: 'VARIABLE', value: 'my variable'])
describe '.new' do
context 'when variables are not provided' do
it 'allows to properly initializes the statement' do
statement = described_class.new('$PRESENT_VARIABLE')
expect(statement.evaluate).to be_nil
end
end
end
describe '#parse_tree' do
......@@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context 'when expression grammar is incorrect' do
table = [
'$VAR "text"', # missing operator
'== "123"', # invalid right side
"'single quotes'", # single quotes string
'$VAR ==', # invalid right side
'12345', # unknown syntax
'' # empty statement
'$VAR "text"', # missing operator
'== "123"', # invalid left side
'"some string"', # only string provided
'$VAR ==', # invalid right side
'12345', # unknown syntax
'' # empty statement
]
table.each do |syntax|
it "raises an error when syntax is `#{syntax}`" do
expect { described_class.new(syntax, pipeline).parse_tree }
.to raise_error described_class::StatementError
context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax }
it 'aises a statement error exception' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
it 'is an invalid statement' do
expect(subject).not_to be_valid
end
end
end
end
......@@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
expect(subject.parse_tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
it 'is a valid statement' do
expect(subject).to be_valid
end
end
context 'when using a single token' do
let(:text) { '$VARIABLE' }
let(:text) { '$PRESENT_VARIABLE' }
it 'returns a single token instance' do
expect(subject.parse_tree)
......@@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
describe '#evaluate' do
statements = [
['$VARIABLE == "my variable"', true],
["$VARIABLE == 'my variable'", true],
['"my variable" == $VARIABLE', true],
['$VARIABLE == null', false],
['$VAR == null', true],
['null == $VAR', true],
['$VARIABLE', 'my variable'],
['$VAR', nil]
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'my variable'", true],
['"my variable" == $PRESENT_VARIABLE', true],
['$PRESENT_VARIABLE == null', false],
['$EMPTY_VARIABLE == null', false],
['"" == $EMPTY_VARIABLE', true],
['$EMPTY_VARIABLE', ''],
['$UNDEFINED_VARIABLE == null', true],
['null == $UNDEFINED_VARIABLE', true],
['$PRESENT_VARIABLE', 'my variable'],
['$UNDEFINED_VARIABLE', nil]
]
statements.each do |expression, value|
......@@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
end
end
describe '#truthful?' do
statements = [
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'no match'", false],
['$UNDEFINED_VARIABLE == null', true],
['$PRESENT_VARIABLE', true],
['$UNDEFINED_VARIABLE', false],
['$EMPTY_VARIABLE', false]
]
statements.each do |expression, value|
context "when using expression `#{expression}`" do
let(:text) { expression }
it "returns `#{value.inspect}`" do
expect(subject.truthful?).to eq value
end
end
end
end
end
......@@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
describe '#user=' do
let(:user) { build(:user) }
it 'assignes user to a build' do
subject.user = user
expect(subject.attributes).to include(user: user)
end
end
describe '#to_resource' do
it 'returns a valid build resource' do
expect(subject.to_resource).to be_a(::Ci::Build)
......
......@@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end
end
describe '#user=' do
let(:user) { build(:user) }
it 'assignes relevant pipeline attributes' do
subject.user = user
expect(subject.seeds.map(&:attributes)).to all(include(user: user))
end
end
describe '#to_resource' do
it 'builds a valid stage object with all builds' do
subject.to_resource.save!
......
......@@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do
end
end
describe '#to_hash' do
it 'returns a hash representation of a collection item' do
expect(described_class.new(**variable).to_hash).to eq variable
describe '#to_runner_variable' do
it 'returns a runner-compatible hash representation' do
runner_variable = described_class
.new(**variable)
.to_runner_variable
expect(runner_variable).to eq variable
end
end
end
......@@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do
collection = described_class.new([variable])
expect(collection.first.to_hash).to eq variable
expect(collection.first.to_runner_variable).to eq variable
end
it 'can be initialized without an argument' do
......@@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do
.to eq [{ key: 'TEST', value: 1, public: true }]
end
end
describe '#to_hash' do
it 'returns regular hash in valid order without duplicates' do
collection = described_class.new
.append(key: 'TEST1', value: 'test-1')
.append(key: 'TEST2', value: 'test-2')
.append(key: 'TEST1', value: 'test-3')
expect(collection.to_hash).to eq('TEST1' => 'test-3',
'TEST2' => 'test-2')
expect(collection.to_hash).to include(TEST1: 'test-3')
expect(collection.to_hash).not_to include(TEST1: 'test-1')
end
end
end
......@@ -1311,6 +1311,14 @@ module Gitlab
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
it 'returns errors if pipeline variables expression is invalid' do
config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } })
expect { Gitlab::Ci::YamlProcessor.new(config) }
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only variables invalid expression syntax')
end
end
describe "Validate configuration templates" do
......
......@@ -1483,24 +1483,24 @@ describe Ci::Build do
let(:container_registry_enabled) { false }
let(:predefined_variables) do
[
{ key: 'CI_JOB_ID', value: build.id.to_s, public: true },
{ key: 'CI_JOB_TOKEN', value: build.token, public: false },
{ key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: build.token, public: false },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
{ key: 'CI_JOB_ID', value: build.id.to_s, public: true },
{ key: 'CI_JOB_NAME', value: 'test', public: true },
{ key: 'CI_JOB_STAGE', value: 'test', public: true },
{ key: 'CI_JOB_TOKEN', value: build.token, public: false },
{ key: 'CI_COMMIT_SHA', value: build.sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
{ key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: build.token, public: false },
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
......@@ -1965,6 +1965,7 @@ describe Ci::Build do
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow_any_instance_of(Project)
.to receive(:predefined_variables) { [project_pre_var] }
......@@ -2013,6 +2014,106 @@ describe Ci::Build do
end
end
end
context 'when build has not been persisted yet' do
let(:build) do
described_class.new(
name: 'rspec',
stage: 'test',
ref: 'feature',
project: project,
pipeline: pipeline
)
end
it 'returns static predefined variables' do
expect(build.variables.size).to be >= 28
expect(build.variables)
.to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
expect(build).not_to be_persisted
end
end
end
describe '#scoped_variables' do
context 'when build has not been persisted yet' do
let(:build) do
described_class.new(
name: 'rspec',
stage: 'test',
ref: 'feature',
project: project,
pipeline: pipeline
)
end
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
build.scoped_variables
expect(build).not_to be_persisted
end
it 'returns static predefined variables' do
keys = %w[CI_JOB_NAME
CI_COMMIT_SHA
CI_COMMIT_REF_NAME
CI_COMMIT_REF_SLUG
CI_JOB_STAGE]
variables = build.scoped_variables
variables.map { |env| env[:key] }.tap do |names|
expect(names).to include(*keys)
end
expect(variables)
.to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
end
it 'does not return prohibited variables' do
keys = %w[CI_JOB_ID
CI_JOB_TOKEN
CI_BUILD_ID
CI_BUILD_TOKEN
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
CI_ENVIRONMENT_URL]
build.scoped_variables.map { |env| env[:key] }.tap do |names|
expect(names).not_to include(*keys)
end
end
end
end
describe '#scoped_variables_hash' do
context 'when overriding secret variables' do
before do
project.variables.create!(key: 'MY_VAR', value: 'my value 1')
pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2')
end
it 'returns a regular hash created using valid ordering' do
expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2')
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
end
end
context 'when overriding user-provided variables' do
before do
pipeline.variables.build(key: 'MY_VAR', value: 'pipeline value')
build.yaml_variables = [{ key: 'MY_VAR', value: 'myvar', public: true }]
end
it 'returns a hash including variable with higher precedence' do
expect(build.scoped_variables_hash).to include('MY_VAR': 'pipeline value')
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar')
end
end
end
describe 'state transition: any => [:pending]' do
......
......@@ -353,6 +353,20 @@ describe Ci::Pipeline, :mailer do
end
end
end
context 'when variables policy is specified' do
let(:config) do
{ unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } },
feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } }
end
it 'returns stage seeds only when variables expression is truthy' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 1
expect(seeds.dig(0, 0, :name)).to eq 'unit'
end
end
end
describe '#seeds_size' do
......
......@@ -2448,15 +2448,12 @@ describe Project do
let(:default_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'default-AAA') }
let(:review_env_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'review-AAA') }
subject { project.deployment_variables(environment: environment) }
context 'when environment name is review/name' do
let!(:environment) { create(:environment, project: project, name: 'review/name') }
it 'returns variables from this service' do
expect(subject).to include(
{ key: 'KUBE_TOKEN', value: 'review-AAA', public: false }
)
expect(project.deployment_variables(environment: 'review/name'))
.to include(key: 'KUBE_TOKEN', value: 'review-AAA', public: false)
end
end
......@@ -2464,9 +2461,8 @@ describe Project do
let!(:environment) { create(:environment, project: project, name: 'staging/name') }
it 'returns variables from this service' do
expect(subject).to include(
{ key: 'KUBE_TOKEN', value: 'default-AAA', public: false }
)
expect(project.deployment_variables(environment: 'staging/name'))
.to include(key: 'KUBE_TOKEN', value: 'default-AAA', public: false)
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