mirror of https://github.com/containers/image.git
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:
parent
830a24cb56
commit
cf9ea2e816
|
|
@ -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
1
go.mod
|
|
@ -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
17
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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=""
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[aliases]
|
||||
"repo/image"="quay.io/repo/image"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
unqualified-search-registries=["quay.io"]
|
||||
|
||||
[aliases]
|
||||
"repo/image"="quay.io/repo/image"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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=""
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
unqualified-search-registries=["quay.io", "registry.com"]
|
||||
|
||||
[aliases]
|
||||
"repo/image"="quay.io/repo/image"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue