pkg/shortnames

Add a new package for short-name resolution. `pkg/shortnames` is built
around the short-name aliasing in the registries.conf and introduces two
functions.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg 2020-11-12 12:17:44 +01:00
parent 830a24cb56
commit cf9ea2e816
15 changed files with 1125 additions and 4 deletions

View File

@ -102,6 +102,75 @@ internet without having to change `Dockerfile`s, or to add redundancy).
*Note*: Redirection and mirrors are currently processed only when reading images, not when pushing
to a registry; that may change in the future.
#### Short-Name Aliasing
The use of unqualified-search registries entails an ambiguity as it is
unclear from which registry a given image, referenced by a short name,
may be pulled from.
As mentioned in the note at the end of this man page, using short names is
subject to the risk of hitting squatted registry namespaces. If the
unqualified-search registries are set to `["registry1.com", "registry2.com"]`
an attacker may take over a namespace of registry1.com such that an image may
be pulled from registry1.com instead of the intended source registry2.com.
While it is highly recommended to always use fully-qualified image references,
existing deployments using short names may not be easily changed. To
circumvent the aforementioned ambiguity, so called short-name aliases can be
configured that point to a fully-qualified image
reference.
Short-name aliases can be configured in the `[aliases]` table in the form of
`"name"="value"` with the left-hand `name` being the short name (e.g., "image")
and the right-hand `value` being the fully-qualified image reference (e.g.,
"registry.com/namespace/image"). Note that neither "name" nor "value" can
include a tag or digest. Moreover, "name" must be a short name and hence
cannot include a registry domain or refer to localhost.
When pulling a short name, the configured aliases table will be used for
resolving the short name. If a matching alias is found, it will be used
without further consulting the unqualified-search registries list. If no
matching alias is found, the behavior can be controlled via the
`short-name-mode` option as described below.
Note that tags and digests are stripped off a user-specified short name for
alias resolution. Hence, "image", "image:tag" and "image@digest" all resolve
to the same alias (i.e., "image"). Stripped off tags and digests are later
appended to the resolved alias.
Further note that drop-in configuration files (see containers-registries.conf.d(5))
can override aliases in the specific loading order of the files. If the "value" of
an alias is empty (i.e., ""), the alias will be erased. However, a given
"name" may only be specified once in a single config file.
#### Short-Name Aliasing: Modes
The `short-name-mode` option supports three modes to control the behaviour of
short-name resolution.
* `enforcing`: If only one unqualified-search registry is set, use it as there
is no ambiguity. If there is more than one registry and the user program is
running in a terminal (i.e., stdout & stdin are a TTY), prompt the user to
select one of the specified search registries. If the program is not running
in a terminal, the ambiguity cannot be resolved which will lead to an error.
* `permissive`: Behaves as enforcing but does not lead to an error if the
program is not running in a terminal. Instead, fallback to using all
unqualified-search registries.
* `disabled`: Use all unqualified-search registries without prompting.
If `short-name-mode` is not specified at all or left empty, default to the
`permissive` mode. If the user-specified short name was not aliased already,
the `enforcing` and `permissive` mode if prompted, will record a new alias
after a successful pull. Note that the recorded alias will be written to
`$XDG_CONFIG_HOME/containers/short-name-aliases.conf` to have a clear
separation between possibly human-edited registries.conf files and the
machine-generated `short-name-aliases-conf`. Note that `$HOME/.config` is used
if `$XDG_CONFIG_HOME` is not set. If an alias is specified in a
`registries.conf` file and also the machine-generated
`short-name-aliases.conf`, the `short-name-aliases.conf` file has precedence.
#### Normalization of docker.io references
The Docker Hub `docker.io` is handled in a special way: every push and pull

1
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/imdario/mergo v0.3.11
github.com/klauspost/compress v1.11.2
github.com/klauspost/pgzip v1.2.5
github.com/manifoldco/promptui v0.8.0
github.com/morikuni/aec v1.0.0 // indirect
github.com/mtrmac/gpgme v0.1.2
github.com/opencontainers/go-digest v1.0.0

17
go.sum
View File

@ -24,6 +24,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.0.0-20200507155900-a9f01edf17e3/go.mod h1:XT+cAw5wfvsodedcijoh1l9cf7v1x9FlFB/3VmF/O8s=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -42,6 +46,8 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/containers/image v1.5.1 h1:ssEuj1c24uJvdMkUa2IrawuEFZBP12p6WzrjNBTQxE0=
github.com/containers/image v3.0.2+incompatible h1:B1lqAE8MUPCrsBLE86J0gnXleeRq8zJnQryhiiGQNyE=
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.0.1 h1:EToign46OSLTFWnb2oNj9RG3XDnkOX8r28ZIXUuk5Pc=
@ -161,6 +167,8 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@ -205,6 +213,14 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
@ -404,6 +420,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -0,0 +1,458 @@
package shortnames
import (
"fmt"
"os"
"strings"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/pkg/sysregistriesv2"
"github.com/containers/image/v5/types"
"github.com/manifoldco/promptui"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh/terminal"
)
// IsShortName returns true if the specified input is a "short name". A "short
// name" refers to a container image without a fully-qualified reference, and
// is hence missing a registry (or domain). Names including a digest are not
// short names.
//
// Examples:
// * short names: "image:tag", "library/fedora"
// * not short names: "quay.io/image", "localhost/image:tag",
// "server.org:5000/lib/image", "image@sha256:..."
func IsShortName(input string) bool {
isShort, _, _ := parseUnnormalizedShortName(input)
return isShort
}
// parseUnnormalizedShortName parses the input and returns if it's short name,
// the unnormalized reference.Named, and a parsing error.
func parseUnnormalizedShortName(input string) (bool, reference.Named, error) {
ref, err := reference.Parse(input)
if err != nil {
return false, nil, errors.Wrapf(err, "cannot parse input: %q", input)
}
named, ok := ref.(reference.Named)
if !ok {
return true, nil, errors.Errorf("%q is not a named reference", input)
}
registry := reference.Domain(named)
if strings.ContainsAny(registry, ".:") || registry == "localhost" {
// A final parse to make sure that docker.io references are correctly
// normalized (e.g., docker.io/alpine to docker.io/library/alpine.
named, err = reference.ParseNormalizedNamed(input)
if err != nil {
return false, nil, errors.Wrapf(err, "cannot normalize input: %q", input)
}
return false, named, nil
}
return true, named, nil
}
// splitUserInput parses the user-specified reference. Namely, it strips off
// the tag or digest and stores it in the return values so that both can be
// re-added to a possible resolved alias' or USRs at a later point.
func splitUserInput(named reference.Named) (isTagged bool, isDigested bool, normalized reference.Named, tag string, digest digest.Digest) {
normalized = named
tagged, isT := named.(reference.NamedTagged)
if isT {
isTagged = true
tag = tagged.Tag()
}
digested, isD := named.(reference.Digested)
if isD {
isDigested = true
digest = digested.Digest()
}
// Strip off tag/digest if present.
normalized = reference.TrimNamed(named)
return
}
// Add records the specified name-value pair as a new short-name alias to the
// user-specific aliases.conf. It may override an existing alias for `name`.
func Add(ctx *types.SystemContext, name string, value reference.Named) error {
isShort, _, err := parseUnnormalizedShortName(name)
if err != nil {
return err
}
if !isShort {
return errors.Errorf("%q is not a short name", name)
}
return sysregistriesv2.AddShortNameAlias(ctx, name, value.String())
}
// Remove clears the short-name alias for the specified name. It throws an
// error in case name does not exist in the machine-generated
// short-name-alias.conf. In such case, the alias must be specified in one of
// the registries.conf files, which is the users' responsibility.
func Remove(ctx *types.SystemContext, name string) error {
isShort, _, err := parseUnnormalizedShortName(name)
if err != nil {
return err
}
if !isShort {
return errors.Errorf("%q is not a short name", name)
}
return sysregistriesv2.RemoveShortNameAlias(ctx, name)
}
// Resolved encapsulates all data for a resolved image name.
type Resolved struct {
PullCandidates []PullCandidate
userInput reference.Named
systemContext *types.SystemContext
rationale rationale
originDescription string
}
func (r *Resolved) addCandidate(named reference.Named) {
r.PullCandidates = append(r.PullCandidates, PullCandidate{named, false, r})
}
func (r *Resolved) addCandidateToRecord(named reference.Named) {
r.PullCandidates = append(r.PullCandidates, PullCandidate{named, true, r})
}
// Allows to reason over pull errors and add some context information.
// Used in (*Resolved).WrapPullError.
type rationale int
const (
// No additional context.
rationaleNone rationale = iota
// Resolved value is a short-name alias.
rationaleAlias
// Resolved value has been completed with an Unqualified Search Registry.
rationaleUSR
// Resolved value has been selected by the user (via the prompt).
rationaleUserSelection
)
// Description returns a human-readable description about the resolution
// process (e.g., short-name alias, unqualified-search registries, etc.).
// It is meant to be printed before attempting to pull the pull candidates
// to make the short-name resolution more transparent to user.
//
// If the returned string is empty, it is not meant to be printed.
func (r *Resolved) Description() string {
switch r.rationale {
case rationaleAlias:
return fmt.Sprintf("Resolved short name %q to a recorded short-name alias (origin: %s)", r.userInput, r.originDescription)
case rationaleUSR:
return fmt.Sprintf("Completed short name %q with unqualified-search registries (origin: %s)", r.userInput, r.originDescription)
case rationaleUserSelection, rationaleNone:
fallthrough
default:
return ""
}
}
// FormatPullErrors is a convenience function to format errors that occurred
// while trying to pull all of the resolved pull candidates.
//
// Note that nil is returned if len(pullErrors) == 0. Otherwise, the amount of
// pull errors must equal the amount of pull candidates.
func (r *Resolved) FormatPullErrors(pullErrors []error) error {
if len(pullErrors) >= 0 && len(pullErrors) != len(r.PullCandidates) {
pullErrors = append(pullErrors,
errors.Errorf("internal error: expected %d instead of %d errors for %d pull candidates",
len(r.PullCandidates), len(pullErrors), len(r.PullCandidates)))
}
switch len(pullErrors) {
case 0:
return nil
case 1:
return pullErrors[0]
default:
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%d errors occurred while pulling:", len(pullErrors)))
for _, e := range pullErrors {
sb.WriteString("\n * ")
sb.WriteString(e.Error())
}
return errors.New(sb.String())
}
}
// PullCandidate is a resolved name. Once the Value has been used
// successfully, users MUST call `(*PullCandidate).Record(..)` to possibly
// record it as a new short-name alias.
type PullCandidate struct {
// Fully-qualified reference with tag or digest.
Value reference.Named
// Control whether to record it permanently as an alias.
record bool
// Backwards pointer to the Resolved "parent".
resolved *Resolved
}
// Record may store a short-name alias for the PullCandidate.
func (c *PullCandidate) Record() error {
if !c.record {
return nil
}
// Strip off tags/digests from name/value.
name := reference.TrimNamed(c.resolved.userInput)
value := reference.TrimNamed(c.Value)
if err := Add(c.resolved.systemContext, name.String(), value); err != nil {
return errors.Wrapf(err, "error recording short-name alias (%q=%q)", c.resolved.userInput, c.Value)
}
return nil
}
// Resolve resolves the specified name to either one or more fully-qualified
// image references that the short name may be *pulled* from. If the specified
// name is already a fully-qualified reference (i.e., not a short name), it is
// returned as is. In case, it's a short name, it's resolved according to the
// ShortNameMode in the SystemContext (if specified) or in the registries.conf.
//
// Note that tags and digests are stripped from the specified name before
// looking up an alias. Stripped off tags and digests are later on appended to
// all candidates. If neither tag nor digest is specified, candidates are
// normalized with the "latest" tag. PullCandidates in the returned value may
// be empty if there is no matching alias and no unqualified-search registries
// are configured.
//
// Note that callers *must* call `(PullCandidate).Record` after a returned
// item has been pulled successfully; this callback will record a new
// short-name alias (depending on the specified short-name mode).
//
// Furthermore, before attempting to pull callers *should* call
// `(Resolved).Description` and afterwards use
// `(Resolved).FormatPullErrors` in case of pull errors.
func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) {
resolved := &Resolved{}
// Create a copy of the system context to make it usable beyond this
// function call.
var sys *types.SystemContext
if ctx != nil {
sys = &(*ctx)
}
resolved.systemContext = ctx
// Detect which mode we're running in.
mode, err := sysregistriesv2.GetShortNameMode(sys)
if err != nil {
return nil, err
}
// Sanity check the short-name mode.
switch mode {
case types.ShortNameModeDisabled, types.ShortNameModePermissive, types.ShortNameModeEnforcing:
// We're good.
default:
return nil, errors.Errorf("unsupported short-name mode (%v)", mode)
}
isShort, shortRef, err := parseUnnormalizedShortName(name)
if err != nil {
return nil, err
}
if !isShort { // no short name
named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed
resolved.addCandidate(named)
return resolved, nil
}
// Strip off the tag to normalize the short name for looking it up in
// the config files.
isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
resolved.userInput = shortNameRepo
// If there's already an alias, use it.
namedAlias, aliasOriginDescription, err := sysregistriesv2.ResolveShortNameAlias(sys, shortNameRepo.String())
if err != nil {
return nil, err
}
// Always use an alias if present.
if namedAlias != nil {
if isTagged {
namedAlias, err = reference.WithTag(namedAlias, tag)
if err != nil {
return nil, err
}
}
if isDigested {
namedAlias, err = reference.WithDigest(namedAlias, digest)
if err != nil {
return nil, err
}
}
// Make sure to add ":latest" if needed
namedAlias = reference.TagNameOnly(namedAlias)
resolved.addCandidate(namedAlias)
resolved.rationale = rationaleAlias
resolved.originDescription = aliasOriginDescription
return resolved, nil
}
resolved.rationale = rationaleUSR
// Query the registry for unqualified-search registries.
unqualifiedSearchRegistries, usrConfig, err := sysregistriesv2.UnqualifiedSearchRegistriesWithOrigin(sys)
if err != nil {
return nil, err
}
resolved.originDescription = usrConfig
for _, reg := range unqualifiedSearchRegistries {
named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
if err != nil {
return nil, errors.Wrapf(err, "error creating reference with unqualified-search registry %q", reg)
}
// Make sure to add ":latest" if needed
named = reference.TagNameOnly(named)
resolved.addCandidate(named)
}
// If we're running in disabled, return the candidates without
// prompting (and without recording).
if mode == types.ShortNameModeDisabled {
return resolved, nil
}
// If we have only one candidate, there's no ambiguity. In case of an
// empty candidate slices, callers can implement custom logic or raise
// an error.
if len(resolved.PullCandidates) <= 1 {
return resolved, nil
}
// If we don't have a TTY, act according to the mode.
if !terminal.IsTerminal(int(os.Stdout.Fd())) || !terminal.IsTerminal(int(os.Stdin.Fd())) {
switch mode {
case types.ShortNameModePermissive:
// Permissive falls back to using all candidates.
return resolved, nil
case types.ShortNameModeEnforcing:
// Enforcing errors out without a prompt.
return nil, errors.New("short-name resolution enforced but cannot prompt without a TTY")
default:
// We should not end up here.
return nil, errors.Errorf("unexpected short-name mode (%v) during resolution", mode)
}
}
// We have a TTY, and can prompt the user with a selection of all
// possible candidates.
strCandidates := []string{}
for _, candidate := range resolved.PullCandidates {
strCandidates = append(strCandidates, candidate.Value.String())
}
prompt := promptui.Select{
Label: "Please select an image",
Items: strCandidates,
HideHelp: true, // do not show navigation help
}
_, selection, err := prompt.Run()
if err != nil {
return nil, err
}
named, err := reference.ParseNormalizedNamed(selection)
if err != nil {
return nil, errors.Wrapf(err, "selection %q is not a valid reference", selection)
}
resolved.PullCandidates = nil
resolved.addCandidateToRecord(named)
resolved.rationale = rationaleUserSelection
return resolved, nil
}
// ResolveLocally resolves the specified name to either one or more local
// images. If the specified name is already a fully-qualified reference (i.e.,
// not a short name), it is returned as is. In case, it's a short name, the
// returned slice of named references looks as follows:
//
// 1) If present, the short-name alias
// 2) "localhost/" as used by many container engines such as Podman and Buildah
// 3) Unqualified-search registries from the registries.conf files
//
// Note that tags and digests are stripped from the specified name before
// looking up an alias. Stripped off tags and digests are later on appended to
// all candidates. If neither tag nor digest is specified, candidates are
// normalized with the "latest" tag. The returned slice contains at least one
// item.
func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, error) {
isShort, shortRef, err := parseUnnormalizedShortName(name)
if err != nil {
return nil, err
}
if !isShort { // no short name
named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed
return []reference.Named{named}, nil
}
var candidates []reference.Named
// Strip off the tag to normalize the short name for looking it up in
// the config files.
isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
// If there's already an alias, use it.
namedAlias, _, err := sysregistriesv2.ResolveShortNameAlias(ctx, shortNameRepo.String())
if err != nil {
return nil, err
}
if namedAlias != nil {
if isTagged {
namedAlias, err = reference.WithTag(namedAlias, tag)
if err != nil {
return nil, err
}
}
if isDigested {
namedAlias, err = reference.WithDigest(namedAlias, digest)
if err != nil {
return nil, err
}
}
// Make sure to add ":latest" if needed
namedAlias = reference.TagNameOnly(namedAlias)
candidates = append(candidates, namedAlias)
}
// Query the registry for unqualified-search registries.
unqualifiedSearchRegistries, err := sysregistriesv2.UnqualifiedSearchRegistries(ctx)
if err != nil {
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, "error 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
}

View File

@ -0,0 +1,518 @@
package shortnames
import (
"io/ioutil"
"os"
"testing"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/pkg/sysregistriesv2"
"github.com/containers/image/v5/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsShortName(t *testing.T) {
tests := []struct {
input string
parseUnnormalizedShortName bool
mustFail bool
}{
// SHORT NAMES
{"fedora", true, false},
{"fedora:latest", true, false},
{"library/fedora", true, false},
{"library/fedora:latest", true, false},
{"busybox@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false},
{"busybox:latest@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false},
// !SHORT NAMES
{"quay.io/fedora", false, false},
{"docker.io/fedora", false, false},
{"docker.io/library/fedora:latest", false, false},
{"localhost/fedora", false, false},
{"localhost:5000/fedora:latest", false, false},
{"example.foo.this.may.be.garbage.but.maybe.not:1234/fedora:latest", false, false},
{"docker.io/library/busybox@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false},
{"docker.io/library/busybox:latest@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false},
{"docker.io/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false},
// INVALID NAMES
{"", false, true},
{"$$$", false, true},
{"::", false, true},
{"docker://quay.io/library/foo:bar", false, true},
{" ", false, true},
}
for _, test := range tests {
res, _, err := parseUnnormalizedShortName(test.input)
if test.mustFail {
require.Error(t, err, "%q should not be parseable")
continue
}
require.NoError(t, err, "%q should be parseable")
assert.Equal(t, test.parseUnnormalizedShortName, res, "%q", test.input)
}
}
func TestSplitUserInput(t *testing.T) {
tests := []struct {
input string
repo string
isTagged bool
isDigested bool
}{
// Neither tags nor digests
{"fedora", "fedora", false, false},
{"repo/fedora", "repo/fedora", false, false},
{"registry.com/fedora", "registry.com/fedora", false, false},
// Tags
{"fedora:tag", "fedora", true, false},
{"repo/fedora:tag", "repo/fedora", true, false},
{"registry.com/fedora:latest", "registry.com/fedora", true, false},
// Digests
{"fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "fedora", false, true},
{"repo/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "repo/fedora", false, true},
{"registry.com/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/fedora", false, true},
}
for _, test := range tests {
_, ref, err := parseUnnormalizedShortName(test.input)
require.NoError(t, err, "%v", test)
isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(ref)
require.NotNil(t, shortNameRepo)
normalized := shortNameRepo.String()
assert.Equal(t, test.repo, normalized)
assert.Equal(t, test.isTagged, isTagged)
assert.Equal(t, test.isDigested, isDigested)
if isTagged {
normalized = normalized + ":" + tag
} else if isDigested {
normalized = normalized + "@" + digest.String()
}
assert.Equal(t, test.input, normalized)
}
}
func TestResolve(t *testing.T) {
tmp, err := ioutil.TempFile("", "aliases.conf")
require.NoError(t, err)
defer os.Remove(tmp.Name())
sys := &types.SystemContext{
SystemRegistriesConfPath: "testdata/aliases.conf",
SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
UserShortNameAliasConfPath: tmp.Name(),
}
_, err = sysregistriesv2.TryUpdatingCache(sys)
require.NoError(t, err)
tests := []struct {
name, value string
}{
{"docker", "docker.io/library/foo:latest"},
{"docker:tag", "docker.io/library/foo:tag"},
{
"docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"docker.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"quay/foo", "quay.io/library/foo:latest"},
{"quay/foo:tag", "quay.io/library/foo:tag"},
{
"quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"quay.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"example", "example.com/library/foo:latest"},
{"example:tag", "example.com/library/foo:tag"},
{
"example@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"example.com/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
}
// All of them should resolve correctly.
for _, test := range tests {
resolved, err := Resolve(sys, test.name)
require.NoError(t, err, "%v", test)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 1)
assert.Equal(t, test.value, resolved.PullCandidates[0].Value.String())
assert.False(t, resolved.PullCandidates[0].record)
}
// Non-existent should return an empty slice as no search registries
// are configured in the config.
resolved, err := Resolve(sys, "dontexist")
require.NoError(t, err)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 0)
// An empty name is not valid.
resolved, err = Resolve(sys, "")
require.Error(t, err)
require.Nil(t, resolved)
// Invalid input.
resolved, err = Resolve(sys, "Invalid#$")
require.Error(t, err)
require.Nil(t, resolved)
// Fully-qualified input will be returned as is.
resolved, err = Resolve(sys, "quay.io/repo/fedora")
require.NoError(t, err)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 1)
assert.Equal(t, "quay.io/repo/fedora:latest", resolved.PullCandidates[0].Value.String())
assert.False(t, resolved.PullCandidates[0].record)
}
func toNamed(t *testing.T, input string, trim bool) reference.Named {
ref, err := reference.Parse(input)
require.NoError(t, err)
named := ref.(reference.Named)
require.NotNil(t, named)
if trim {
named = reference.TrimNamed(named)
}
return named
}
func addAlias(t *testing.T, sys *types.SystemContext, name string, value string, mustFail bool) {
namedValue := toNamed(t, value, false)
if mustFail {
require.Error(t, Add(sys, name, namedValue))
} else {
require.NoError(t, Add(sys, name, namedValue))
}
}
func removeAlias(t *testing.T, sys *types.SystemContext, name string, mustFail bool, trim bool) {
namedName := toNamed(t, name, trim)
if mustFail {
require.Error(t, Remove(sys, namedName.String()))
} else {
require.NoError(t, Remove(sys, namedName.String()))
}
}
func TestResolveWithDropInConfigs(t *testing.T) {
tmp, err := ioutil.TempFile("", "aliases.conf")
require.NoError(t, err)
defer os.Remove(tmp.Name())
sys := &types.SystemContext{
SystemRegistriesConfPath: "testdata/aliases.conf",
SystemRegistriesConfDirPath: "testdata/registries.conf.d",
UserShortNameAliasConfPath: tmp.Name(),
}
_, err = sysregistriesv2.TryUpdatingCache(sys)
require.NoError(t, err)
tests := []struct {
name, value string
}{
{"docker", "docker.io/library/config1:latest"}, // overriden by config1
{"docker:tag", "docker.io/library/config1:tag"},
{
"docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"docker.io/library/config1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"quay/foo", "quay.io/library/foo:latest"},
{"quay/foo:tag", "quay.io/library/foo:tag"},
{
"quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"quay.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"config1", "config1.com/image:latest"},
{"config1:tag", "config1.com/image:tag"},
{
"config1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"config1.com/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"barz", "barz.com/config2:latest"}, // from config1, overridden by config2
{"barz:tag", "barz.com/config2:tag"},
{
"barz@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"barz.com/config2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"added1", "aliases.conf/added1:latest"}, // from Add()
{"added1:tag", "aliases.conf/added1:tag"},
{
"added1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"aliases.conf/added1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"added2", "aliases.conf/added2:latest"}, // from Add()
{"added2:tag", "aliases.conf/added2:tag"},
{
"added2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"aliases.conf/added2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
{"added3", "aliases.conf/added3:latest"}, // from Add()
{"added3:tag", "aliases.conf/added3:tag"},
{
"added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
"aliases.conf/added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
},
}
addAlias(t, sys, "added1", "aliases.conf/added1", false)
addAlias(t, sys, "added2", "aliases.conf/added2", false)
addAlias(t, sys, "added3", "aliases.conf/added3", false)
// Tags/digests are invalid!
addAlias(t, sys, "added3", "aliases.conf/added3:tag", true)
addAlias(t, sys, "added3:tag", "aliases.conf/added3", true)
addAlias(t, sys, "added3", "aliases.conf/added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true)
addAlias(t, sys, "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "aliases.conf/added3", true)
// All of them should resolve correctly.
for _, test := range tests {
resolved, err := Resolve(sys, test.name)
require.NoError(t, err)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 1)
assert.Equal(t, test.value, resolved.PullCandidates[0].Value.String())
assert.False(t, resolved.PullCandidates[0].record)
}
// config1 sets one search registry.
resolved, err := Resolve(sys, "dontexist")
require.NoError(t, err)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 1)
assert.Equal(t, "example-overwrite.com/dontexist:latest", resolved.PullCandidates[0].Value.String())
// An empty name is not valid.
resolved, err = Resolve(sys, "")
require.Error(t, err)
require.Nil(t, resolved)
// Invalid input.
resolved, err = Resolve(sys, "Invalid#$")
require.Error(t, err)
require.Nil(t, resolved)
// Fully-qualified input will be returned as is.
resolved, err = Resolve(sys, "quay.io/repo/fedora")
require.NoError(t, err)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 1)
assert.Equal(t, "quay.io/repo/fedora:latest", resolved.PullCandidates[0].Value.String())
assert.False(t, resolved.PullCandidates[0].record)
resolved, err = Resolve(sys, "localhost/repo/fedora:sometag")
require.NoError(t, err)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, 1)
assert.Equal(t, "localhost/repo/fedora:sometag", resolved.PullCandidates[0].Value.String())
assert.False(t, resolved.PullCandidates[0].record)
// Now test removal.
// Stored in aliases.conf, so we can remove it.
removeAlias(t, sys, "added1", false, false)
removeAlias(t, sys, "added2", false, false)
removeAlias(t, sys, "added3", false, false)
removeAlias(t, sys, "added2:tag", true, false)
removeAlias(t, sys, "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false)
// Doesn't exist -> error.
removeAlias(t, sys, "added1", true, false)
removeAlias(t, sys, "added2", true, false)
removeAlias(t, sys, "added3", true, false)
// Cannot remove entries from registries.conf files -> error.
removeAlias(t, sys, "docker", true, false)
removeAlias(t, sys, "docker", true, false)
removeAlias(t, sys, "docker", true, false)
}
func TestResolveWithVaryingShortNameModes(t *testing.T) {
tmp, err := ioutil.TempFile("", "aliases.conf")
require.NoError(t, err)
defer os.Remove(tmp.Name())
tests := []struct {
confPath string
mode types.ShortNameMode
name string
mustFail bool
numAliases int
}{
// Invalid -> error
{"testdata/no-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0},
{"testdata/one-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0},
{"testdata/two-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0},
// Permisive + match -> return alias
{"testdata/no-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1},
{"testdata/one-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1},
{"testdata/two-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1},
// Permisive + no match -> search (no tty)
{"testdata/no-reg.conf", types.ShortNameModePermissive, "donotexist", false, 0},
{"testdata/one-reg.conf", types.ShortNameModePermissive, "donotexist", false, 1},
{"testdata/two-reg.conf", types.ShortNameModePermissive, "donotexist", false, 2},
// Disabled + match -> return alias
{"testdata/no-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1},
{"testdata/one-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1},
{"testdata/two-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1},
// Disabled + no match -> search
{"testdata/no-reg.conf", types.ShortNameModeDisabled, "donotexist", false, 0},
{"testdata/one-reg.conf", types.ShortNameModeDisabled, "donotexist", false, 1},
{"testdata/two-reg.conf", types.ShortNameModeDisabled, "donotexist", false, 2},
// Enforcing + match -> return alias
{"testdata/no-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1},
{"testdata/one-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1},
{"testdata/two-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1},
// Enforcing + no match -> error if search regs > 1 and no tty
{"testdata/no-reg.conf", types.ShortNameModeEnforcing, "donotexist", false, 0},
{"testdata/one-reg.conf", types.ShortNameModeEnforcing, "donotexist", false, 1},
{"testdata/two-reg.conf", types.ShortNameModeEnforcing, "donotexist", true, 0},
}
for _, test := range tests {
sys := &types.SystemContext{
SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
UserShortNameAliasConfPath: tmp.Name(),
// From test
SystemRegistriesConfPath: test.confPath,
ShortNameMode: &test.mode,
}
_, err := sysregistriesv2.TryUpdatingCache(sys)
require.NoError(t, err)
resolved, err := Resolve(sys, test.name)
if test.mustFail {
require.Error(t, err, "%v", test)
continue
}
require.NoError(t, err, "%v", test)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, test.numAliases, "%v", test)
}
}
func TestResolveAndRecord(t *testing.T) {
tmp, err := ioutil.TempFile("", "aliases.conf")
require.NoError(t, err)
defer os.Remove(tmp.Name())
sys := &types.SystemContext{
SystemRegistriesConfPath: "testdata/two-reg.conf",
SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
UserShortNameAliasConfPath: tmp.Name(),
}
_, err = sysregistriesv2.TryUpdatingCache(sys)
require.NoError(t, err)
tests := []struct {
name string
expected []string
}{
// No alias -> USRs
{"foo", []string{"quay.io/foo:latest", "registry.com/foo:latest"}},
{"foo:tag", []string{"quay.io/foo:tag", "registry.com/foo:tag"}},
{"foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}},
{"repo/foo", []string{"quay.io/repo/foo:latest", "registry.com/repo/foo:latest"}},
{"repo/foo:tag", []string{"quay.io/repo/foo:tag", "registry.com/repo/foo:tag"}},
{"repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}},
// Alias
{"repo/image", []string{"quay.io/repo/image:latest"}},
{"repo/image:tag", []string{"quay.io/repo/image:tag"}},
{"repo/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/repo/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}},
}
for _, test := range tests {
resolved, err := Resolve(sys, test.name)
require.NoError(t, err, "%v", test)
require.NotNil(t, resolved)
require.Len(t, resolved.PullCandidates, len(test.expected), "%v", test)
for i, candidate := range resolved.PullCandidates {
require.Equal(t, test.expected[i], candidate.Value.String(), "%v", test)
require.False(t, candidate.record, "%v", test)
candidate.record = true // make sure we can actually record
// Record the alias, look it up another time and make
// sure there's only one match (i.e., the new alias)
// and that is has the expected value.
require.NoError(t, candidate.Record())
newResolved, err := Resolve(sys, test.name)
require.NoError(t, err, "%v", test)
require.Len(t, newResolved.PullCandidates, 1, "%v", test)
require.Equal(t, candidate.Value.String(), newResolved.PullCandidates[0].Value.String(), "%v", test)
// Now remove the alias again.
removeAlias(t, sys, test.name, false, true)
// Now set recording to false and try recording again.
candidate.record = false
require.NoError(t, candidate.Record())
removeAlias(t, sys, test.name, true, true) // must error out now
}
}
}
func TestResolveLocally(t *testing.T) {
tmp, err := ioutil.TempFile("", "aliases.conf")
require.NoError(t, err)
defer os.Remove(tmp.Name())
sys := &types.SystemContext{
SystemRegistriesConfPath: "testdata/two-reg.conf",
SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
UserShortNameAliasConfPath: tmp.Name(),
}
aliases, err := ResolveLocally(sys, "repo/image") // alias match
require.NoError(t, err)
require.Len(t, aliases, 4) // alias + localhost + two regs
assert.Equal(t, "quay.io/repo/image:latest", aliases[0].String()) // alias
assert.Equal(t, "localhost/repo/image:latest", aliases[1].String()) // localhost
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(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(sys, "foo:tag") // no alias match tagged
require.NoError(t, err)
require.Len(t, aliases, 3) // localhost + two regs
assert.Equal(t, "localhost/foo:tag", aliases[0].String()) // localhost
assert.Equal(t, "quay.io/foo:tag", aliases[1].String()) // registry 0
assert.Equal(t, "registry.com/foo:tag", aliases[2].String()) // registry 0
aliases, err = ResolveLocally(sys, "foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a") // no alias match digested
require.NoError(t, err)
require.Len(t, aliases, 3) // localhost + two regs
assert.Equal(t, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[0].String()) // localhost
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(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:tag") // localhost + tag
require.NoError(t, err)
require.Len(t, aliases, 1)
assert.Equal(t, "localhost/foo:tag", aliases[0].String())
aliases, err = ResolveLocally(sys, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a") // localhost + digest
require.NoError(t, err)
require.Len(t, aliases, 1)
assert.Equal(t, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[0].String())
}

7
pkg/shortnames/testdata/aliases.conf vendored Normal file
View File

@ -0,0 +1,7 @@
short-name-mode="enforcing"
[aliases]
docker="docker.io/library/foo"
"quay/foo"="quay.io/library/foo"
example="example.com/library/foo"
empty=""

2
pkg/shortnames/testdata/no-reg.conf vendored Normal file
View File

@ -0,0 +1,2 @@
[aliases]
"repo/image"="quay.io/repo/image"

4
pkg/shortnames/testdata/one-reg.conf vendored Normal file
View File

@ -0,0 +1,4 @@
unqualified-search-registries=["quay.io"]
[aliases]
"repo/image"="quay.io/repo/image"

View File

@ -0,0 +1,9 @@
unqualified-search-registries = ["example-overwrite.com"]
[[registry]]
location = "1.com"
[aliases]
docker="docker.io/library/config1"
config1="config1.com/image"
barz="barz.com/image/config1"

View File

@ -0,0 +1,14 @@
short-name-mode="permissive"
[[registry]]
location = "2.com"
[[registry]]
location = "base.com"
blocked = true
[aliases]
config2="config2.com/image"
barz="barz.com/config2"
added3="xxx.com/image"
example=""

View File

@ -0,0 +1,7 @@
unqualified-search-registries = ["ignore-example-overwrite.com"]
[[registry]]
location = "ignore-me-because-i-have-a-wrong-suffix.com"
[aliases]
ignore="me because i have a wrong suffix"

4
pkg/shortnames/testdata/two-reg.conf vendored Normal file
View File

@ -0,0 +1,4 @@
unqualified-search-registries=["quay.io", "registry.com"]
[aliases]
"repo/image"="quay.io/repo/image"

View File

@ -647,6 +647,9 @@ func parseShortNameMode(mode string) (types.ShortNameMode, error) {
// GetShortNameMode returns the configured types.ShortNameMode.
func GetShortNameMode(ctx *types.SystemContext) (types.ShortNameMode, error) {
if ctx != nil && ctx.ShortNameMode != nil {
return *ctx.ShortNameMode, nil
}
config, err := getConfig(ctx)
if err != nil {
return -1, err

View File

@ -500,13 +500,19 @@ const (
// Use all configured unqualified-search registries without prompting
// the user.
ShortNameModeDisabled
// If stdout is a TTY, prompt the user to select a configured
// If stdout and stdin are a TTY, prompt the user to select a configured
// unqualified-search registry. Otherwise, use all configured
// unqualified-search registries.
//
// Note that if only one unqualified-search registry is set, it will be
// used without prompting.
ShortNameModePermissive
// Always prompt the user to select a configured unqualified-serach
// registry. Throw an error if stdout is not a TTY as prompting
// isn't possible.
// Always prompt the user to select a configured unqualified-search
// registry. Throw an error if stdout or stdin is not a TTY as
// prompting isn't possible.
//
// Note that if only one unqualified-search registry is set, it will be
// used without prompting.
ShortNameModeEnforcing
)
@ -535,6 +541,8 @@ type SystemContext struct {
SystemRegistriesConfDirPath string
// Path to the user-specific short-names configuration file
UserShortNameAliasConfPath string
// If set, short-name resolution in pkg/shortnames must follow the specified mode
ShortNameMode *ShortNameMode
// 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;