Skip to content

Commit

Permalink
Improve Docker tag component detection and comparison (#11679)
Browse files Browse the repository at this point in the history
* Improve Docker tag component detection and comparison

* lint

* Adding `:docker_tag_component_comparison` feature flag

* lint

* fixing rubocops catch 22 between explicit and implicit subject

* refactoring code
  • Loading branch information
robaiken authored Feb 26, 2025
1 parent d00807a commit 7323e6c
Show file tree
Hide file tree
Showing 3 changed files with 860 additions and 0 deletions.
58 changes: 58 additions & 0 deletions docker/lib/dependabot/docker/update_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,65 @@ def fetch_latest_tag(version_tag)

sig { params(original_tag: Dependabot::Docker::Tag).returns(T::Array[Dependabot::Docker::Tag]) }
def comparable_tags_from_registry(original_tag)
unless Experiments.enabled?(:docker_tag_component_comparison)
return tags_from_registry.select { |tag| tag.comparable_to?(original_tag) }
end

common_components = identify_common_components(tags_from_registry)
original_components = extract_tag_components(original_tag.name, common_components)
Dependabot.logger.info("Original tag components: #{original_components.join(',')}")

tags_from_registry.select { |tag| tag.comparable_to?(original_tag) }
tags_from_registry.select do |tag|
tag.comparable_to?(original_tag) &&
(original_components.empty? ||
compatible_components?(extract_tag_components(tag.name, common_components), original_components))
end
end

sig { params(tags: T::Array[Dependabot::Docker::Tag]).returns(T::Array[String]) }
def identify_common_components(tags)
tag_parts = tags.map do |tag|
# replace version parts with VERSION
processed_tag = tag.name.gsub(/\d+\.\d+\.\d+_\d+/, "VERSION")

parts = processed_tag.split(%r{[-\./]})
parts.reject(&:empty?)
end

part_counts = tag_parts.flatten.tally

part_counts.select do |part|
part.length > 1 &&
part != "VERSION" &&
!version_related_pattern?(part)
end.keys
end

sig { params(part: String).returns(T::Boolean) }
def version_related_pattern?(part)
patterns = {
number: /^\d+$/,
semver: /^\d+\.\d+$/,
v_prefix: /^v\d+/,
version_marker: /^(rc|jre)$/,
prerelease: /^(?=.*\d)(?=.*[a-z])[a-z\d]+$/i,
sha: /^g[0-9a-f]{5,}$/,
timestamp: /^\d{8,14}$/,
underscore_parts: /\d+_\d+/
}

patterns.values.any? { |pattern| part.match?(pattern) }
end

sig { params(tag_name: String, common_components: T::Array[String]).returns(T::Array[String]) }
def extract_tag_components(tag_name, common_components)
common_components.select { |component| tag_name.match?(/\b#{Regexp.escape(component)}\b/) }
end

sig { params(tag_components: T::Array[String], original_components: T::Array[String]).returns(T::Boolean) }
def compatible_components?(tag_components, original_components)
tag_components.sort == original_components.sort
end

sig do
Expand Down
61 changes: 61 additions & 0 deletions docker/spec/dependabot/docker/update_checker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,67 @@ def stub_tag_with_no_digest(tag)

it { is_expected.to eq("v1.7.2") }
end

context "when versions have different components but similar structure" do
let(:dependency_name) { "owasp/modsecurity-crs" }
let(:version) { "3.3-apache-202209221209" }
let(:tags_fixture_name) { "owasp.json" }
let(:repo_url) { "https://registry.hub.docker.com/v2/owasp/modsecurity-crs/" }

new_headers =
fixture("docker", "registry_manifest_headers", "generic.json")

before do
tags_url = repo_url + "/tags/list"
stub_request(:get, tags_url)
.and_return(status: 200, body: registry_tags)

stub_request(:head, repo_url + "manifests/4.11-apache-202502070602")
.and_return(status: 200, body: "", headers: JSON.parse(new_headers))
stub_request(:head, repo_url + "manifests/4-apache-202502070602")
.and_return(status: 200, body: "", headers: JSON.parse(new_headers))
end

context "with feature flag" do
before do
allow(Dependabot::Experiments).to receive(:enabled?).with(:docker_tag_component_comparison).and_return(true)
end

it { is_expected.to eq("4-apache-202502070602") }

context "with multiple components to match" do
let(:version) { "3.3-nginx-alpine-202209221209" }

before do
stub_request(:head, repo_url + "manifests/4.11-nginx-alpine-202502070602")
.and_return(status: 200, body: "", headers: JSON.parse(new_headers))
stub_request(:head, repo_url + "manifests/4-nginx-alpine-202502070602")
.and_return(status: 200, body: "", headers: JSON.parse(new_headers))
end

it { is_expected.to eq("4-nginx-alpine-202502070602") }
end

context "when components are in a different order" do
before do
stub_request(:head, repo_url + "manifests/4-202502070602-apache")
.and_return(status: 200, body: "", headers: JSON.parse(new_headers))
end

it { is_expected.to eq("4-apache-202502070602") }
end
end

context "without feature flag" do
before do
stub_request(:head, repo_url + "manifests/4-nginx-alpine-202502070602")
.and_return(status: 200, body: "", headers: JSON.parse(new_headers))
allow(Dependabot::Experiments).to receive(:enabled?).with(:docker_tag_component_comparison).and_return(false)
end

it { is_expected.to eq("4-nginx-alpine-202502070602") }
end
end
end

describe "#latest_resolvable_version" do
Expand Down
Loading

0 comments on commit 7323e6c

Please sign in to comment.