Codebase list golang-github-containers-image / cf9ea2e
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> Valentin Rothberg 3 years ago
15 changed file(s) with 1125 addition(s) and 4 deletion(s). Raw diff Collapse all Expand all
100100
101101 *Note*: Redirection and mirrors are currently processed only when reading images, not when pushing
102102 to a registry; that may change in the future.
103
104 #### Short-Name Aliasing
105 The use of unqualified-search registries entails an ambiguity as it is
106 unclear from which registry a given image, referenced by a short name,
107 may be pulled from.
108
109 As mentioned in the note at the end of this man page, using short names is
110 subject to the risk of hitting squatted registry namespaces. If the
111 unqualified-search registries are set to `["registry1.com", "registry2.com"]`
112 an attacker may take over a namespace of registry1.com such that an image may
113 be pulled from registry1.com instead of the intended source registry2.com.
114
115 While it is highly recommended to always use fully-qualified image references,
116 existing deployments using short names may not be easily changed. To
117 circumvent the aforementioned ambiguity, so called short-name aliases can be
118 configured that point to a fully-qualified image
119 reference.
120
121 Short-name aliases can be configured in the `[aliases]` table in the form of
122 `"name"="value"` with the left-hand `name` being the short name (e.g., "image")
123 and the right-hand `value` being the fully-qualified image reference (e.g.,
124 "registry.com/namespace/image"). Note that neither "name" nor "value" can
125 include a tag or digest. Moreover, "name" must be a short name and hence
126 cannot include a registry domain or refer to localhost.
127
128 When pulling a short name, the configured aliases table will be used for
129 resolving the short name. If a matching alias is found, it will be used
130 without further consulting the unqualified-search registries list. If no
131 matching alias is found, the behavior can be controlled via the
132 `short-name-mode` option as described below.
133
134 Note that tags and digests are stripped off a user-specified short name for
135 alias resolution. Hence, "image", "image:tag" and "image@digest" all resolve
136 to the same alias (i.e., "image"). Stripped off tags and digests are later
137 appended to the resolved alias.
138
139 Further note that drop-in configuration files (see containers-registries.conf.d(5))
140 can override aliases in the specific loading order of the files. If the "value" of
141 an alias is empty (i.e., ""), the alias will be erased. However, a given
142 "name" may only be specified once in a single config file.
143
144
145 #### Short-Name Aliasing: Modes
146
147 The `short-name-mode` option supports three modes to control the behaviour of
148 short-name resolution.
149
150 * `enforcing`: If only one unqualified-search registry is set, use it as there
151 is no ambiguity. If there is more than one registry and the user program is
152 running in a terminal (i.e., stdout & stdin are a TTY), prompt the user to
153 select one of the specified search registries. If the program is not running
154 in a terminal, the ambiguity cannot be resolved which will lead to an error.
155
156 * `permissive`: Behaves as enforcing but does not lead to an error if the
157 program is not running in a terminal. Instead, fallback to using all
158 unqualified-search registries.
159
160 * `disabled`: Use all unqualified-search registries without prompting.
161
162 If `short-name-mode` is not specified at all or left empty, default to the
163 `permissive` mode. If the user-specified short name was not aliased already,
164 the `enforcing` and `permissive` mode if prompted, will record a new alias
165 after a successful pull. Note that the recorded alias will be written to
166 `$XDG_CONFIG_HOME/containers/short-name-aliases.conf` to have a clear
167 separation between possibly human-edited registries.conf files and the
168 machine-generated `short-name-aliases-conf`. Note that `$HOME/.config` is used
169 if `$XDG_CONFIG_HOME` is not set. If an alias is specified in a
170 `registries.conf` file and also the machine-generated
171 `short-name-aliases.conf`, the `short-name-aliases.conf` file has precedence.
103172
104173 #### Normalization of docker.io references
105174
1919 github.com/imdario/mergo v0.3.11
2020 github.com/klauspost/compress v1.11.2
2121 github.com/klauspost/pgzip v1.2.5
22 github.com/manifoldco/promptui v0.8.0
2223 github.com/morikuni/aec v1.0.0 // indirect
2324 github.com/mtrmac/gpgme v0.1.2
2425 github.com/opencontainers/go-digest v1.0.0
2323 github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
2424 github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
2525 github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
26 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
27 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
28 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
29 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
2630 github.com/cilium/ebpf v0.0.0-20200507155900-a9f01edf17e3/go.mod h1:XT+cAw5wfvsodedcijoh1l9cf7v1x9FlFB/3VmF/O8s=
2731 github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
2832 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
4145 github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
4246 github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
4347 github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
48 github.com/containers/image v1.5.1 h1:ssEuj1c24uJvdMkUa2IrawuEFZBP12p6WzrjNBTQxE0=
49 github.com/containers/image v3.0.2+incompatible h1:B1lqAE8MUPCrsBLE86J0gnXleeRq8zJnQryhiiGQNyE=
4450 github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
4551 github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
4652 github.com/containers/ocicrypt v1.0.1 h1:EToign46OSLTFWnb2oNj9RG3XDnkOX8r28ZIXUuk5Pc=
160166 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
161167 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
162168 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
169 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
170 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
163171 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
164172 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
165173 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
204212 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
205213 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
206214 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
215 github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
216 github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
217 github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
218 github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
219 github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
220 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
221 github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
222 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
207223 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
208224 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
209225 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
403419 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
404420 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
405421 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
422 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
406423 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
407424 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
408425 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
0 package shortnames
1
2 import (
3 "fmt"
4 "os"
5 "strings"
6
7 "github.com/containers/image/v5/docker/reference"
8 "github.com/containers/image/v5/pkg/sysregistriesv2"
9 "github.com/containers/image/v5/types"
10 "github.com/manifoldco/promptui"
11 "github.com/opencontainers/go-digest"
12 "github.com/pkg/errors"
13 "golang.org/x/crypto/ssh/terminal"
14 )
15
16 // IsShortName returns true if the specified input is a "short name". A "short
17 // name" refers to a container image without a fully-qualified reference, and
18 // is hence missing a registry (or domain). Names including a digest are not
19 // short names.
20 //
21 // Examples:
22 // * short names: "image:tag", "library/fedora"
23 // * not short names: "quay.io/image", "localhost/image:tag",
24 // "server.org:5000/lib/image", "image@sha256:..."
25 func IsShortName(input string) bool {
26 isShort, _, _ := parseUnnormalizedShortName(input)
27 return isShort
28 }
29
30 // parseUnnormalizedShortName parses the input and returns if it's short name,
31 // the unnormalized reference.Named, and a parsing error.
32 func parseUnnormalizedShortName(input string) (bool, reference.Named, error) {
33 ref, err := reference.Parse(input)
34 if err != nil {
35 return false, nil, errors.Wrapf(err, "cannot parse input: %q", input)
36 }
37
38 named, ok := ref.(reference.Named)
39 if !ok {
40 return true, nil, errors.Errorf("%q is not a named reference", input)
41 }
42
43 registry := reference.Domain(named)
44 if strings.ContainsAny(registry, ".:") || registry == "localhost" {
45 // A final parse to make sure that docker.io references are correctly
46 // normalized (e.g., docker.io/alpine to docker.io/library/alpine.
47 named, err = reference.ParseNormalizedNamed(input)
48 if err != nil {
49 return false, nil, errors.Wrapf(err, "cannot normalize input: %q", input)
50 }
51 return false, named, nil
52 }
53
54 return true, named, nil
55 }
56
57 // splitUserInput parses the user-specified reference. Namely, it strips off
58 // the tag or digest and stores it in the return values so that both can be
59 // re-added to a possible resolved alias' or USRs at a later point.
60 func splitUserInput(named reference.Named) (isTagged bool, isDigested bool, normalized reference.Named, tag string, digest digest.Digest) {
61 normalized = named
62
63 tagged, isT := named.(reference.NamedTagged)
64 if isT {
65 isTagged = true
66 tag = tagged.Tag()
67 }
68
69 digested, isD := named.(reference.Digested)
70 if isD {
71 isDigested = true
72 digest = digested.Digest()
73 }
74
75 // Strip off tag/digest if present.
76 normalized = reference.TrimNamed(named)
77
78 return
79 }
80
81 // Add records the specified name-value pair as a new short-name alias to the
82 // user-specific aliases.conf. It may override an existing alias for `name`.
83 func Add(ctx *types.SystemContext, name string, value reference.Named) error {
84 isShort, _, err := parseUnnormalizedShortName(name)
85 if err != nil {
86 return err
87 }
88 if !isShort {
89 return errors.Errorf("%q is not a short name", name)
90 }
91 return sysregistriesv2.AddShortNameAlias(ctx, name, value.String())
92 }
93
94 // Remove clears the short-name alias for the specified name. It throws an
95 // error in case name does not exist in the machine-generated
96 // short-name-alias.conf. In such case, the alias must be specified in one of
97 // the registries.conf files, which is the users' responsibility.
98 func Remove(ctx *types.SystemContext, name string) error {
99 isShort, _, err := parseUnnormalizedShortName(name)
100 if err != nil {
101 return err
102 }
103 if !isShort {
104 return errors.Errorf("%q is not a short name", name)
105 }
106 return sysregistriesv2.RemoveShortNameAlias(ctx, name)
107 }
108
109 // Resolved encapsulates all data for a resolved image name.
110 type Resolved struct {
111 PullCandidates []PullCandidate
112
113 userInput reference.Named
114 systemContext *types.SystemContext
115 rationale rationale
116 originDescription string
117 }
118
119 func (r *Resolved) addCandidate(named reference.Named) {
120 r.PullCandidates = append(r.PullCandidates, PullCandidate{named, false, r})
121 }
122
123 func (r *Resolved) addCandidateToRecord(named reference.Named) {
124 r.PullCandidates = append(r.PullCandidates, PullCandidate{named, true, r})
125 }
126
127 // Allows to reason over pull errors and add some context information.
128 // Used in (*Resolved).WrapPullError.
129 type rationale int
130
131 const (
132 // No additional context.
133 rationaleNone rationale = iota
134 // Resolved value is a short-name alias.
135 rationaleAlias
136 // Resolved value has been completed with an Unqualified Search Registry.
137 rationaleUSR
138 // Resolved value has been selected by the user (via the prompt).
139 rationaleUserSelection
140 )
141
142 // Description returns a human-readable description about the resolution
143 // process (e.g., short-name alias, unqualified-search registries, etc.).
144 // It is meant to be printed before attempting to pull the pull candidates
145 // to make the short-name resolution more transparent to user.
146 //
147 // If the returned string is empty, it is not meant to be printed.
148 func (r *Resolved) Description() string {
149 switch r.rationale {
150 case rationaleAlias:
151 return fmt.Sprintf("Resolved short name %q to a recorded short-name alias (origin: %s)", r.userInput, r.originDescription)
152 case rationaleUSR:
153 return fmt.Sprintf("Completed short name %q with unqualified-search registries (origin: %s)", r.userInput, r.originDescription)
154 case rationaleUserSelection, rationaleNone:
155 fallthrough
156 default:
157 return ""
158 }
159 }
160
161 // FormatPullErrors is a convenience function to format errors that occurred
162 // while trying to pull all of the resolved pull candidates.
163 //
164 // Note that nil is returned if len(pullErrors) == 0. Otherwise, the amount of
165 // pull errors must equal the amount of pull candidates.
166 func (r *Resolved) FormatPullErrors(pullErrors []error) error {
167 if len(pullErrors) >= 0 && len(pullErrors) != len(r.PullCandidates) {
168 pullErrors = append(pullErrors,
169 errors.Errorf("internal error: expected %d instead of %d errors for %d pull candidates",
170 len(r.PullCandidates), len(pullErrors), len(r.PullCandidates)))
171 }
172
173 switch len(pullErrors) {
174 case 0:
175 return nil
176 case 1:
177 return pullErrors[0]
178 default:
179 var sb strings.Builder
180 sb.WriteString(fmt.Sprintf("%d errors occurred while pulling:", len(pullErrors)))
181 for _, e := range pullErrors {
182 sb.WriteString("\n * ")
183 sb.WriteString(e.Error())
184 }
185 return errors.New(sb.String())
186 }
187 }
188
189 // PullCandidate is a resolved name. Once the Value has been used
190 // successfully, users MUST call `(*PullCandidate).Record(..)` to possibly
191 // record it as a new short-name alias.
192 type PullCandidate struct {
193 // Fully-qualified reference with tag or digest.
194 Value reference.Named
195 // Control whether to record it permanently as an alias.
196 record bool
197
198 // Backwards pointer to the Resolved "parent".
199 resolved *Resolved
200 }
201
202 // Record may store a short-name alias for the PullCandidate.
203 func (c *PullCandidate) Record() error {
204 if !c.record {
205 return nil
206 }
207
208 // Strip off tags/digests from name/value.
209 name := reference.TrimNamed(c.resolved.userInput)
210 value := reference.TrimNamed(c.Value)
211
212 if err := Add(c.resolved.systemContext, name.String(), value); err != nil {
213 return errors.Wrapf(err, "error recording short-name alias (%q=%q)", c.resolved.userInput, c.Value)
214 }
215 return nil
216 }
217
218 // Resolve resolves the specified name to either one or more fully-qualified
219 // image references that the short name may be *pulled* from. If the specified
220 // name is already a fully-qualified reference (i.e., not a short name), it is
221 // returned as is. In case, it's a short name, it's resolved according to the
222 // ShortNameMode in the SystemContext (if specified) or in the registries.conf.
223 //
224 // Note that tags and digests are stripped from the specified name before
225 // looking up an alias. Stripped off tags and digests are later on appended to
226 // all candidates. If neither tag nor digest is specified, candidates are
227 // normalized with the "latest" tag. PullCandidates in the returned value may
228 // be empty if there is no matching alias and no unqualified-search registries
229 // are configured.
230 //
231 // Note that callers *must* call `(PullCandidate).Record` after a returned
232 // item has been pulled successfully; this callback will record a new
233 // short-name alias (depending on the specified short-name mode).
234 //
235 // Furthermore, before attempting to pull callers *should* call
236 // `(Resolved).Description` and afterwards use
237 // `(Resolved).FormatPullErrors` in case of pull errors.
238 func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) {
239 resolved := &Resolved{}
240
241 // Create a copy of the system context to make it usable beyond this
242 // function call.
243 var sys *types.SystemContext
244 if ctx != nil {
245 sys = &(*ctx)
246 }
247 resolved.systemContext = ctx
248
249 // Detect which mode we're running in.
250 mode, err := sysregistriesv2.GetShortNameMode(sys)
251 if err != nil {
252 return nil, err
253 }
254
255 // Sanity check the short-name mode.
256 switch mode {
257 case types.ShortNameModeDisabled, types.ShortNameModePermissive, types.ShortNameModeEnforcing:
258 // We're good.
259 default:
260 return nil, errors.Errorf("unsupported short-name mode (%v)", mode)
261 }
262
263 isShort, shortRef, err := parseUnnormalizedShortName(name)
264 if err != nil {
265 return nil, err
266 }
267 if !isShort { // no short name
268 named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed
269 resolved.addCandidate(named)
270 return resolved, nil
271 }
272
273 // Strip off the tag to normalize the short name for looking it up in
274 // the config files.
275 isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
276 resolved.userInput = shortNameRepo
277
278 // If there's already an alias, use it.
279 namedAlias, aliasOriginDescription, err := sysregistriesv2.ResolveShortNameAlias(sys, shortNameRepo.String())
280 if err != nil {
281 return nil, err
282 }
283
284 // Always use an alias if present.
285 if namedAlias != nil {
286 if isTagged {
287 namedAlias, err = reference.WithTag(namedAlias, tag)
288 if err != nil {
289 return nil, err
290 }
291 }
292 if isDigested {
293 namedAlias, err = reference.WithDigest(namedAlias, digest)
294 if err != nil {
295 return nil, err
296 }
297 }
298 // Make sure to add ":latest" if needed
299 namedAlias = reference.TagNameOnly(namedAlias)
300
301 resolved.addCandidate(namedAlias)
302 resolved.rationale = rationaleAlias
303 resolved.originDescription = aliasOriginDescription
304 return resolved, nil
305 }
306
307 resolved.rationale = rationaleUSR
308
309 // Query the registry for unqualified-search registries.
310 unqualifiedSearchRegistries, usrConfig, err := sysregistriesv2.UnqualifiedSearchRegistriesWithOrigin(sys)
311 if err != nil {
312 return nil, err
313 }
314 resolved.originDescription = usrConfig
315
316 for _, reg := range unqualifiedSearchRegistries {
317 named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
318 if err != nil {
319 return nil, errors.Wrapf(err, "error creating reference with unqualified-search registry %q", reg)
320 }
321 // Make sure to add ":latest" if needed
322 named = reference.TagNameOnly(named)
323
324 resolved.addCandidate(named)
325 }
326
327 // If we're running in disabled, return the candidates without
328 // prompting (and without recording).
329 if mode == types.ShortNameModeDisabled {
330 return resolved, nil
331 }
332
333 // If we have only one candidate, there's no ambiguity. In case of an
334 // empty candidate slices, callers can implement custom logic or raise
335 // an error.
336 if len(resolved.PullCandidates) <= 1 {
337 return resolved, nil
338 }
339
340 // If we don't have a TTY, act according to the mode.
341 if !terminal.IsTerminal(int(os.Stdout.Fd())) || !terminal.IsTerminal(int(os.Stdin.Fd())) {
342 switch mode {
343 case types.ShortNameModePermissive:
344 // Permissive falls back to using all candidates.
345 return resolved, nil
346 case types.ShortNameModeEnforcing:
347 // Enforcing errors out without a prompt.
348 return nil, errors.New("short-name resolution enforced but cannot prompt without a TTY")
349 default:
350 // We should not end up here.
351 return nil, errors.Errorf("unexpected short-name mode (%v) during resolution", mode)
352 }
353 }
354
355 // We have a TTY, and can prompt the user with a selection of all
356 // possible candidates.
357 strCandidates := []string{}
358 for _, candidate := range resolved.PullCandidates {
359 strCandidates = append(strCandidates, candidate.Value.String())
360 }
361 prompt := promptui.Select{
362 Label: "Please select an image",
363 Items: strCandidates,
364 HideHelp: true, // do not show navigation help
365 }
366
367 _, selection, err := prompt.Run()
368 if err != nil {
369 return nil, err
370 }
371
372 named, err := reference.ParseNormalizedNamed(selection)
373 if err != nil {
374 return nil, errors.Wrapf(err, "selection %q is not a valid reference", selection)
375 }
376
377 resolved.PullCandidates = nil
378 resolved.addCandidateToRecord(named)
379 resolved.rationale = rationaleUserSelection
380
381 return resolved, nil
382 }
383
384 // ResolveLocally resolves the specified name to either one or more local
385 // images. If the specified name is already a fully-qualified reference (i.e.,
386 // not a short name), it is returned as is. In case, it's a short name, the
387 // returned slice of named references looks as follows:
388 //
389 // 1) If present, the short-name alias
390 // 2) "localhost/" as used by many container engines such as Podman and Buildah
391 // 3) Unqualified-search registries from the registries.conf files
392 //
393 // Note that tags and digests are stripped from the specified name before
394 // looking up an alias. Stripped off tags and digests are later on appended to
395 // all candidates. If neither tag nor digest is specified, candidates are
396 // normalized with the "latest" tag. The returned slice contains at least one
397 // item.
398 func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, error) {
399 isShort, shortRef, err := parseUnnormalizedShortName(name)
400 if err != nil {
401 return nil, err
402 }
403 if !isShort { // no short name
404 named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed
405 return []reference.Named{named}, nil
406 }
407
408 var candidates []reference.Named
409
410 // Strip off the tag to normalize the short name for looking it up in
411 // the config files.
412 isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
413
414 // If there's already an alias, use it.
415 namedAlias, _, err := sysregistriesv2.ResolveShortNameAlias(ctx, shortNameRepo.String())
416 if err != nil {
417 return nil, err
418 }
419 if namedAlias != nil {
420 if isTagged {
421 namedAlias, err = reference.WithTag(namedAlias, tag)
422 if err != nil {
423 return nil, err
424 }
425 }
426 if isDigested {
427 namedAlias, err = reference.WithDigest(namedAlias, digest)
428 if err != nil {
429 return nil, err
430 }
431 }
432 // Make sure to add ":latest" if needed
433 namedAlias = reference.TagNameOnly(namedAlias)
434
435 candidates = append(candidates, namedAlias)
436 }
437
438 // Query the registry for unqualified-search registries.
439 unqualifiedSearchRegistries, err := sysregistriesv2.UnqualifiedSearchRegistries(ctx)
440 if err != nil {
441 return nil, err
442 }
443
444 // Note that "localhost" has precedence over the unqualified-search registries.
445 for _, reg := range append([]string{"localhost"}, unqualifiedSearchRegistries...) {
446 named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
447 if err != nil {
448 return nil, errors.Wrapf(err, "error creating reference with unqualified-search registry %q", reg)
449 }
450 // Make sure to add ":latest" if needed
451 named = reference.TagNameOnly(named)
452
453 candidates = append(candidates, named)
454 }
455
456 return candidates, nil
457 }
0 package shortnames
1
2 import (
3 "io/ioutil"
4 "os"
5 "testing"
6
7 "github.com/containers/image/v5/docker/reference"
8 "github.com/containers/image/v5/pkg/sysregistriesv2"
9 "github.com/containers/image/v5/types"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12 )
13
14 func TestIsShortName(t *testing.T) {
15 tests := []struct {
16 input string
17 parseUnnormalizedShortName bool
18 mustFail bool
19 }{
20 // SHORT NAMES
21 {"fedora", true, false},
22 {"fedora:latest", true, false},
23 {"library/fedora", true, false},
24 {"library/fedora:latest", true, false},
25 {"busybox@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false},
26 {"busybox:latest@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false},
27 // !SHORT NAMES
28 {"quay.io/fedora", false, false},
29 {"docker.io/fedora", false, false},
30 {"docker.io/library/fedora:latest", false, false},
31 {"localhost/fedora", false, false},
32 {"localhost:5000/fedora:latest", false, false},
33 {"example.foo.this.may.be.garbage.but.maybe.not:1234/fedora:latest", false, false},
34 {"docker.io/library/busybox@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false},
35 {"docker.io/library/busybox:latest@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false},
36 {"docker.io/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false},
37 // INVALID NAMES
38 {"", false, true},
39 {"$$$", false, true},
40 {"::", false, true},
41 {"docker://quay.io/library/foo:bar", false, true},
42 {" ", false, true},
43 }
44
45 for _, test := range tests {
46 res, _, err := parseUnnormalizedShortName(test.input)
47 if test.mustFail {
48 require.Error(t, err, "%q should not be parseable")
49 continue
50 }
51 require.NoError(t, err, "%q should be parseable")
52 assert.Equal(t, test.parseUnnormalizedShortName, res, "%q", test.input)
53 }
54 }
55
56 func TestSplitUserInput(t *testing.T) {
57 tests := []struct {
58 input string
59 repo string
60 isTagged bool
61 isDigested bool
62 }{
63 // Neither tags nor digests
64 {"fedora", "fedora", false, false},
65 {"repo/fedora", "repo/fedora", false, false},
66 {"registry.com/fedora", "registry.com/fedora", false, false},
67 // Tags
68 {"fedora:tag", "fedora", true, false},
69 {"repo/fedora:tag", "repo/fedora", true, false},
70 {"registry.com/fedora:latest", "registry.com/fedora", true, false},
71 // Digests
72 {"fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "fedora", false, true},
73 {"repo/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "repo/fedora", false, true},
74 {"registry.com/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/fedora", false, true},
75 }
76
77 for _, test := range tests {
78 _, ref, err := parseUnnormalizedShortName(test.input)
79 require.NoError(t, err, "%v", test)
80
81 isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(ref)
82 require.NotNil(t, shortNameRepo)
83 normalized := shortNameRepo.String()
84 assert.Equal(t, test.repo, normalized)
85 assert.Equal(t, test.isTagged, isTagged)
86 assert.Equal(t, test.isDigested, isDigested)
87 if isTagged {
88 normalized = normalized + ":" + tag
89 } else if isDigested {
90 normalized = normalized + "@" + digest.String()
91 }
92 assert.Equal(t, test.input, normalized)
93 }
94 }
95
96 func TestResolve(t *testing.T) {
97 tmp, err := ioutil.TempFile("", "aliases.conf")
98 require.NoError(t, err)
99 defer os.Remove(tmp.Name())
100
101 sys := &types.SystemContext{
102 SystemRegistriesConfPath: "testdata/aliases.conf",
103 SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
104 UserShortNameAliasConfPath: tmp.Name(),
105 }
106
107 _, err = sysregistriesv2.TryUpdatingCache(sys)
108 require.NoError(t, err)
109
110 tests := []struct {
111 name, value string
112 }{
113 {"docker", "docker.io/library/foo:latest"},
114 {"docker:tag", "docker.io/library/foo:tag"},
115 {
116 "docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
117 "docker.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
118 },
119 {"quay/foo", "quay.io/library/foo:latest"},
120 {"quay/foo:tag", "quay.io/library/foo:tag"},
121 {
122 "quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
123 "quay.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
124 },
125 {"example", "example.com/library/foo:latest"},
126 {"example:tag", "example.com/library/foo:tag"},
127 {
128 "example@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
129 "example.com/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
130 },
131 }
132
133 // All of them should resolve correctly.
134 for _, test := range tests {
135 resolved, err := Resolve(sys, test.name)
136 require.NoError(t, err, "%v", test)
137 require.NotNil(t, resolved)
138 require.Len(t, resolved.PullCandidates, 1)
139 assert.Equal(t, test.value, resolved.PullCandidates[0].Value.String())
140 assert.False(t, resolved.PullCandidates[0].record)
141 }
142
143 // Non-existent should return an empty slice as no search registries
144 // are configured in the config.
145 resolved, err := Resolve(sys, "dontexist")
146 require.NoError(t, err)
147 require.NotNil(t, resolved)
148 require.Len(t, resolved.PullCandidates, 0)
149
150 // An empty name is not valid.
151 resolved, err = Resolve(sys, "")
152 require.Error(t, err)
153 require.Nil(t, resolved)
154
155 // Invalid input.
156 resolved, err = Resolve(sys, "Invalid#$")
157 require.Error(t, err)
158 require.Nil(t, resolved)
159
160 // Fully-qualified input will be returned as is.
161 resolved, err = Resolve(sys, "quay.io/repo/fedora")
162 require.NoError(t, err)
163 require.NotNil(t, resolved)
164 require.Len(t, resolved.PullCandidates, 1)
165 assert.Equal(t, "quay.io/repo/fedora:latest", resolved.PullCandidates[0].Value.String())
166 assert.False(t, resolved.PullCandidates[0].record)
167 }
168
169 func toNamed(t *testing.T, input string, trim bool) reference.Named {
170 ref, err := reference.Parse(input)
171 require.NoError(t, err)
172 named := ref.(reference.Named)
173 require.NotNil(t, named)
174
175 if trim {
176 named = reference.TrimNamed(named)
177 }
178
179 return named
180 }
181
182 func addAlias(t *testing.T, sys *types.SystemContext, name string, value string, mustFail bool) {
183 namedValue := toNamed(t, value, false)
184
185 if mustFail {
186 require.Error(t, Add(sys, name, namedValue))
187 } else {
188 require.NoError(t, Add(sys, name, namedValue))
189 }
190 }
191
192 func removeAlias(t *testing.T, sys *types.SystemContext, name string, mustFail bool, trim bool) {
193 namedName := toNamed(t, name, trim)
194
195 if mustFail {
196 require.Error(t, Remove(sys, namedName.String()))
197 } else {
198 require.NoError(t, Remove(sys, namedName.String()))
199 }
200 }
201
202 func TestResolveWithDropInConfigs(t *testing.T) {
203 tmp, err := ioutil.TempFile("", "aliases.conf")
204 require.NoError(t, err)
205 defer os.Remove(tmp.Name())
206
207 sys := &types.SystemContext{
208 SystemRegistriesConfPath: "testdata/aliases.conf",
209 SystemRegistriesConfDirPath: "testdata/registries.conf.d",
210 UserShortNameAliasConfPath: tmp.Name(),
211 }
212
213 _, err = sysregistriesv2.TryUpdatingCache(sys)
214 require.NoError(t, err)
215
216 tests := []struct {
217 name, value string
218 }{
219 {"docker", "docker.io/library/config1:latest"}, // overriden by config1
220 {"docker:tag", "docker.io/library/config1:tag"},
221 {
222 "docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
223 "docker.io/library/config1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
224 },
225 {"quay/foo", "quay.io/library/foo:latest"},
226 {"quay/foo:tag", "quay.io/library/foo:tag"},
227 {
228 "quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
229 "quay.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
230 },
231 {"config1", "config1.com/image:latest"},
232 {"config1:tag", "config1.com/image:tag"},
233 {
234 "config1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
235 "config1.com/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
236 },
237 {"barz", "barz.com/config2:latest"}, // from config1, overridden by config2
238 {"barz:tag", "barz.com/config2:tag"},
239 {
240 "barz@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
241 "barz.com/config2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
242 },
243 {"added1", "aliases.conf/added1:latest"}, // from Add()
244 {"added1:tag", "aliases.conf/added1:tag"},
245 {
246 "added1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
247 "aliases.conf/added1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
248 },
249 {"added2", "aliases.conf/added2:latest"}, // from Add()
250 {"added2:tag", "aliases.conf/added2:tag"},
251 {
252 "added2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
253 "aliases.conf/added2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
254 },
255 {"added3", "aliases.conf/added3:latest"}, // from Add()
256 {"added3:tag", "aliases.conf/added3:tag"},
257 {
258 "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
259 "aliases.conf/added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
260 },
261 }
262
263 addAlias(t, sys, "added1", "aliases.conf/added1", false)
264 addAlias(t, sys, "added2", "aliases.conf/added2", false)
265 addAlias(t, sys, "added3", "aliases.conf/added3", false)
266
267 // Tags/digests are invalid!
268 addAlias(t, sys, "added3", "aliases.conf/added3:tag", true)
269 addAlias(t, sys, "added3:tag", "aliases.conf/added3", true)
270 addAlias(t, sys, "added3", "aliases.conf/added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true)
271 addAlias(t, sys, "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "aliases.conf/added3", true)
272
273 // All of them should resolve correctly.
274 for _, test := range tests {
275 resolved, err := Resolve(sys, test.name)
276 require.NoError(t, err)
277 require.NotNil(t, resolved)
278 require.Len(t, resolved.PullCandidates, 1)
279 assert.Equal(t, test.value, resolved.PullCandidates[0].Value.String())
280 assert.False(t, resolved.PullCandidates[0].record)
281 }
282
283 // config1 sets one search registry.
284 resolved, err := Resolve(sys, "dontexist")
285 require.NoError(t, err)
286 require.NotNil(t, resolved)
287 require.Len(t, resolved.PullCandidates, 1)
288 assert.Equal(t, "example-overwrite.com/dontexist:latest", resolved.PullCandidates[0].Value.String())
289
290 // An empty name is not valid.
291 resolved, err = Resolve(sys, "")
292 require.Error(t, err)
293 require.Nil(t, resolved)
294
295 // Invalid input.
296 resolved, err = Resolve(sys, "Invalid#$")
297 require.Error(t, err)
298 require.Nil(t, resolved)
299
300 // Fully-qualified input will be returned as is.
301 resolved, err = Resolve(sys, "quay.io/repo/fedora")
302 require.NoError(t, err)
303 require.NotNil(t, resolved)
304 require.Len(t, resolved.PullCandidates, 1)
305 assert.Equal(t, "quay.io/repo/fedora:latest", resolved.PullCandidates[0].Value.String())
306 assert.False(t, resolved.PullCandidates[0].record)
307
308 resolved, err = Resolve(sys, "localhost/repo/fedora:sometag")
309 require.NoError(t, err)
310 require.NotNil(t, resolved)
311 require.Len(t, resolved.PullCandidates, 1)
312 assert.Equal(t, "localhost/repo/fedora:sometag", resolved.PullCandidates[0].Value.String())
313 assert.False(t, resolved.PullCandidates[0].record)
314
315 // Now test removal.
316
317 // Stored in aliases.conf, so we can remove it.
318 removeAlias(t, sys, "added1", false, false)
319 removeAlias(t, sys, "added2", false, false)
320 removeAlias(t, sys, "added3", false, false)
321 removeAlias(t, sys, "added2:tag", true, false)
322 removeAlias(t, sys, "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false)
323
324 // Doesn't exist -> error.
325 removeAlias(t, sys, "added1", true, false)
326 removeAlias(t, sys, "added2", true, false)
327 removeAlias(t, sys, "added3", true, false)
328
329 // Cannot remove entries from registries.conf files -> error.
330 removeAlias(t, sys, "docker", true, false)
331 removeAlias(t, sys, "docker", true, false)
332 removeAlias(t, sys, "docker", true, false)
333 }
334
335 func TestResolveWithVaryingShortNameModes(t *testing.T) {
336 tmp, err := ioutil.TempFile("", "aliases.conf")
337 require.NoError(t, err)
338 defer os.Remove(tmp.Name())
339
340 tests := []struct {
341 confPath string
342 mode types.ShortNameMode
343 name string
344 mustFail bool
345 numAliases int
346 }{
347 // Invalid -> error
348 {"testdata/no-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0},
349 {"testdata/one-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0},
350 {"testdata/two-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0},
351 // Permisive + match -> return alias
352 {"testdata/no-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1},
353 {"testdata/one-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1},
354 {"testdata/two-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1},
355 // Permisive + no match -> search (no tty)
356 {"testdata/no-reg.conf", types.ShortNameModePermissive, "donotexist", false, 0},
357 {"testdata/one-reg.conf", types.ShortNameModePermissive, "donotexist", false, 1},
358 {"testdata/two-reg.conf", types.ShortNameModePermissive, "donotexist", false, 2},
359 // Disabled + match -> return alias
360 {"testdata/no-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1},
361 {"testdata/one-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1},
362 {"testdata/two-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1},
363 // Disabled + no match -> search
364 {"testdata/no-reg.conf", types.ShortNameModeDisabled, "donotexist", false, 0},
365 {"testdata/one-reg.conf", types.ShortNameModeDisabled, "donotexist", false, 1},
366 {"testdata/two-reg.conf", types.ShortNameModeDisabled, "donotexist", false, 2},
367 // Enforcing + match -> return alias
368 {"testdata/no-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1},
369 {"testdata/one-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1},
370 {"testdata/two-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1},
371 // Enforcing + no match -> error if search regs > 1 and no tty
372 {"testdata/no-reg.conf", types.ShortNameModeEnforcing, "donotexist", false, 0},
373 {"testdata/one-reg.conf", types.ShortNameModeEnforcing, "donotexist", false, 1},
374 {"testdata/two-reg.conf", types.ShortNameModeEnforcing, "donotexist", true, 0},
375 }
376
377 for _, test := range tests {
378 sys := &types.SystemContext{
379 SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
380 UserShortNameAliasConfPath: tmp.Name(),
381 // From test
382 SystemRegistriesConfPath: test.confPath,
383 ShortNameMode: &test.mode,
384 }
385
386 _, err := sysregistriesv2.TryUpdatingCache(sys)
387 require.NoError(t, err)
388
389 resolved, err := Resolve(sys, test.name)
390 if test.mustFail {
391 require.Error(t, err, "%v", test)
392 continue
393 }
394 require.NoError(t, err, "%v", test)
395 require.NotNil(t, resolved)
396 require.Len(t, resolved.PullCandidates, test.numAliases, "%v", test)
397 }
398 }
399
400 func TestResolveAndRecord(t *testing.T) {
401 tmp, err := ioutil.TempFile("", "aliases.conf")
402 require.NoError(t, err)
403 defer os.Remove(tmp.Name())
404
405 sys := &types.SystemContext{
406 SystemRegistriesConfPath: "testdata/two-reg.conf",
407 SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
408 UserShortNameAliasConfPath: tmp.Name(),
409 }
410
411 _, err = sysregistriesv2.TryUpdatingCache(sys)
412 require.NoError(t, err)
413
414 tests := []struct {
415 name string
416 expected []string
417 }{
418 // No alias -> USRs
419 {"foo", []string{"quay.io/foo:latest", "registry.com/foo:latest"}},
420 {"foo:tag", []string{"quay.io/foo:tag", "registry.com/foo:tag"}},
421 {"foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}},
422 {"repo/foo", []string{"quay.io/repo/foo:latest", "registry.com/repo/foo:latest"}},
423 {"repo/foo:tag", []string{"quay.io/repo/foo:tag", "registry.com/repo/foo:tag"}},
424 {"repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}},
425 // Alias
426 {"repo/image", []string{"quay.io/repo/image:latest"}},
427 {"repo/image:tag", []string{"quay.io/repo/image:tag"}},
428 {"repo/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/repo/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}},
429 }
430 for _, test := range tests {
431 resolved, err := Resolve(sys, test.name)
432 require.NoError(t, err, "%v", test)
433 require.NotNil(t, resolved)
434 require.Len(t, resolved.PullCandidates, len(test.expected), "%v", test)
435
436 for i, candidate := range resolved.PullCandidates {
437 require.Equal(t, test.expected[i], candidate.Value.String(), "%v", test)
438
439 require.False(t, candidate.record, "%v", test)
440 candidate.record = true // make sure we can actually record
441
442 // Record the alias, look it up another time and make
443 // sure there's only one match (i.e., the new alias)
444 // and that is has the expected value.
445 require.NoError(t, candidate.Record())
446
447 newResolved, err := Resolve(sys, test.name)
448 require.NoError(t, err, "%v", test)
449 require.Len(t, newResolved.PullCandidates, 1, "%v", test)
450 require.Equal(t, candidate.Value.String(), newResolved.PullCandidates[0].Value.String(), "%v", test)
451
452 // Now remove the alias again.
453 removeAlias(t, sys, test.name, false, true)
454
455 // Now set recording to false and try recording again.
456 candidate.record = false
457 require.NoError(t, candidate.Record())
458 removeAlias(t, sys, test.name, true, true) // must error out now
459 }
460 }
461 }
462
463 func TestResolveLocally(t *testing.T) {
464 tmp, err := ioutil.TempFile("", "aliases.conf")
465 require.NoError(t, err)
466 defer os.Remove(tmp.Name())
467
468 sys := &types.SystemContext{
469 SystemRegistriesConfPath: "testdata/two-reg.conf",
470 SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
471 UserShortNameAliasConfPath: tmp.Name(),
472 }
473
474 aliases, err := ResolveLocally(sys, "repo/image") // alias match
475 require.NoError(t, err)
476 require.Len(t, aliases, 4) // alias + localhost + two regs
477 assert.Equal(t, "quay.io/repo/image:latest", aliases[0].String()) // alias
478 assert.Equal(t, "localhost/repo/image:latest", aliases[1].String()) // localhost
479 assert.Equal(t, "quay.io/repo/image:latest", aliases[2].String()) // registry 0
480 assert.Equal(t, "registry.com/repo/image:latest", aliases[3].String()) // registry 0
481
482 aliases, err = ResolveLocally(sys, "foo") // no alias match
483 require.NoError(t, err)
484 require.Len(t, aliases, 3) // localhost + two regs
485 assert.Equal(t, "localhost/foo:latest", aliases[0].String()) // localhost
486 assert.Equal(t, "quay.io/foo:latest", aliases[1].String()) // registry 0
487 assert.Equal(t, "registry.com/foo:latest", aliases[2].String()) // registry 0
488
489 aliases, err = ResolveLocally(sys, "foo:tag") // no alias match tagged
490 require.NoError(t, err)
491 require.Len(t, aliases, 3) // localhost + two regs
492 assert.Equal(t, "localhost/foo:tag", aliases[0].String()) // localhost
493 assert.Equal(t, "quay.io/foo:tag", aliases[1].String()) // registry 0
494 assert.Equal(t, "registry.com/foo:tag", aliases[2].String()) // registry 0
495
496 aliases, err = ResolveLocally(sys, "foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a") // no alias match digested
497 require.NoError(t, err)
498 require.Len(t, aliases, 3) // localhost + two regs
499 assert.Equal(t, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[0].String()) // localhost
500 assert.Equal(t, "quay.io/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[1].String()) // registry 0
501 assert.Equal(t, "registry.com/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[2].String()) // registry 0
502
503 aliases, err = ResolveLocally(sys, "localhost/foo") // localhost
504 require.NoError(t, err)
505 require.Len(t, aliases, 1)
506 assert.Equal(t, "localhost/foo:latest", aliases[0].String())
507
508 aliases, err = ResolveLocally(sys, "localhost/foo:tag") // localhost + tag
509 require.NoError(t, err)
510 require.Len(t, aliases, 1)
511 assert.Equal(t, "localhost/foo:tag", aliases[0].String())
512
513 aliases, err = ResolveLocally(sys, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a") // localhost + digest
514 require.NoError(t, err)
515 require.Len(t, aliases, 1)
516 assert.Equal(t, "localhost/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", aliases[0].String())
517 }
0 short-name-mode="enforcing"
1
2 [aliases]
3 docker="docker.io/library/foo"
4 "quay/foo"="quay.io/library/foo"
5 example="example.com/library/foo"
6 empty=""
0 [aliases]
1 "repo/image"="quay.io/repo/image"
0 unqualified-search-registries=["quay.io"]
1
2 [aliases]
3 "repo/image"="quay.io/repo/image"
0 unqualified-search-registries = ["example-overwrite.com"]
1
2 [[registry]]
3 location = "1.com"
4
5 [aliases]
6 docker="docker.io/library/config1"
7 config1="config1.com/image"
8 barz="barz.com/image/config1"
0 short-name-mode="permissive"
1
2 [[registry]]
3 location = "2.com"
4
5 [[registry]]
6 location = "base.com"
7 blocked = true
8
9 [aliases]
10 config2="config2.com/image"
11 barz="barz.com/config2"
12 added3="xxx.com/image"
13 example=""
0 unqualified-search-registries = ["ignore-example-overwrite.com"]
1
2 [[registry]]
3 location = "ignore-me-because-i-have-a-wrong-suffix.com"
4
5 [aliases]
6 ignore="me because i have a wrong suffix"
0 unqualified-search-registries=["quay.io", "registry.com"]
1
2 [aliases]
3 "repo/image"="quay.io/repo/image"
646646
647647 // GetShortNameMode returns the configured types.ShortNameMode.
648648 func GetShortNameMode(ctx *types.SystemContext) (types.ShortNameMode, error) {
649 if ctx != nil && ctx.ShortNameMode != nil {
650 return *ctx.ShortNameMode, nil
651 }
649652 config, err := getConfig(ctx)
650653 if err != nil {
651654 return -1, err
499499 // Use all configured unqualified-search registries without prompting
500500 // the user.
501501 ShortNameModeDisabled
502 // If stdout is a TTY, prompt the user to select a configured
502 // If stdout and stdin are a TTY, prompt the user to select a configured
503503 // unqualified-search registry. Otherwise, use all configured
504504 // unqualified-search registries.
505 //
506 // Note that if only one unqualified-search registry is set, it will be
507 // used without prompting.
505508 ShortNameModePermissive
506 // Always prompt the user to select a configured unqualified-serach
507 // registry. Throw an error if stdout is not a TTY as prompting
508 // isn't possible.
509 // Always prompt the user to select a configured unqualified-search
510 // registry. Throw an error if stdout or stdin is not a TTY as
511 // prompting isn't possible.
512 //
513 // Note that if only one unqualified-search registry is set, it will be
514 // used without prompting.
509515 ShortNameModeEnforcing
510516 )
511517
534540 SystemRegistriesConfDirPath string
535541 // Path to the user-specific short-names configuration file
536542 UserShortNameAliasConfPath string
543 // If set, short-name resolution in pkg/shortnames must follow the specified mode
544 ShortNameMode *ShortNameMode
537545 // If not "", overrides the default path for the authentication file, but only new format files
538546 AuthFilePath string
539547 // if not "", overrides the default path for the authentication file, but with the legacy format;