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 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package flux
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "github.com/pkg/errors"
  8. )
  9. const (
  10. dockerHubHost = "index.docker.io"
  11. dockerHubLibrary = "library"
  12. oldDockerHubHost = "docker.io"
  13. )
  14. var (
  15. ErrInvalidImageID = errors.New("invalid image ID")
  16. ErrBlankImageID = errors.Wrap(ErrInvalidImageID, "blank image name")
  17. ErrMalformedImageID = errors.Wrap(ErrInvalidImageID, `expected image name as either <image>:<tag> or just <image>`)
  18. )
  19. // ImageID is a fully qualified name that refers to a particular Image.
  20. // It is in the format: host[:port]/Namespace/Image[:tag]
  21. // Here, we refer to the "name" == Namespace/Image
  22. type ImageID struct {
  23. Host, Namespace, Image, Tag string
  24. }
  25. func ParseImageID(s string) (ImageID, error) {
  26. if s == "" {
  27. return ImageID{}, ErrBlankImageID
  28. }
  29. var img ImageID
  30. parts := strings.Split(s, ":")
  31. switch len(parts) {
  32. case 0:
  33. return ImageID{}, ErrMalformedImageID
  34. case 1:
  35. img.Tag = "latest"
  36. case 2:
  37. img.Tag = parts[1]
  38. s = parts[0]
  39. case 3: // There might be three parts if there is a host with a custom port
  40. img.Tag = parts[2]
  41. s = s[:strings.LastIndex(s, ":")]
  42. default:
  43. return ImageID{}, ErrMalformedImageID
  44. }
  45. if s == "" {
  46. return ImageID{}, ErrBlankImageID
  47. }
  48. parts = strings.Split(s, "/")
  49. switch len(parts) {
  50. case 1:
  51. img.Host = dockerHubHost
  52. img.Namespace = dockerHubLibrary
  53. img.Image = parts[0]
  54. case 2:
  55. img.Host = dockerHubHost
  56. img.Namespace = parts[0]
  57. img.Image = parts[1]
  58. case 3:
  59. // Replace docker.io with index.docker.io (#692)
  60. if parts[0] == oldDockerHubHost {
  61. parts[0] = dockerHubHost
  62. }
  63. img.Host = parts[0]
  64. img.Namespace = parts[1]
  65. img.Image = parts[2]
  66. default:
  67. return ImageID{}, ErrMalformedImageID
  68. }
  69. return img, nil
  70. }
  71. // Fully qualified name
  72. func (i ImageID) String() string {
  73. if i.Image == "" {
  74. return "" // Doesn't make sense to return anything if it doesn't even have an image
  75. }
  76. var ta string
  77. if i.Tag != "" {
  78. ta = fmt.Sprintf(":%s", i.Tag)
  79. }
  80. return fmt.Sprintf("%s%s", i.Repository(), ta)
  81. }
  82. // ImageID is serialized/deserialized as a string
  83. func (i ImageID) MarshalJSON() ([]byte, error) {
  84. return json.Marshal(i.String())
  85. }
  86. // ImageID is serialized/deserialized as a string
  87. func (i *ImageID) UnmarshalJSON(data []byte) (err error) {
  88. var str string
  89. if err := json.Unmarshal(data, &str); err != nil {
  90. return err
  91. }
  92. *i, err = ParseImageID(string(str))
  93. return err
  94. }
  95. // Repository returns the short version of an image's repository (trimming if dockerhub)
  96. func (i ImageID) Repository() string {
  97. r := i.HostNamespaceImage()
  98. r = strings.TrimPrefix(r, dockerHubHost+"/")
  99. r = strings.TrimPrefix(r, dockerHubLibrary+"/")
  100. return r
  101. }
  102. // HostNamespaceImage includes all parts of the image, even if it is from dockerhub.
  103. func (i ImageID) HostNamespaceImage() string {
  104. return fmt.Sprintf("%s/%s/%s", i.Host, i.Namespace, i.Image)
  105. }
  106. func (i ImageID) NamespaceImage() string {
  107. return fmt.Sprintf("%s/%s", i.Namespace, i.Image)
  108. }
  109. func (i ImageID) FullID() string {
  110. return fmt.Sprintf("%s/%s/%s:%s", i.Host, i.Namespace, i.Image, i.Tag)
  111. }
  112. func (i ImageID) Components() (host, repo, tag string) {
  113. return i.Host, fmt.Sprintf("%s/%s", i.Namespace, i.Image), i.Tag
  114. }
  115. // WithNewTag makes a new copy of an ImageID with a new tag
  116. func (i ImageID) WithNewTag(t string) ImageID {
  117. var img ImageID
  118. img = i
  119. img.Tag = t
  120. return img
  121. }
  122. // Image can't really be a primitive string only, because we need to also
  123. // record information about its creation time. (maybe more in the future)
  124. type Image struct {
  125. ID ImageID
  126. CreatedAt time.Time
  127. }
  128. func (im Image) MarshalJSON() ([]byte, error) {
  129. var t string
  130. if !im.CreatedAt.IsZero() {
  131. t = im.CreatedAt.UTC().Format(time.RFC3339Nano)
  132. }
  133. encode := struct {
  134. ID ImageID
  135. CreatedAt string `json:",omitempty"`
  136. }{im.ID, t}
  137. return json.Marshal(encode)
  138. }
  139. func (im *Image) UnmarshalJSON(b []byte) error {
  140. unencode := struct {
  141. ID ImageID
  142. CreatedAt string `json:",omitempty"`
  143. }{}
  144. json.Unmarshal(b, &unencode)
  145. im.ID = unencode.ID
  146. if unencode.CreatedAt == "" {
  147. im.CreatedAt = time.Time{}
  148. } else {
  149. t, err := time.Parse(time.RFC3339, unencode.CreatedAt)
  150. if err != nil {
  151. return err
  152. }
  153. im.CreatedAt = t.UTC()
  154. }
  155. return nil
  156. }
  157. func ParseImage(s string, createdAt time.Time) (Image, error) {
  158. id, err := ParseImageID(s)
  159. if err != nil {
  160. return Image{}, err
  161. }
  162. return Image{
  163. ID: id,
  164. CreatedAt: createdAt,
  165. }, nil
  166. }
  167. // Sort image by creation date
  168. type ByCreatedDesc []Image
  169. func (is ByCreatedDesc) Len() int { return len(is) }
  170. func (is ByCreatedDesc) Swap(i, j int) { is[i], is[j] = is[j], is[i] }
  171. func (is ByCreatedDesc) Less(i, j int) bool {
  172. switch {
  173. case is[i].CreatedAt.IsZero():
  174. return true
  175. case is[j].CreatedAt.IsZero():
  176. return false
  177. case is[i].CreatedAt.Equal(is[j].CreatedAt):
  178. return is[i].ID.String() < is[j].ID.String()
  179. default:
  180. return is[i].CreatedAt.After(is[j].CreatedAt)
  181. }
  182. }