mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
55cdb6dcd0
This fix addresses an issue where including the `{{.Size}}` format field in conjunction with `docker ps --format`, without the `--size` flag, would not correctly display the size of containers. This is done by doing a check on the custom format string, and setting the size flag on the options struct if the field is found. This struct gets passed to the engine API which then generates the correct query. An integration test is included which runs `docker ps --format "table {{.Size}}"` without `--size`, and checks that the returned output is not `0 B`. Fixes #21991 As suggested by @cpuguy83, a parser is implemented to process the format string as a template, and then traverses the template tree to determine if `.Size` was called. This was then reworked by making use of template execution with a pre-processor struct that will set the `--size` option if the template calls for the field. The pre-processor now also sets a boolean in the context passed to the writer. There is an integration test for this that calls `docker ps --size --format "{{.Size}}"` and then checks that `size: {{.Size}}` is not appended, as it would with previous behavior. Finally, a change was made to the formatter to not automatically add a `{{.Size}}` if a custom format is provided. Signed-off-by: Paulo Ribeiro <paigr.io@gmail.com> Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
247 lines
6.1 KiB
Go
247 lines
6.1 KiB
Go
package formatter
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"text/template"
|
|
|
|
"github.com/docker/docker/reference"
|
|
"github.com/docker/docker/utils/templates"
|
|
"github.com/docker/engine-api/types"
|
|
)
|
|
|
|
const (
|
|
tableFormatKey = "table"
|
|
rawFormatKey = "raw"
|
|
|
|
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
defaultQuietFormat = "{{.ID}}"
|
|
)
|
|
|
|
// Context contains information required by the formatter to print the output as desired.
|
|
type Context struct {
|
|
// Output is the output stream to which the formatted string is written.
|
|
Output io.Writer
|
|
// Format is used to choose raw, table or custom format for the output.
|
|
Format string
|
|
// Quiet when set to true will simply print minimal information.
|
|
Quiet bool
|
|
// Trunc when set to true will truncate the output of certain fields such as Container ID.
|
|
Trunc bool
|
|
|
|
// internal element
|
|
table bool
|
|
finalFormat string
|
|
header string
|
|
buffer *bytes.Buffer
|
|
}
|
|
|
|
func (c *Context) preformat() {
|
|
c.finalFormat = c.Format
|
|
|
|
if strings.HasPrefix(c.Format, tableKey) {
|
|
c.table = true
|
|
c.finalFormat = c.finalFormat[len(tableKey):]
|
|
}
|
|
|
|
c.finalFormat = strings.Trim(c.finalFormat, " ")
|
|
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
|
|
c.finalFormat = r.Replace(c.finalFormat)
|
|
}
|
|
|
|
func (c *Context) parseFormat() (*template.Template, error) {
|
|
tmpl, err := templates.Parse(c.finalFormat)
|
|
if err != nil {
|
|
c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
c.buffer.WriteTo(c.Output)
|
|
}
|
|
return tmpl, err
|
|
}
|
|
|
|
func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
|
if c.table {
|
|
if len(c.header) == 0 {
|
|
// 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
|
|
tmpl.Execute(bytes.NewBufferString(""), subContext)
|
|
c.header = subContext.fullHeader()
|
|
}
|
|
|
|
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
|
|
t.Write([]byte(c.header))
|
|
t.Write([]byte("\n"))
|
|
c.buffer.WriteTo(t)
|
|
t.Flush()
|
|
} else {
|
|
c.buffer.WriteTo(c.Output)
|
|
}
|
|
}
|
|
|
|
func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
|
|
if err := tmpl.Execute(c.buffer, subContext); err != nil {
|
|
c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
c.buffer.WriteTo(c.Output)
|
|
return err
|
|
}
|
|
if c.table && len(c.header) == 0 {
|
|
c.header = subContext.fullHeader()
|
|
}
|
|
c.buffer.WriteString("\n")
|
|
return nil
|
|
}
|
|
|
|
// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
|
|
type ContainerContext struct {
|
|
Context
|
|
// Size when set to true will display the size of the output.
|
|
Size bool
|
|
// Containers
|
|
Containers []types.Container
|
|
}
|
|
|
|
// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
|
|
type ImageContext struct {
|
|
Context
|
|
Digest bool
|
|
// Images
|
|
Images []types.Image
|
|
}
|
|
|
|
func (ctx ContainerContext) Write() {
|
|
switch ctx.Format {
|
|
case tableFormatKey:
|
|
if ctx.Quiet {
|
|
ctx.Format = defaultQuietFormat
|
|
} else {
|
|
ctx.Format = defaultContainerTableFormat
|
|
if ctx.Size {
|
|
ctx.Format += `\t{{.Size}}`
|
|
}
|
|
}
|
|
case rawFormatKey:
|
|
if ctx.Quiet {
|
|
ctx.Format = `container_id: {{.ID}}`
|
|
} else {
|
|
ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
|
if ctx.Size {
|
|
ctx.Format += `size: {{.Size}}\n`
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.buffer = bytes.NewBufferString("")
|
|
ctx.preformat()
|
|
|
|
tmpl, err := ctx.parseFormat()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, container := range ctx.Containers {
|
|
containerCtx := &containerContext{
|
|
trunc: ctx.Trunc,
|
|
c: container,
|
|
}
|
|
err = ctx.contextFormat(tmpl, containerCtx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.postformat(tmpl, &containerContext{})
|
|
}
|
|
|
|
func (ctx ImageContext) Write() {
|
|
switch ctx.Format {
|
|
case tableFormatKey:
|
|
ctx.Format = defaultImageTableFormat
|
|
if ctx.Digest {
|
|
ctx.Format = defaultImageTableFormatWithDigest
|
|
}
|
|
if ctx.Quiet {
|
|
ctx.Format = defaultQuietFormat
|
|
}
|
|
case rawFormatKey:
|
|
if ctx.Quiet {
|
|
ctx.Format = `image_id: {{.ID}}`
|
|
} else {
|
|
if ctx.Digest {
|
|
ctx.Format = `repository: {{ .Repository }}
|
|
tag: {{.Tag}}
|
|
digest: {{.Digest}}
|
|
image_id: {{.ID}}
|
|
created_at: {{.CreatedAt}}
|
|
virtual_size: {{.Size}}
|
|
`
|
|
} else {
|
|
ctx.Format = `repository: {{ .Repository }}
|
|
tag: {{.Tag}}
|
|
image_id: {{.ID}}
|
|
created_at: {{.CreatedAt}}
|
|
virtual_size: {{.Size}}
|
|
`
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.buffer = bytes.NewBufferString("")
|
|
ctx.preformat()
|
|
if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
|
ctx.finalFormat += "\t{{.Digest}}"
|
|
}
|
|
|
|
tmpl, err := ctx.parseFormat()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, image := range ctx.Images {
|
|
|
|
repoTags := image.RepoTags
|
|
repoDigests := image.RepoDigests
|
|
|
|
if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
|
|
// dangling image - clear out either repoTags or repoDigests so we only show it once below
|
|
repoDigests = []string{}
|
|
}
|
|
// combine the tags and digests lists
|
|
tagsAndDigests := append(repoTags, repoDigests...)
|
|
for _, repoAndRef := range tagsAndDigests {
|
|
repo := "<none>"
|
|
tag := "<none>"
|
|
digest := "<none>"
|
|
|
|
if !strings.HasPrefix(repoAndRef, "<none>") {
|
|
ref, err := reference.ParseNamed(repoAndRef)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
repo = ref.Name()
|
|
|
|
switch x := ref.(type) {
|
|
case reference.Canonical:
|
|
digest = x.Digest().String()
|
|
case reference.NamedTagged:
|
|
tag = x.Tag()
|
|
}
|
|
}
|
|
imageCtx := &imageContext{
|
|
trunc: ctx.Trunc,
|
|
i: image,
|
|
repo: repo,
|
|
tag: tag,
|
|
digest: digest,
|
|
}
|
|
err = ctx.contextFormat(tmpl, imageCtx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.postformat(tmpl, &imageContext{})
|
|
}
|