dependabot-docker 0.382.0 → 0.384.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dependabot/docker/update_checker.rb +403 -48
- data/lib/dependabot/shared/shared_file_parser.rb +1 -0
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62ae318b274003dd8cb082364cb06233a54d27c993e96ebd598128b198f280e7
|
|
4
|
+
data.tar.gz: 3c2c803bab214190b06d60ffe76c4de7781cc81254403a616220d685f1040347
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91b741842929d3bb485325f0315a71defae699ea33b16e0c4fe7f605dfc3e3caeb4db99c2751ccb170059d3e7a741d6c1cc77483ff79ac0e52c18d16be6b6141
|
|
7
|
+
data.tar.gz: f3d93ecfb28273c0af1fe959cc830b67121c20ec8010f086865bbcf3191bbff301da0e406b7305a609e946ae6342898a50b815eea776bf782f09512f3dccc585
|
|
@@ -60,6 +60,17 @@ module Dependabot
|
|
|
60
60
|
T::Array[String]
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
+
# Media types returned for single-platform (non manifest-list) images.
|
|
64
|
+
# Used to cheaply rule out manifest-list comparison via a HEAD request's
|
|
65
|
+
# negotiated Content-Type, avoiding a full manifest GET for these images.
|
|
66
|
+
SINGLE_PLATFORM_MANIFEST_TYPES = T.let(
|
|
67
|
+
[
|
|
68
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
69
|
+
"application/vnd.oci.image.manifest.v1+json"
|
|
70
|
+
].freeze,
|
|
71
|
+
T::Array[String]
|
|
72
|
+
)
|
|
73
|
+
|
|
63
74
|
# Tolerance window for platform timestamp comparison.
|
|
64
75
|
# Multi-arch CI builds may finish platforms at slightly different times.
|
|
65
76
|
PLATFORM_TIMESTAMP_TOLERANCE_SECONDS = T.let(3 * 60 * 60, Integer)
|
|
@@ -141,9 +152,9 @@ module Dependabot
|
|
|
141
152
|
if tag
|
|
142
153
|
updated_tag = latest_version_from(tag)
|
|
143
154
|
updated_source[:tag] = updated_tag
|
|
144
|
-
updated_source[:digest] =
|
|
155
|
+
updated_source[:digest] = resolved_digest_for(tag, updated_tag, digest) if digest || pin_digests?
|
|
145
156
|
elsif digest
|
|
146
|
-
updated_source[:digest] = digest_of("latest")
|
|
157
|
+
updated_source[:digest] = digest_within_cooldown?("latest") ? digest : digest_of("latest")
|
|
147
158
|
end
|
|
148
159
|
|
|
149
160
|
req.merge(source: updated_source)
|
|
@@ -206,37 +217,51 @@ module Dependabot
|
|
|
206
217
|
|
|
207
218
|
sig { returns(T::Boolean) }
|
|
208
219
|
def digest_up_to_date?
|
|
209
|
-
digest_requirements.all?
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
digest_of(latest_tag.name)
|
|
230
|
-
else
|
|
231
|
-
updated_digest
|
|
232
|
-
end
|
|
220
|
+
digest_requirements.all? { |req| digest_requirement_up_to_date?(req) }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
|
|
224
|
+
def digest_requirement_up_to_date?(req)
|
|
225
|
+
source = req.fetch(:source)
|
|
226
|
+
source_digest = source.fetch(:digest)
|
|
227
|
+
source_tag = source[:tag]
|
|
228
|
+
|
|
229
|
+
if source_tag
|
|
230
|
+
latest_tag = latest_tag_from(source_tag)
|
|
231
|
+
digest_only_refresh = latest_tag.name == source_tag
|
|
232
|
+
|
|
233
|
+
# Digest-only refresh (tag unchanged): respect the cooldown window
|
|
234
|
+
# so a freshly-pushed digest isn't proposed before it matures. This
|
|
235
|
+
# covers both comparable and non-comparable tags (e.g. "alpine"),
|
|
236
|
+
# which the version-tag cooldown (apply_cooldown) never reaches.
|
|
237
|
+
return true if digest_only_refresh && digest_within_cooldown?(source_tag)
|
|
233
238
|
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
|
|
239
|
+
# When digest-only updates are suppressed and the tag hasn't changed,
|
|
240
|
+
# treat the digest as up-to-date to avoid proposing a PR that only
|
|
241
|
+
# bumps the digest without a corresponding version change.
|
|
242
|
+
# Only apply to comparable (versioned) tags — non-comparable tags like
|
|
243
|
+
# "latest" or distro codenames should still get digest updates.
|
|
244
|
+
return true if digest_only_update_suppressed?(source_tag, latest_tag)
|
|
237
245
|
|
|
238
|
-
|
|
246
|
+
expected_digest = digest_of(latest_tag.name)
|
|
247
|
+
else
|
|
248
|
+
digest_only_refresh = true
|
|
249
|
+
# Pure digest pin (no tag): the proposed digest resolves from the
|
|
250
|
+
# "latest" tag, so gate it on the same cooldown window.
|
|
251
|
+
return true if digest_within_cooldown?("latest")
|
|
252
|
+
|
|
253
|
+
expected_digest = updated_digest
|
|
239
254
|
end
|
|
255
|
+
|
|
256
|
+
# If we can't determine an expected digest (for example if the registry does not return digests)
|
|
257
|
+
# assume it's up to date
|
|
258
|
+
return true if expected_digest.nil?
|
|
259
|
+
|
|
260
|
+
# Multi-arch no-op: a digest-only refresh whose index digest changed only
|
|
261
|
+
# because an unconsumed platform was rebuilt should not open a PR.
|
|
262
|
+
return true if digest_refresh_is_noop?(source, source_digest, expected_digest, digest_only_refresh)
|
|
263
|
+
|
|
264
|
+
source_digest == expected_digest
|
|
240
265
|
end
|
|
241
266
|
|
|
242
267
|
sig { params(version: String).returns(String) }
|
|
@@ -263,6 +288,7 @@ module Dependabot
|
|
|
263
288
|
# (which requires a call to the registry for each tag, so can be slow)
|
|
264
289
|
candidate_tags = comparable_tags_from_registry(version_tag)
|
|
265
290
|
candidate_tags = remove_version_downgrades(candidate_tags, version_tag)
|
|
291
|
+
candidate_tags = remove_more_precise_tags(candidate_tags, version_tag)
|
|
266
292
|
candidate_tags = remove_prereleases(candidate_tags, version_tag)
|
|
267
293
|
candidate_tags = filter_ignored(candidate_tags)
|
|
268
294
|
candidate_tags = sort_tags(candidate_tags, version_tag)
|
|
@@ -285,14 +311,26 @@ module Dependabot
|
|
|
285
311
|
candidate_tags.reverse_each do |candidate|
|
|
286
312
|
selected = select_tag_with_precision(candidate, same_precision_tags, version_tag)
|
|
287
313
|
|
|
314
|
+
# Content check (runs regardless of experiments): if the candidate tag
|
|
315
|
+
# resolves to the exact same image contents as the current tag, there
|
|
316
|
+
# is no real update to make. This catches rolling tags (e.g. "9.0")
|
|
317
|
+
# that point to the same image as a pinned tag (e.g. "9.0.11").
|
|
318
|
+
if selected.name != version_tag.name && same_image_contents?(selected, version_tag)
|
|
319
|
+
Dependabot.logger.info(
|
|
320
|
+
"Digest check: #{selected.name} has the same image contents as #{version_tag.name} " \
|
|
321
|
+
"— no update needed, staying on #{version_tag.name}"
|
|
322
|
+
)
|
|
323
|
+
next
|
|
324
|
+
end
|
|
325
|
+
|
|
288
326
|
if Dependabot::Experiments.enabled?(:docker_created_timestamp_validation) &&
|
|
289
327
|
selected.name != version_tag.name
|
|
290
328
|
if validation_attempts >= MAX_PLATFORM_VALIDATION_ATTEMPTS
|
|
291
329
|
Dependabot.logger.info(
|
|
292
330
|
"Platform validation: reached max attempts (#{MAX_PLATFORM_VALIDATION_ATTEMPTS}), " \
|
|
293
|
-
"
|
|
331
|
+
"skipping #{selected.name} — continuing to find a validated candidate"
|
|
294
332
|
)
|
|
295
|
-
|
|
333
|
+
next
|
|
296
334
|
end
|
|
297
335
|
validation_attempts += 1
|
|
298
336
|
end
|
|
@@ -433,7 +471,7 @@ module Dependabot
|
|
|
433
471
|
published_date = last_modified ? Time.parse(last_modified) : nil
|
|
434
472
|
|
|
435
473
|
Dependabot::Package::PackageRelease.new(
|
|
436
|
-
version:
|
|
474
|
+
version: release_version_for(tag),
|
|
437
475
|
released_at: published_date,
|
|
438
476
|
latest: false,
|
|
439
477
|
yanked: false,
|
|
@@ -562,6 +600,26 @@ module Dependabot
|
|
|
562
600
|
end
|
|
563
601
|
end
|
|
564
602
|
|
|
603
|
+
# When the current tag pins only the major or major.minor version (e.g.
|
|
604
|
+
# "9.0"), a candidate that additionally specifies a patch version (e.g.
|
|
605
|
+
# "9.0.17") is not a wanted upgrade: pinning the less precise tag opts into
|
|
606
|
+
# a rolling tag, so the patch should not be specified for them. Only true
|
|
607
|
+
# semver tags (":normal" format) are filtered here — build-number and date
|
|
608
|
+
# based formats have their own comparability rules and are left untouched.
|
|
609
|
+
sig do
|
|
610
|
+
params(
|
|
611
|
+
candidate_tags: T::Array[Dependabot::Docker::Tag],
|
|
612
|
+
version_tag: Dependabot::Docker::Tag
|
|
613
|
+
)
|
|
614
|
+
.returns(T::Array[Dependabot::Docker::Tag])
|
|
615
|
+
end
|
|
616
|
+
def remove_more_precise_tags(candidate_tags, version_tag)
|
|
617
|
+
return candidate_tags unless version_tag.format == :normal
|
|
618
|
+
return candidate_tags if version_tag.precision > 2
|
|
619
|
+
|
|
620
|
+
candidate_tags.select { |tag| tag.precision <= version_tag.precision }
|
|
621
|
+
end
|
|
622
|
+
|
|
565
623
|
sig do
|
|
566
624
|
params(
|
|
567
625
|
candidate_tags: T::Array[Dependabot::Docker::Tag],
|
|
@@ -899,6 +957,130 @@ module Dependabot
|
|
|
899
957
|
Dependabot::UpdateCheckers::CooldownCalculation.within_cooldown_window?(release_date, days)
|
|
900
958
|
end
|
|
901
959
|
|
|
960
|
+
# Builds the PackageRelease version for a tag. Non-comparable tags (e.g.
|
|
961
|
+
# "alpine") have no semver, so Docker::Version.new would raise. The version
|
|
962
|
+
# is only consumed by semver-aware version-tag cooldown; the digest cooldown
|
|
963
|
+
# path relies solely on the release date, so a sentinel keeps PackageRelease
|
|
964
|
+
# construction safe for non-comparable tags.
|
|
965
|
+
sig { params(tag: Dependabot::Docker::Tag).returns(Dependabot::Version) }
|
|
966
|
+
def release_version_for(tag)
|
|
967
|
+
Docker::Version.new(tag.name)
|
|
968
|
+
rescue ArgumentError, TypeError
|
|
969
|
+
Dependabot::Version.new("0")
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
# Whether the digest currently served for the given tag was published too
|
|
973
|
+
# recently to satisfy the configured cooldown window. Digest-only refreshes
|
|
974
|
+
# don't change the version string, so the default cooldown window applies
|
|
975
|
+
# (semver-specific windows require a version delta, and the tag may be
|
|
976
|
+
# non-comparable like "alpine"). Fails open (returns false) when the
|
|
977
|
+
# publication date can't be determined, so a missing Last-Modified header
|
|
978
|
+
# never permanently blocks an update.
|
|
979
|
+
sig { params(tag_name: String).returns(T::Boolean) }
|
|
980
|
+
def digest_within_cooldown?(tag_name)
|
|
981
|
+
return false if should_skip_cooldown?
|
|
982
|
+
|
|
983
|
+
cooldown = @update_cooldown
|
|
984
|
+
return false unless cooldown
|
|
985
|
+
|
|
986
|
+
released_at = publication_detail(Tag.new(tag_name))&.released_at
|
|
987
|
+
return false unless released_at
|
|
988
|
+
|
|
989
|
+
Dependabot::UpdateCheckers::CooldownCalculation.within_cooldown_window?(
|
|
990
|
+
released_at, cooldown.default_days
|
|
991
|
+
)
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# Resolves the digest to write for a tag during a requirement update.
|
|
995
|
+
# Keeps the existing digest for a digest-only refresh that's still within
|
|
996
|
+
# the cooldown window; otherwise resolves the latest digest for the tag.
|
|
997
|
+
sig do
|
|
998
|
+
params(
|
|
999
|
+
current_tag: String,
|
|
1000
|
+
updated_tag: String,
|
|
1001
|
+
current_digest: T.nilable(String)
|
|
1002
|
+
).returns(T.nilable(String))
|
|
1003
|
+
end
|
|
1004
|
+
def resolved_digest_for(current_tag, updated_tag, current_digest)
|
|
1005
|
+
# Keep the existing digest for a digest-only refresh that's still within
|
|
1006
|
+
# the cooldown window; otherwise resolve the latest digest for the tag.
|
|
1007
|
+
return current_digest if current_digest && updated_tag == current_tag &&
|
|
1008
|
+
digest_within_cooldown?(updated_tag)
|
|
1009
|
+
|
|
1010
|
+
digest_of(updated_tag)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# Whether a digest-only update for an unchanged comparable tag is suppressed
|
|
1014
|
+
# by the docker_digest_only_update_suppression experiment. Non-comparable
|
|
1015
|
+
# tags like "latest" or distro codenames are excluded so they still update.
|
|
1016
|
+
sig do
|
|
1017
|
+
params(source_tag: String, latest_tag: Dependabot::Docker::Tag).returns(T::Boolean)
|
|
1018
|
+
end
|
|
1019
|
+
def digest_only_update_suppressed?(source_tag, latest_tag)
|
|
1020
|
+
Dependabot::Experiments.enabled?(:docker_digest_only_update_suppression) &&
|
|
1021
|
+
Tag.new(source_tag).comparable? &&
|
|
1022
|
+
latest_tag.name == source_tag
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
# Whether a digest-only refresh is a no-op for the platform(s) the user
|
|
1026
|
+
# actually consumes. Only applies when the tag is unchanged and the index
|
|
1027
|
+
# digest moved; otherwise this is a genuine version/digest change.
|
|
1028
|
+
sig do
|
|
1029
|
+
params(
|
|
1030
|
+
source: T::Hash[Symbol, T.untyped],
|
|
1031
|
+
current_digest: String,
|
|
1032
|
+
expected_digest: String,
|
|
1033
|
+
digest_only_refresh: T::Boolean
|
|
1034
|
+
).returns(T::Boolean)
|
|
1035
|
+
end
|
|
1036
|
+
def digest_refresh_is_noop?(source, current_digest, expected_digest, digest_only_refresh)
|
|
1037
|
+
digest_only_refresh &&
|
|
1038
|
+
current_digest != expected_digest &&
|
|
1039
|
+
multiarch_noop?(source, current_digest, expected_digest)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
# Compares the per-platform manifest digests of the currently-pinned index
|
|
1043
|
+
# against the candidate index. When the Dockerfile pins `--platform`, only
|
|
1044
|
+
# that platform is compared; otherwise every platform must be identical.
|
|
1045
|
+
# Fails open (returns false) whenever a confident comparison isn't possible.
|
|
1046
|
+
sig do
|
|
1047
|
+
params(
|
|
1048
|
+
source: T::Hash[Symbol, T.untyped],
|
|
1049
|
+
current_digest: String,
|
|
1050
|
+
candidate_digest: String
|
|
1051
|
+
).returns(T::Boolean)
|
|
1052
|
+
end
|
|
1053
|
+
def multiarch_noop?(source, current_digest, candidate_digest)
|
|
1054
|
+
current = platform_digests("sha256:#{current_digest}")
|
|
1055
|
+
candidate = platform_digests("sha256:#{candidate_digest}")
|
|
1056
|
+
|
|
1057
|
+
# Either reference is single-platform or couldn't be fetched: can't prove a no-op.
|
|
1058
|
+
return false if current.nil? || candidate.nil?
|
|
1059
|
+
|
|
1060
|
+
pinned = pinned_platform_key(source[:platform])
|
|
1061
|
+
if pinned
|
|
1062
|
+
current_platform = current[pinned]
|
|
1063
|
+
candidate_platform = candidate[pinned]
|
|
1064
|
+
return false if current_platform.nil? || candidate_platform.nil?
|
|
1065
|
+
|
|
1066
|
+
current_platform == candidate_platform
|
|
1067
|
+
else
|
|
1068
|
+
# No platform pin: only a no-op if nothing changed for any platform.
|
|
1069
|
+
current == candidate
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Normalizes a Dockerfile `--platform` value into a manifest platform key
|
|
1074
|
+
# (e.g. "linux/amd64", "linux/arm64/v8"). Returns nil for build-arg
|
|
1075
|
+
# placeholders like `$BUILDPLATFORM` so we fall back to the strict check.
|
|
1076
|
+
sig { params(platform: T.nilable(String)).returns(T.nilable(String)) }
|
|
1077
|
+
def pinned_platform_key(platform)
|
|
1078
|
+
return nil if platform.nil?
|
|
1079
|
+
return nil unless platform.match?(%r{\A[a-z0-9]+/[a-z0-9]+(?:/[a-z0-9]+)?\z})
|
|
1080
|
+
|
|
1081
|
+
platform
|
|
1082
|
+
end
|
|
1083
|
+
|
|
902
1084
|
# Fetches the "created" timestamp from the image config blob for a given tag.
|
|
903
1085
|
# This represents the actual build time, which is more reliable than semver
|
|
904
1086
|
# for determining which image is truly newer.
|
|
@@ -1083,6 +1265,153 @@ module Dependabot
|
|
|
1083
1265
|
candidate_created > current_created
|
|
1084
1266
|
end
|
|
1085
1267
|
|
|
1268
|
+
# Returns true when the candidate tag and current tag reference the exact
|
|
1269
|
+
# same image contents. Only multi-arch (manifest list) images are compared
|
|
1270
|
+
# here, by checking that both tags expose the identical set of platforms
|
|
1271
|
+
# with matching per-platform manifest digests. For single-platform images this
|
|
1272
|
+
# returns false (empty digest map), so this check is effectively a no-op and
|
|
1273
|
+
# we fall back to the existing semver/precision selection logic.
|
|
1274
|
+
#
|
|
1275
|
+
# Single-platform current tags are ruled out up front with a cheap HEAD
|
|
1276
|
+
# request, so the default path doesn't pay for a full manifest GET that
|
|
1277
|
+
# would only ever return an empty digest map.
|
|
1278
|
+
sig do
|
|
1279
|
+
params(
|
|
1280
|
+
candidate_tag: Dependabot::Docker::Tag,
|
|
1281
|
+
current_tag: Dependabot::Docker::Tag
|
|
1282
|
+
).returns(T::Boolean)
|
|
1283
|
+
end
|
|
1284
|
+
def same_image_contents?(candidate_tag, current_tag)
|
|
1285
|
+
# Only manifest lists can match here, so confirm the current tag is
|
|
1286
|
+
# multi-arch with a cheap HEAD request before paying for the full
|
|
1287
|
+
# manifest GET that fetch_platform_digests performs. Single-platform
|
|
1288
|
+
# images (the default path) short-circuit without an extra manifest GET.
|
|
1289
|
+
return false if single_platform_image?(current_tag.name)
|
|
1290
|
+
|
|
1291
|
+
current_digests = fetch_platform_digests(current_tag.name)
|
|
1292
|
+
return false if current_digests.empty?
|
|
1293
|
+
|
|
1294
|
+
candidate_digests = fetch_platform_digests(candidate_tag.name)
|
|
1295
|
+
return false if candidate_digests.empty?
|
|
1296
|
+
|
|
1297
|
+
# The contents are unchanged only when both tags expose the exact same set
|
|
1298
|
+
# of platforms with identical per-platform manifest digests. A candidate
|
|
1299
|
+
# with extra platforms is a different image and must not match.
|
|
1300
|
+
current_digests == candidate_digests
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
# Returns true when a tag points at a single-platform image rather than a
|
|
1304
|
+
# multi-arch manifest list. Uses a HEAD request — the registry reports the
|
|
1305
|
+
# negotiated media type in the Content-Type header — so we avoid the more
|
|
1306
|
+
# expensive full-manifest GET that fetch_platform_digests performs.
|
|
1307
|
+
#
|
|
1308
|
+
# Reuses a manifest list already fetched for the tag when available, and
|
|
1309
|
+
# only treats positively recognised single-platform media types as
|
|
1310
|
+
# single-platform. An ambiguous, missing or errored response returns false
|
|
1311
|
+
# so the caller falls back to the per-platform comparison rather than risk
|
|
1312
|
+
# skipping a real manifest list.
|
|
1313
|
+
sig { params(tag_name: String).returns(T::Boolean) }
|
|
1314
|
+
def single_platform_image?(tag_name)
|
|
1315
|
+
return manifest_list_cache[tag_name].nil? if manifest_list_cache.key?(tag_name)
|
|
1316
|
+
return T.must(single_platform_cache[tag_name]) if single_platform_cache.key?(tag_name)
|
|
1317
|
+
|
|
1318
|
+
single_platform_cache[tag_name] = fetch_single_platform_flag(tag_name)
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
sig { params(tag_name: String).returns(T::Boolean) }
|
|
1322
|
+
def fetch_single_platform_flag(tag_name)
|
|
1323
|
+
response = with_retries(max_attempts: 3, errors: transient_docker_errors) do
|
|
1324
|
+
docker_registry_client.dohead("v2/#{docker_repo_name}/manifests/#{tag_name}")
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
media_type = response.headers[:content_type]&.split(";")&.first&.strip
|
|
1328
|
+
SINGLE_PLATFORM_MANIFEST_TYPES.include?(media_type)
|
|
1329
|
+
rescue DockerRegistry2::Exception => e
|
|
1330
|
+
Dependabot.logger.info(
|
|
1331
|
+
"Failed to determine manifest media type for #{docker_repo_name}:#{tag_name}, " \
|
|
1332
|
+
"falling back to per-platform comparison: #{e.class} - #{e.message}"
|
|
1333
|
+
)
|
|
1334
|
+
false
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
# Fetches a tag's manifest list (multi-arch image index) and returns the
|
|
1338
|
+
# array of per-platform manifest entries, or nil for single-platform images
|
|
1339
|
+
# (not a manifest list). Cached so the platform-inspection methods below
|
|
1340
|
+
# share a single registry GET per tag.
|
|
1341
|
+
sig { params(tag_name: String).returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
|
|
1342
|
+
def fetch_manifest_list(tag_name)
|
|
1343
|
+
return manifest_list_cache[tag_name] if manifest_list_cache.key?(tag_name)
|
|
1344
|
+
|
|
1345
|
+
manifest_list_cache[tag_name] = fetch_manifest_list_from_registry(tag_name)
|
|
1346
|
+
rescue DockerRegistry2::Exception, JSON::ParserError => e
|
|
1347
|
+
# Do NOT cache fetch failures. A cached nil is reserved for "definitely
|
|
1348
|
+
# not a manifest list" (single-platform), which single_platform_image?
|
|
1349
|
+
# relies on. Caching a transient error as nil would misclassify the tag
|
|
1350
|
+
# as single-platform and permanently short-circuit same-content
|
|
1351
|
+
# suppression, even if a later manifest fetch would succeed. Returning
|
|
1352
|
+
# nil without caching lets a subsequent call retry.
|
|
1353
|
+
Dependabot.logger.info(
|
|
1354
|
+
"Failed to fetch manifest list for #{docker_repo_name}:#{tag_name}: #{e.message}"
|
|
1355
|
+
)
|
|
1356
|
+
nil
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
sig { params(tag_name: String).returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
|
|
1360
|
+
def fetch_manifest_list_from_registry(tag_name)
|
|
1361
|
+
manifest = with_retries(max_attempts: 3, errors: transient_docker_errors) do
|
|
1362
|
+
docker_registry_client.manifest(docker_repo_name, tag_name)
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
media_type = manifest["mediaType"] || manifest[:mediaType]
|
|
1366
|
+
return nil unless MANIFEST_LIST_TYPES.include?(media_type)
|
|
1367
|
+
|
|
1368
|
+
manifest["manifests"] || manifest[:manifests] || []
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
# Fetches a map of platform key (e.g. "linux/amd64") to platform manifest
|
|
1372
|
+
# digest for a tag's manifest list. Returns an empty hash for single-platform
|
|
1373
|
+
# images or when the manifest can't be fetched.
|
|
1374
|
+
sig { params(tag_name: String).returns(T::Hash[String, String]) }
|
|
1375
|
+
def fetch_platform_digests(tag_name)
|
|
1376
|
+
return T.must(platform_digests_cache[tag_name]) if platform_digests_cache.key?(tag_name)
|
|
1377
|
+
|
|
1378
|
+
digests = fetch_platform_digests_from_registry(tag_name)
|
|
1379
|
+
platform_digests_cache[tag_name] = digests
|
|
1380
|
+
digests
|
|
1381
|
+
rescue *transient_docker_errors, DockerRegistry2::RegistryAuthenticationException,
|
|
1382
|
+
RestClient::Forbidden, RestClient::TooManyRequests, JSON::ParserError => e
|
|
1383
|
+
Dependabot.logger.info(
|
|
1384
|
+
"Failed to fetch platform digests for #{docker_repo_name}:#{tag_name}: #{e.message}"
|
|
1385
|
+
)
|
|
1386
|
+
platform_digests_cache[tag_name] = {}
|
|
1387
|
+
{}
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
sig { params(tag_name: String).returns(T::Hash[String, String]) }
|
|
1391
|
+
def fetch_platform_digests_from_registry(tag_name)
|
|
1392
|
+
manifests = fetch_manifest_list(tag_name)
|
|
1393
|
+
return {} unless manifests
|
|
1394
|
+
|
|
1395
|
+
collect_platform_digests(manifests)
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
sig { params(manifests: T.untyped).returns(T::Hash[String, String]) }
|
|
1399
|
+
def collect_platform_digests(manifests)
|
|
1400
|
+
digests = {}
|
|
1401
|
+
|
|
1402
|
+
manifests.each do |m|
|
|
1403
|
+
platform = extract_platform(m)
|
|
1404
|
+
next unless platform
|
|
1405
|
+
|
|
1406
|
+
digest = m["digest"] || m[:digest]
|
|
1407
|
+
next unless digest
|
|
1408
|
+
|
|
1409
|
+
digests[platform_key(platform)] = digest
|
|
1410
|
+
end
|
|
1411
|
+
|
|
1412
|
+
digests
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1086
1415
|
# Fetches the platform entries from a manifest list for a given tag.
|
|
1087
1416
|
# Returns nil if the tag is a single-platform image (not a manifest list).
|
|
1088
1417
|
sig { params(tag_name: String).returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
|
|
@@ -1103,19 +1432,26 @@ module Dependabot
|
|
|
1103
1432
|
|
|
1104
1433
|
sig { params(tag_name: String).returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
|
|
1105
1434
|
def fetch_manifest_platforms_from_registry(tag_name)
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
end
|
|
1109
|
-
|
|
1110
|
-
media_type = manifest["mediaType"] || manifest[:mediaType]
|
|
1111
|
-
return nil unless MANIFEST_LIST_TYPES.include?(media_type)
|
|
1112
|
-
|
|
1113
|
-
manifests = manifest["manifests"] || manifest[:manifests] || []
|
|
1435
|
+
manifests = fetch_manifest_list(tag_name)
|
|
1436
|
+
return nil unless manifests
|
|
1114
1437
|
|
|
1115
1438
|
# Filter to actual image manifests (exclude attestations/signatures)
|
|
1116
1439
|
manifests.filter_map { |m| extract_platform(m) }
|
|
1117
1440
|
end
|
|
1118
1441
|
|
|
1442
|
+
# Maps each platform in a manifest list to its per-platform manifest digest,
|
|
1443
|
+
# e.g. { "linux/amd64" => "sha256:…", "linux/arm64/v8" => "sha256:…" }.
|
|
1444
|
+
# Accepts a tag name or a digest reference (e.g. "sha256:…"). Returns nil
|
|
1445
|
+
# for single-platform images or when the manifest can't be fetched, so
|
|
1446
|
+
# callers fail open rather than treat an unknown image as a no-op.
|
|
1447
|
+
sig { params(reference: String).returns(T.nilable(T::Hash[String, String])) }
|
|
1448
|
+
def platform_digests(reference)
|
|
1449
|
+
digests = fetch_platform_digests(reference)
|
|
1450
|
+
return nil if digests.empty?
|
|
1451
|
+
|
|
1452
|
+
digests
|
|
1453
|
+
end
|
|
1454
|
+
|
|
1119
1455
|
sig { params(manifest_entry: T.untyped).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
1120
1456
|
def extract_platform(manifest_entry)
|
|
1121
1457
|
platform = manifest_entry["platform"] || manifest_entry[:platform]
|
|
@@ -1160,14 +1496,9 @@ module Dependabot
|
|
|
1160
1496
|
|
|
1161
1497
|
sig { params(tag_name: String).returns(T::Hash[String, T.nilable(Time)]) }
|
|
1162
1498
|
def fetch_all_platform_timestamps_from_registry(tag_name)
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
end
|
|
1499
|
+
manifests = fetch_manifest_list(tag_name)
|
|
1500
|
+
return {} unless manifests
|
|
1166
1501
|
|
|
1167
|
-
media_type = manifest["mediaType"] || manifest[:mediaType]
|
|
1168
|
-
return {} unless MANIFEST_LIST_TYPES.include?(media_type)
|
|
1169
|
-
|
|
1170
|
-
manifests = manifest["manifests"] || manifest[:manifests] || []
|
|
1171
1502
|
collect_platform_timestamps(manifests)
|
|
1172
1503
|
end
|
|
1173
1504
|
|
|
@@ -1202,6 +1533,22 @@ module Dependabot
|
|
|
1202
1533
|
parse_created_from_config_blob(config_digest)
|
|
1203
1534
|
end
|
|
1204
1535
|
|
|
1536
|
+
sig { returns(T::Hash[String, T.nilable(T::Array[T::Hash[String, T.untyped]])]) }
|
|
1537
|
+
def manifest_list_cache
|
|
1538
|
+
@manifest_list_cache ||= T.let(
|
|
1539
|
+
{},
|
|
1540
|
+
T.nilable(T::Hash[String, T.nilable(T::Array[T::Hash[String, T.untyped]])])
|
|
1541
|
+
)
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
sig { returns(T::Hash[String, T::Boolean]) }
|
|
1545
|
+
def single_platform_cache
|
|
1546
|
+
@single_platform_cache ||= T.let(
|
|
1547
|
+
{},
|
|
1548
|
+
T.nilable(T::Hash[String, T::Boolean])
|
|
1549
|
+
)
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1205
1552
|
sig { returns(T::Hash[String, T.nilable(T::Array[T::Hash[String, T.untyped]])]) }
|
|
1206
1553
|
def manifest_platforms_cache
|
|
1207
1554
|
@manifest_platforms_cache ||= T.let(
|
|
@@ -1218,6 +1565,14 @@ module Dependabot
|
|
|
1218
1565
|
)
|
|
1219
1566
|
end
|
|
1220
1567
|
|
|
1568
|
+
sig { returns(T::Hash[String, T::Hash[String, String]]) }
|
|
1569
|
+
def platform_digests_cache
|
|
1570
|
+
@platform_digests_cache ||= T.let(
|
|
1571
|
+
{},
|
|
1572
|
+
T.nilable(T::Hash[String, T::Hash[String, String]])
|
|
1573
|
+
)
|
|
1574
|
+
end
|
|
1575
|
+
|
|
1221
1576
|
sig { returns(T::Hash[String, T.nilable(Time)]) }
|
|
1222
1577
|
def config_created_timestamps
|
|
1223
1578
|
@config_created_timestamps ||= T.let(
|
|
@@ -43,6 +43,7 @@ module Dependabot
|
|
|
43
43
|
source[:registry] = parsed_line.fetch("registry") if parsed_line.fetch("registry")
|
|
44
44
|
source[:tag] = parsed_line.fetch("tag") if parsed_line.fetch("tag")
|
|
45
45
|
source[:digest] = parsed_line.fetch("digest") if parsed_line.fetch("digest")
|
|
46
|
+
source[:platform] = parsed_line["platform"] if parsed_line["platform"]
|
|
46
47
|
|
|
47
48
|
source
|
|
48
49
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-docker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.384.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.384.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.384.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -266,7 +266,7 @@ licenses:
|
|
|
266
266
|
- MIT
|
|
267
267
|
metadata:
|
|
268
268
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
269
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
269
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.384.0
|
|
270
270
|
rdoc_options: []
|
|
271
271
|
require_paths:
|
|
272
272
|
- lib
|
|
@@ -281,7 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
281
281
|
- !ruby/object:Gem::Version
|
|
282
282
|
version: 3.3.0
|
|
283
283
|
requirements: []
|
|
284
|
-
rubygems_version:
|
|
284
|
+
rubygems_version: 4.0.14
|
|
285
285
|
specification_version: 4
|
|
286
286
|
summary: Provides Dependabot support for Docker
|
|
287
287
|
test_files: []
|