class Dependabot::Docker::UpdateChecker

Constants

NAME_WITH_VERSION
VERSION_REGEX
VERSION_WITH_PFX
VERSION_WITH_PFX_AND_SFX
VERSION_WITH_SFX

Public Instance Methods

latest_resolvable_version() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 63
def latest_resolvable_version
  # Resolvability isn't an issue for Docker containers.
  latest_version
end
latest_resolvable_version_with_no_unlock() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 68
def latest_resolvable_version_with_no_unlock
  # No concept of "unlocking" for Docker containers
  dependency.version
end
latest_version() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 59
def latest_version
  fetch_latest_version(dependency.version)
end
updated_requirements() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 73
def updated_requirements
  dependency.requirements.map do |req|
    updated_source = req.fetch(:source).dup
    updated_source[:digest] = updated_digest if req[:source][:digest]
    updated_source[:tag] = fetch_latest_version(req[:source][:tag]) if req[:source][:tag]

    req.merge(source: updated_source)
  end
end

Private Instance Methods

canonical_version?(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 212
def canonical_version?(tag)
  return false unless numeric_version_from(tag)
  return true if tag == numeric_version_from(tag)

  # .NET tags are suffixed with -sdk
  return true if tag == numeric_version_from(tag) + "-sdk"

  tag == "jdk-" + numeric_version_from(tag)
end
commit_sha_suffix?(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 187
def commit_sha_suffix?(tag)
  # Some people suffix their versions with commit SHAs. Dependabot
  # can't order on those but will try to, so instead we should exclude
  # them (unless there's a `latest` version pushed to the registry, in
  # which case we'll use that to find the latest version)
  return false unless tag.match?(/(^|\-g?)[0-9a-f]{7,}$/)

  !tag.match?(/(^|\-)\d+$/)
end
comparable_tags_from_registry(version) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 168
def comparable_tags_from_registry(version)
  original_prefix = prefix_of(version)
  original_suffix = suffix_of(version)
  original_format = format_of(version)

  tags_from_registry.
    select { |tag| tag.match?(NAME_WITH_VERSION) }.
    select { |tag| prefix_of(tag) == original_prefix }.
    select { |tag| suffix_of(tag) == original_suffix || commit_sha_suffix?(tag) }.
    select { |tag| format_of(tag) == original_format }
end
credentials_finder() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 335
def credentials_finder
  @credentials_finder ||= Utils::CredentialsFinder.new(credentials)
end
digest_of(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 255
def digest_of(tag)
  @digests ||= {}
  return @digests[tag] if @digests.key?(tag)

  @digests[tag] =
    begin
      docker_registry_client.digest(docker_repo_name, tag)
    rescue *transient_docker_errors => e
      attempt ||= 1
      attempt += 1
      return if attempt > 3 && e.is_a?(DockerRegistry2::NotFound)
      raise if attempt > 3

      retry
    end
rescue DockerRegistry2::RegistryAuthenticationException,
       RestClient::Forbidden
  raise PrivateSourceAuthenticationFailure, registry_hostname
end
digest_up_to_date?() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 128
def digest_up_to_date?
  dependency.requirements.all? do |req|
    next true unless req.fetch(:source)[:digest]
    next true unless (new_digest = digest_of(dependency.version))

    req.fetch(:source).fetch(:digest) == new_digest
  end
end
docker_registry_client() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 346
def docker_registry_client
  @docker_registry_client ||=
    DockerRegistry2::Registry.new(
      "https://#{registry_hostname}",
      user: registry_credentials&.fetch("username", nil),
      password: registry_credentials&.fetch("password", nil)
    )
end
docker_repo_name() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 339
def docker_repo_name
  return dependency.name unless using_dockerhub?
  return dependency.name unless dependency.name.split("/").count < 2

  "library/#{dependency.name}"
end
fetch_latest_version(version) click to toggle source

NOTE: It's important that this always returns a version (even if it's the existing one) as it is what we later check the digest of.

# File lib/dependabot/docker/update_checker.rb, line 139
def fetch_latest_version(version)
  @versions ||= {}
  return @versions[version] if @versions.key?(version)

  @versions[version] = begin
    return version unless version.match?(NAME_WITH_VERSION)

    # Prune out any downgrade tags before checking for pre-releases
    # (which requires a call to the registry for each tag, so can be slow)
    candidate_tags = comparable_tags_from_registry(version)
    non_downgrade_tags = remove_version_downgrades(candidate_tags, version)
    candidate_tags = non_downgrade_tags if non_downgrade_tags.any?

    unless prerelease?(version)
      candidate_tags =
        candidate_tags.
        reject { |tag| prerelease?(tag) }
    end

    latest_tag =
      filter_ignored(candidate_tags).
      max_by do |tag|
        [version_class.new(numeric_version_from(tag)), tag.length]
      end

    latest_tag || version
  end
end
filter_ignored(candidate_tags) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 355
def filter_ignored(candidate_tags)
  filtered =
    candidate_tags.
    reject do |tag|
      version = version_class.new(numeric_version_from(tag))
      ignore_requirements.any? { |r| r.satisfied_by?(version) }
    end
  if @raise_on_ignored && filter_lower_versions(filtered).empty? && filter_lower_versions(candidate_tags).any?
    raise AllVersionsIgnored
  end

  filtered
end
filter_lower_versions(tags) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 369
def filter_lower_versions(tags)
  versions_array = tags.map { |tag| version_class.new(numeric_version_from(tag)) }
  versions_array.
    select { |version| version > version_class.new(numeric_version_from(dependency.version)) }
end
format_of(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 294
def format_of(tag)
  version = numeric_version_from(tag)

  return :year_month if version.match?(/^[12]\d{3}(?:[.\-]|$)/)
  return :year_month_day if version.match?(/^[12]\d{5}(?:[.\-]|$)/)
  return :build_num if version.match?(/^\d+$/)

  :normal
end
latest_digest() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 249
def latest_digest
  return unless tags_from_registry.include?("latest")

  digest_of("latest")
end
latest_version_resolvable_with_full_unlock?() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 85
def latest_version_resolvable_with_full_unlock?
  # Full unlock checks aren't relevant for Dockerfiles
  false
end
numeric_version_from(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 316
def numeric_version_from(tag)
  return unless tag.match?(NAME_WITH_VERSION)

  tag.match(NAME_WITH_VERSION).named_captures.fetch("version").downcase
end
prefix_of(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 286
def prefix_of(tag)
  tag.match(NAME_WITH_VERSION).named_captures.fetch("prefix")
end
prerelease?(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 304
def prerelease?(tag)
  return true if numeric_version_from(tag).gsub(/kb/i, "").match?(/[a-zA-Z]/)

  # If we're dealing with a numeric version we can compare it against
  # the digest for the `latest` tag.
  return false unless numeric_version_from(tag)
  return false unless latest_digest
  return false unless version_of_latest_tag

  version_class.new(numeric_version_from(tag)) > version_of_latest_tag
end
registry_credentials() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 331
def registry_credentials
  credentials_finder.credentials_for_registry(registry_hostname)
end
registry_hostname() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 322
def registry_hostname
  dependency.requirements.first[:source][:registry] ||
    "registry.hub.docker.com"
end
remove_version_downgrades(candidate_tags, version) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 180
def remove_version_downgrades(candidate_tags, version)
  candidate_tags.select do |tag|
    version_class.new(numeric_version_from(tag)) >=
      version_class.new(numeric_version_from(version))
  end
end
suffix_of(tag) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 290
def suffix_of(tag)
  tag.match(NAME_WITH_VERSION).named_captures.fetch("suffix")
end
tags_from_registry() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 226
def tags_from_registry
  @tags_from_registry ||=
    begin
      client = docker_registry_client

      client.tags(docker_repo_name, auto_paginate: true).fetch("tags")
    rescue *transient_docker_errors
      attempt ||= 1
      attempt += 1
      raise if attempt > 3

      retry
    end
rescue DockerRegistry2::RegistryAuthenticationException,
       RestClient::Forbidden
  raise PrivateSourceAuthenticationFailure, registry_hostname
rescue RestClient::Exceptions::OpenTimeout,
       RestClient::Exceptions::ReadTimeout
  raise if using_dockerhub?

  raise PrivateSourceTimedOut, registry_hostname
end
transient_docker_errors() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 275
def transient_docker_errors
  [
    RestClient::Exceptions::Timeout,
    RestClient::ServerBrokeConnection,
    RestClient::ServiceUnavailable,
    RestClient::InternalServerError,
    RestClient::BadGateway,
    DockerRegistry2::NotFound
  ]
end
updated_dependencies_after_full_unlock() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 90
def updated_dependencies_after_full_unlock
  raise NotImplementedError
end
updated_digest() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 222
def updated_digest
  @updated_digest ||= digest_of(latest_version)
end
using_dockerhub?() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 327
def using_dockerhub?
  registry_hostname == "registry.hub.docker.com"
end
version_can_update?(*) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 94
def version_can_update?(*)
  !version_up_to_date?
end
version_of_latest_tag() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 197
def version_of_latest_tag
  return unless latest_digest

  candidate_tag =
    tags_from_registry.
    select { |tag| canonical_version?(tag) }.
    sort_by { |t| version_class.new(numeric_version_from(t)) }.
    reverse.
    find { |t| digest_of(t) == latest_digest }

  return unless candidate_tag

  version_class.new(numeric_version_from(candidate_tag))
end
version_tag_up_to_date?(version) click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 110
def version_tag_up_to_date?(version)
  return unless version&.match?(NAME_WITH_VERSION)

  latest_version = fetch_latest_version(version)

  old_v = numeric_version_from(version)
  latest_v = numeric_version_from(latest_version)

  return true if version_class.new(latest_v) <= version_class.new(old_v)

  # Check the precision of the potentially higher tag is the same as the
  # one it would replace. In the event that it's not the same, check the
  # digests are also unequal. Avoids 'updating' ruby-2 -> ruby-2.5.1
  return false if old_v.split(".").count == latest_v.split(".").count

  digest_of(version) == digest_of(latest_version)
end
version_up_to_date?() click to toggle source
# File lib/dependabot/docker/update_checker.rb, line 98
def version_up_to_date?
  # If the tag isn't up-to-date then we can definitely update
  return false if version_tag_up_to_date?(dependency.version) == false
  return false if dependency.requirements.any? do |req|
                    version_tag_up_to_date?(req.fetch(:source, {})[:tag]) == false
                  end

  # Otherwise, if the Dockerfile specifies a digest check that that is
  # up-to-date
  digest_up_to_date?
end