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
169
170
171
172
173
174
175
|
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module External
module File
class Base
include Gitlab::Utils::StrongMemoize
attr_reader :location, :params, :context, :errors
YAML_WHITELIST_EXTENSION = /.+\.(yml|yaml)$/i.freeze
def initialize(params, context)
@params = params
@context = context
@errors = []
end
def matching?
location.present?
end
def invalid_location_type?
!location.is_a?(String)
end
def invalid_extension?
location.nil? || !::File.basename(location).match?(YAML_WHITELIST_EXTENSION)
end
def valid?
errors.none?
end
def error_message
errors.first
end
def content
raise NotImplementedError, 'subclass must implement fetching raw content'
end
def to_hash
expanded_content_hash
end
def metadata
{
context_project: context.project&.full_path,
context_sha: context.sha
}
end
def eql?(other)
other.hash == hash
end
def hash
[params, context.project&.full_path, context.sha].hash
end
# This method is overridden to load context into the memoized result
# or to lazily load context via BatchLoader
def preload_context
# no-op
end
def preload_content
# calling the `content` method either loads content into the memoized result
# or lazily loads it via BatchLoader
content
end
def validate_location!
if invalid_location_type?
errors.push("Included file `#{masked_location}` needs to be a string")
elsif invalid_extension?
errors.push("Included file `#{masked_location}` does not have YAML extension!")
end
end
def validate_context!
raise NotImplementedError, 'subclass must implement `validate_context!`'
end
def validate_content!
errors.push("Included file `#{masked_location}` is empty or does not exist!") if content.blank?
end
def load_and_validate_expanded_hash!
context.logger.instrument(:config_file_fetch_content_hash) do
content_result # calling the method loads YAML then memoizes the content result
end
context.logger.instrument(:config_file_interpolate_result) do
interpolator.interpolate!
end
return validate_interpolation! unless interpolator.valid?
context.logger.instrument(:config_file_expand_content_includes) do
expanded_content_hash # calling the method expands then memoizes the result
end
validate_hash!
end
protected
def content_result
::Gitlab::Ci::Config::Yaml
.load_result!(content, project: context.project)
end
strong_memoize_attr :content_result
def content_inputs
# TODO: remove support for `with` syntax in 16.1, see https://gitlab.com/gitlab-org/gitlab/-/issues/408369
# In the interim prefer `inputs` over `with` while allow either syntax.
params.to_h.slice(:inputs, :with).each_value.first
end
strong_memoize_attr :content_inputs
def content_hash
interpolator.interpolate!
interpolator.to_hash
end
strong_memoize_attr :content_hash
def interpolator
Yaml::Interpolator.new(content_result, content_inputs, context)
end
strong_memoize_attr :interpolator
def expanded_content_hash
return if content_hash.blank?
strong_memoize(:expanded_content_hash) do
expand_includes(content_hash)
end
end
def validate_hash!
if to_hash.blank?
errors.push("Included file `#{masked_location}` does not have valid YAML syntax!")
end
end
def validate_interpolation!
return if interpolator.valid?
errors.push("`#{masked_location}`: #{interpolator.error_message}")
end
def expand_includes(hash)
External::Processor.new(hash, context.mutate(expand_context_attrs)).perform
end
def expand_context_attrs
{}
end
def masked_location
strong_memoize(:masked_location) do
context.mask_variables_from(location)
end
end
end
end
end
end
end
end
|