reference.go 11 KB

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