blob: 2d71fc5e9ffd974b49b44b2d26e2125b69b00ee4 [file] [log] [blame]
David K. Bainbridge215e0242017-09-05 23:18:24 -07001package reference
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7
8 "github.com/docker/distribution/digestset"
9 "github.com/opencontainers/go-digest"
10)
11
12var (
13 legacyDefaultDomain = "index.docker.io"
14 defaultDomain = "docker.io"
15 officialRepoName = "library"
16 defaultTag = "latest"
17)
18
19// normalizedNamed represents a name which has been
20// normalized and has a familiar form. A familiar name
21// is what is used in Docker UI. An example normalized
22// name is "docker.io/library/ubuntu" and corresponding
23// familiar name of "ubuntu".
24type normalizedNamed interface {
25 Named
26 Familiar() Named
27}
28
29// ParseNormalizedNamed parses a string into a named reference
30// transforming a familiar name from Docker UI to a fully
31// qualified reference. If the value may be an identifier
32// use ParseAnyReference.
33func ParseNormalizedNamed(s string) (Named, error) {
34 if ok := anchoredIdentifierRegexp.MatchString(s); ok {
35 return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
36 }
37 domain, remainder := splitDockerDomain(s)
38 var remoteName string
39 if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
40 remoteName = remainder[:tagSep]
41 } else {
42 remoteName = remainder
43 }
44 if strings.ToLower(remoteName) != remoteName {
45 return nil, errors.New("invalid reference format: repository name must be lowercase")
46 }
47
48 ref, err := Parse(domain + "/" + remainder)
49 if err != nil {
50 return nil, err
51 }
52 named, isNamed := ref.(Named)
53 if !isNamed {
54 return nil, fmt.Errorf("reference %s has no name", ref.String())
55 }
56 return named, nil
57}
58
59// splitDockerDomain splits a repository name to domain and remotename string.
60// If no valid domain is found, the default domain is used. Repository name
61// needs to be already validated before.
62func splitDockerDomain(name string) (domain, remainder string) {
63 i := strings.IndexRune(name, '/')
64 if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
65 domain, remainder = defaultDomain, name
66 } else {
67 domain, remainder = name[:i], name[i+1:]
68 }
69 if domain == legacyDefaultDomain {
70 domain = defaultDomain
71 }
72 if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
73 remainder = officialRepoName + "/" + remainder
74 }
75 return
76}
77
78// familiarizeName returns a shortened version of the name familiar
79// to to the Docker UI. Familiar names have the default domain
80// "docker.io" and "library/" repository prefix removed.
81// For example, "docker.io/library/redis" will have the familiar
82// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
83// Returns a familiarized named only reference.
84func familiarizeName(named namedRepository) repository {
85 repo := repository{
86 domain: named.Domain(),
87 path: named.Path(),
88 }
89
90 if repo.domain == defaultDomain {
91 repo.domain = ""
92 // Handle official repositories which have the pattern "library/<official repo name>"
93 if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
94 repo.path = split[1]
95 }
96 }
97 return repo
98}
99
100func (r reference) Familiar() Named {
101 return reference{
102 namedRepository: familiarizeName(r.namedRepository),
103 tag: r.tag,
104 digest: r.digest,
105 }
106}
107
108func (r repository) Familiar() Named {
109 return familiarizeName(r)
110}
111
112func (t taggedReference) Familiar() Named {
113 return taggedReference{
114 namedRepository: familiarizeName(t.namedRepository),
115 tag: t.tag,
116 }
117}
118
119func (c canonicalReference) Familiar() Named {
120 return canonicalReference{
121 namedRepository: familiarizeName(c.namedRepository),
122 digest: c.digest,
123 }
124}
125
126// TagNameOnly adds the default tag "latest" to a reference if it only has
127// a repo name.
128func TagNameOnly(ref Named) Named {
129 if IsNameOnly(ref) {
130 namedTagged, err := WithTag(ref, defaultTag)
131 if err != nil {
132 // Default tag must be valid, to create a NamedTagged
133 // type with non-validated input the WithTag function
134 // should be used instead
135 panic(err)
136 }
137 return namedTagged
138 }
139 return ref
140}
141
142// ParseAnyReference parses a reference string as a possible identifier,
143// full digest, or familiar name.
144func ParseAnyReference(ref string) (Reference, error) {
145 if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
146 return digestReference("sha256:" + ref), nil
147 }
148 if dgst, err := digest.Parse(ref); err == nil {
149 return digestReference(dgst), nil
150 }
151
152 return ParseNormalizedNamed(ref)
153}
154
155// ParseAnyReferenceWithSet parses a reference string as a possible short
156// identifier to be matched in a digest set, a full digest, or familiar name.
157func ParseAnyReferenceWithSet(ref string, ds *digestset.Set) (Reference, error) {
158 if ok := anchoredShortIdentifierRegexp.MatchString(ref); ok {
159 dgst, err := ds.Lookup(ref)
160 if err == nil {
161 return digestReference(dgst), nil
162 }
163 } else {
164 if dgst, err := digest.Parse(ref); err == nil {
165 return digestReference(dgst), nil
166 }
167 }
168
169 return ParseNormalizedNamed(ref)
170}