1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# frozen_string_literal: true
module Gitlab
module Ci
module Variables
class Collection
include Enumerable
attr_reader :errors
def self.fabricate(input)
case input
when Array
new(input)
when Hash
new(input.map { |key, value| { key: key, value: value } })
when Proc
fabricate(input.call)
when self
input
else
raise ArgumentError, "Unknown `#{input.class}` variable collection!"
end
end
def initialize(variables = [], errors = nil)
@variables = []
@variables_by_key = Hash.new { |h, k| h[k] = [] }
@errors = errors
variables.each { |variable| self.append(variable) }
end
def append(resource)
item = Collection::Item.fabricate(resource)
@variables.append(item)
@variables_by_key[item[:key]] << item
self
end
def compact
Collection.new(select { |variable| !variable.value.nil? })
end
def concat(resources)
return self if resources.nil?
tap { resources.each { |variable| self.append(variable) } }
end
def each
@variables.each { |variable| yield variable }
end
def +(other)
self.class.new.tap do |collection|
self.each { |variable| collection.append(variable) }
other.each { |variable| collection.append(variable) }
end
end
def [](key)
all(key)&.last
end
def all(key)
vars = @variables_by_key[key]
vars unless vars.empty?
end
def size
@variables.size
end
def to_runner_variables
self.map(&:to_runner_variable)
end
def to_hash
self.to_runner_variables
.to_h { |env| [env.fetch(:key), env.fetch(:value)] }
.with_indifferent_access
end
def reject(&block)
Collection.new(@variables.reject(&block))
end
def sort_and_expand_all(keep_undefined: false, expand_file_refs: true, expand_raw_refs: true)
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,
expand_file_refs: expand_file_refs,
expand_raw_refs: expand_raw_refs)
new_collection.append(variable)
end
new_collection
end
def to_s
"#{@variables_by_key.keys}, @errors='#{@errors}'"
end
protected
def expand_value(value, keep_undefined: false, expand_file_refs: true, expand_raw_refs: true)
value.gsub(Item::VARIABLES_REGEXP) do
match = Regexp.last_match # it is either a valid variable definition or a ($$ / %%)
full_match = match[0]
variable_name = match[:key]
next full_match unless variable_name # it is a ($$ / %%), so we don't touch it
# now we know that it is a valid variable definition: $VARIABLE_NAME / %VARIABLE_NAME / ${VARIABLE_NAME}
# we are trying to find a variable with key VARIABLE_NAME
variable = self[variable_name]
if variable # VARIABLE_NAME is an existing variable
if variable.file?
expand_file_refs ? variable.value : full_match
elsif variable.raw?
# Normally, it's okay to expand a raw variable if it's referenced in another variable because
# its rawness is not broken. However, the runner also tries to expand variables.
# Here, with `full_match`, we defer the expansion of raw variables to the runner.
# If we expand them here, the runner will not know that the expanded value is a raw variable
# and it tries to expand it again.
# Example: `A` is a normal variable with value `normal`.
# `B` is a raw variable with value `raw-$A`.
# `C` is a normal variable with value `$B`.
# If we expanded `C` here, the runner would receive `C` as `raw-$A`. And since `A` is a normal
# variable, the runner would expand it. So, the result would be `raw-normal`.
# With `full_match`, the runner receives `C` as `$B`. And since `B` is a raw variable, the
# runner expanded it as `raw-$A`, which is what we want.
# Discussion: https://gitlab.com/gitlab-org/gitlab/-/issues/353991#note_1103274951
expand_raw_refs ? variable.value : full_match
else
variable.value
end
elsif keep_undefined
full_match # we do not touch the variable definition
else
nil # we remove the variable definition
end
end
end
private
attr_reader :variables
end
end
end
end
|