image.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. package formatter
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/docker/distribution/reference"
  6. "github.com/docker/docker/api/types"
  7. "github.com/docker/docker/pkg/stringid"
  8. units "github.com/docker/go-units"
  9. )
  10. const (
  11. defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
  12. defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
  13. imageIDHeader = "IMAGE ID"
  14. repositoryHeader = "REPOSITORY"
  15. tagHeader = "TAG"
  16. digestHeader = "DIGEST"
  17. )
  18. // ImageContext contains image specific information required by the formatter, encapsulate a Context struct.
  19. type ImageContext struct {
  20. Context
  21. Digest bool
  22. }
  23. func isDangling(image types.ImageSummary) bool {
  24. return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
  25. }
  26. // NewImageFormat returns a format for rendering an ImageContext
  27. func NewImageFormat(source string, quiet bool, digest bool) Format {
  28. switch source {
  29. case TableFormatKey:
  30. switch {
  31. case quiet:
  32. return defaultQuietFormat
  33. case digest:
  34. return defaultImageTableFormatWithDigest
  35. default:
  36. return defaultImageTableFormat
  37. }
  38. case RawFormatKey:
  39. switch {
  40. case quiet:
  41. return `image_id: {{.ID}}`
  42. case digest:
  43. return `repository: {{ .Repository }}
  44. tag: {{.Tag}}
  45. digest: {{.Digest}}
  46. image_id: {{.ID}}
  47. created_at: {{.CreatedAt}}
  48. virtual_size: {{.Size}}
  49. `
  50. default:
  51. return `repository: {{ .Repository }}
  52. tag: {{.Tag}}
  53. image_id: {{.ID}}
  54. created_at: {{.CreatedAt}}
  55. virtual_size: {{.Size}}
  56. `
  57. }
  58. }
  59. format := Format(source)
  60. if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
  61. format += "\t{{.Digest}}"
  62. }
  63. return format
  64. }
  65. // ImageWrite writes the formatter images using the ImageContext
  66. func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
  67. render := func(format func(subContext subContext) error) error {
  68. return imageFormat(ctx, images, format)
  69. }
  70. return ctx.Write(&imageContext{}, render)
  71. }
  72. func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
  73. for _, image := range images {
  74. images := []*imageContext{}
  75. if isDangling(image) {
  76. images = append(images, &imageContext{
  77. trunc: ctx.Trunc,
  78. i: image,
  79. repo: "<none>",
  80. tag: "<none>",
  81. digest: "<none>",
  82. })
  83. } else {
  84. repoTags := map[string][]string{}
  85. repoDigests := map[string][]string{}
  86. for _, refString := range image.RepoTags {
  87. ref, err := reference.ParseNormalizedNamed(refString)
  88. if err != nil {
  89. continue
  90. }
  91. if nt, ok := ref.(reference.NamedTagged); ok {
  92. familiarRef := reference.FamiliarName(ref)
  93. repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
  94. }
  95. }
  96. for _, refString := range image.RepoDigests {
  97. ref, err := reference.ParseNormalizedNamed(refString)
  98. if err != nil {
  99. continue
  100. }
  101. if c, ok := ref.(reference.Canonical); ok {
  102. familiarRef := reference.FamiliarName(ref)
  103. repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
  104. }
  105. }
  106. for repo, tags := range repoTags {
  107. digests := repoDigests[repo]
  108. // Do not display digests as their own row
  109. delete(repoDigests, repo)
  110. if !ctx.Digest {
  111. // Ignore digest references, just show tag once
  112. digests = nil
  113. }
  114. for _, tag := range tags {
  115. if len(digests) == 0 {
  116. images = append(images, &imageContext{
  117. trunc: ctx.Trunc,
  118. i: image,
  119. repo: repo,
  120. tag: tag,
  121. digest: "<none>",
  122. })
  123. continue
  124. }
  125. // Display the digests for each tag
  126. for _, dgst := range digests {
  127. images = append(images, &imageContext{
  128. trunc: ctx.Trunc,
  129. i: image,
  130. repo: repo,
  131. tag: tag,
  132. digest: dgst,
  133. })
  134. }
  135. }
  136. }
  137. // Show rows for remaining digest only references
  138. for repo, digests := range repoDigests {
  139. // If digests are displayed, show row per digest
  140. if ctx.Digest {
  141. for _, dgst := range digests {
  142. images = append(images, &imageContext{
  143. trunc: ctx.Trunc,
  144. i: image,
  145. repo: repo,
  146. tag: "<none>",
  147. digest: dgst,
  148. })
  149. }
  150. } else {
  151. images = append(images, &imageContext{
  152. trunc: ctx.Trunc,
  153. i: image,
  154. repo: repo,
  155. tag: "<none>",
  156. })
  157. }
  158. }
  159. }
  160. for _, imageCtx := range images {
  161. if err := format(imageCtx); err != nil {
  162. return err
  163. }
  164. }
  165. }
  166. return nil
  167. }
  168. type imageContext struct {
  169. HeaderContext
  170. trunc bool
  171. i types.ImageSummary
  172. repo string
  173. tag string
  174. digest string
  175. }
  176. func (c *imageContext) MarshalJSON() ([]byte, error) {
  177. return marshalJSON(c)
  178. }
  179. func (c *imageContext) ID() string {
  180. c.AddHeader(imageIDHeader)
  181. if c.trunc {
  182. return stringid.TruncateID(c.i.ID)
  183. }
  184. return c.i.ID
  185. }
  186. func (c *imageContext) Repository() string {
  187. c.AddHeader(repositoryHeader)
  188. return c.repo
  189. }
  190. func (c *imageContext) Tag() string {
  191. c.AddHeader(tagHeader)
  192. return c.tag
  193. }
  194. func (c *imageContext) Digest() string {
  195. c.AddHeader(digestHeader)
  196. return c.digest
  197. }
  198. func (c *imageContext) CreatedSince() string {
  199. c.AddHeader(createdSinceHeader)
  200. createdAt := time.Unix(int64(c.i.Created), 0)
  201. return units.HumanDuration(time.Now().UTC().Sub(createdAt))
  202. }
  203. func (c *imageContext) CreatedAt() string {
  204. c.AddHeader(createdAtHeader)
  205. return time.Unix(int64(c.i.Created), 0).String()
  206. }
  207. func (c *imageContext) Size() string {
  208. c.AddHeader(sizeHeader)
  209. return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
  210. }
  211. func (c *imageContext) Containers() string {
  212. c.AddHeader(containersHeader)
  213. if c.i.Containers == -1 {
  214. return "N/A"
  215. }
  216. return fmt.Sprintf("%d", c.i.Containers)
  217. }
  218. func (c *imageContext) VirtualSize() string {
  219. c.AddHeader(sizeHeader)
  220. return units.HumanSize(float64(c.i.VirtualSize))
  221. }
  222. func (c *imageContext) SharedSize() string {
  223. c.AddHeader(sharedSizeHeader)
  224. if c.i.SharedSize == -1 {
  225. return "N/A"
  226. }
  227. return units.HumanSize(float64(c.i.SharedSize))
  228. }
  229. func (c *imageContext) UniqueSize() string {
  230. c.AddHeader(uniqueSizeHeader)
  231. if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
  232. return "N/A"
  233. }
  234. return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize))
  235. }