blob: 2f66cca87a3c1d26c1ace8cd5e26a8cd8ee34733 [file] [log] [blame]
David K. Bainbridge215e0242017-09-05 23:18:24 -07001// Package reference provides a general type to represent any way of referencing images within the registry.
2// Its main purpose is to abstract tags and digests (content-addressable hash).
3//
4// Grammar
5//
6// reference := name [ ":" tag ] [ "@" digest ]
7// name := [domain '/'] path-component ['/' path-component]*
8// domain := domain-component ['.' domain-component]* [':' port-number]
9// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
10// port-number := /[0-9]+/
11// path-component := alpha-numeric [separator alpha-numeric]*
12// alpha-numeric := /[a-z0-9]+/
13// separator := /[_.]|__|[-]*/
14//
15// tag := /[\w][\w.-]{0,127}/
16//
17// digest := digest-algorithm ":" digest-hex
18// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
19// digest-algorithm-separator := /[+.-_]/
20// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
21// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
22//
23// identifier := /[a-f0-9]{64}/
24// short-identifier := /[a-f0-9]{6,64}/
25package reference
26
27import (
28 "errors"
29 "fmt"
30 "strings"
31
32 "github.com/opencontainers/go-digest"
33)
34
35const (
36 // NameTotalLengthMax is the maximum total number of characters in a repository name.
37 NameTotalLengthMax = 255
38)
39
40var (
41 // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
42 ErrReferenceInvalidFormat = errors.New("invalid reference format")
43
44 // ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
45 ErrTagInvalidFormat = errors.New("invalid tag format")
46
47 // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
48 ErrDigestInvalidFormat = errors.New("invalid digest format")
49
50 // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
51 ErrNameContainsUppercase = errors.New("repository name must be lowercase")
52
53 // ErrNameEmpty is returned for empty, invalid repository names.
54 ErrNameEmpty = errors.New("repository name must have at least one component")
55
56 // ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
57 ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
58
59 // ErrNameNotCanonical is returned when a name is not canonical.
60 ErrNameNotCanonical = errors.New("repository name must be canonical")
61)
62
63// Reference is an opaque object reference identifier that may include
64// modifiers such as a hostname, name, tag, and digest.
65type Reference interface {
66 // String returns the full reference
67 String() string
68}
69
70// Field provides a wrapper type for resolving correct reference types when
71// working with encoding.
72type Field struct {
73 reference Reference
74}
75
76// AsField wraps a reference in a Field for encoding.
77func AsField(reference Reference) Field {
78 return Field{reference}
79}
80
81// Reference unwraps the reference type from the field to
82// return the Reference object. This object should be
83// of the appropriate type to further check for different
84// reference types.
85func (f Field) Reference() Reference {
86 return f.reference
87}
88
89// MarshalText serializes the field to byte text which
90// is the string of the reference.
91func (f Field) MarshalText() (p []byte, err error) {
92 return []byte(f.reference.String()), nil
93}
94
95// UnmarshalText parses text bytes by invoking the
96// reference parser to ensure the appropriately
97// typed reference object is wrapped by field.
98func (f *Field) UnmarshalText(p []byte) error {
99 r, err := Parse(string(p))
100 if err != nil {
101 return err
102 }
103
104 f.reference = r
105 return nil
106}
107
108// Named is an object with a full name
109type Named interface {
110 Reference
111 Name() string
112}
113
114// Tagged is an object which has a tag
115type Tagged interface {
116 Reference
117 Tag() string
118}
119
120// NamedTagged is an object including a name and tag.
121type NamedTagged interface {
122 Named
123 Tag() string
124}
125
126// Digested is an object which has a digest
127// in which it can be referenced by
128type Digested interface {
129 Reference
130 Digest() digest.Digest
131}
132
133// Canonical reference is an object with a fully unique
134// name including a name with domain and digest
135type Canonical interface {
136 Named
137 Digest() digest.Digest
138}
139
140// namedRepository is a reference to a repository with a name.
141// A namedRepository has both domain and path components.
142type namedRepository interface {
143 Named
144 Domain() string
145 Path() string
146}
147
148// Domain returns the domain part of the Named reference
149func Domain(named Named) string {
150 if r, ok := named.(namedRepository); ok {
151 return r.Domain()
152 }
153 domain, _ := splitDomain(named.Name())
154 return domain
155}
156
157// Path returns the name without the domain part of the Named reference
158func Path(named Named) (name string) {
159 if r, ok := named.(namedRepository); ok {
160 return r.Path()
161 }
162 _, path := splitDomain(named.Name())
163 return path
164}
165
166func splitDomain(name string) (string, string) {
167 match := anchoredNameRegexp.FindStringSubmatch(name)
168 if len(match) != 3 {
169 return "", name
170 }
171 return match[1], match[2]
172}
173
174// SplitHostname splits a named reference into a
175// hostname and name string. If no valid hostname is
176// found, the hostname is empty and the full value
177// is returned as name
178// DEPRECATED: Use Domain or Path
179func SplitHostname(named Named) (string, string) {
180 if r, ok := named.(namedRepository); ok {
181 return r.Domain(), r.Path()
182 }
183 return splitDomain(named.Name())
184}
185
186// Parse parses s and returns a syntactically valid Reference.
187// If an error was encountered it is returned, along with a nil Reference.
188// NOTE: Parse will not handle short digests.
189func Parse(s string) (Reference, error) {
190 matches := ReferenceRegexp.FindStringSubmatch(s)
191 if matches == nil {
192 if s == "" {
193 return nil, ErrNameEmpty
194 }
195 if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
196 return nil, ErrNameContainsUppercase
197 }
198 return nil, ErrReferenceInvalidFormat
199 }
200
201 if len(matches[1]) > NameTotalLengthMax {
202 return nil, ErrNameTooLong
203 }
204
205 var repo repository
206
207 nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
208 if nameMatch != nil && len(nameMatch) == 3 {
209 repo.domain = nameMatch[1]
210 repo.path = nameMatch[2]
211 } else {
212 repo.domain = ""
213 repo.path = matches[1]
214 }
215
216 ref := reference{
217 namedRepository: repo,
218 tag: matches[2],
219 }
220 if matches[3] != "" {
221 var err error
222 ref.digest, err = digest.Parse(matches[3])
223 if err != nil {
224 return nil, err
225 }
226 }
227
228 r := getBestReferenceType(ref)
229 if r == nil {
230 return nil, ErrNameEmpty
231 }
232
233 return r, nil
234}
235
236// ParseNamed parses s and returns a syntactically valid reference implementing
237// the Named interface. The reference must have a name and be in the canonical
238// form, otherwise an error is returned.
239// If an error was encountered it is returned, along with a nil Reference.
240// NOTE: ParseNamed will not handle short digests.
241func ParseNamed(s string) (Named, error) {
242 named, err := ParseNormalizedNamed(s)
243 if err != nil {
244 return nil, err
245 }
246 if named.String() != s {
247 return nil, ErrNameNotCanonical
248 }
249 return named, nil
250}
251
252// WithName returns a named object representing the given string. If the input
253// is invalid ErrReferenceInvalidFormat will be returned.
254func WithName(name string) (Named, error) {
255 if len(name) > NameTotalLengthMax {
256 return nil, ErrNameTooLong
257 }
258
259 match := anchoredNameRegexp.FindStringSubmatch(name)
260 if match == nil || len(match) != 3 {
261 return nil, ErrReferenceInvalidFormat
262 }
263 return repository{
264 domain: match[1],
265 path: match[2],
266 }, nil
267}
268
269// WithTag combines the name from "name" and the tag from "tag" to form a
270// reference incorporating both the name and the tag.
271func WithTag(name Named, tag string) (NamedTagged, error) {
272 if !anchoredTagRegexp.MatchString(tag) {
273 return nil, ErrTagInvalidFormat
274 }
275 var repo repository
276 if r, ok := name.(namedRepository); ok {
277 repo.domain = r.Domain()
278 repo.path = r.Path()
279 } else {
280 repo.path = name.Name()
281 }
282 if canonical, ok := name.(Canonical); ok {
283 return reference{
284 namedRepository: repo,
285 tag: tag,
286 digest: canonical.Digest(),
287 }, nil
288 }
289 return taggedReference{
290 namedRepository: repo,
291 tag: tag,
292 }, nil
293}
294
295// WithDigest combines the name from "name" and the digest from "digest" to form
296// a reference incorporating both the name and the digest.
297func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
298 if !anchoredDigestRegexp.MatchString(digest.String()) {
299 return nil, ErrDigestInvalidFormat
300 }
301 var repo repository
302 if r, ok := name.(namedRepository); ok {
303 repo.domain = r.Domain()
304 repo.path = r.Path()
305 } else {
306 repo.path = name.Name()
307 }
308 if tagged, ok := name.(Tagged); ok {
309 return reference{
310 namedRepository: repo,
311 tag: tagged.Tag(),
312 digest: digest,
313 }, nil
314 }
315 return canonicalReference{
316 namedRepository: repo,
317 digest: digest,
318 }, nil
319}
320
321// TrimNamed removes any tag or digest from the named reference.
322func TrimNamed(ref Named) Named {
323 domain, path := SplitHostname(ref)
324 return repository{
325 domain: domain,
326 path: path,
327 }
328}
329
330func getBestReferenceType(ref reference) Reference {
331 if ref.Name() == "" {
332 // Allow digest only references
333 if ref.digest != "" {
334 return digestReference(ref.digest)
335 }
336 return nil
337 }
338 if ref.tag == "" {
339 if ref.digest != "" {
340 return canonicalReference{
341 namedRepository: ref.namedRepository,
342 digest: ref.digest,
343 }
344 }
345 return ref.namedRepository
346 }
347 if ref.digest == "" {
348 return taggedReference{
349 namedRepository: ref.namedRepository,
350 tag: ref.tag,
351 }
352 }
353
354 return ref
355}
356
357type reference struct {
358 namedRepository
359 tag string
360 digest digest.Digest
361}
362
363func (r reference) String() string {
364 return r.Name() + ":" + r.tag + "@" + r.digest.String()
365}
366
367func (r reference) Tag() string {
368 return r.tag
369}
370
371func (r reference) Digest() digest.Digest {
372 return r.digest
373}
374
375type repository struct {
376 domain string
377 path string
378}
379
380func (r repository) String() string {
381 return r.Name()
382}
383
384func (r repository) Name() string {
385 if r.domain == "" {
386 return r.path
387 }
388 return r.domain + "/" + r.path
389}
390
391func (r repository) Domain() string {
392 return r.domain
393}
394
395func (r repository) Path() string {
396 return r.path
397}
398
399type digestReference digest.Digest
400
401func (d digestReference) String() string {
402 return digest.Digest(d).String()
403}
404
405func (d digestReference) Digest() digest.Digest {
406 return digest.Digest(d)
407}
408
409type taggedReference struct {
410 namedRepository
411 tag string
412}
413
414func (t taggedReference) String() string {
415 return t.Name() + ":" + t.tag
416}
417
418func (t taggedReference) Tag() string {
419 return t.tag
420}
421
422type canonicalReference struct {
423 namedRepository
424 digest digest.Digest
425}
426
427func (c canonicalReference) String() string {
428 return c.Name() + "@" + c.digest.String()
429}
430
431func (c canonicalReference) Digest() digest.Digest {
432 return c.digest
433}