diff --git a/pkg/dind/registry.go b/pkg/dind/registry.go index 0641ed8..736eed4 100644 --- a/pkg/dind/registry.go +++ b/pkg/dind/registry.go @@ -338,20 +338,32 @@ func tailHex(s string) string { // qualifyDockerHubRef ensures a reference carries an explicit registry // host. containerd's resolver treats the first path segment of an // unqualified reference as the hostname (so "ephpm/ephemerd:tag" → host -// "ephpm" → DNS lookup fails). Docker CLI conventions default unqualified -// refs to docker.io, prepending "library/" for single-segment names like +// "ephpm" → DNS lookup fails; "alpine:3.20" → host "alpine", port "3.20" +// → url.Parse rejects). Docker CLI conventions default unqualified refs +// to docker.io, prepending "library/" for single-segment names like // "ubuntu". This helper applies the same rule. +// +// The slash is the disambiguator between a "host[:port]/path" reference +// and a "name[:tag]" reference. With a slash, the part before it is a +// host candidate and a dot or colon there means we've got a real +// registry host (gcr.io, localhost:5000, my-registry.com:443, etc.). +// Without a slash, there's no path and the whole string is a single- +// segment image name with an optional tag — always docker.io/library/. func qualifyDockerHubRef(ref string) string { - first := ref - if i := strings.IndexByte(ref, '/'); i >= 0 { - first = ref[:i] + i := strings.IndexByte(ref, '/') + if i < 0 { + // No slash → single-segment name (possibly with tag/digest). + // Always qualifies with docker.io/library/. This covers the + // `alpine`, `alpine:3.20`, `alpine@sha256:...` cases. + return "docker.io/library/" + ref } + first := ref[:i] if strings.ContainsAny(first, ".:") || first == "localhost" { + // Looks like a real host (has a dot, or has a port, or is + // literally "localhost") — already qualified. return ref } - if !strings.Contains(ref, "/") { - return "docker.io/library/" + ref - } + // Multi-segment name without a host (e.g. "myorg/myimage[:tag]"). return "docker.io/" + ref } diff --git a/pkg/dind/registry_helpers_test.go b/pkg/dind/registry_helpers_test.go index 43ac3de..1db8120 100644 --- a/pkg/dind/registry_helpers_test.go +++ b/pkg/dind/registry_helpers_test.go @@ -80,11 +80,14 @@ func TestQualifyDockerHubRef(t *testing.T) { }{ // Single-segment unqualified names get library/ prepended. {"alpine", "docker.io/library/alpine"}, - // Single-segment with tag — qualifyDockerHubRef sees the ":" and - // thinks "alpine:latest" is already a host, so it passes through - // unchanged. This is a known limitation; passing "alpine" without - // a tag is the supported form when callers want library/ added. - {"alpine:latest", "alpine:latest"}, + // Single-segment WITH tag: the previous implementation bailed on the + // tag colon and passed it through unchanged, which made containerd's + // resolver try to parse "alpine" as a host and "3.20" as a port + // number. The disambiguator is the slash — no slash means no path, + // so the colon must be a tag separator. + {"alpine:latest", "docker.io/library/alpine:latest"}, + {"alpine:3.20", "docker.io/library/alpine:3.20"}, + {"alpine@sha256:abc123", "docker.io/library/alpine@sha256:abc123"}, // Two-segment names get docker.io/ prepended. {"myorg/myimage", "docker.io/myorg/myimage"}, {"myorg/myimage:tag", "docker.io/myorg/myimage:tag"},