diff --git a/pkg/shortnames/shortnames.go b/pkg/shortnames/shortnames.go index fb0a15b993..5b11e60551 100644 --- a/pkg/shortnames/shortnames.go +++ b/pkg/shortnames/shortnames.go @@ -138,6 +138,8 @@ const ( rationaleUSR // Resolved value has been selected by the user (via the prompt). rationaleUserSelection + // Resolved value has been enforced to use Docker Hub (via SystemContext). + rationaleEnforcedDockerHub ) // Description returns a human-readable description about the resolution @@ -152,6 +154,8 @@ func (r *Resolved) Description() string { return fmt.Sprintf("Resolved %q as an alias (%s)", r.userInput, r.originDescription) case rationaleUSR: return fmt.Sprintf("Resolving %q using unqualified-search registries (%s)", r.userInput, r.originDescription) + case rationaleEnforcedDockerHub: + return fmt.Sprintf("Resolving %q to docker.io (%s)", r.userInput, r.originDescription) case rationaleUserSelection, rationaleNone: fallthrough default: @@ -246,6 +250,21 @@ func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) { } resolved.systemContext = ctx + // Resolve to docker.io only if enforced by the caller (e.g., Podman's + // Docker-compatible REST API). + if ctx.ResolveShortNamesToDockerHub { + named, err := reference.ParseNormalizedNamed(name) + if err != nil { + return nil, errors.Wrapf(err, "cannot normalize input: %q", name) + } + // Make sure to add ":latest" if needed + named = reference.TagNameOnly(named) + resolved.addCandidate(named) + resolved.rationale = rationaleEnforcedDockerHub + resolved.originDescription = "enforced by caller" + return resolved, nil + } + // Detect which mode we're running in. mode, err := sysregistriesv2.GetShortNameMode(ctx) if err != nil { @@ -412,6 +431,26 @@ func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, e var candidates []reference.Named + // Complete the candidates with the specified registries. Note that + // "localhost/" has precedence. + completeCandidates := func(registries []string) ([]reference.Named, error) { + for _, reg := range append([]string{"localhost"}, registries...) { + named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name)) + if err != nil { + return nil, errors.Wrapf(err, "creating reference with unqualified-search registry %q", reg) + } + // Make sure to add ":latest" if needed + named = reference.TagNameOnly(named) + + candidates = append(candidates, named) + } + return candidates, nil + } + + if ctx != nil && ctx.ResolveShortNamesToDockerHub { + return completeCandidates([]string{"docker.io"}) + } + // Strip off the tag to normalize the short name for looking it up in // the config files. isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef) @@ -446,17 +485,5 @@ func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, e return nil, err } - // Note that "localhost" has precedence over the unqualified-search registries. - for _, reg := range append([]string{"localhost"}, unqualifiedSearchRegistries...) { - named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name)) - if err != nil { - return nil, errors.Wrapf(err, "creating reference with unqualified-search registry %q", reg) - } - // Make sure to add ":latest" if needed - named = reference.TagNameOnly(named) - - candidates = append(candidates, named) - } - - return candidates, nil + return completeCandidates(unqualifiedSearchRegistries) } diff --git a/pkg/shortnames/shortnames_test.go b/pkg/shortnames/shortnames_test.go index e7558bb3a7..382a80254a 100644 --- a/pkg/shortnames/shortnames_test.go +++ b/pkg/shortnames/shortnames_test.go @@ -105,29 +105,39 @@ func TestResolve(t *testing.T) { UserShortNameAliasConfPath: tmp.Name(), } + sysResolveToDockerHub := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/aliases.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + ResolveShortNamesToDockerHub: true, + } + _, err = sysregistriesv2.TryUpdatingCache(sys) require.NoError(t, err) tests := []struct { - name, value string + name, value, dockerHubValue string }{ - {"docker", "docker.io/library/foo:latest"}, - {"docker:tag", "docker.io/library/foo:tag"}, + {"docker", "docker.io/library/foo:latest", "docker.io/library/docker:latest"}, + {"docker:tag", "docker.io/library/foo:tag", "docker.io/library/docker:tag"}, { "docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "docker.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "docker.io/library/docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", }, - {"quay/foo", "quay.io/library/foo:latest"}, - {"quay/foo:tag", "quay.io/library/foo:tag"}, + {"quay/foo", "quay.io/library/foo:latest", "docker.io/quay/foo:latest"}, + {"quay/foo:tag", "quay.io/library/foo:tag", "docker.io/quay/foo:tag"}, { "quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "quay.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "docker.io/quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", }, - {"example", "example.com/library/foo:latest"}, - {"example:tag", "example.com/library/foo:tag"}, + {"example", "example.com/library/foo:latest", "docker.io/library/example:latest"}, + {"example:tag", "example.com/library/foo:tag", "docker.io/library/example:tag"}, { "example@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "example.com/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "docker.io/library/example@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", }, } @@ -141,6 +151,16 @@ func TestResolve(t *testing.T) { assert.False(t, resolved.PullCandidates[0].record) } + // Now another run with enforcing resolution to Docker Hub. + for _, test := range tests { + resolved, err := Resolve(sysResolveToDockerHub, test.name) + require.NoError(t, err, "%v", test) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, test.dockerHubValue, resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + } + // Non-existent should return an error as no search registries are // configured in the config. resolved, err := Resolve(sys, "doesnotexist") @@ -470,6 +490,12 @@ func TestResolveLocally(t *testing.T) { SystemRegistriesConfDirPath: "testdata/this-does-not-exist", UserShortNameAliasConfPath: tmp.Name(), } + sysResolveToDockerHub := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/two-reg.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + ResolveShortNamesToDockerHub: true, + } aliases, err := ResolveLocally(sys, "repo/image") // alias match require.NoError(t, err) @@ -479,6 +505,12 @@ func TestResolveLocally(t *testing.T) { assert.Equal(t, "quay.io/repo/image:latest", aliases[2].String()) // registry 0 assert.Equal(t, "registry.com/repo/image:latest", aliases[3].String()) // registry 0 + aliases, err = ResolveLocally(sysResolveToDockerHub, "repo/image") // alias match but enforced + require.NoError(t, err) + require.Len(t, aliases, 2) // localhost + docker.io enforced + assert.Equal(t, "localhost/repo/image:latest", aliases[0].String()) // localhost + assert.Equal(t, "docker.io/repo/image:latest", aliases[1].String()) // docker.io enforced + aliases, err = ResolveLocally(sys, "foo") // no alias match require.NoError(t, err) require.Len(t, aliases, 3) // localhost + two regs @@ -486,6 +518,12 @@ func TestResolveLocally(t *testing.T) { assert.Equal(t, "quay.io/foo:latest", aliases[1].String()) // registry 0 assert.Equal(t, "registry.com/foo:latest", aliases[2].String()) // registry 0 + aliases, err = ResolveLocally(sysResolveToDockerHub, "foo") // no alias match but enforced + require.NoError(t, err) + require.Len(t, aliases, 2) // localhost + docker.io enforced + assert.Equal(t, "localhost/foo:latest", aliases[0].String()) // localhost + assert.Equal(t, "docker.io/library/foo:latest", aliases[1].String()) // docker.io enforced + aliases, err = ResolveLocally(sys, "foo:tag") // no alias match tagged require.NoError(t, err) require.Len(t, aliases, 3) // localhost + two regs @@ -500,11 +538,22 @@ func TestResolveLocally(t *testing.T) { assert.Equal(t, "quay.io/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[1].String()) // registry 0 assert.Equal(t, "registry.com/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[2].String()) // registry 0 + aliases, err = ResolveLocally(sysResolveToDockerHub, "foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a") // no alias match digested + docker.io enforced + require.NoError(t, err) + require.Len(t, aliases, 2) // localhost + docker.io enforced + assert.Equal(t, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[0].String()) // localhost + assert.Equal(t, "docker.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[1].String()) // docker.io enforced + aliases, err = ResolveLocally(sys, "localhost/foo") // localhost require.NoError(t, err) require.Len(t, aliases, 1) assert.Equal(t, "localhost/foo:latest", aliases[0].String()) + aliases, err = ResolveLocally(sys, "localhost/foo") // localhost + docker.io enforced + require.NoError(t, err) + require.Len(t, aliases, 1) + assert.Equal(t, "localhost/foo:latest", aliases[0].String()) + aliases, err = ResolveLocally(sys, "localhost/foo:tag") // localhost + tag require.NoError(t, err) require.Len(t, aliases, 1) diff --git a/types/types.go b/types/types.go index c98a6c6fda..c5d1cc39bb 100644 --- a/types/types.go +++ b/types/types.go @@ -561,6 +561,8 @@ type SystemContext struct { UserShortNameAliasConfPath string // If set, short-name resolution in pkg/shortnames must follow the specified mode ShortNameMode *ShortNameMode + // If set, short names will resolve docker.io, and unqualified-search registries and short-name alias are ignored. + ResolveShortNamesToDockerHub bool // If not "", overrides the default path for the authentication file, but only new format files AuthFilePath string // if not "", overrides the default path for the authentication file, but with the legacy format;