image.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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 append(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 append(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) ID() string {
  177. c.AddHeader(imageIDHeader)
  178. if c.trunc {
  179. return stringid.TruncateID(c.i.ID)
  180. }
  181. return c.i.ID
  182. }
  183. func (c *imageContext) Repository() string {
  184. c.AddHeader(repositoryHeader)
  185. return c.repo
  186. }
  187. func (c *imageContext) Tag() string {
  188. c.AddHeader(tagHeader)
  189. return c.tag
  190. }
  191. func (c *imageContext) Digest() string {
  192. c.AddHeader(digestHeader)
  193. return c.digest
  194. }
  195. func (c *imageContext) CreatedSince() string {
  196. c.AddHeader(createdSinceHeader)
  197. createdAt := time.Unix(int64(c.i.Created), 0)
  198. return units.HumanDuration(time.Now().UTC().Sub(createdAt))
  199. }
  200. func (c *imageContext) CreatedAt() string {
  201. c.AddHeader(createdAtHeader)
  202. return time.Unix(int64(c.i.Created), 0).String()
  203. }
  204. func (c *imageContext) Size() string {
  205. c.AddHeader(sizeHeader)
  206. return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
  207. }
  208. func (c *imageContext) Containers() string {
  209. c.AddHeader(containersHeader)
  210. if c.i.Containers == -1 {
  211. return "N/A"
  212. }
  213. return fmt.Sprintf("%d", c.i.Containers)
  214. }
  215. func (c *imageContext) VirtualSize() string {
  216. c.AddHeader(sizeHeader)
  217. return units.HumanSize(float64(c.i.VirtualSize))
  218. }
  219. func (c *imageContext) SharedSize() string {
  220. c.AddHeader(sharedSizeHeader)
  221. if c.i.SharedSize == -1 {
  222. return "N/A"
  223. }
  224. return units.HumanSize(float64(c.i.SharedSize))
  225. }
  226. func (c *imageContext) UniqueSize() string {
  227. c.AddHeader(uniqueSizeHeader)
  228. if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
  229. return "N/A"
  230. }
  231. return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize))
  232. }