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 ...@@ -6,6 +6,8 @@ module Ci
include ObjectStorage::BackgroundMove include ObjectStorage::BackgroundMove
include Presentable include Presentable
include Importable include Importable
include Gitlab::Utils::StrongMemoize
prepend EE::Ci::Build prepend EE::Ci::Build
MissingDependenciesError = Class.new(StandardError) MissingDependenciesError = Class.new(StandardError)
...@@ -28,15 +30,17 @@ module Ci ...@@ -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 :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' has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true 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 def persisted_environment
@persisted_environment ||= Environment.find_by( return unless has_environment?
name: expanded_environment_name,
project: project strong_memoize(:persisted_environment) do
) Environment.find_by(name: expanded_environment_name, project: project)
end
end end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize serialize :options # rubocop:disable Cop/ActiveRecordSerialize
...@@ -215,7 +219,11 @@ module Ci ...@@ -215,7 +219,11 @@ module Ci
end end
def expanded_environment_name 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 end
def has_environment? def has_environment?
...@@ -261,31 +269,52 @@ module Ci ...@@ -261,31 +269,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s) Gitlab::Utils.slugify(ref.to_s)
end end
# Variables whose value does not depend on environment ##
def simple_variables # Variables in the environment name scope.
variables(environment: nil) #
end def scoped_variables(environment: expanded_environment_name)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
# 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.concat(predefined_variables) variables.concat(predefined_variables)
variables.concat(project.predefined_variables) variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables) variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner 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(yaml_variables)
variables.concat(user_variables) variables.concat(user_variables)
variables.concat(project.group.secret_variables_for(ref, project)) if project.group variables.concat(secret_group_variables)
variables.concat(secret_variables(environment: environment)) variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables) variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables.concat(persisted_environment_variables) if environment
end 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 end
def features def features
...@@ -462,9 +491,14 @@ module Ci ...@@ -462,9 +491,14 @@ module Ci
end end
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) project.secret_variables_for(ref: ref, environment: environment)
.map(&:to_runner_variable)
end end
def steps def steps
...@@ -561,6 +595,21 @@ module Ci ...@@ -561,6 +595,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze 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 def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true') variables.append(key: 'CI', value: 'true')
...@@ -569,16 +618,11 @@ module Ci ...@@ -569,16 +618,11 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab') variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION) 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_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage) 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_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref) variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug) 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_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
...@@ -586,23 +630,8 @@ module Ci ...@@ -586,23 +630,8 @@ module Ci
end end
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 def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |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_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha) variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref) variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
...@@ -615,6 +644,19 @@ module Ci ...@@ -615,6 +644,19 @@ module Ci
end end
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 def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url options&.dig(:environment, :url) || persisted_environment&.external_url
end end
......
---
title: Add support for pipeline variables expressions in only/except
merge_request: 17316
author:
type: added
...@@ -6,7 +6,8 @@ module EE ...@@ -6,7 +6,8 @@ module EE
def find_cluster_platform_kubernetes(environment: nil) def find_cluster_platform_kubernetes(environment: nil)
return super unless environment && feature_available?(:multiple_clusters) return super unless environment && feature_available?(:multiple_clusters)
clusters.enabled.on_environment(environment.name) clusters.enabled
.on_environment(environment)
.last&.platform_kubernetes .last&.platform_kubernetes
end end
end end
......
...@@ -286,7 +286,7 @@ module EE ...@@ -286,7 +286,7 @@ module EE
return super.where(environment_scope: '*') unless return super.where(environment_scope: '*') unless
environment && feature_available?(:variable_environment_scope) environment && feature_available?(:variable_environment_scope)
super.on_environment(environment.name) super.on_environment(environment)
end end
def execute_hooks(data, hooks_scope = :push_hooks) 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 ...@@ -51,7 +51,8 @@ describe EE::DeploymentPlatform do
context 'when environment is specified' do context 'when environment is specified' do
let!(:default_cluster) { create(:cluster, :provided_by_user, projects: [project], environment_scope: '*') } 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!(: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) } subject { project.deployment_platform(environment: environment) }
...@@ -90,11 +91,14 @@ describe EE::DeploymentPlatform do ...@@ -90,11 +91,14 @@ describe EE::DeploymentPlatform do
is_expected.to eq(default_cluster.platform_kubernetes) is_expected.to eq(default_cluster.platform_kubernetes)
end end
it 'matches literally for _' do context 'when environment name contains an underscore' do
cluster.update!(environment_scope: 'foo_bar/*') let(:environment) { 'foo_bar/test' }
environment.update!(name: '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
end end
...@@ -113,11 +117,14 @@ describe EE::DeploymentPlatform do ...@@ -113,11 +117,14 @@ describe EE::DeploymentPlatform do
is_expected.to eq(default_cluster.platform_kubernetes) is_expected.to eq(default_cluster.platform_kubernetes)
end end
it 'matches literally for %' do context 'when environment name contains a percent char' do
cluster.update_attribute(:environment_scope, 'foo%bar/*') let(:environment) { 'foo%bar/test' }
environment.update_attribute(:name, '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
end end
...@@ -137,8 +144,9 @@ describe EE::DeploymentPlatform do ...@@ -137,8 +144,9 @@ describe EE::DeploymentPlatform do
context 'with multiple clusters and multiple environments' do context 'with multiple clusters and multiple environments' do
let!(:cluster_1) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'staging/*') } 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!(: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 before do
stub_licensed_features(multiple_clusters: true) stub_licensed_features(multiple_clusters: true)
......
...@@ -852,8 +852,8 @@ describe Project do ...@@ -852,8 +852,8 @@ describe Project do
default_branch_protection: Gitlab::Access::PROTECTION_NONE) default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end end
context 'when environment is specified' do context 'when environment name is specified' do
let(:environment) { create(:environment, name: 'review/name') } let(:environment) { 'review/name' }
subject do subject do
project.secret_variables_for(ref: 'ref', environment: environment) project.secret_variables_for(ref: 'ref', environment: environment)
...@@ -938,11 +938,14 @@ describe Project do ...@@ -938,11 +938,14 @@ describe Project do
is_expected.not_to contain_exactly(secret_variable) is_expected.not_to contain_exactly(secret_variable)
end end
it 'matches literally for _' do context 'when environment name contains underscore' do
secret_variable.update(environment_scope: 'foo_bar/*') let(:environment) { 'foo_bar/test' }
environment.update(name: '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
end end
...@@ -961,11 +964,14 @@ describe Project do ...@@ -961,11 +964,14 @@ describe Project do
is_expected.not_to contain_exactly(secret_variable) is_expected.not_to contain_exactly(secret_variable)
end end
it 'matches literally for _' do context 'when environment name contains a percent' do
secret_variable.update(environment_scope: 'foo%bar/*') let(:environment) { 'foo%bar/test' }
environment.update_attribute(:name, '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
end end
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
end end
end end
def satisfied_by?(pipeline) def satisfied_by?(pipeline, seed = nil)
pipeline.has_kubernetes_active? pipeline.has_kubernetes_active?
end end
end end
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
@patterns = Array(refs) @patterns = Array(refs)
end end
def satisfied_by?(pipeline) def satisfied_by?(pipeline, seed = nil)
@patterns.any? do |pattern| @patterns.any? do |pattern|
pattern, path = pattern.split('@', 2) pattern, path = pattern.split('@', 2)
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
@spec = spec @spec = spec
end end
def satisfied_by?(pipeline) def satisfied_by?(pipeline, seed = nil)
raise NotImplementedError raise NotImplementedError
end end
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 ...@@ -25,15 +25,31 @@ module Gitlab
include Entry::Validatable include Entry::Validatable
include Entry::Attributable include Entry::Attributable
attributes :refs, :kubernetes attributes :refs, :kubernetes, :variables
validations do validations do
validates :config, presence: true 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 with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true validates :refs, array_of_strings_or_regexps: true
validates :kubernetes, allowed_values: %w[active] 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 end
end end
......
...@@ -17,8 +17,6 @@ module Gitlab ...@@ -17,8 +17,6 @@ module Gitlab
# Populate pipeline with all stages and builds from pipeline seeds. # Populate pipeline with all stages and builds from pipeline seeds.
# #
pipeline.stage_seeds.each do |stage| pipeline.stage_seeds.each do |stage|
stage.user = current_user
pipeline.stages << stage.to_resource pipeline.stages << stage.to_resource
stage.seeds.each do |build| stage.seeds.each do |build|
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module Expression module Expression
module Lexeme module Lexeme
class String < Lexeme::Value class String < Lexeme::Value
PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
def initialize(value) def initialize(value)
@value = value @value = value
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def evaluate(variables = {}) def evaluate(variables = {})
HashWithIndifferentAccess.new(variables).fetch(@name, nil) variables.with_indifferent_access.fetch(@name, nil)
end end
def self.build(string) def self.build(string)
......
...@@ -14,12 +14,9 @@ module Gitlab ...@@ -14,12 +14,9 @@ module Gitlab
%w[variable] %w[variable]
].freeze ].freeze
def initialize(statement, pipeline) def initialize(statement, variables = {})
@lexer = Expression::Lexer.new(statement) @lexer = Expression::Lexer.new(statement)
@variables = variables.with_indifferent_access
@variables = pipeline.variables.map do |variable|
[variable.key, variable.value]
end
end end
def parse_tree def parse_tree
...@@ -35,6 +32,16 @@ module Gitlab ...@@ -35,6 +32,16 @@ module Gitlab
def evaluate def evaluate
parse_tree.evaluate(@variables.to_h) parse_tree.evaluate(@variables.to_h)
end end
def truthful?
evaluate.present?
end
def valid?
parse_tree.is_a?(Lexeme::Base)
rescue StatementError
false
end
end end
end end
end end
......
...@@ -11,21 +11,16 @@ module Gitlab ...@@ -11,21 +11,16 @@ module Gitlab
@pipeline = pipeline @pipeline = pipeline
@attributes = attributes @attributes = attributes
@only = attributes.delete(:only) @only = Gitlab::Ci::Build::Policy
@except = attributes.delete(:except) .fabricate(attributes.delete(:only))
end @except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
def user=(current_user)
@attributes.merge!(user: current_user)
end end
def included? def included?
strong_memoize(:inclusion) do strong_memoize(:inclusion) do
only_specs = Gitlab::Ci::Build::Policy.fabricate(@only) @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } &&
except_specs = Gitlab::Ci::Build::Policy.fabricate(@except) @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
end end
end end
...@@ -33,6 +28,7 @@ module Gitlab ...@@ -33,6 +28,7 @@ module Gitlab
@attributes.merge( @attributes.merge(
pipeline: @pipeline, pipeline: @pipeline,
project: @pipeline.project, project: @pipeline.project,
user: @pipeline.user,
ref: @pipeline.ref, ref: @pipeline.ref,
tag: @pipeline.tag, tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger, trigger_request: @pipeline.legacy_trigger,
......
...@@ -17,10 +17,6 @@ module Gitlab ...@@ -17,10 +17,6 @@ module Gitlab
end end
end end
def user=(current_user)
@builds.each { |seed| seed.user = current_user }
end
def attributes def attributes
{ name: @attributes.fetch(:name), { name: @attributes.fetch(:name),
pipeline: @pipeline, pipeline: @pipeline,
......
...@@ -30,7 +30,13 @@ module Gitlab ...@@ -30,7 +30,13 @@ module Gitlab
end end
def to_runner_variables 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 end
end end
......
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
end end
def ==(other) def ==(other)
to_hash == self.class.fabricate(other).to_hash to_runner_variable == self.class.fabricate(other).to_runner_variable
end end
## ##
...@@ -25,7 +25,7 @@ module Gitlab ...@@ -25,7 +25,7 @@ module Gitlab
# don't expose `file` attribute at all (stems from what the runner # don't expose `file` attribute at all (stems from what the runner
# expects). # expects).
# #
def to_hash def to_runner_variable
@variable.reject do |hash_key, hash_value| @variable.reject do |hash_key, hash_value|
hash_key == :file && hash_value == false hash_key == :file && hash_value == false
end 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 ...@@ -83,6 +83,39 @@ describe Gitlab::Ci::Config::Entry::Policy do
end end
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 context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } } let(:config) { { refs: ['master'], invalid: :something } }
......
...@@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do ...@@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
let(:pipeline) do let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project, build(:ci_pipeline_with_one_job, project: project,
ref: 'master') ref: 'master',
user: user)
end end
let(:command) do let(:command) do
...@@ -42,6 +43,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate 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).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted expect(pipeline.stages.first.builds.first).not_to be_persisted
end end
it 'correctly assigns user' do
expect(pipeline.builds).to all(have_attributes(user: user))
end
end end
context 'when pipeline is empty' do context 'when pipeline is empty' do
......
...@@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do ...@@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
expect(token).not_to be_nil expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some " string' expect(token.build.evaluate).to eq 'some " string'
end 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
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Statement do describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:pipeline) { build(:ci_pipeline) }
subject do subject do
described_class.new(text, pipeline) described_class.new(text, variables)
end
let(:variables) do
{ 'PRESENT_VARIABLE' => 'my variable',
EMPTY_VARIABLE: '' }
end end
before do describe '.new' do
pipeline.variables.build([key: 'VARIABLE', value: 'my variable']) 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 end
describe '#parse_tree' do describe '#parse_tree' do
...@@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context 'when expression grammar is incorrect' do context 'when expression grammar is incorrect' do
table = [ table = [
'$VAR "text"', # missing operator '$VAR "text"', # missing operator
'== "123"', # invalid right side '== "123"', # invalid left side
"'single quotes'", # single quotes string '"some string"', # only string provided
'$VAR ==', # invalid right side '$VAR ==', # invalid right side
'12345', # unknown syntax '12345', # unknown syntax
'' # empty statement '' # empty statement
] ]
table.each do |syntax| table.each do |syntax|
it "raises an error when syntax is `#{syntax}`" do context "when expression grammar is #{syntax.inspect}" do
expect { described_class.new(syntax, pipeline).parse_tree } let(:text) { syntax }
.to raise_error described_class::StatementError
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 end
end end
...@@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
expect(subject.parse_tree) expect(subject.parse_tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end end
it 'is a valid statement' do
expect(subject).to be_valid
end
end end
context 'when using a single token' do context 'when using a single token' do
let(:text) { '$VARIABLE' } let(:text) { '$PRESENT_VARIABLE' }
it 'returns a single token instance' do it 'returns a single token instance' do
expect(subject.parse_tree) expect(subject.parse_tree)
...@@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
describe '#evaluate' do describe '#evaluate' do
statements = [ statements = [
['$VARIABLE == "my variable"', true], ['$PRESENT_VARIABLE == "my variable"', true],
["$VARIABLE == 'my variable'", true], ["$PRESENT_VARIABLE == 'my variable'", true],
['"my variable" == $VARIABLE', true], ['"my variable" == $PRESENT_VARIABLE', true],
['$VARIABLE == null', false], ['$PRESENT_VARIABLE == null', false],
['$VAR == null', true], ['$EMPTY_VARIABLE == null', false],
['null == $VAR', true], ['"" == $EMPTY_VARIABLE', true],
['$VARIABLE', 'my variable'], ['$EMPTY_VARIABLE', ''],
['$VAR', nil] ['$UNDEFINED_VARIABLE == null', true],
['null == $UNDEFINED_VARIABLE', true],
['$PRESENT_VARIABLE', 'my variable'],
['$UNDEFINED_VARIABLE', nil]
] ]
statements.each do |expression, value| statements.each do |expression, value|
...@@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end end
end 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 end
...@@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end end
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 describe '#to_resource' do
it 'returns a valid build resource' do it 'returns a valid build resource' do
expect(subject.to_resource).to be_a(::Ci::Build) expect(subject.to_resource).to be_a(::Ci::Build)
......
...@@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do ...@@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end end
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 describe '#to_resource' do
it 'builds a valid stage object with all builds' do it 'builds a valid stage object with all builds' do
subject.to_resource.save! subject.to_resource.save!
......
...@@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do ...@@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do
end end
end end
describe '#to_hash' do describe '#to_runner_variable' do
it 'returns a hash representation of a collection item' do it 'returns a runner-compatible hash representation' do
expect(described_class.new(**variable).to_hash).to eq variable runner_variable = described_class
.new(**variable)
.to_runner_variable
expect(runner_variable).to eq variable
end end
end end
end end
...@@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do ...@@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do
collection = described_class.new([variable]) collection = described_class.new([variable])
expect(collection.first.to_hash).to eq variable expect(collection.first.to_runner_variable).to eq variable
end end
it 'can be initialized without an argument' do it 'can be initialized without an argument' do
...@@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do ...@@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do
.to eq [{ key: 'TEST', value: 1, public: true }] .to eq [{ key: 'TEST', value: 1, public: true }]
end end
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 end
...@@ -1311,6 +1311,14 @@ module Gitlab ...@@ -1311,6 +1311,14 @@ module Gitlab
Gitlab::Ci::YamlProcessor.new(config) Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end 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 end
describe "Validate configuration templates" do describe "Validate configuration templates" do
......
...@@ -1483,24 +1483,24 @@ describe Ci::Build do ...@@ -1483,24 +1483,24 @@ describe Ci::Build do
let(:container_registry_enabled) { false } let(:container_registry_enabled) { false }
let(:predefined_variables) do 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: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true }, { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, 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_NAME', value: 'test', public: true },
{ key: 'CI_JOB_STAGE', 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_SHA', value: build.sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, 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_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_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true }, { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
...@@ -1965,6 +1965,7 @@ describe Ci::Build do ...@@ -1965,6 +1965,7 @@ describe Ci::Build do
before do before do
allow(build).to receive(:predefined_variables) { [build_pre_var] } allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow(build).to receive(:yaml_variables) { [build_yaml_var] } allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow_any_instance_of(Project) allow_any_instance_of(Project)
.to receive(:predefined_variables) { [project_pre_var] } .to receive(:predefined_variables) { [project_pre_var] }
...@@ -2013,6 +2014,106 @@ describe Ci::Build do ...@@ -2013,6 +2014,106 @@ describe Ci::Build do
end end
end 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 end
describe 'state transition: any => [:pending]' do describe 'state transition: any => [:pending]' do
......
...@@ -353,6 +353,20 @@ describe Ci::Pipeline, :mailer do ...@@ -353,6 +353,20 @@ describe Ci::Pipeline, :mailer do
end end
end 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 end
describe '#seeds_size' do describe '#seeds_size' do
......
...@@ -2448,15 +2448,12 @@ describe Project do ...@@ -2448,15 +2448,12 @@ describe Project do
let(:default_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'default-AAA') } let(:default_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'default-AAA') }
let(:review_env_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'review-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 context 'when environment name is review/name' do
let!(:environment) { create(:environment, project: project, name: 'review/name') } let!(:environment) { create(:environment, project: project, name: 'review/name') }
it 'returns variables from this service' do it 'returns variables from this service' do
expect(subject).to include( expect(project.deployment_variables(environment: 'review/name'))
{ key: 'KUBE_TOKEN', value: 'review-AAA', public: false } .to include(key: 'KUBE_TOKEN', value: 'review-AAA', public: false)
)
end end
end end
...@@ -2464,9 +2461,8 @@ describe Project do ...@@ -2464,9 +2461,8 @@ describe Project do
let!(:environment) { create(:environment, project: project, name: 'staging/name') } let!(:environment) { create(:environment, project: project, name: 'staging/name') }
it 'returns variables from this service' do it 'returns variables from this service' do
expect(subject).to include( expect(project.deployment_variables(environment: 'staging/name'))
{ key: 'KUBE_TOKEN', value: 'default-AAA', public: false } .to include(key: 'KUBE_TOKEN', value: 'default-AAA', public: false)
)
end end
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