dependabot-docker_compose 0.297.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,479 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "docker_registry2"
5
+ require "sorbet-runtime"
6
+
7
+ require "dependabot/update_checkers"
8
+ require "dependabot/update_checkers/base"
9
+ require "dependabot/errors"
10
+ require "dependabot/docker_compose/tag"
11
+ require "dependabot/docker_compose/file_parser"
12
+ require "dependabot/docker_compose/version"
13
+ require "dependabot/docker_compose/requirement"
14
+ require "dependabot/shared/utils/credentials_finder"
15
+
16
+ module Dependabot
17
+ module DockerCompose
18
+ # rubocop:disable Metrics/ClassLength
19
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
20
+ extend T::Sig
21
+
22
+ sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
23
+ def latest_version
24
+ latest_version_from(T.must(dependency.version))
25
+ end
26
+
27
+ sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
28
+ def latest_resolvable_version
29
+ # Resolvability isn't an issue for Docker containers.
30
+ latest_version
31
+ end
32
+
33
+ sig { override.returns(T.nilable(String)) }
34
+ def latest_resolvable_version_with_no_unlock
35
+ # No concept of "unlocking" for Docker containers
36
+ dependency.version
37
+ end
38
+
39
+ sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
40
+ def updated_requirements
41
+ dependency.requirements.map do |req|
42
+ updated_source = req.fetch(:source).dup
43
+
44
+ tag = req[:source][:tag]
45
+ digest = req[:source][:digest]
46
+
47
+ if tag
48
+ updated_tag = latest_version_from(tag)
49
+ updated_source[:tag] = updated_tag
50
+ updated_source[:digest] = digest_of(updated_tag) if digest
51
+ elsif digest
52
+ updated_source[:digest] = digest_of("latest")
53
+ end
54
+
55
+ req.merge(source: updated_source)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ sig { override.returns(T::Boolean) }
62
+ def latest_version_resolvable_with_full_unlock?
63
+ # Full unlock checks aren't relevant for Dockerfiles
64
+ false
65
+ end
66
+
67
+ sig { override.returns(T::Array[Dependabot::Dependency]) }
68
+ def updated_dependencies_after_full_unlock
69
+ raise NotImplementedError
70
+ end
71
+
72
+ sig { params(requirements_to_unlock: T.nilable(Symbol)).returns(T::Boolean) }
73
+ def version_can_update?(requirements_to_unlock:) # rubocop:disable Lint/UnusedMethodArgument
74
+ if digest_requirements.any?
75
+ !digest_up_to_date?
76
+ else
77
+ !version_up_to_date?
78
+ end
79
+ end
80
+
81
+ sig { returns(T::Boolean) }
82
+ def version_up_to_date?
83
+ if digest_requirements.any?
84
+ version_tag_up_to_date? && digest_up_to_date?
85
+ else
86
+ version_tag_up_to_date?
87
+ end
88
+ end
89
+
90
+ sig { returns(T::Boolean) }
91
+ def version_tag_up_to_date?
92
+ version = dependency.version
93
+ return false unless version
94
+
95
+ return true unless version_tag.comparable?
96
+
97
+ latest_tag = latest_tag_from(version)
98
+
99
+ comparable_version_from(latest_tag) <= comparable_version_from(version_tag)
100
+ end
101
+
102
+ sig { returns(T::Boolean) }
103
+ def digest_up_to_date?
104
+ digest_requirements.all? do |req|
105
+ next true unless updated_digest
106
+
107
+ req.fetch(:source).fetch(:digest) == updated_digest
108
+ end
109
+ end
110
+
111
+ sig { params(version: String).returns(String) }
112
+ def latest_version_from(version)
113
+ latest_tag_from(version).name
114
+ end
115
+
116
+ sig { params(version: String).returns(Dependabot::DockerCompose::Tag) }
117
+ def latest_tag_from(version)
118
+ @tags ||= T.let({}, T.nilable(T::Hash[String, Dependabot::DockerCompose::Tag]))
119
+ return T.must(@tags[version]) if @tags.key?(version)
120
+
121
+ @tags[version] = fetch_latest_tag(Tag.new(version))
122
+ end
123
+
124
+ # NOTE: It's important that this *always* returns a tag (even if
125
+ # it's the existing one) as it is what we later check the digest of.
126
+ sig { params(version_tag: Dependabot::DockerCompose::Tag).returns(Dependabot::DockerCompose::Tag) }
127
+ def fetch_latest_tag(version_tag)
128
+ return Tag.new(T.must(latest_digest)) if version_tag.digest? && latest_digest
129
+ return version_tag unless version_tag.comparable?
130
+
131
+ # Prune out any downgrade tags before checking for pre-releases
132
+ # (which requires a call to the registry for each tag, so can be slow)
133
+ candidate_tags = comparable_tags_from_registry(version_tag)
134
+ candidate_tags = remove_version_downgrades(candidate_tags, version_tag)
135
+ candidate_tags = remove_prereleases(candidate_tags, version_tag)
136
+ candidate_tags = filter_ignored(candidate_tags)
137
+ candidate_tags = sort_tags(candidate_tags, version_tag)
138
+
139
+ latest_tag = candidate_tags.last
140
+ return version_tag unless latest_tag
141
+
142
+ return latest_tag if latest_tag.same_precision?(version_tag)
143
+
144
+ latest_same_precision_tag = remove_precision_changes(candidate_tags, version_tag).last
145
+ return latest_tag unless latest_same_precision_tag
146
+
147
+ latest_same_precision_digest = digest_of(latest_same_precision_tag.name)
148
+ latest_digest = digest_of(latest_tag.name)
149
+
150
+ # NOTE: Some registries don't provide digests (the API documents them as
151
+ # optional: https://docs.docker.com/registry/spec/api/#content-digests).
152
+ #
153
+ # In that case we can't know for sure whether the latest tag keeping
154
+ # existing precision is the same as the absolute latest tag.
155
+ #
156
+ # We can however, make a best-effort to avoid unwanted changes by
157
+ # directly looking at version numbers and checking whether the absolute
158
+ # latest tag is just a more precise version of the latest tag that keeps
159
+ # existing precision.
160
+
161
+ if latest_same_precision_digest == latest_digest && latest_same_precision_tag.same_but_less_precise?(latest_tag)
162
+ latest_same_precision_tag
163
+ else
164
+ latest_tag
165
+ end
166
+ end
167
+
168
+ sig { params(original_tag: Dependabot::DockerCompose::Tag).returns(T::Array[Dependabot::DockerCompose::Tag]) }
169
+ def comparable_tags_from_registry(original_tag)
170
+ tags_from_registry.select { |tag| tag.comparable_to?(original_tag) }
171
+ end
172
+
173
+ sig do
174
+ params(
175
+ candidate_tags: T::Array[Dependabot::DockerCompose::Tag],
176
+ version_tag: Dependabot::DockerCompose::Tag
177
+ )
178
+ .returns(T::Array[Dependabot::DockerCompose::Tag])
179
+ end
180
+ def remove_version_downgrades(candidate_tags, version_tag)
181
+ current_version = comparable_version_from(version_tag)
182
+
183
+ candidate_tags.select do |tag|
184
+ comparable_version_from(tag) >= current_version
185
+ end
186
+ end
187
+
188
+ sig do
189
+ params(
190
+ candidate_tags: T::Array[Dependabot::DockerCompose::Tag],
191
+ version_tag: Dependabot::DockerCompose::Tag
192
+ )
193
+ .returns(T::Array[Dependabot::DockerCompose::Tag])
194
+ end
195
+ def remove_prereleases(candidate_tags, version_tag)
196
+ return candidate_tags if prerelease?(version_tag)
197
+
198
+ candidate_tags.reject { |tag| prerelease?(tag) }
199
+ end
200
+
201
+ sig do
202
+ params(
203
+ candidate_tags: T::Array[Dependabot::DockerCompose::Tag],
204
+ version_tag: Dependabot::DockerCompose::Tag
205
+ )
206
+ .returns(T::Array[Dependabot::DockerCompose::Tag])
207
+ end
208
+ def remove_precision_changes(candidate_tags, version_tag)
209
+ candidate_tags.select do |tag|
210
+ tag.same_precision?(version_tag)
211
+ end
212
+ end
213
+
214
+ sig { returns(T.nilable(Dependabot::DockerCompose::Tag)) }
215
+ def latest_tag
216
+ return unless latest_digest
217
+
218
+ tags_from_registry
219
+ .select(&:canonical?)
220
+ .sort_by { |t| comparable_version_from(t) }
221
+ .reverse
222
+ .find { |t| digest_of(t.name) == latest_digest }
223
+ end
224
+
225
+ sig { returns(T.nilable(String)) }
226
+ def updated_digest
227
+ @updated_digest ||= T.let(
228
+ if latest_tag_from(T.must(dependency.version)).digest?
229
+ latest_digest
230
+ else
231
+ digest_of(T.cast(latest_version, String))
232
+ end,
233
+ T.nilable(String)
234
+ )
235
+ end
236
+
237
+ sig { returns(T::Array[Dependabot::DockerCompose::Tag]) }
238
+ def tags_from_registry
239
+ @tags_from_registry ||= T.let(
240
+ begin
241
+ client = docker_registry_client
242
+
243
+ client.tags(docker_repo_name, auto_paginate: true).fetch("tags").map { |name| Tag.new(name) }
244
+ rescue *transient_docker_errors
245
+ attempt ||= 1
246
+ attempt += 1
247
+ raise if attempt > 3
248
+
249
+ retry
250
+ end,
251
+ T.nilable(T::Array[Dependabot::DockerCompose::Tag])
252
+ )
253
+ rescue DockerRegistry2::RegistryAuthenticationException,
254
+ RestClient::Forbidden
255
+ raise PrivateSourceAuthenticationFailure, registry_hostname
256
+ rescue RestClient::Exceptions::OpenTimeout,
257
+ RestClient::Exceptions::ReadTimeout
258
+ raise if using_dockerhub?
259
+
260
+ raise PrivateSourceTimedOut, T.must(registry_hostname)
261
+ rescue RestClient::ServerBrokeConnection,
262
+ RestClient::TooManyRequests
263
+ raise PrivateSourceBadResponse, registry_hostname
264
+ rescue JSON::ParserError => e
265
+ if e.message.include?("unexpected token")
266
+ raise DependencyFileNotResolvable, "Error while accessing docker image at #{registry_hostname}"
267
+ end
268
+
269
+ raise
270
+ end
271
+
272
+ sig { returns(T.nilable(String)) }
273
+ def latest_digest
274
+ return unless tags_from_registry.map(&:name).include?("latest")
275
+
276
+ digest_of("latest")
277
+ end
278
+
279
+ sig { params(tag: String).returns(T.nilable(String)) }
280
+ def digest_of(tag)
281
+ @digests ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
282
+ return @digests[tag] if @digests.key?(tag)
283
+
284
+ @digests[tag] = fetch_digest_of(tag)
285
+ end
286
+
287
+ sig { params(tag: String).returns(T.nilable(String)) }
288
+ def fetch_digest_of(tag)
289
+ docker_registry_client.manifest_digest(docker_repo_name, tag)&.delete_prefix("sha256:")
290
+ rescue *transient_docker_errors => e
291
+ attempt ||= 1
292
+ attempt += 1
293
+ return if attempt > 3 && e.is_a?(DockerRegistry2::NotFound)
294
+ raise PrivateSourceBadResponse, registry_hostname if attempt > 3
295
+
296
+ retry
297
+ rescue DockerRegistry2::RegistryAuthenticationException,
298
+ RestClient::Forbidden
299
+ raise PrivateSourceAuthenticationFailure, registry_hostname
300
+ rescue RestClient::ServerBrokeConnection,
301
+ RestClient::TooManyRequests
302
+ raise PrivateSourceBadResponse, registry_hostname
303
+ rescue JSON::ParserError
304
+ Dependabot.logger.info \
305
+ "docker_registry_client.manifest_digest(#{docker_repo_name}, #{tag}) returned an empty string"
306
+ nil
307
+ end
308
+
309
+ sig { returns(T::Array[T.class_of(StandardError)]) }
310
+ def transient_docker_errors
311
+ [
312
+ RestClient::Exceptions::Timeout,
313
+ RestClient::ServerBrokeConnection,
314
+ RestClient::ServiceUnavailable,
315
+ RestClient::InternalServerError,
316
+ RestClient::BadGateway,
317
+ DockerRegistry2::NotFound
318
+ ]
319
+ end
320
+
321
+ sig { params(tag: Dependabot::DockerCompose::Tag).returns(T::Boolean) }
322
+ def prerelease?(tag)
323
+ return true if tag.looks_like_prerelease?
324
+
325
+ # Compare the numeric version against the version of the `latest` tag.
326
+ return false unless latest_tag
327
+
328
+ if comparable_version_from(tag) > comparable_version_from(T.must(latest_tag))
329
+ Dependabot.logger.info \
330
+ "The `latest` tag points to the same image as the `#{T.must(latest_tag).name}` image, " \
331
+ "so dependabot is treating `#{tag.name}` as a pre-release. " \
332
+ "The `latest` tag needs to point to `#{tag.name}` for Dependabot to consider it."
333
+
334
+ true
335
+ else
336
+ false
337
+ end
338
+ end
339
+
340
+ sig { params(tag: Dependabot::DockerCompose::Tag).returns(Dependabot::Version) }
341
+ def comparable_version_from(tag)
342
+ version_class.new(tag.numeric_version)
343
+ end
344
+
345
+ sig { returns(T.nilable(String)) }
346
+ def registry_hostname
347
+ if dependency.requirements.first&.dig(:source, :registry)
348
+ return T.must(dependency.requirements.first).dig(:source, :registry)
349
+ end
350
+
351
+ credentials_finder.base_registry
352
+ end
353
+
354
+ sig { returns(T::Boolean) }
355
+ def using_dockerhub?
356
+ registry_hostname == "registry.hub.docker.com"
357
+ end
358
+
359
+ sig { returns(T.nilable(Dependabot::Credential)) }
360
+ def registry_credentials
361
+ credentials_finder.credentials_for_registry(registry_hostname)
362
+ end
363
+
364
+ sig { returns(Dependabot::Shared::Utils::CredentialsFinder) }
365
+ def credentials_finder
366
+ @credentials_finder ||= T.let(
367
+ Dependabot::Shared::Utils::CredentialsFinder.new(credentials),
368
+ T.nilable(Dependabot::Shared::Utils::CredentialsFinder)
369
+ )
370
+ end
371
+
372
+ sig { returns(String) }
373
+ def docker_repo_name
374
+ return dependency.name unless using_dockerhub?
375
+ return dependency.name unless dependency.name.split("/").count < 2
376
+
377
+ "library/#{dependency.name}"
378
+ end
379
+
380
+ # Defaults from https://github.com/deitch/docker_registry2/blob/bfde04144f0b7fd63c156a1aca83efe19ee78ffd/lib/registry/registry.rb#L26-L27
381
+ DEFAULT_DOCKER_OPEN_TIMEOUT_IN_SECONDS = 2
382
+ DEFAULT_DOCKER_READ_TIMEOUT_IN_SECONDS = 5
383
+
384
+ sig { returns(DockerRegistry2::Registry) }
385
+ def docker_registry_client
386
+ @docker_registry_client ||= T.let(
387
+ DockerRegistry2::Registry.new(
388
+ "https://#{registry_hostname}",
389
+ user: registry_credentials&.fetch("username", nil),
390
+ password: registry_credentials&.fetch("password", nil),
391
+ read_timeout: docker_read_timeout_in_seconds,
392
+ open_timeout: docker_open_timeout_in_seconds,
393
+ http_options: { proxy: ENV.fetch("HTTPS_PROXY", nil) }
394
+ ),
395
+ T.nilable(DockerRegistry2::Registry)
396
+ )
397
+ end
398
+
399
+ sig { returns(Integer) }
400
+ def docker_open_timeout_in_seconds
401
+ ENV.fetch("DEPENDABOT_DOCKER_OPEN_TIMEOUT_IN_SECONDS", DEFAULT_DOCKER_OPEN_TIMEOUT_IN_SECONDS).to_i
402
+ end
403
+
404
+ sig { returns(Integer) }
405
+ def docker_read_timeout_in_seconds
406
+ ENV.fetch("DEPENDABOT_DOCKER_READ_TIMEOUT_IN_SECONDS", DEFAULT_DOCKER_READ_TIMEOUT_IN_SECONDS).to_i
407
+ end
408
+
409
+ sig do
410
+ params(
411
+ candidate_tags: T::Array[Dependabot::DockerCompose::Tag],
412
+ version_tag: Dependabot::DockerCompose::Tag
413
+ )
414
+ .returns(T::Array[Dependabot::DockerCompose::Tag])
415
+ end
416
+ def sort_tags(candidate_tags, version_tag)
417
+ candidate_tags.sort do |tag_a, tag_b|
418
+ if comparable_version_from(tag_a) > comparable_version_from(tag_b)
419
+ 1
420
+ elsif comparable_version_from(tag_a) < comparable_version_from(tag_b)
421
+ -1
422
+ elsif tag_a.same_precision?(version_tag)
423
+ 1
424
+ elsif tag_b.same_precision?(version_tag)
425
+ -1
426
+ else
427
+ 0
428
+ end
429
+ end
430
+ end
431
+
432
+ sig do
433
+ params(candidate_tags: T::Array[Dependabot::DockerCompose::Tag])
434
+ .returns(T::Array[Dependabot::DockerCompose::Tag])
435
+ end
436
+ def filter_ignored(candidate_tags)
437
+ filtered =
438
+ candidate_tags
439
+ .reject do |tag|
440
+ version = comparable_version_from(tag)
441
+ ignore_requirements.any? { |r| r.satisfied_by?(version) }
442
+ end
443
+ if @raise_on_ignored &&
444
+ filter_lower_versions(filtered).empty? &&
445
+ filter_lower_versions(candidate_tags).any? &&
446
+ digest_requirements.none?
447
+ raise AllVersionsIgnored
448
+ end
449
+
450
+ filtered
451
+ end
452
+
453
+ sig { params(tags: T::Array[Dependabot::DockerCompose::Tag]).returns(T::Array[Dependabot::DockerCompose::Tag]) }
454
+ def filter_lower_versions(tags)
455
+ tags.select do |tag|
456
+ comparable_version_from(tag) > comparable_version_from(version_tag)
457
+ end
458
+ end
459
+
460
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
461
+ def digest_requirements
462
+ dependency.requirements.select do |requirement|
463
+ requirement.dig(:source, :digest)
464
+ end
465
+ end
466
+
467
+ sig { returns(Dependabot::DockerCompose::Tag) }
468
+ def version_tag
469
+ @version_tag ||= T.let(
470
+ Tag.new(T.must(dependency.version)),
471
+ T.nilable(Dependabot::DockerCompose::Tag)
472
+ )
473
+ end
474
+ end
475
+ # rubocop:enable Metrics/ClassLength
476
+ end
477
+ end
478
+
479
+ Dependabot::UpdateCheckers.register("docker_compose", Dependabot::DockerCompose::UpdateChecker)
@@ -0,0 +1,84 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/version"
5
+ require "dependabot/utils"
6
+ require "dependabot/docker_compose/tag"
7
+ require "sorbet-runtime"
8
+
9
+ module Dependabot
10
+ module DockerCompose
11
+ # In the special case of Java, the version string may also contain
12
+ # optional "update number" and "identifier" components.
13
+ # See https://www.oracle.com/java/technologies/javase/versioning-naming.html
14
+ # for a description of Java versions.
15
+ #
16
+ class Version < Dependabot::Version
17
+ extend T::Sig
18
+ # The regex has limits for the 0,255 and 1,255 repetitions to avoid infinite limits which makes codeql angry.
19
+ # A docker image cannot be longer than 255 characters anyways.
20
+ DOCKER_VERSION_REGEX = /^(?<prefix>[a-z._\-]{0,255})[_\-v]?(?<version>.{1,255})$/
21
+
22
+ sig { override.params(version: VersionParameter).void }
23
+ def initialize(version)
24
+ parsed_version = version.to_s.match(DOCKER_VERSION_REGEX)
25
+ release_part, update_part = T.must(T.must(parsed_version)[:version]).split("_", 2)
26
+
27
+ # The numeric_version is needed here to validate the version string (ex: 20.9.0-alpine3.18)
28
+ # when the call is made via Dependabot Api to convert the image version to semver.
29
+ release_part = Tag.new(T.must(release_part).chomp(".").chomp("-").chomp("_")).numeric_version
30
+
31
+ @release_part = T.let(Dependabot::Version.new(T.must(release_part).tr("-", ".")), Dependabot::Version)
32
+ @update_part = T.let(
33
+ Dependabot::Version.new(update_part&.start_with?(/[0-9]/) ? update_part : 0),
34
+ Dependabot::Version
35
+ )
36
+
37
+ super(@release_part)
38
+ end
39
+
40
+ sig { override.params(version: VersionParameter).returns(T::Boolean) }
41
+ def self.correct?(version)
42
+ return true if version.is_a?(Gem::Version)
43
+
44
+ # We can't call new here because Gem::Version calls self.correct? in its initialize method
45
+ # causing an infinite loop, so instead we check if the release_part of the version is correct
46
+ parsed_version = version.to_s.match(DOCKER_VERSION_REGEX)
47
+ return false if parsed_version.nil?
48
+
49
+ release_part, = T.must(parsed_version[:version]).split("_", 2)
50
+ release_part = Tag.new(T.must(release_part).chomp(".").chomp("-").chomp("_")).numeric_version || parsed_version
51
+ super(release_part.to_s)
52
+ rescue ArgumentError
53
+ # if we can't instantiate a version, it can't be correct
54
+ false
55
+ end
56
+
57
+ sig { override.returns(String) }
58
+ def to_semver
59
+ @release_part.to_semver
60
+ end
61
+
62
+ sig { returns(T::Array[String]) }
63
+ def segments
64
+ @release_part.segments
65
+ end
66
+
67
+ sig { returns(Dependabot::Version) }
68
+ attr_reader :release_part
69
+
70
+ sig { params(other: Dependabot::DockerCompose::Version).returns(T.nilable(Integer)) }
71
+ def <=>(other)
72
+ sort_criteria <=> other.sort_criteria
73
+ end
74
+
75
+ sig { returns(T::Array[Dependabot::Version]) }
76
+ def sort_criteria
77
+ [@release_part, @update_part]
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ Dependabot::Utils
84
+ .register_version_class("docker_compose", Dependabot::DockerCompose::Version)
@@ -0,0 +1,19 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ # These all need to be required so the various classes can be registered in a
5
+ # lookup table of package manager names to concrete classes.
6
+ require "dependabot/docker_compose/file_fetcher"
7
+ require "dependabot/docker_compose/file_parser"
8
+ require "dependabot/docker_compose/update_checker"
9
+ require "dependabot/docker_compose/file_updater"
10
+ require "dependabot/docker_compose/metadata_finder"
11
+ require "dependabot/docker_compose/requirement"
12
+ require "dependabot/docker_compose/version"
13
+
14
+ require "dependabot/pull_request_creator/labeler"
15
+ Dependabot::PullRequestCreator::Labeler
16
+ .register_label_details("docker_compose", name: "docker_compose", colour: "E5F2FC")
17
+
18
+ require "dependabot/dependency"
19
+ Dependabot::Dependency.register_production_check("docker_compose", ->(_) { true })
@@ -0,0 +1,99 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/file_fetchers"
6
+ require "dependabot/file_fetchers/base"
7
+ require "dependabot/shared/utils/helpers"
8
+
9
+ module Dependabot
10
+ module Shared
11
+ class SharedFileFetcher < Dependabot::FileFetchers::Base
12
+ extend T::Sig
13
+ extend T::Helpers
14
+
15
+ abstract!
16
+
17
+ YAML_REGEXP = /^[^\.].*\.ya?ml$/i
18
+
19
+ sig { abstract.returns(Regexp) }
20
+ def self.filename_regex; end
21
+
22
+ sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
23
+ def self.required_files_in?(filenames)
24
+ filenames.any? { |f| f.match?(filename_regex) }
25
+ end
26
+
27
+ sig { override.returns(T::Array[DependencyFile]) }
28
+ def fetch_files
29
+ fetched_files = []
30
+ fetched_files + correctly_encoded_yamlfiles
31
+ end
32
+
33
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
34
+ def correctly_encoded_yamlfiles
35
+ candidate_files = yamlfiles.select { |f| f.content&.valid_encoding? }
36
+ candidate_files.select do |f|
37
+ if f.type == "file" && Utils.likely_helm_chart?(f)
38
+ true
39
+ else
40
+ # This doesn't handle multi-resource files, but it shouldn't matter, since the first resource
41
+ # in a multi-resource file had better be a valid k8s resource
42
+ content = YAML.safe_load(T.must(f.content), aliases: true)
43
+ likely_kubernetes_resource?(content)
44
+ end
45
+ rescue ::Psych::Exception
46
+ false
47
+ end
48
+ end
49
+
50
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
51
+ def incorrectly_encoded_yamlfiles
52
+ yamlfiles.reject { |f| f.content&.valid_encoding? }
53
+ end
54
+
55
+ sig do
56
+ params(
57
+ incorrectly_encoded_files: T::Array[Dependabot::DependencyFile]
58
+ ).returns(T.noreturn)
59
+ end
60
+ def raise_appropriate_error(
61
+ incorrectly_encoded_files = []
62
+ )
63
+ if incorrectly_encoded_files.none? && incorrectly_encoded_yamlfiles.none?
64
+ raise Dependabot::DependencyFileNotFound.new(
65
+ File.join(directory, "Dockerfile"),
66
+ "No Docker Compose manifest found in #{directory}"
67
+ )
68
+ end
69
+
70
+ invalid_files = incorrectly_encoded_files.any? ? incorrectly_encoded_files : incorrectly_encoded_yamlfiles
71
+ raise Dependabot::DependencyFileNotParseable, T.must(invalid_files.first).path
72
+ end
73
+
74
+ sig { returns(T::Array[DependencyFile]) }
75
+ def yamlfiles
76
+ @yamlfiles ||= T.let(
77
+ repo_contents(raise_errors: false)
78
+ .select { |f| f.type == "file" && f.name.match?(YAML_REGEXP) }
79
+ .map { |f| fetch_file_from_host(f.name) },
80
+ T.nilable(T::Array[DependencyFile])
81
+ )
82
+ end
83
+
84
+ private
85
+
86
+ sig { params(resource: Object).returns(T.nilable(T::Boolean)) }
87
+ def likely_kubernetes_resource?(resource)
88
+ # Heuristic for being a Kubernetes resource. We could make this tighter but this probably works well.
89
+ resource.is_a?(::Hash) && resource.key?("apiVersion") && resource.key?("kind")
90
+ end
91
+
92
+ sig { abstract.returns(String) }
93
+ def default_file_name; end
94
+
95
+ sig { abstract.returns(String) }
96
+ def file_type; end
97
+ end
98
+ end
99
+ end