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
100 | 100 | |
101 | 101 | *Note*: Redirection and mirrors are currently processed only when reading images, not when pushing |
102 | 102 | 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. | |
103 | 172 | |
104 | 173 | #### Normalization of docker.io references |
105 | 174 |
19 | 19 | github.com/imdario/mergo v0.3.11 |
20 | 20 | github.com/klauspost/compress v1.11.2 |
21 | 21 | github.com/klauspost/pgzip v1.2.5 |
22 | github.com/manifoldco/promptui v0.8.0 | |
22 | 23 | github.com/morikuni/aec v1.0.0 // indirect |
23 | 24 | github.com/mtrmac/gpgme v0.1.2 |
24 | 25 | github.com/opencontainers/go-digest v1.0.0 |
23 | 23 | github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= |
24 | 24 | github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= |
25 | 25 | 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= | |
26 | 30 | github.com/cilium/ebpf v0.0.0-20200507155900-a9f01edf17e3/go.mod h1:XT+cAw5wfvsodedcijoh1l9cf7v1x9FlFB/3VmF/O8s= |
27 | 31 | github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= |
28 | 32 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= |
41 | 45 | github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= |
42 | 46 | github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= |
43 | 47 | 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= | |
44 | 50 | github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE= |
45 | 51 | github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= |
46 | 52 | github.com/containers/ocicrypt v1.0.1 h1:EToign46OSLTFWnb2oNj9RG3XDnkOX8r28ZIXUuk5Pc= |
160 | 166 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= |
161 | 167 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= |
162 | 168 | 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= | |
163 | 171 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= |
164 | 172 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= |
165 | 173 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= |
204 | 212 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
205 | 213 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= |
206 | 214 | 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= | |
207 | 223 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= |
208 | 224 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= |
209 | 225 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= |
403 | 419 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
404 | 420 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
405 | 421 | 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= | |
406 | 423 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
407 | 424 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
408 | 425 | 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 | 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" |
646 | 646 | |
647 | 647 | // GetShortNameMode returns the configured types.ShortNameMode. |
648 | 648 | func GetShortNameMode(ctx *types.SystemContext) (types.ShortNameMode, error) { |
649 | if ctx != nil && ctx.ShortNameMode != nil { | |
650 | return *ctx.ShortNameMode, nil | |
651 | } | |
649 | 652 | config, err := getConfig(ctx) |
650 | 653 | if err != nil { |
651 | 654 | return -1, err |
499 | 499 | // Use all configured unqualified-search registries without prompting |
500 | 500 | // the user. |
501 | 501 | 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 | |
503 | 503 | // unqualified-search registry. Otherwise, use all configured |
504 | 504 | // unqualified-search registries. |
505 | // | |
506 | // Note that if only one unqualified-search registry is set, it will be | |
507 | // used without prompting. | |
505 | 508 | 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. | |
509 | 515 | ShortNameModeEnforcing |
510 | 516 | ) |
511 | 517 | |
534 | 540 | SystemRegistriesConfDirPath string |
535 | 541 | // Path to the user-specific short-names configuration file |
536 | 542 | UserShortNameAliasConfPath string |
543 | // If set, short-name resolution in pkg/shortnames must follow the specified mode | |
544 | ShortNameMode *ShortNameMode | |
537 | 545 | // If not "", overrides the default path for the authentication file, but only new format files |
538 | 546 | AuthFilePath string |
539 | 547 | // if not "", overrides the default path for the authentication file, but with the legacy format; |