dependabot-docker_compose 0.297.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/dependabot/docker_compose/file_fetcher.rb +67 -0
- data/lib/dependabot/docker_compose/file_parser.rb +76 -0
- data/lib/dependabot/docker_compose/file_updater.rb +106 -0
- data/lib/dependabot/docker_compose/metadata_finder.rb +39 -0
- data/lib/dependabot/docker_compose/package_manager.rb +53 -0
- data/lib/dependabot/docker_compose/requirement.rb +43 -0
- data/lib/dependabot/docker_compose/tag.rb +144 -0
- data/lib/dependabot/docker_compose/update_checker.rb +479 -0
- data/lib/dependabot/docker_compose/version.rb +84 -0
- data/lib/dependabot/docker_compose.rb +19 -0
- data/lib/dependabot/shared/shared_file_fetcher.rb +99 -0
- data/lib/dependabot/shared/shared_file_parser.rb +80 -0
- data/lib/dependabot/shared/shared_file_updater.rb +261 -0
- data/lib/dependabot/shared/utils/credentials_finder.rb +101 -0
- data/lib/dependabot/shared/utils/helpers.rb +19 -0
- metadata +285 -0
@@ -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
|