container.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package formatter
  14. import (
  15. "fmt"
  16. "net/netip"
  17. "strconv"
  18. "strings"
  19. "time"
  20. "github.com/docker/cli/cli/command/formatter"
  21. "github.com/docker/go-units"
  22. "github.com/moby/moby/api/types/container"
  23. "github.com/moby/moby/client/pkg/stringid"
  24. "github.com/docker/compose/v5/pkg/api"
  25. )
  26. const (
  27. defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
  28. nameHeader = "NAME"
  29. projectHeader = "PROJECT"
  30. serviceHeader = "SERVICE"
  31. commandHeader = "COMMAND"
  32. runningForHeader = "CREATED"
  33. mountsHeader = "MOUNTS"
  34. localVolumes = "LOCAL VOLUMES"
  35. networksHeader = "NETWORKS"
  36. )
  37. // NewContainerFormat returns a Format for rendering using a Context
  38. func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
  39. switch source {
  40. case formatter.TableFormatKey, "": // table formatting is the default if none is set.
  41. if quiet {
  42. return formatter.DefaultQuietFormat
  43. }
  44. format := defaultContainerTableFormat
  45. if size {
  46. format += `\t{{.Size}}`
  47. }
  48. return formatter.Format(format)
  49. case formatter.RawFormatKey:
  50. if quiet {
  51. return `container_id: {{.ID}}`
  52. }
  53. format := `container_id: {{.ID}}
  54. image: {{.Image}}
  55. command: {{.Command}}
  56. created_at: {{.CreatedAt}}
  57. state: {{- pad .State 1 0}}
  58. status: {{- pad .Status 1 0}}
  59. names: {{.Names}}
  60. labels: {{- pad .Labels 1 0}}
  61. ports: {{- pad .Ports 1 0}}
  62. `
  63. if size {
  64. format += `size: {{.Size}}\n`
  65. }
  66. return formatter.Format(format)
  67. default: // custom format
  68. if quiet {
  69. return formatter.DefaultQuietFormat
  70. }
  71. return formatter.Format(source)
  72. }
  73. }
  74. // ContainerWrite renders the context for a list of containers
  75. func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
  76. render := func(format func(subContext formatter.SubContext) error) error {
  77. for _, container := range containers {
  78. err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
  79. if err != nil {
  80. return err
  81. }
  82. }
  83. return nil
  84. }
  85. return ctx.Write(NewContainerContext(), render)
  86. }
  87. // ContainerContext is a struct used for rendering a list of containers in a Go template.
  88. type ContainerContext struct {
  89. formatter.HeaderContext
  90. trunc bool
  91. c api.ContainerSummary
  92. // FieldsUsed is used in the pre-processing step to detect which fields are
  93. // used in the template. It's currently only used to detect use of the .Size
  94. // field which (if used) automatically sets the '--size' option when making
  95. // the API call.
  96. FieldsUsed map[string]any
  97. }
  98. // NewContainerContext creates a new context for rendering containers
  99. func NewContainerContext() *ContainerContext {
  100. containerCtx := ContainerContext{}
  101. containerCtx.Header = formatter.SubHeaderContext{
  102. "ID": formatter.ContainerIDHeader,
  103. "Name": nameHeader,
  104. "Project": projectHeader,
  105. "Service": serviceHeader,
  106. "Image": formatter.ImageHeader,
  107. "Command": commandHeader,
  108. "CreatedAt": formatter.CreatedAtHeader,
  109. "RunningFor": runningForHeader,
  110. "Ports": formatter.PortsHeader,
  111. "State": formatter.StateHeader,
  112. "Status": formatter.StatusHeader,
  113. "Size": formatter.SizeHeader,
  114. "Labels": formatter.LabelsHeader,
  115. }
  116. return &containerCtx
  117. }
  118. // MarshalJSON makes ContainerContext implement json.Marshaler
  119. func (c *ContainerContext) MarshalJSON() ([]byte, error) {
  120. return formatter.MarshalJSON(c)
  121. }
  122. // ID returns the container's ID as a string. Depending on the `--no-trunc`
  123. // option being set, the full or truncated ID is returned.
  124. func (c *ContainerContext) ID() string {
  125. if c.trunc {
  126. return stringid.TruncateID(c.c.ID)
  127. }
  128. return c.c.ID
  129. }
  130. func (c *ContainerContext) Name() string {
  131. return c.c.Name
  132. }
  133. // Names returns a comma-separated string of the container's names, with their
  134. // slash (/) prefix stripped. Additional names for the container (related to the
  135. // legacy `--link` feature) are omitted.
  136. func (c *ContainerContext) Names() string {
  137. names := formatter.StripNamePrefix(c.c.Names)
  138. if c.trunc {
  139. for _, name := range names {
  140. if len(strings.Split(name, "/")) == 1 {
  141. names = []string{name}
  142. break
  143. }
  144. }
  145. }
  146. return strings.Join(names, ",")
  147. }
  148. func (c *ContainerContext) Service() string {
  149. return c.c.Service
  150. }
  151. func (c *ContainerContext) Project() string {
  152. return c.c.Project
  153. }
  154. func (c *ContainerContext) Image() string {
  155. return c.c.Image
  156. }
  157. func (c *ContainerContext) Command() string {
  158. command := c.c.Command
  159. if c.trunc {
  160. command = formatter.Ellipsis(command, 20)
  161. }
  162. return strconv.Quote(command)
  163. }
  164. func (c *ContainerContext) CreatedAt() string {
  165. return time.Unix(c.c.Created, 0).String()
  166. }
  167. func (c *ContainerContext) RunningFor() string {
  168. createdAt := time.Unix(c.c.Created, 0)
  169. return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
  170. }
  171. func (c *ContainerContext) ExitCode() int {
  172. return c.c.ExitCode
  173. }
  174. func (c *ContainerContext) State() string {
  175. return string(c.c.State)
  176. }
  177. func (c *ContainerContext) Status() string {
  178. return c.c.Status
  179. }
  180. func (c *ContainerContext) Health() string {
  181. return string(c.c.Health)
  182. }
  183. func (c *ContainerContext) Publishers() api.PortPublishers {
  184. return c.c.Publishers
  185. }
  186. func (c *ContainerContext) Ports() string {
  187. var ports []container.PortSummary
  188. for _, publisher := range c.c.Publishers {
  189. var pIP netip.Addr
  190. if publisher.URL != "" {
  191. if p, err := netip.ParseAddr(publisher.URL); err == nil {
  192. pIP = p
  193. }
  194. }
  195. ports = append(ports, container.PortSummary{
  196. IP: pIP,
  197. PrivatePort: uint16(publisher.TargetPort),
  198. PublicPort: uint16(publisher.PublishedPort),
  199. Type: publisher.Protocol,
  200. })
  201. }
  202. return formatter.DisplayablePorts(ports)
  203. }
  204. // Labels returns a comma-separated string of labels present on the container.
  205. func (c *ContainerContext) Labels() string {
  206. if c.c.Labels == nil {
  207. return ""
  208. }
  209. var joinLabels []string
  210. for k, v := range c.c.Labels {
  211. joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
  212. }
  213. return strings.Join(joinLabels, ",")
  214. }
  215. // Label returns the value of the label with the given name or an empty string
  216. // if the given label does not exist.
  217. func (c *ContainerContext) Label(name string) string {
  218. if c.c.Labels == nil {
  219. return ""
  220. }
  221. return c.c.Labels[name]
  222. }
  223. // Mounts returns a comma-separated string of mount names present on the container.
  224. // If the trunc option is set, names can be truncated (ellipsized).
  225. func (c *ContainerContext) Mounts() string {
  226. var mounts []string
  227. for _, name := range c.c.Mounts {
  228. if c.trunc {
  229. name = formatter.Ellipsis(name, 15)
  230. }
  231. mounts = append(mounts, name)
  232. }
  233. return strings.Join(mounts, ",")
  234. }
  235. // LocalVolumes returns the number of volumes using the "local" volume driver.
  236. func (c *ContainerContext) LocalVolumes() string {
  237. return fmt.Sprintf("%d", c.c.LocalVolumes)
  238. }
  239. // Networks returns a comma-separated string of networks that the container is
  240. // attached to.
  241. func (c *ContainerContext) Networks() string {
  242. return strings.Join(c.c.Networks, ",")
  243. }
  244. // Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
  245. func (c *ContainerContext) Size() string {
  246. if c.FieldsUsed == nil {
  247. c.FieldsUsed = map[string]any{}
  248. }
  249. c.FieldsUsed["Size"] = struct{}{}
  250. srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
  251. sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
  252. sf := srw
  253. if c.c.SizeRootFs > 0 {
  254. sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
  255. }
  256. return sf
  257. }