formatter.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package formatter
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "strings"
  7. "text/tabwriter"
  8. "text/template"
  9. "github.com/docker/docker/reference"
  10. "github.com/docker/docker/utils/templates"
  11. "github.com/docker/engine-api/types"
  12. )
  13. const (
  14. tableFormatKey = "table"
  15. rawFormatKey = "raw"
  16. defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
  17. defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
  18. defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
  19. defaultQuietFormat = "{{.ID}}"
  20. )
  21. // Context contains information required by the formatter to print the output as desired.
  22. type Context struct {
  23. // Output is the output stream to which the formatted string is written.
  24. Output io.Writer
  25. // Format is used to choose raw, table or custom format for the output.
  26. Format string
  27. // Quiet when set to true will simply print minimal information.
  28. Quiet bool
  29. // Trunc when set to true will truncate the output of certain fields such as Container ID.
  30. Trunc bool
  31. // internal element
  32. table bool
  33. finalFormat string
  34. header string
  35. buffer *bytes.Buffer
  36. }
  37. func (c *Context) preformat() {
  38. c.finalFormat = c.Format
  39. if strings.HasPrefix(c.Format, tableKey) {
  40. c.table = true
  41. c.finalFormat = c.finalFormat[len(tableKey):]
  42. }
  43. c.finalFormat = strings.Trim(c.finalFormat, " ")
  44. r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
  45. c.finalFormat = r.Replace(c.finalFormat)
  46. }
  47. func (c *Context) parseFormat() (*template.Template, error) {
  48. tmpl, err := templates.Parse(c.finalFormat)
  49. if err != nil {
  50. c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
  51. c.buffer.WriteTo(c.Output)
  52. }
  53. return tmpl, err
  54. }
  55. func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
  56. if c.table {
  57. if len(c.header) == 0 {
  58. // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
  59. tmpl.Execute(bytes.NewBufferString(""), subContext)
  60. c.header = subContext.fullHeader()
  61. }
  62. t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
  63. t.Write([]byte(c.header))
  64. t.Write([]byte("\n"))
  65. c.buffer.WriteTo(t)
  66. t.Flush()
  67. } else {
  68. c.buffer.WriteTo(c.Output)
  69. }
  70. }
  71. func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
  72. if err := tmpl.Execute(c.buffer, subContext); err != nil {
  73. c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
  74. c.buffer.WriteTo(c.Output)
  75. return err
  76. }
  77. if c.table && len(c.header) == 0 {
  78. c.header = subContext.fullHeader()
  79. }
  80. c.buffer.WriteString("\n")
  81. return nil
  82. }
  83. // ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
  84. type ContainerContext struct {
  85. Context
  86. // Size when set to true will display the size of the output.
  87. Size bool
  88. // Containers
  89. Containers []types.Container
  90. }
  91. // ImageContext contains image specific information required by the formater, encapsulate a Context struct.
  92. type ImageContext struct {
  93. Context
  94. Digest bool
  95. // Images
  96. Images []types.Image
  97. }
  98. func (ctx ContainerContext) Write() {
  99. switch ctx.Format {
  100. case tableFormatKey:
  101. if ctx.Quiet {
  102. ctx.Format = defaultQuietFormat
  103. } else {
  104. ctx.Format = defaultContainerTableFormat
  105. if ctx.Size {
  106. ctx.Format += `\t{{.Size}}`
  107. }
  108. }
  109. case rawFormatKey:
  110. if ctx.Quiet {
  111. ctx.Format = `container_id: {{.ID}}`
  112. } else {
  113. ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
  114. if ctx.Size {
  115. ctx.Format += `size: {{.Size}}\n`
  116. }
  117. }
  118. }
  119. ctx.buffer = bytes.NewBufferString("")
  120. ctx.preformat()
  121. tmpl, err := ctx.parseFormat()
  122. if err != nil {
  123. return
  124. }
  125. for _, container := range ctx.Containers {
  126. containerCtx := &containerContext{
  127. trunc: ctx.Trunc,
  128. c: container,
  129. }
  130. err = ctx.contextFormat(tmpl, containerCtx)
  131. if err != nil {
  132. return
  133. }
  134. }
  135. ctx.postformat(tmpl, &containerContext{})
  136. }
  137. func isDangling(image types.Image) bool {
  138. return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
  139. }
  140. func (ctx ImageContext) Write() {
  141. switch ctx.Format {
  142. case tableFormatKey:
  143. ctx.Format = defaultImageTableFormat
  144. if ctx.Digest {
  145. ctx.Format = defaultImageTableFormatWithDigest
  146. }
  147. if ctx.Quiet {
  148. ctx.Format = defaultQuietFormat
  149. }
  150. case rawFormatKey:
  151. if ctx.Quiet {
  152. ctx.Format = `image_id: {{.ID}}`
  153. } else {
  154. if ctx.Digest {
  155. ctx.Format = `repository: {{ .Repository }}
  156. tag: {{.Tag}}
  157. digest: {{.Digest}}
  158. image_id: {{.ID}}
  159. created_at: {{.CreatedAt}}
  160. virtual_size: {{.Size}}
  161. `
  162. } else {
  163. ctx.Format = `repository: {{ .Repository }}
  164. tag: {{.Tag}}
  165. image_id: {{.ID}}
  166. created_at: {{.CreatedAt}}
  167. virtual_size: {{.Size}}
  168. `
  169. }
  170. }
  171. }
  172. ctx.buffer = bytes.NewBufferString("")
  173. ctx.preformat()
  174. if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
  175. ctx.finalFormat += "\t{{.Digest}}"
  176. }
  177. tmpl, err := ctx.parseFormat()
  178. if err != nil {
  179. return
  180. }
  181. for _, image := range ctx.Images {
  182. images := []*imageContext{}
  183. if isDangling(image) {
  184. images = append(images, &imageContext{
  185. trunc: ctx.Trunc,
  186. i: image,
  187. repo: "<none>",
  188. tag: "<none>",
  189. digest: "<none>",
  190. })
  191. } else {
  192. repoTags := map[string][]string{}
  193. repoDigests := map[string][]string{}
  194. for _, refString := range append(image.RepoTags) {
  195. ref, err := reference.ParseNamed(refString)
  196. if err != nil {
  197. continue
  198. }
  199. if nt, ok := ref.(reference.NamedTagged); ok {
  200. repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag())
  201. }
  202. }
  203. for _, refString := range append(image.RepoDigests) {
  204. ref, err := reference.ParseNamed(refString)
  205. if err != nil {
  206. continue
  207. }
  208. if c, ok := ref.(reference.Canonical); ok {
  209. repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String())
  210. }
  211. }
  212. for repo, tags := range repoTags {
  213. digests := repoDigests[repo]
  214. // Do not display digests as their own row
  215. delete(repoDigests, repo)
  216. if !ctx.Digest {
  217. // Ignore digest references, just show tag once
  218. digests = nil
  219. }
  220. for _, tag := range tags {
  221. if len(digests) == 0 {
  222. images = append(images, &imageContext{
  223. trunc: ctx.Trunc,
  224. i: image,
  225. repo: repo,
  226. tag: tag,
  227. digest: "<none>",
  228. })
  229. continue
  230. }
  231. // Display the digests for each tag
  232. for _, dgst := range digests {
  233. images = append(images, &imageContext{
  234. trunc: ctx.Trunc,
  235. i: image,
  236. repo: repo,
  237. tag: tag,
  238. digest: dgst,
  239. })
  240. }
  241. }
  242. }
  243. // Show rows for remaining digest only references
  244. for repo, digests := range repoDigests {
  245. // If digests are displayed, show row per digest
  246. if ctx.Digest {
  247. for _, dgst := range digests {
  248. images = append(images, &imageContext{
  249. trunc: ctx.Trunc,
  250. i: image,
  251. repo: repo,
  252. tag: "<none>",
  253. digest: dgst,
  254. })
  255. }
  256. } else {
  257. images = append(images, &imageContext{
  258. trunc: ctx.Trunc,
  259. i: image,
  260. repo: repo,
  261. tag: "<none>",
  262. })
  263. }
  264. }
  265. }
  266. for _, imageCtx := range images {
  267. err = ctx.contextFormat(tmpl, imageCtx)
  268. if err != nil {
  269. return
  270. }
  271. }
  272. }
  273. ctx.postformat(tmpl, &imageContext{})
  274. }