GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

image.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package image
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "regexp"
  6. "sort"
  7. "strings"
  8. "time"
  9. "github.com/Masterminds/semver"
  10. "github.com/pkg/errors"
  11. )
  12. const (
  13. dockerHubHost = "index.docker.io"
  14. oldDockerHubHost = "docker.io"
  15. )
  16. var (
  17. ErrInvalidImageID = errors.New("invalid image ID")
  18. ErrBlankImageID = errors.Wrap(ErrInvalidImageID, "blank image name")
  19. ErrMalformedImageID = errors.Wrap(ErrInvalidImageID, `expected image name as either <image>:<tag> or just <image>`)
  20. )
  21. // Name represents an unversioned (i.e., untagged) image a.k.a.,
  22. // an image repo. These sometimes include a domain, e.g., quay.io, and
  23. // always include a path with at least one element. By convention,
  24. // images at DockerHub may have the domain omitted; and, if they only
  25. // have single path element, the prefix `library` is implied.
  26. //
  27. // Examples (stringified):
  28. // * alpine
  29. // * library/alpine
  30. // * docker.io/weaveworks/flux
  31. // * localhost:5000/arbitrary/path/to/repo
  32. type Name struct {
  33. Domain, Image string
  34. }
  35. // CanonicalName is an image name with none of the fields left to be
  36. // implied by convention.
  37. type CanonicalName struct {
  38. Name
  39. }
  40. //
  41. func (i Name) String() string {
  42. if i.Image == "" {
  43. return "" // Doesn't make sense to return anything if it doesn't even have an image
  44. }
  45. var host string
  46. if i.Domain != "" {
  47. host = i.Domain + "/"
  48. }
  49. return fmt.Sprintf("%s%s", host, i.Image)
  50. }
  51. // Repository returns the canonicalised path part of an Name.
  52. func (i Name) Repository() string {
  53. switch i.Domain {
  54. case "", oldDockerHubHost, dockerHubHost:
  55. path := strings.Split(i.Image, "/")
  56. if len(path) == 1 {
  57. return "library/" + i.Image
  58. }
  59. return i.Image
  60. default:
  61. return i.Image
  62. }
  63. }
  64. // Registry returns the domain name of the Docker image registry, to
  65. // use to fetch the image or image metadata.
  66. func (i Name) Registry() string {
  67. switch i.Domain {
  68. case "", oldDockerHubHost:
  69. return dockerHubHost
  70. default:
  71. return i.Domain
  72. }
  73. }
  74. // CanonicalName returns the canonicalised registry host and image parts
  75. // of the ID.
  76. func (i Name) CanonicalName() CanonicalName {
  77. return CanonicalName{
  78. Name: Name{
  79. Domain: i.Registry(),
  80. Image: i.Repository(),
  81. },
  82. }
  83. }
  84. func (i Name) ToRef(tag string) Ref {
  85. return Ref{
  86. Name: i,
  87. Tag: tag,
  88. }
  89. }
  90. // Ref represents a versioned (i.e., tagged) image. The tag is
  91. // allowed to be empty, though it is in general undefined what that
  92. // means. As such, `Ref` also includes all `Name` values.
  93. //
  94. // Examples (stringified):
  95. // * alpine:3.5
  96. // * library/alpine:3.5
  97. // * docker.io/weaveworks/flux:1.1.0
  98. // * localhost:5000/arbitrary/path/to/repo:revision-sha1
  99. type Ref struct {
  100. Name
  101. Tag string
  102. }
  103. // CanonicalRef is an image ref with none of the fields left to be
  104. // implied by convention.
  105. type CanonicalRef struct {
  106. Ref
  107. }
  108. // String returns the Ref as a string (i.e., unparsed) without canonicalising it.
  109. func (i Ref) String() string {
  110. var tag string
  111. if i.Tag != "" {
  112. tag = ":" + i.Tag
  113. }
  114. return fmt.Sprintf("%s%s", i.Name.String(), tag)
  115. }
  116. // ParseRef parses a string representation of an image id into an
  117. // Ref value. The grammar is shown here:
  118. // https://github.com/docker/distribution/blob/master/reference/reference.go
  119. // (but we do not care about all the productions.)
  120. func ParseRef(s string) (Ref, error) {
  121. var id Ref
  122. if s == "" {
  123. return id, errors.Wrapf(ErrBlankImageID, "parsing %q", s)
  124. }
  125. if strings.HasPrefix(s, "/") || strings.HasSuffix(s, "/") {
  126. return id, errors.Wrapf(ErrMalformedImageID, "parsing %q", s)
  127. }
  128. elements := strings.Split(s, "/")
  129. switch len(elements) {
  130. case 0: // NB strings.Split will never return []
  131. return id, errors.Wrapf(ErrMalformedImageID, "parsing %q", s)
  132. case 1: // no slashes, e.g., "alpine:1.5"; treat as library image
  133. id.Image = s
  134. case 2: // may have a domain e.g., "localhost/foo", or not e.g., "weaveworks/scope"
  135. if domainRegexp.MatchString(elements[0]) {
  136. id.Domain = elements[0]
  137. id.Image = elements[1]
  138. } else {
  139. id.Image = s
  140. }
  141. default: // cannot be a library image, so the first element is assumed to be a domain
  142. id.Domain = elements[0]
  143. id.Image = strings.Join(elements[1:], "/")
  144. }
  145. // Figure out if there's a tag
  146. imageParts := strings.Split(id.Image, ":")
  147. switch len(imageParts) {
  148. case 1:
  149. break
  150. case 2:
  151. if imageParts[0] == "" || imageParts[1] == "" {
  152. return id, errors.Wrapf(ErrMalformedImageID, "parsing %q", s)
  153. }
  154. id.Image = imageParts[0]
  155. id.Tag = imageParts[1]
  156. default:
  157. return id, ErrMalformedImageID
  158. }
  159. return id, nil
  160. }
  161. var (
  162. domainComponent = `([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
  163. domain = fmt.Sprintf(`localhost|(%s([.]%s)+)(:[0-9]+)?`, domainComponent, domainComponent)
  164. domainRegexp = regexp.MustCompile(domain)
  165. )
  166. // ImageID is serialized/deserialized as a string
  167. func (i Ref) MarshalJSON() ([]byte, error) {
  168. return json.Marshal(i.String())
  169. }
  170. // ImageID is serialized/deserialized as a string
  171. func (i *Ref) UnmarshalJSON(data []byte) (err error) {
  172. var str string
  173. if err := json.Unmarshal(data, &str); err != nil {
  174. return err
  175. }
  176. *i, err = ParseRef(string(str))
  177. return err
  178. }
  179. // CanonicalRef returns the canonicalised reference including the tag
  180. // if present.
  181. func (i Ref) CanonicalRef() CanonicalRef {
  182. name := i.CanonicalName()
  183. return CanonicalRef{
  184. Ref: Ref{
  185. Name: name.Name,
  186. Tag: i.Tag,
  187. },
  188. }
  189. }
  190. func (i Ref) Components() (domain, repo, tag string) {
  191. return i.Domain, i.Image, i.Tag
  192. }
  193. // WithNewTag makes a new copy of an ImageID with a new tag
  194. func (i Ref) WithNewTag(t string) Ref {
  195. var img Ref
  196. img = i
  197. img.Tag = t
  198. return img
  199. }
  200. // Info has the metadata we are able to determine about an image ref,
  201. // from its registry.
  202. type Info struct {
  203. // the reference to this image; probably a tagged image name
  204. ID Ref `json:",omitempty"`
  205. // the digest we got when fetching the metadata, which will be
  206. // different each time a manifest is uploaded for the reference
  207. Digest string `json:",omitempty"`
  208. // an identifier for the *image* this reference points to; this
  209. // will be the same for references that point at the same image
  210. // (but does not necessarily equal Docker's image ID)
  211. ImageID string `json:",omitempty"`
  212. // the time at which the image pointed at was created
  213. CreatedAt time.Time `json:",omitempty"`
  214. // the last time this image manifest was fetched
  215. LastFetched time.Time `json:",omitempty"`
  216. }
  217. // MarshalJSON returns the Info value in JSON (as bytes). It is
  218. // implemented so that we can omit the `CreatedAt` value when it's
  219. // zero, which would otherwise be tricky for e.g., JavaScript to
  220. // detect.
  221. func (im Info) MarshalJSON() ([]byte, error) {
  222. type InfoAlias Info // alias to shed existing MarshalJSON implementation
  223. var ca, lf string
  224. if !im.CreatedAt.IsZero() {
  225. ca = im.CreatedAt.UTC().Format(time.RFC3339Nano)
  226. }
  227. if !im.LastFetched.IsZero() {
  228. lf = im.LastFetched.UTC().Format(time.RFC3339Nano)
  229. }
  230. encode := struct {
  231. InfoAlias
  232. CreatedAt string `json:",omitempty"`
  233. LastFetched string `json:",omitempty"`
  234. }{InfoAlias(im), ca, lf}
  235. return json.Marshal(encode)
  236. }
  237. // UnmarshalJSON populates an Info from JSON (as bytes). It's the
  238. // companion to MarshalJSON above.
  239. func (im *Info) UnmarshalJSON(b []byte) error {
  240. type InfoAlias Info
  241. unencode := struct {
  242. InfoAlias
  243. CreatedAt string `json:",omitempty"`
  244. LastFetched string `json:",omitempty"`
  245. }{}
  246. json.Unmarshal(b, &unencode)
  247. *im = Info(unencode.InfoAlias)
  248. var err error
  249. if err = decodeTime(unencode.CreatedAt, &im.CreatedAt); err == nil {
  250. err = decodeTime(unencode.LastFetched, &im.LastFetched)
  251. }
  252. return err
  253. }
  254. // RepositoryMetadata contains the image metadata information found in an
  255. // image repository.
  256. //
  257. // `Images` is indexed by `Tags`. Note that `Images` may be partial/incomplete,
  258. // (i.e. entries from `Tags` may not have a corresponding key in `Images`),
  259. // this indicates that the tag manifest was missing or corrupted in the
  260. // repository.
  261. type RepositoryMetadata struct {
  262. Tags []string // all the tags found in the repository
  263. Images map[string]Info // indexed by `Tags`, but may not include keys for all entries in `Tags`
  264. }
  265. // FindImageWithRef returns image.Info given an image ref. If the image cannot be
  266. // found, it returns the image.Info with the ID provided.
  267. func (rm RepositoryMetadata) FindImageWithRef(ref Ref) Info {
  268. for _, img := range rm.Images {
  269. if img.ID == ref {
  270. return img
  271. }
  272. }
  273. return Info{ID: ref}
  274. }
  275. // GetImageTagInfo gets the information of all image tags.
  276. // If there are tags missing information, an error is returned
  277. func (rm RepositoryMetadata) GetImageTagInfo() ([]Info, error) {
  278. result := make([]Info, len(rm.Tags), len(rm.Tags))
  279. for i, tag := range rm.Tags {
  280. info, ok := rm.Images[tag]
  281. if !ok {
  282. return nil, fmt.Errorf("missing metadata for image tag %q", tag)
  283. }
  284. result[i] = info
  285. }
  286. return result, nil
  287. }
  288. func decodeTime(s string, t *time.Time) error {
  289. if s == "" {
  290. *t = time.Time{}
  291. } else {
  292. var err error
  293. *t, err = time.Parse(time.RFC3339, s)
  294. if err != nil {
  295. return err
  296. }
  297. }
  298. return nil
  299. }
  300. // NewerByCreated returns true if lhs image should be sorted
  301. // before rhs with regard to their creation date descending.
  302. func NewerByCreated(lhs, rhs *Info) bool {
  303. if lhs.CreatedAt.Equal(rhs.CreatedAt) {
  304. return lhs.ID.String() < rhs.ID.String()
  305. }
  306. return lhs.CreatedAt.After(rhs.CreatedAt)
  307. }
  308. // NewerBySemver returns true if lhs image should be sorted
  309. // before rhs with regard to their semver order descending.
  310. func NewerBySemver(lhs, rhs *Info) bool {
  311. lv, lerr := semver.NewVersion(lhs.ID.Tag)
  312. rv, rerr := semver.NewVersion(rhs.ID.Tag)
  313. if (lerr != nil && rerr != nil) || (lv == rv) {
  314. return lhs.ID.String() < rhs.ID.String()
  315. }
  316. if lerr != nil {
  317. return false
  318. }
  319. if rerr != nil {
  320. return true
  321. }
  322. cmp := lv.Compare(rv)
  323. // In semver, `1.10` and `1.10.0` is the same but in favor of explicitness
  324. // we should consider the latter newer.
  325. if cmp == 0 {
  326. return lhs.ID.String() > rhs.ID.String()
  327. }
  328. return cmp > 0
  329. }
  330. // Sort orders the given image infos according to `newer` func.
  331. func Sort(infos []Info, newer func(a, b *Info) bool) {
  332. if newer == nil {
  333. newer = NewerByCreated
  334. }
  335. sort.Sort(&infoSort{infos: infos, newer: newer})
  336. }
  337. type infoSort struct {
  338. infos []Info
  339. newer func(a, b *Info) bool
  340. }
  341. func (s *infoSort) Len() int {
  342. return len(s.infos)
  343. }
  344. func (s *infoSort) Swap(i, j int) {
  345. s.infos[i], s.infos[j] = s.infos[j], s.infos[i]
  346. }
  347. func (s *infoSort) Less(i, j int) bool {
  348. return s.newer(&s.infos[i], &s.infos[j])
  349. }