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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module External
module File
class Project < Base
extend ::Gitlab::Utils::Override
include Gitlab::Utils::StrongMemoize
attr_reader :project_name, :ref_name
def initialize(params, context)
# `Repository#blobs_at` does not support files with the `/` prefix.
@location = Gitlab::Utils.remove_leading_slashes(params[:file])
# We are using the same downcase in the `project` method.
@project_name = get_project_name(params[:project]).to_s.downcase
@ref_name = params[:ref] || 'HEAD'
super
end
def matching?
super && project_name.present?
end
def content
strong_memoize(:content) { fetch_local_content }
end
def metadata
super.merge(
type: :file,
location: masked_location,
blob: masked_blob,
raw: masked_raw,
extra: { project: masked_project_name, ref: masked_ref_name }
)
end
def preload_context
#
# calling these methods lazily loads them via BatchLoader
#
project
can_access_local_content?
sha
end
def validate_context!
if !can_access_local_content?
errors.push("Project `#{masked_project_name}` not found or access denied! Make sure any includes in the pipeline configuration are correctly defined.")
elsif sha.nil?
errors.push("Project `#{masked_project_name}` reference `#{masked_ref_name}` does not exist!")
end
end
def validate_content!
if content.nil?
errors.push("Project `#{masked_project_name}` file `#{masked_location}` does not exist!")
elsif content.blank?
errors.push("Project `#{masked_project_name}` file `#{masked_location}` is empty!")
end
end
private
def project
return legacy_project if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
# Although we use `where_full_path_in`, this BatchLoader does not reduce the number of queries to 1.
# That's because we use it in the `can_access_local_content?` and `sha` BatchLoaders
# as the `for` parameter. And this loads the project immediately.
BatchLoader.for(project_name)
.batch do |project_names, loader|
::Project.where_full_path_in(project_names.uniq).each do |project|
# We are using the same downcase in the `initialize` method.
loader.call(project.full_path.downcase, project)
end
end
end
def can_access_local_content?
if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
return legacy_can_access_local_content?
end
return if project.nil?
# We are force-loading the project with the `itself` method
# because the `project` variable can be a `BatchLoader` object and we should not
# pass a `BatchLoader` object in the `for` method to prevent unwanted behaviors.
BatchLoader.for(project.itself)
.batch(key: context.user) do |projects, loader, args|
projects.uniq.each do |project|
context.logger.instrument(:config_file_project_validate_access) do
loader.call(project, Ability.allowed?(args[:key], :download_code, project))
end
end
end
end
def sha
return legacy_sha if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
return if project.nil?
# with `itself`, we are force-loading the project
BatchLoader.for([project.itself, ref_name])
.batch do |project_ref_pairs, loader|
project_ref_pairs.uniq.each do |project, ref_name|
loader.call([project, ref_name], project.commit(ref_name).try(:sha))
end
end
end
def fetch_local_content
BatchLoader.for([sha.to_s, location])
.batch(key: project) do |locations, loader, args|
context.logger.instrument(:config_file_fetch_project_content) do
args[:key].repository.blobs_at(locations).each do |blob|
loader.call([blob.commit_id, blob.path], blob.data)
end
end
rescue GRPC::NotFound, GRPC::Internal
# no-op
end
end
def legacy_project
strong_memoize(:legacy_project) do
::Project.find_by_full_path(project_name)
end
end
def legacy_can_access_local_content?
strong_memoize(:legacy_can_access_local_content) do
context.logger.instrument(:config_file_project_validate_access) do
Ability.allowed?(context.user, :download_code, project)
end
end
end
def legacy_sha
strong_memoize(:legacy_sha) do
project.commit(ref_name).try(:sha)
end
end
override :expand_context_attrs
def expand_context_attrs
{
project: project,
sha: sha.to_s, # we need to use `.to_s` to load the value from the BatchLoader
user: context.user,
parent_pipeline: context.parent_pipeline,
variables: context.variables
}
end
def masked_project_name
strong_memoize(:masked_project_name) do
context.mask_variables_from(project_name)
end
end
def masked_ref_name
strong_memoize(:masked_ref_name) do
context.mask_variables_from(ref_name)
end
end
def masked_blob
return unless valid?
strong_memoize(:masked_blob) do
context.mask_variables_from(
Gitlab::Routing.url_helpers.project_blob_url(project, ::File.join(sha, location))
)
end
end
def masked_raw
return unless valid?
strong_memoize(:masked_raw) do
context.mask_variables_from(
Gitlab::Routing.url_helpers.project_raw_url(project, ::File.join(sha, location))
)
end
end
# TODO: To be removed after we deprecate usage of array in `project` keyword.
# https://gitlab.com/gitlab-org/gitlab/-/issues/365975
def get_project_name(project_name)
if project_name.is_a?(Array)
project_name.first
else
project_name
end
end
end
end
end
end
end
end
|