Commit 250fede4 authored by Pedro Pombeiro's avatar Pedro Pombeiro

Address MR review comments

parent d565ff04
...@@ -16,18 +16,6 @@ module ExpandVariables ...@@ -16,18 +16,6 @@ module ExpandVariables
end end
end end
# expand_variables_collection expands a Gitlab::Ci::Variables::Collection, ignoring unknown variable references.
# If a circular variable reference is found, the original Collection is returned
def expand_variables_collection(variables, project)
return Gitlab::Ci::Variables::Collection.new(variables) if
Feature.disabled?(:variable_inside_variable, project)
sorted_variables = variables.sorted_collection(project)
return sorted_variables if sorted_variables.errors
expand_sorted_variables_collection(sorted_variables)
end
def possible_var_reference?(value) def possible_var_reference?(value)
return unless value return unless value
...@@ -74,16 +62,5 @@ module ExpandVariables ...@@ -74,16 +62,5 @@ module ExpandVariables
variables variables
end end
def expand_sorted_variables_collection(sorted_variables)
expanded_vars = {}
sorted_variables.each_with_object(Gitlab::Ci::Variables::Collection.new) do |item, collection|
item = item.merge(value: expand_existing(item.value, expanded_vars)) if item.depends_on
expanded_vars.store(item[:key], item)
collection.append(item)
end
end
end end
end end
...@@ -162,8 +162,8 @@ module Gitlab ...@@ -162,8 +162,8 @@ module Gitlab
end end
def variable_expansion_errors def variable_expansion_errors
sorted_collection = evaluate_context.variables.sorted_collection(@pipeline.project) expanded_collection = evaluate_context.variables.expand_all(@pipeline.project)
errors = sorted_collection.errors errors = expanded_collection.errors
["#{name}: #{errors}"] if errors ["#{name}: #{errors}"] if errors
end end
......
...@@ -63,16 +63,36 @@ module Gitlab ...@@ -63,16 +63,36 @@ module Gitlab
Collection.new(@variables.reject(&block)) Collection.new(@variables.reject(&block))
end end
def ==(other) def expand_value(value, keep_undefined: false)
return @variables == other if other.is_a?(Array) value.gsub(ExpandVariables::VARIABLES_REGEXP) do
return false unless other.class == self.class match = Regexp.last_match
result = @variables_by_key[match[1] || match[2]]&.value
@variables == other.variables result ||= match[0] if keep_undefined
result
end
end end
# Returns a sorted Collection object, and sets errors property in case of an error def expand_all(project, keep_undefined: false)
def sorted_collection(project) return self if Feature.disabled?(:variable_inside_variable, project)
Sort.new(self, project).collection
sorted = Sort.new(self)
return self.class.new(self, sorted.errors) unless sorted.valid?
new_collection = self.class.new
sorted.tsort.each do |item|
unless item.depends_on
new_collection.append(item)
next
end
# expand variables as they are added
variable = item.to_runner_variable
variable[:value] = new_collection.expand_value(variable[:value], keep_undefined: keep_undefined)
new_collection.append(variable)
end
new_collection
end end
protected protected
......
...@@ -7,26 +7,23 @@ module Gitlab ...@@ -7,26 +7,23 @@ module Gitlab
class Item class Item
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :raw
def initialize(key:, value:, public: true, file: false, masked: false, raw: false) def initialize(key:, value:, public: true, file: false, masked: false, raw: false)
raise ArgumentError, "`#{key}` must be of type String or nil value, while it was: #{value.class}" unless raise ArgumentError, "`#{key}` must be of type String or nil value, while it was: #{value.class}" unless
value.is_a?(String) || value.nil? value.is_a?(String) || value.nil?
@variable = { key: key, value: value, public: public, file: file, masked: masked } @variable = { key: key, value: value, public: public, file: file, masked: masked, raw: raw }
@raw = raw
end end
def value def value
@variable.fetch(:value) @variable.fetch(:value)
end end
def [](key) def raw
@variable.fetch(key) @variable.fetch(:raw)
end end
def merge(*other_hashes) def [](key)
self.class.fabricate(@variable.merge(*other_hashes)) @variable.fetch(key)
end end
def ==(other) def ==(other)
...@@ -50,7 +47,7 @@ module Gitlab ...@@ -50,7 +47,7 @@ module Gitlab
# #
def to_runner_variable 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_key == :raw) && hash_value == false
end end
end end
......
...@@ -8,12 +8,11 @@ module Gitlab ...@@ -8,12 +8,11 @@ module Gitlab
include TSort include TSort
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def initialize(collection, project) def initialize(collection)
raise(ArgumentError, "A Gitlab::Ci::Variables::Collection object was expected") unless raise(ArgumentError, "A Gitlab::Ci::Variables::Collection object was expected") unless
collection.is_a?(Collection) collection.is_a?(Collection)
@collection = collection @collection = collection
@project = project
end end
def valid? def valid?
...@@ -23,8 +22,6 @@ module Gitlab ...@@ -23,8 +22,6 @@ module Gitlab
# errors sorts an array of variables, ignoring unknown variable references, # errors sorts an array of variables, ignoring unknown variable references,
# and returning an error string if a circular variable reference is found # and returning an error string if a circular variable reference is found
def errors def errors
return if Feature.disabled?(:variable_inside_variable, @project)
strong_memoize(:errors) do strong_memoize(:errors) do
# Check for cyclic dependencies and build error message in that case # Check for cyclic dependencies and build error message in that case
cyclic_vars = each_strongly_connected_component.filter_map do |component| cyclic_vars = each_strongly_connected_component.filter_map do |component|
...@@ -35,16 +32,6 @@ module Gitlab ...@@ -35,16 +32,6 @@ module Gitlab
end end
end end
# collection sorts a collection of variables, ignoring unknown variable references.
# If a circular variable reference is found, a new collection with the original array and an error is returned
def collection
return @collection if Feature.disabled?(:variable_inside_variable, @project)
return Gitlab::Ci::Variables::Collection.new(@collection, errors) if errors
Gitlab::Ci::Variables::Collection.new(tsort)
end
private private
def tsort_each_node(&block) def tsort_each_node(&block)
......
...@@ -268,236 +268,4 @@ RSpec.describe ExpandVariables do ...@@ -268,236 +268,4 @@ RSpec.describe ExpandVariables do
end end
end end
end end
describe '#expand_variables_collection' do
context 'when FF :variable_inside_variable is disabled' do
let_it_be(:project_with_flag_disabled) { create(:project) }
let_it_be(:project_with_flag_enabled) { create(:project) }
before do
stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do
using RSpec::Parameterized::TableSyntax
where do
{
"empty array": {
variables: []
},
"simple expansions": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key$variable$variable2' }
]
},
"complex expansion": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'key${variable}' }
]
},
"out-of-order variable reference": {
variables: [
{ key: 'variable2', value: 'key${variable}' },
{ key: 'variable', value: 'value' }
]
},
"complex expansions with raw variable": {
variables: [
{ key: 'variable3', value: 'key_${variable}_${variable2}' },
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' }
]
},
"array with cyclic dependency": {
variables: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
]
}
}
end
with_them do
subject { ExpandVariables.expand_variables_collection(variables, project_with_flag_disabled) }
it 'returns Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
end
it 'does not expand variables' do
is_expected.to eq(variables)
end
end
end
end
context 'when FF :variable_inside_variable is enabled' do
let_it_be(:project_with_flag_disabled) { create(:project) }
let_it_be(:project_with_flag_enabled) { create(:project) }
before do
stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do
using RSpec::Parameterized::TableSyntax
where do
{
"empty array": {
variables: [],
result: []
},
"simple expansions": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key$variable$variable2' },
{ key: 'variable4', value: 'key$variable$variable3' }
],
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyvalueresult' },
{ key: 'variable4', value: 'keyvaluekeyvalueresult' }
]
},
"complex expansion": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'key${variable}' }
],
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'keyvalue' }
]
},
"unused variables": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result2' },
{ key: 'variable3', value: 'result3' },
{ key: 'variable4', value: 'key$variable$variable3' }
],
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result2' },
{ key: 'variable3', value: 'result3' },
{ key: 'variable4', value: 'keyvalueresult3' }
]
},
"complex expansions": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key${variable}${variable2}' }
],
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyvalueresult' }
]
},
"out-of-order expansion": {
variables: [
{ key: 'variable3', value: 'key$variable2$variable' },
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
],
result: [
{ key: 'variable2', value: 'result' },
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"out-of-order complex expansion": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key${variable2}${variable}' }
],
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"missing variable": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
result: [
{ key: 'variable2', value: 'key$variable' }
]
},
"complex expansions with missing variable": {
variables: [
{ key: 'variable4', value: 'key${variable}${variable2}${variable3}' },
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'value3' }
],
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'value3' },
{ key: 'variable4', value: 'keyvalue${variable2}value3' }
]
},
"complex expansions with raw variable": {
variables: [
{ key: 'variable3', value: 'key_${variable}_${variable2}' },
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' }
],
result: [
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' },
{ key: 'variable3', value: 'key_$variable2_value2' }
]
},
"cyclic dependency causes original array to be returned": {
variables: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
],
result: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
]
}
}
end
with_them do
let!(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
subject! do
ExpandVariables.expand_variables_collection(collection, project_with_flag_enabled)
end
it 'returns Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
end
it 'expands variables' do
is_expected.to eq(result)
end
it 'preserves raw attribute' do
collection.each do |v|
k = v[:key]
subject_item = subject.find { |sv| sv[:key] == k }
expect(subject_item.raw).to eq(v.raw)
end
end
end
end
end
end
end end
...@@ -180,16 +180,6 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do ...@@ -180,16 +180,6 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end end
end end
describe '#merge' do
it 'behaves like hash merge' do
item = described_class.new(**variable)
subject = item.merge(value: 'another thing')
expect(subject).not_to eq item
expect(subject[:value]).to eq 'another thing'
end
end
describe '#to_runner_variable' do describe '#to_runner_variable' do
context 'when variable is not a file-related' do context 'when variable is not a file-related' do
it 'returns a runner-compatible hash representation' do it 'returns a runner-compatible hash representation' do
...@@ -212,6 +202,26 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do ...@@ -212,6 +202,26 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end end
end end
context 'when variable is raw' do
it 'does not export raw value when it is false' do
runner_variable = described_class
.new(key: 'VAR', value: 'value', raw: false)
.to_runner_variable
expect(runner_variable)
.to eq(key: 'VAR', value: 'value', public: true, masked: false)
end
it 'exports raw value when it is true' do
runner_variable = described_class
.new(key: 'VAR', value: 'value', raw: true)
.to_runner_variable
expect(runner_variable)
.to eq(key: 'VAR', value: 'value', public: true, raw: true, masked: false)
end
end
context 'when referencing a variable' do context 'when referencing a variable' do
it '#depends_on contains names of dependencies' do it '#depends_on contains names of dependencies' do
runner_variable = described_class.new(key: 'CI_VAR', value: '${CI_VAR_2}-123-$CI_VAR_3') runner_variable = described_class.new(key: 'CI_VAR', value: '${CI_VAR_2}-123-$CI_VAR_3')
......
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