1 package reference 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/opencontainers/go-digest" 8 ) 9 10 const ( 11 // legacyDefaultDomain is the legacy domain for Docker Hub (which was 12 // originally named "the Docker Index"). This domain is still used for 13 // authentication and image search, which were part of the "v1" Docker 14 // registry specification. 15 // 16 // This domain will continue to be supported, but there are plans to consolidate 17 // legacy domains to new "canonical" domains. Once those domains are decided 18 // on, we must update the normalization functions, but preserve compatibility 19 // with existing installs, clients, and user configuration. 20 legacyDefaultDomain = "index.docker.io" 21 22 // defaultDomain is the default domain used for images on Docker Hub. 23 // It is used to normalize "familiar" names to canonical names, for example, 24 // to convert "ubuntu" to "docker.io/library/ubuntu:latest". 25 // 26 // Note that actual domain of Docker Hub's registry is registry-1.docker.io. 27 // This domain will continue to be supported, but there are plans to consolidate 28 // legacy domains to new "canonical" domains. Once those domains are decided 29 // on, we must update the normalization functions, but preserve compatibility 30 // with existing installs, clients, and user configuration. 31 defaultDomain = "docker.io" 32 33 // officialRepoPrefix is the namespace used for official images on Docker Hub. 34 // It is used to normalize "familiar" names to canonical names, for example, 35 // to convert "ubuntu" to "docker.io/library/ubuntu:latest". 36 officialRepoPrefix = "library/" 37 38 // defaultTag is the default tag if no tag is provided. 39 defaultTag = "latest" 40 ) 41 42 // normalizedNamed represents a name which has been 43 // normalized and has a familiar form. A familiar name 44 // is what is used in Docker UI. An example normalized 45 // name is "docker.io/library/ubuntu" and corresponding 46 // familiar name of "ubuntu". 47 type normalizedNamed interface { 48 Named 49 Familiar() Named 50 } 51 52 // ParseNormalizedNamed parses a string into a named reference 53 // transforming a familiar name from Docker UI to a fully 54 // qualified reference. If the value may be an identifier 55 // use ParseAnyReference. 56 func ParseNormalizedNamed(s string) (Named, error) { 57 if ok := anchoredIdentifierRegexp.MatchString(s); ok { 58 return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s) 59 } 60 domain, remainder := splitDockerDomain(s) 61 var remote string 62 if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 { 63 remote = remainder[:tagSep] 64 } else { 65 remote = remainder 66 } 67 if strings.ToLower(remote) != remote { 68 return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remote) 69 } 70 71 ref, err := Parse(domain + "/" + remainder) 72 if err != nil { 73 return nil, err 74 } 75 named, isNamed := ref.(Named) 76 if !isNamed { 77 return nil, fmt.Errorf("reference %s has no name", ref.String()) 78 } 79 return named, nil 80 } 81 82 // namedTaggedDigested is a reference that has both a tag and a digest. 83 type namedTaggedDigested interface { 84 NamedTagged 85 Digested 86 } 87 88 // ParseDockerRef normalizes the image reference following the docker convention, 89 // which allows for references to contain both a tag and a digest. It returns a 90 // reference that is either tagged or digested. For references containing both 91 // a tag and a digest, it returns a digested reference. For example, the following 92 // reference: 93 // 94 // docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa 95 // 96 // Is returned as a digested reference (with the ":latest" tag removed): 97 // 98 // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa 99 // 100 // References that are already "tagged" or "digested" are returned unmodified: 101 // 102 // // Already a digested reference 103 // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa 104 // 105 // // Already a named reference 106 // docker.io/library/busybox:latest 107 func ParseDockerRef(ref string) (Named, error) { 108 named, err := ParseNormalizedNamed(ref) 109 if err != nil { 110 return nil, err 111 } 112 if canonical, ok := named.(namedTaggedDigested); ok { 113 // The reference is both tagged and digested; only return digested. 114 newNamed, err := WithName(canonical.Name()) 115 if err != nil { 116 return nil, err 117 } 118 return WithDigest(newNamed, canonical.Digest()) 119 } 120 return TagNameOnly(named), nil 121 } 122 123 // splitDockerDomain splits a repository name to domain and remote-name. 124 // If no valid domain is found, the default domain is used. Repository name 125 // needs to be already validated before. 126 func splitDockerDomain(name string) (domain, remainder string) { 127 i := strings.IndexRune(name, '/') 128 if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != localhost && strings.ToLower(name[:i]) == name[:i]) { 129 domain, remainder = defaultDomain, name 130 } else { 131 domain, remainder = name[:i], name[i+1:] 132 } 133 if domain == legacyDefaultDomain { 134 domain = defaultDomain 135 } 136 if domain == defaultDomain && !strings.ContainsRune(remainder, '/') { 137 remainder = officialRepoPrefix + remainder 138 } 139 return 140 } 141 142 // familiarizeName returns a shortened version of the name familiar 143 // to the Docker UI. Familiar names have the default domain 144 // "docker.io" and "library/" repository prefix removed. 145 // For example, "docker.io/library/redis" will have the familiar 146 // name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp". 147 // Returns a familiarized named only reference. 148 func familiarizeName(named namedRepository) repository { 149 repo := repository{ 150 domain: named.Domain(), 151 path: named.Path(), 152 } 153 154 if repo.domain == defaultDomain { 155 repo.domain = "" 156 // Handle official repositories which have the pattern "library/<official repo name>" 157 if strings.HasPrefix(repo.path, officialRepoPrefix) { 158 // TODO(thaJeztah): this check may be too strict, as it assumes the 159 // "library/" namespace does not have nested namespaces. While this 160 // is true (currently), technically it would be possible for Docker 161 // Hub to use those (e.g. "library/distros/ubuntu:latest"). 162 // See https://github.com/distribution/distribution/pull/3769#issuecomment-1302031785. 163 if remainder := strings.TrimPrefix(repo.path, officialRepoPrefix); !strings.ContainsRune(remainder, '/') { 164 repo.path = remainder 165 } 166 } 167 } 168 return repo 169 } 170 171 func (r reference) Familiar() Named { 172 return reference{ 173 namedRepository: familiarizeName(r.namedRepository), 174 tag: r.tag, 175 digest: r.digest, 176 } 177 } 178 179 func (r repository) Familiar() Named { 180 return familiarizeName(r) 181 } 182 183 func (t taggedReference) Familiar() Named { 184 return taggedReference{ 185 namedRepository: familiarizeName(t.namedRepository), 186 tag: t.tag, 187 } 188 } 189 190 func (c canonicalReference) Familiar() Named { 191 return canonicalReference{ 192 namedRepository: familiarizeName(c.namedRepository), 193 digest: c.digest, 194 } 195 } 196 197 // TagNameOnly adds the default tag "latest" to a reference if it only has 198 // a repo name. 199 func TagNameOnly(ref Named) Named { 200 if IsNameOnly(ref) { 201 namedTagged, err := WithTag(ref, defaultTag) 202 if err != nil { 203 // Default tag must be valid, to create a NamedTagged 204 // type with non-validated input the WithTag function 205 // should be used instead 206 panic(err) 207 } 208 return namedTagged 209 } 210 return ref 211 } 212 213 // ParseAnyReference parses a reference string as a possible identifier, 214 // full digest, or familiar name. 215 func ParseAnyReference(ref string) (Reference, error) { 216 if ok := anchoredIdentifierRegexp.MatchString(ref); ok { 217 return digestReference("sha256:" + ref), nil 218 } 219 if dgst, err := digest.Parse(ref); err == nil { 220 return digestReference(dgst), nil 221 } 222 223 return ParseNormalizedNamed(ref) 224 } 225