normalize.go 7.5 KB

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