dependabot-docker 0.395.0 → 0.397.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 631a60bfcb79a600a89602befd184d8af5f3aff6e93ddcd636b0ba3a57092b67
4
- data.tar.gz: 9c19a951e4fc1eeaa5448706eda907a35c15fd3ff3d8226a0b67531f437f8a08
3
+ metadata.gz: b43ff0be6b23394fc47285dce19edb7b8e90bcc6a859243315758559dbd5fac4
4
+ data.tar.gz: 0b12ca42143a2d08547b575d4b4373ce4cf0490f22fc41f8c2fb2ff4a944ef4f
5
5
  SHA512:
6
- metadata.gz: 4f129aaf0c6612c022f276e4b84233c7d7f0276a2b393d603fdffa69a209c4f292eea55c7d7981ab365ee209d6f477a36f95c16cdcbbdf0d1515149e3f848320
7
- data.tar.gz: 82d0113a5eb41bee3c43a0840c55a3b7738c2c1b45d24edc39a1bee41cd2af8a5d6d282a39154555a5862e42f21314583ec58fec6e52ceaf7265c149b43faf31
6
+ metadata.gz: 338d342f4d99ee355295af0581139e6a121c580bcc636d54cd8bde653b4169b569e80b5371ab1b0bd321e421df07fb13f4275eea8e4aebe2384a55afc014e18d
7
+ data.tar.gz: a7feda400c5b1fc35e0ac60b36d37c7bfa57295416ff1e230fe656311700573b4fae9d0ac0097702e8325d94a2f245ba56f668ddca145c2df3f63c8f49ef8bee
@@ -145,7 +145,11 @@ module Dependabot
145
145
  repo = img_hash.fetch("repository", nil)
146
146
  return [] unless repo.is_a?(String)
147
147
 
148
- match = tag_value.to_s.match(TAG_WITH_DIGEST)
148
+ tag = tag_value.to_s
149
+ # Helm template expressions cannot be resolved without rendering the chart.
150
+ return [] if tag.include?("{{")
151
+
152
+ match = tag.match(TAG_WITH_DIGEST)
149
153
  return [] unless match
150
154
 
151
155
  tag_details = match.named_captures
@@ -4,7 +4,6 @@
4
4
  require "sorbet-runtime"
5
5
  require "dependabot/file_fetchers"
6
6
  require "dependabot/file_fetchers/base"
7
- require "dependabot/notices"
8
7
  require "dependabot/shared/shared_file_updater"
9
8
 
10
9
  module Dependabot
@@ -29,21 +28,6 @@ module Dependabot
29
28
  def container_image_regex
30
29
  %r{^#{FROM_REGEX}\s+(docker\.io/)?}o
31
30
  end
32
-
33
- sig { override.returns(T::Array[Dependabot::Notice]) }
34
- def notices
35
- return [] unless dependencies.any? { |dependency| dependency.metadata[:docker_cooldown_date_unavailable] }
36
-
37
- [Dependabot::Notice.new(
38
- mode: Dependabot::Notice::NoticeMode::WARN,
39
- type: "docker_cooldown_date_unavailable",
40
- package_manager_name: "docker",
41
- title: "Docker cooldown was not applied",
42
- description: "Cooldown could not be applied because no publication date was available from the registry.",
43
- show_in_pr: true,
44
- show_alert: false
45
- )]
46
- end
47
31
  end
48
32
  end
49
33
  end
@@ -2,7 +2,10 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "docker_registry2"
5
+ require "excon"
6
+ require "json"
5
7
  require "sorbet-runtime"
8
+ require "uri"
6
9
 
7
10
  begin
8
11
  require "rest-client"
@@ -36,6 +39,7 @@ end
36
39
  require "dependabot/update_checkers"
37
40
  require "dependabot/update_checkers/base"
38
41
  require "dependabot/update_checkers/cooldown_calculation"
42
+ require "dependabot/registry_client"
39
43
  require "dependabot/errors"
40
44
  require "dependabot/docker/tag"
41
45
  require "dependabot/docker/file_parser"
@@ -444,7 +448,10 @@ module Dependabot
444
448
  # If we can't determine publication details, skip cooldown for this tag and use it
445
449
  # rather than blocking the update when the registry doesn't support the required API calls
446
450
  if !details || !details.released_at
447
- mark_cooldown_date_unavailable if cooldown_days_for(tag).positive?
451
+ Dependabot::UpdateCheckers::CooldownCalculation.mark_cooldown_date_unavailable(
452
+ dependency,
453
+ cooldown_days: cooldown_days_for(tag)
454
+ )
448
455
  return [tag]
449
456
  end
450
457
 
@@ -468,25 +475,8 @@ module Dependabot
468
475
 
469
476
  sig { params(tag: Dependabot::Docker::Tag).returns(T.nilable(Dependabot::Package::PackageRelease)) }
470
477
  def get_tag_publication_details(tag)
471
- digest_info = with_retries(max_attempts: 3, errors: transient_docker_errors) do
472
- client = docker_registry_client
473
- client.digest(docker_repo_name, tag.name)
474
- end
475
-
476
- first_digest = extract_digest_from_response(digest_info, tag)
477
- return nil unless first_digest
478
-
479
- # When digest_info is an Array the registry returned a manifest list
480
- # (OCI image index) and the extracted digest points at a platform-
481
- # specific *manifest*, not a blob. Use the correct endpoint so the
482
- # HEAD request succeeds on registries like ghcr.io.
483
- endpoint = digest_info.is_a?(Array) ? "manifests" : "blobs"
484
- head_response = with_retries(max_attempts: 3, errors: transient_docker_errors) do
485
- client = docker_registry_client
486
- client.dohead "v2/#{docker_repo_name}/#{endpoint}/#{first_digest}"
487
- end
488
-
489
- published_date = published_date_from_response_headers(head_response.headers, tag.name)
478
+ published_date = registry_tag_release_date(tag)
479
+ return nil unless published_date
490
480
 
491
481
  Dependabot::Package::PackageRelease.new(
492
482
  version: release_version_for(tag),
@@ -496,58 +486,75 @@ module Dependabot
496
486
  url: nil,
497
487
  package_type: "docker"
498
488
  )
499
- rescue *transient_docker_errors,
489
+ end
490
+
491
+ sig { params(tag: Dependabot::Docker::Tag).returns(T.nilable(Time)) }
492
+ def registry_tag_release_date(tag)
493
+ return docker_hub_tag_release_date(tag) if using_dockerhub?
494
+
495
+ Dependabot.logger.info(
496
+ "No verified registry publication date source for #{registry_hostname}; " \
497
+ "skipping cooldown for #{docker_repo_name}:#{tag.name}"
498
+ )
499
+ nil
500
+ rescue JSON::ParserError, ArgumentError, TypeError, Excon::Error::Socket, Excon::Error::Timeout,
501
+ RegistryError, PrivateSourceBadResponse, PrivateSourceAuthenticationFailure,
502
+ *transient_docker_errors,
500
503
  DockerRegistry2::RegistryAuthenticationException,
501
504
  DockerRegistry2::RegistryAuthorizationException,
502
505
  RestClient::Forbidden,
503
506
  RestClient::TooManyRequests => e
504
- Dependabot.logger.warn(
505
- "Failed to fetch publication details for #{docker_repo_name}:#{tag.name}, " \
506
- "skipping cooldown: #{e.class} - #{e.message}"
507
+ Dependabot.logger.info(
508
+ "Failed to fetch registry tag metadata for #{docker_repo_name}:#{tag.name}: #{e.message}"
507
509
  )
508
510
  nil
509
511
  end
510
512
 
511
- sig { params(headers: T::Hash[Symbol, String], tag_name: String).returns(T.nilable(Time)) }
512
- def published_date_from_response_headers(headers, tag_name)
513
- last_modified = headers[:last_modified]
514
- return nil unless last_modified
513
+ sig { params(tag: Dependabot::Docker::Tag).returns(T.nilable(Time)) }
514
+ def docker_hub_tag_release_date(tag)
515
+ namespace, repository = docker_repo_name.split("/", 2)
516
+ return unless namespace && repository
515
517
 
516
- Time.parse(last_modified)
517
- rescue ArgumentError, TypeError => e
518
- Dependabot.logger.info(
519
- "Invalid Last-Modified header for #{docker_repo_name}:#{tag_name}: #{e.message}"
518
+ digest = digest_of(tag.name)
519
+ return unless digest
520
+
521
+ response = Dependabot::RegistryClient.get(
522
+ url: docker_hub_tag_url(namespace, repository, tag.name),
523
+ headers: { "Accept" => "application/json" },
524
+ options: {
525
+ connect_timeout: docker_open_timeout_in_seconds,
526
+ read_timeout: docker_read_timeout_in_seconds,
527
+ write_timeout: docker_read_timeout_in_seconds
528
+ }
520
529
  )
521
- nil
530
+ return unless response.status == 200
531
+
532
+ metadata = JSON.parse(response.body)
533
+ return unless metadata.is_a?(Hash)
534
+ return unless docker_hub_digest_matches_update?(metadata["digest"], digest)
535
+
536
+ timestamp = metadata["tag_last_pushed"]
537
+ Time.iso8601(timestamp) if timestamp.is_a?(String)
522
538
  end
523
539
 
524
- sig do
525
- params(
526
- digest_info: Object,
527
- tag: Dependabot::Docker::Tag
528
- ).returns(T.nilable(String))
540
+ sig { params(metadata_digest: Object, digest: String).returns(T::Boolean) }
541
+ def docker_hub_digest_matches_update?(metadata_digest, digest)
542
+ return digest_requirements.empty? && !pin_digests? if metadata_digest.nil?
543
+
544
+ matching_digest?(metadata_digest, digest)
529
545
  end
530
- def extract_digest_from_response(digest_info, tag)
531
- # digest_info can be either a String or an Array depending on the registry response
532
- case digest_info
533
- when Array
534
- if digest_info.empty?
535
- Dependabot.logger.warn(
536
- "Empty digest_info array for #{docker_repo_name}:#{tag.name}"
537
- )
538
- return nil
539
- end
540
- digest = digest_info.first&.fetch("digest")
541
- digest if digest.is_a?(String)
542
- when String
543
- digest_info
544
- else
545
- Dependabot.logger.warn(
546
- "Unexpected digest_info type for #{docker_repo_name}:#{tag.name}: " \
547
- "#{digest_info.class} (expected String or Array)"
548
- )
549
- nil
550
- end
546
+
547
+ sig { params(namespace: String, repository: String, tag: String).returns(String) }
548
+ def docker_hub_tag_url(namespace, repository, tag)
549
+ escaped = [namespace, repository, tag].map { |value| URI.encode_www_form_component(value) }
550
+ "https://hub.docker.com/v2/namespaces/#{escaped[0]}/repositories/#{escaped[1]}/tags/#{escaped[2]}"
551
+ end
552
+
553
+ sig { params(first: Object, second: Object).returns(T::Boolean) }
554
+ def matching_digest?(first, second)
555
+ return false unless first.is_a?(String) && second.is_a?(String)
556
+
557
+ first.delete_prefix("sha256:").casecmp?(second.delete_prefix("sha256:")) || false
551
558
  end
552
559
 
553
560
  sig do
@@ -1068,11 +1075,6 @@ module Dependabot
1068
1075
  )
1069
1076
  end
1070
1077
 
1071
- sig { void }
1072
- def mark_cooldown_date_unavailable
1073
- dependency.metadata[:docker_cooldown_date_unavailable] = true
1074
- end
1075
-
1076
1078
  # Builds the PackageRelease version for a tag. Non-comparable tags (e.g.
1077
1079
  # "alpine") have no semver, so Docker::Version.new would raise. The version
1078
1080
  # is only consumed by semver-aware version-tag cooldown; the digest cooldown
@@ -1090,8 +1092,7 @@ module Dependabot
1090
1092
  # don't change the version string, so the default cooldown window applies
1091
1093
  # (semver-specific windows require a version delta, and the tag may be
1092
1094
  # non-comparable like "alpine"). Fails open (returns false) when the
1093
- # publication date can't be determined, so a missing Last-Modified header
1094
- # never permanently blocks an update.
1095
+ # publication date can't be determined and records an unavailable-date warning.
1095
1096
  sig { params(tag_name: String).returns(T::Boolean) }
1096
1097
  def digest_within_cooldown?(tag_name)
1097
1098
  return false if should_skip_cooldown?
@@ -1101,7 +1102,10 @@ module Dependabot
1101
1102
 
1102
1103
  released_at = publication_detail(Tag.new(tag_name))&.released_at
1103
1104
  unless released_at
1104
- mark_cooldown_date_unavailable if cooldown.default_days.positive?
1105
+ Dependabot::UpdateCheckers::CooldownCalculation.mark_cooldown_date_unavailable(
1106
+ dependency,
1107
+ cooldown_days: cooldown.default_days
1108
+ )
1105
1109
  return false
1106
1110
  end
1107
1111
 
@@ -154,11 +154,12 @@ module Dependabot
154
154
  return if old_tags.empty?
155
155
 
156
156
  modified_content = content
157
+ replacement = new_helm_tag(file)
157
158
 
158
159
  old_tags.each do |old_tag|
159
- old_tag_regex = /^\s*(?:-\s)?(?:tag|version):\s+["']?#{old_tag}["']?(?=\s|$)/
160
+ old_tag_regex = /^\s*(?:-\s)?(?:tag|version):\s+["']?#{Regexp.escape(old_tag)}["']?(?=\s|$)/
160
161
  modified_content = modified_content&.gsub(old_tag_regex) do |old_img_tag|
161
- old_img_tag.gsub(old_tag.to_s, new_helm_tag(file).to_s)
162
+ old_img_tag.gsub(old_tag.to_s, replacement.to_s)
162
163
  end
163
164
  end
164
165
  modified_content
@@ -170,11 +171,12 @@ module Dependabot
170
171
  return if old_images.empty?
171
172
 
172
173
  modified_content = content
174
+ replacement = new_yaml_image(file)
173
175
 
174
176
  old_images.each do |old_image|
175
- old_image_regex = /^\s*(?:-\s)?image:\s+#{old_image}(?=\s|$)/
177
+ old_image_regex = %r{^\s*(?:-\s)?image:\s+(?:docker\.io/)?#{Regexp.escape(old_image)}(?=\s|$)}
176
178
  modified_content = modified_content&.gsub(old_image_regex) do |old_img|
177
- old_img.gsub(old_image.to_s, new_yaml_image(file).to_s)
179
+ old_img.gsub(old_image.to_s, replacement.to_s)
178
180
  end
179
181
  end
180
182
  modified_content
@@ -182,7 +184,7 @@ module Dependabot
182
184
 
183
185
  sig { params(file: Dependabot::DependencyFile).returns(String) }
184
186
  def new_yaml_image(file)
185
- element = T.must(dependency).requirements.find { |r| r.file == file.name }
187
+ element = requirement_for_file(file)
186
188
  source = image_source(element)
187
189
  prefix = source[:registry] ? "#{source[:registry]}/" : ""
188
190
  digest = source[:digest] ? "@sha256:#{source[:digest]}" : ""
@@ -198,7 +200,7 @@ module Dependabot
198
200
  digest = source[:digest] ? "@sha256:#{source[:digest]}" : ""
199
201
  tag = source[:tag] ? ":#{source[:tag]}" : ""
200
202
  "#{prefix}#{T.must(dependency).name}#{tag}#{digest}"
201
- end
203
+ end.uniq
202
204
  end
203
205
 
204
206
  sig { params(file: Dependabot::DependencyFile).returns(T::Array[String]) }
@@ -208,18 +210,23 @@ module Dependabot
208
210
  tag = source[:tag] || ""
209
211
  digest = source[:digest] ? "@sha256:#{source[:digest]}" : ""
210
212
  "#{tag}#{digest}"
211
- end
213
+ end.uniq
212
214
  end
213
215
 
214
216
  sig { params(file: Dependabot::DependencyFile).returns(String) }
215
217
  def new_helm_tag(file)
216
- element = T.must(dependency).requirements.find { |r| r.file == file.name }
218
+ element = requirement_for_file(file)
217
219
  source = image_source(element)
218
220
  tag = source[:tag] || ""
219
221
  digest = source[:digest] ? "@sha256:#{source[:digest]}" : ""
220
222
  "#{tag}#{digest}"
221
223
  end
222
224
 
225
+ sig { params(file: Dependabot::DependencyFile).returns(T.nilable(Dependabot::DependencyRequirement)) }
226
+ def requirement_for_file(file)
227
+ requirements(file).first
228
+ end
229
+
223
230
  protected
224
231
 
225
232
  sig { params(file: Dependabot::DependencyFile, dependency: Dependabot::Dependency).returns(T::Boolean) }
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.395.0
4
+ version: 0.397.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.395.0
18
+ version: 0.397.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.395.0
25
+ version: 0.397.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.395.0
269
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.397.0
270
270
  rdoc_options: []
271
271
  require_paths:
272
272
  - lib