Skip to content

Commit

Permalink
shortnames: mechanism to enforce resolving to Docker Hub
Browse files Browse the repository at this point in the history
Podman's Docker-compatible REST API is in need of a mechanism to enforce
resolving to Docker Hub only.  Yet there is the desire for the rest of
the stack to continue honoring the system's registries.conf.

We recently added a new field to containers.conf [1] which allows for
opting out from enforcing Docker Hub for Podman's compat API but we
still lack a way of enforcement when resolving short names; which
ultimately is *the* place to do that.

This change does the necessary plumbing.  The compat REST handlers will
set the new field in the `types.SystemContext` and pass that down to
libimage and buildah.

[1] containers/common@e698b8c

Context: containers/podman/issues/12320
Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
vrothberg committed Nov 23, 2021
1 parent a760f06 commit 707359b
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 20 deletions.
53 changes: 40 additions & 13 deletions pkg/shortnames/shortnames.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
63 changes: 56 additions & 7 deletions pkg/shortnames/shortnames_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -479,13 +505,25 @@ 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
assert.Equal(t, "localhost/foo:latest", aliases[0].String()) // localhost
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
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 707359b

Please sign in to comment.