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
|
# 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!
return errors.push("`#{masked_location}`: #{content_result.error}") unless content_result.valid?
if content_result.interpolated? && context.user.present?
::Gitlab::UsageDataCounters::HLLRedisCounter
.track_event('ci_interpolation_users', values: context.user.id)
end
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_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
def content_result
context.logger.instrument(:config_file_fetch_content_hash) do
::Gitlab::Ci::Config::Yaml::Loader.new(content, inputs: content_inputs).load
end
end
strong_memoize_attr :content_result
def expanded_content_hash
return if content_result.content.blank?
strong_memoize(:expanded_content_hash) do
expand_includes(content_result.content)
end
end
def validate_hash!
if to_hash.blank?
errors.push("Included file `#{masked_location}` does not have valid YAML syntax!")
end
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
|