Skip to content

Prefer daemon registry mirrors for Docker Hub digest checks#1555

Open
chenjia404 wants to merge 3 commits intonicholas-fedor:mainfrom
chenjia404:main
Open

Prefer daemon registry mirrors for Docker Hub digest checks#1555
chenjia404 wants to merge 3 commits intonicholas-fedor:mainfrom
chenjia404:main

Conversation

@chenjia404
Copy link
Copy Markdown

@chenjia404 chenjia404 commented Apr 11, 2026

You can use this as a PR description or upstream submission note:

Title

Prefer daemon registry mirrors for Docker Hub digest checks

Description

This change fixes Docker Hub update checks for environments where Docker is configured with registry-mirrors and direct access to index.docker.io is unavailable.

Today, Watchtower’s digest lookup path for Docker Hub images assumes the canonical registry endpoint (index.docker.io) and sends challenge / manifest requests there during update checks. In practice, many users who configure registry-mirrors do so specifically because they cannot reliably reach Docker Hub directly. In those setups, the current behavior causes update checks to fail before a pull is even attempted.

This patch changes the Docker Hub digest-check flow to prefer the Docker daemon’s configured registry mirrors when querying remote image metadata.

What changed

For Docker Hub images only:

  • Watchtower now reads the daemon registry configuration from Docker Info().
  • If RegistryConfig.Mirrors is present, digest checks try those mirror endpoints first.
  • Authentication challenge requests and manifest requests both use the selected mirror endpoint.
  • If all configured mirrors fail, Watchtower falls back to the default Docker Hub endpoint as before.

The existing behavior for non-Docker-Hub registries is unchanged.

Implementation details

The change is intentionally scoped to the digest-check path rather than image pull behavior:

  • pkg/registry/auth/auth.go

    • Added support for building challenge URLs from an explicit registry endpoint override.
    • Added GetTokenWithRegistryEndpoint(...) so auth requests can target a mirror instead of the canonical registry host.
  • pkg/registry/digest/digest.go

    • Added mirror-aware manifest URL construction.
    • Added logic to resolve daemon mirrors from Docker Info() and iterate over candidate endpoints.
    • Digest retrieval now attempts each configured mirror first, then falls back to the canonical Docker Hub host.
  • pkg/registry/digest/digest_test.go

    • Added a regression test that verifies Docker Hub digest checks prefer a configured daemon mirror.
  • pkg/container/image_test.go

    • Updated affected tests to account for the new daemon Info() lookup performed during Docker Hub digest checks.

Why this approach

This uses the Docker daemon’s effective registry configuration instead of parsing daemon.json directly.

That has a few advantages:

  • It reflects the daemon’s actual runtime configuration.
  • It works even when daemon settings come from sources other than a local JSON file.
  • It avoids duplicating Docker configuration parsing logic inside Watchtower.

Behavioral impact

Before this change:

  • Docker Hub digest checks always contacted index.docker.io first.

After this change:

  • Docker Hub digest checks prefer daemon-configured mirrors.
  • Environments that rely on mirrors to reach Docker Hub can complete update checks without requiring direct access to index.docker.io.
  • If no mirrors are configured, behavior remains the same as before.

Testing

Verified with:

  • targeted registry package tests
  • updated container-level tests
  • full test suite: go test ./...

If you want, I can also turn this into a shorter upstream-friendly PR body plus a separate “Problem / Solution / Testing” version.

#1102

Summary by CodeRabbit

  • New Features

    • Prefer configured registry mirrors for Docker Hub pulls and support explicit registry endpoint overrides for token and manifest resolution.
    • Improve registry-endpoint URL handling (accept bare hosts, apply default scheme, validate/normalize endpoints).
  • Tests

    • Added tests for mirror-preference in digest comparison, unit tests for registry-endpoint URL building, and tightened image-pull mocks to include registry info checks.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Walkthrough

Extends registry interactions to support explicit registry endpoints/mirrors: adds endpoint-aware token and challenge URL functions, builds manifest URLs with registry endpoint overrides, discovers daemon-configured mirrors, and retries digest fetches across ordered registry endpoints. Also adds tests and a small test HTTP mock adjustment.

Changes

Cohort / File(s) Summary
Tests — container pull precondition
pkg/container/image_test.go
Added an initial HTTP mock handler for GET /info (matches /info$) returning 200 with {} to satisfy test preconditions for image-pull scenarios.
Auth module (registry endpoint support)
pkg/registry/auth/auth.go
Added GetTokenWithRegistryEndpoint(...) and GetChallengeURLWithRegistryEndpoint(...); existing GetToken/GetChallengeURL delegate to new variants with empty endpoint. Added registry-endpoint URL building and logging including getRegistryScheme() usage.
Digest module (mirror-aware manifest/digest fetch)
pkg/registry/digest/digest.go
Added BuildManifestURLWithRegistryEndpoint(...), introduced registryInfoProvider, changed fetchDigest to derive ordered registry endpoints and iterate retries, and extracted per-endpoint logic into fetchDigestFromRegistryEndpoint(...) that forwards registryEndpoint to auth and URL builders.
Digest tests (daemon mirror preference)
pkg/registry/digest/digest_test.go
Added mockImageInspectorWithInfo and test TestCompareDigest_PrefersDaemonRegistryMirrorsForDockerHub validating that daemon RegistryConfig.Mirrors are used to prefer mirror endpoints when comparing digests.
URL util (registry endpoint builder)
internal/urlutil/registry_endpoint.go, internal/urlutil/registry_endpoint_test.go
Added BuildRegistryEndpointURL(registryEndpoint, resourcePath, defaultScheme) and joinURLPath with tests covering scheme injection, path joining, trailing-slash behavior, and missing-host validation.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as CompareDigest()
    participant Daemon as Docker Daemon (Info)
    participant Discovery as Mirror Discovery
    participant Auth as Auth (GetTokenWithRegistryEndpoint)
    participant Registry as Registry Endpoint

    Caller->>Daemon: Request Info (RegistryConfig.Mirrors)
    Daemon-->>Caller: Mirrors list
    Caller->>Discovery: Build ordered endpoints (mirrors + fallback)
    Discovery-->>Caller: [endpoint1, endpoint2, ...]
    loop per endpoint
        Caller->>Auth: GetTokenWithRegistryEndpoint(endpoint)
        Auth->>Registry: Request challenge at endpoint (/v2/)
        Registry-->>Auth: Challenge / Auth response
        Auth-->>Caller: Token
        Caller->>Registry: HEAD /manifests/<tag> (with token)
        alt Success
            Registry-->>Caller: Content-Digest -> return match
        else Failure
            Registry-->>Caller: Error -> log and continue
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: preferring daemon registry mirrors for Docker Hub digest checks, which is the primary focus of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 11, 2026

Not up to standards ⛔

🔴 Issues 7 medium

Alerts:
⚠ 7 issues (≤ 0 issues of at least minor severity)

Results:
7 new issues

Category Results
Complexity 7 medium

View in Codacy

🟢 Metrics 21 complexity · 1 duplication

Metric Results
Complexity 21
Duplication 1

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/registry/auth/auth.go (1)

898-943: Consider consolidating duplicated URL helpers with digest.go.

buildRegistryEndpointURL and joinURLPath are duplicated between auth.go and digest.go with slightly different signatures:

  • auth.go: returns (url.URL, bool)
  • digest.go: returns (*url.URL, error)

This duplication increases maintenance burden and risks divergence. Consider extracting these helpers into a shared internal package (e.g., internal/urlutil).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/registry/auth/auth.go` around lines 898 - 943, buildRegistryEndpointURL
and joinURLPath are duplicated in auth.go and digest.go with different return
signatures (auth.go: buildRegistryEndpointURL, joinURLPath; digest.go: similar
helpers returning (*url.URL, error)), so extract a single canonical
implementation into a shared internal package (e.g., internal/urlutil) and
update callers: move the logic for building and joining URL paths into functions
like urlutil.BuildRegistryEndpointURL and urlutil.JoinURLPath with a stable
signature (choose one return style, e.g., (*url.URL, error) or (url.URL, bool)
and convert callers accordingly), remove the duplicates from auth.go and
digest.go, update imports and adapt error handling in callers (replace boolean
checks with error handling if you choose error returns) to ensure consistent
behavior across buildRegistryEndpointURL and digest.go usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/registry/auth/auth.go`:
- Around line 898-943: buildRegistryEndpointURL and joinURLPath are duplicated
in auth.go and digest.go with different return signatures (auth.go:
buildRegistryEndpointURL, joinURLPath; digest.go: similar helpers returning
(*url.URL, error)), so extract a single canonical implementation into a shared
internal package (e.g., internal/urlutil) and update callers: move the logic for
building and joining URL paths into functions like
urlutil.BuildRegistryEndpointURL and urlutil.JoinURLPath with a stable signature
(choose one return style, e.g., (*url.URL, error) or (url.URL, bool) and convert
callers accordingly), remove the duplicates from auth.go and digest.go, update
imports and adapt error handling in callers (replace boolean checks with error
handling if you choose error returns) to ensure consistent behavior across
buildRegistryEndpointURL and digest.go usages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 33e9ee71-5534-4d6f-8b6d-90917fda793b

📥 Commits

Reviewing files that changed from the base of the PR and between 404c157 and f87a7ac.

📒 Files selected for processing (4)
  • pkg/container/image_test.go
  • pkg/registry/auth/auth.go
  • pkg/registry/digest/digest.go
  • pkg/registry/digest/digest_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/urlutil/registry_endpoint.go`:
- Around line 12-29: The parsing logic for registryEndpoint should first detect
whether the raw registryEndpoint string contains "://" and, if not, prepend
defaultScheme + "://" before calling url.Parse so inputs like
"mirror.example.com:5000" are parsed with a host rather than misinterpreted as a
scheme; update the code around endpointURL and url.Parse to remove the current
reparse fallback (the block that reparses when Scheme and Host are empty) and
instead always parse the possibly-prefixed string, then validate
endpointURL.Host (returning the existing "missing host" error if still empty)
and ensure endpointURL.Scheme is set to defaultScheme when missing.

In `@pkg/registry/digest/digest.go`:
- Around line 369-373: The function BuildManifestURLWithRegistryEndpoint (and
the similar overload around lines 595-606) currently accepts only a hostOverride
string, which causes path-prefixed mirror redirects to lose their path when
rebuilding manifest URLs; change the API to accept and thread a full redirected
base URL (e.g., *url.URL or a full base string) instead of just hostOverride,
update the URL construction logic inside BuildManifestURLWithRegistryEndpoint to
use the redirected base's Scheme/Host and Path prefix when resolving the
manifest path, and update all callers to pass the redirected base URL so mirror
prefixes are preserved during redirects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d592d853-b062-47e4-afc2-ea25e4416b5a

📥 Commits

Reviewing files that changed from the base of the PR and between f87a7ac and c36b6e8.

📒 Files selected for processing (4)
  • internal/urlutil/registry_endpoint.go
  • internal/urlutil/registry_endpoint_test.go
  • pkg/registry/auth/auth.go
  • pkg/registry/digest/digest.go

Comment on lines +12 to +29
endpointURL, err := url.Parse(registryEndpoint)
if err != nil {
return nil, err
}

if endpointURL.Scheme == "" && endpointURL.Host == "" && endpointURL.Path != "" {
endpointURL, err = url.Parse(defaultScheme + "://" + registryEndpoint)
if err != nil {
return nil, err
}
}

if endpointURL.Scheme == "" {
endpointURL.Scheme = defaultScheme
}

if endpointURL.Host == "" {
return nil, fmt.Errorf("missing host")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "registry_endpoint.go" -type f

Repository: nicholas-fedor/watchtower

Length of output: 108


🏁 Script executed:

# Also test Go's url.Parse behavior with the claimed problematic input
cat > /tmp/test_parse.go << 'EOF'
package main

import (
	"fmt"
	"net/url"
)

func main() {
	// Test cases from the review
	testCases := []string{
		"mirror.example.com:5000",
		"example.com",
		"example.com:8080",
		"http://mirror.example.com:5000",
		"mirror.example.com",
		"http://example.com",
	}

	for _, tc := range testCases {
		u, err := url.Parse(tc)
		if err != nil {
			fmt.Printf("Input: %q -> Error: %v\n", tc, err)
		} else {
			fmt.Printf("Input: %q -> Scheme: %q, Host: %q, Path: %q\n", 
				tc, u.Scheme, u.Host, u.Path)
		}
	}
}
EOF
go run /tmp/test_parse.go

Repository: nicholas-fedor/watchtower

Length of output: 604


🏁 Script executed:

cat -n ./internal/urlutil/registry_endpoint.go

Repository: nicholas-fedor/watchtower

Length of output: 1754


Handle scheme-less host:port endpoints by checking for :// before parsing.

The current heuristic on line 17 misses inputs like mirror.example.com:5000. Go's url.Parse treats that as Scheme="mirror.example.com", Host="", so the reparse condition fails and the function incorrectly returns missing host. This breaks valid mirror endpoints that include explicit ports. Check for the presence of :// before the first parse and prepend defaultScheme if absent, eliminating the need for the fallback reparse logic.

Proposed fix
 func BuildRegistryEndpointURL(registryEndpoint, resourcePath, defaultScheme string) (*url.URL, error) {
+	if !strings.Contains(registryEndpoint, "://") {
+		registryEndpoint = defaultScheme + "://" + registryEndpoint
+	}
+
 	endpointURL, err := url.Parse(registryEndpoint)
 	if err != nil {
 		return nil, err
 	}
-
-	if endpointURL.Scheme == "" && endpointURL.Host == "" && endpointURL.Path != "" {
-		endpointURL, err = url.Parse(defaultScheme + "://" + registryEndpoint)
-		if err != nil {
-			return nil, err
-		}
-	}
 
 	if endpointURL.Scheme == "" {
 		endpointURL.Scheme = defaultScheme
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
endpointURL, err := url.Parse(registryEndpoint)
if err != nil {
return nil, err
}
if endpointURL.Scheme == "" && endpointURL.Host == "" && endpointURL.Path != "" {
endpointURL, err = url.Parse(defaultScheme + "://" + registryEndpoint)
if err != nil {
return nil, err
}
}
if endpointURL.Scheme == "" {
endpointURL.Scheme = defaultScheme
}
if endpointURL.Host == "" {
return nil, fmt.Errorf("missing host")
if !strings.Contains(registryEndpoint, "://") {
registryEndpoint = defaultScheme + "://" + registryEndpoint
}
endpointURL, err := url.Parse(registryEndpoint)
if err != nil {
return nil, err
}
if endpointURL.Scheme == "" {
endpointURL.Scheme = defaultScheme
}
if endpointURL.Host == "" {
return nil, fmt.Errorf("missing host")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/urlutil/registry_endpoint.go` around lines 12 - 29, The parsing
logic for registryEndpoint should first detect whether the raw registryEndpoint
string contains "://" and, if not, prepend defaultScheme + "://" before calling
url.Parse so inputs like "mirror.example.com:5000" are parsed with a host rather
than misinterpreted as a scheme; update the code around endpointURL and
url.Parse to remove the current reparse fallback (the block that reparses when
Scheme and Host are empty) and instead always parse the possibly-prefixed
string, then validate endpointURL.Host (returning the existing "missing host"
error if still empty) and ensure endpointURL.Scheme is set to defaultScheme when
missing.

Comment on lines +369 to 373
func BuildManifestURLWithRegistryEndpoint(
container types.Container,
hostOverride string,
registryEndpoint string,
) (string, string, *url.URL, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Carry a full redirected base URL here, not just hostOverride.

The new endpoint flow now supports mirror URLs with path prefixes (internal/urlutil/registry_endpoint_test.go, Lines 24-36), but this API can only swap the host. If a challenge redirect moves from something like /cache/v2/... to a canonical /v2/... endpoint, the rebuilt manifest URL keeps the mirror prefix and retries the wrong path on the redirect target. Thread the full redirected base URL/path through this flow instead of only the host.

Also applies to: 595-606

🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis

[warning] 369-369: pkg/registry/digest/digest.go#L369
Method BuildManifestURLWithRegistryEndpoint has 53 lines of code (limit is 50)


[warning] 369-369: pkg/registry/digest/digest.go#L369
Method BuildManifestURLWithRegistryEndpoint has a cyclomatic complexity of 9 (limit is 8)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/registry/digest/digest.go` around lines 369 - 373, The function
BuildManifestURLWithRegistryEndpoint (and the similar overload around lines
595-606) currently accepts only a hostOverride string, which causes
path-prefixed mirror redirects to lose their path when rebuilding manifest URLs;
change the API to accept and thread a full redirected base URL (e.g., *url.URL
or a full base string) instead of just hostOverride, update the URL construction
logic inside BuildManifestURLWithRegistryEndpoint to use the redirected base's
Scheme/Host and Path prefix when resolving the manifest path, and update all
callers to pass the redirected base URL so mirror prefixes are preserved during
redirects.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 62.99213% with 47 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/registry/auth/auth.go 25.92% 19 Missing and 1 partial ⚠️
pkg/registry/digest/digest.go 73.97% 14 Missing and 5 partials ⚠️
internal/urlutil/registry_endpoint.go 70.37% 5 Missing and 3 partials ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1555      +/-   ##
==========================================
- Coverage   75.13%   74.95%   -0.18%     
==========================================
  Files          58       59       +1     
  Lines        9763     9882     +119     
==========================================
+ Hits         7335     7407      +72     
- Misses       2168     2206      +38     
- Partials      260      269       +9     
Files with missing lines Coverage Δ
internal/urlutil/registry_endpoint.go 70.37% <70.37%> (ø)
pkg/registry/digest/digest.go 71.42% <73.97%> (+0.02%) ⬆️
pkg/registry/auth/auth.go 75.52% <25.92%> (-3.87%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants