diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 4e5a1cd750..7e10091a8d 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -68,16 +68,14 @@ ports: {{- pad .Ports 1 0}} // ContainerWrite renders the context for a list of containers func ContainerWrite(ctx Context, containers []container.Summary) error { - render := func(format func(subContext SubContext) error) error { + return ctx.Write(NewContainerContext(), func(format func(subContext SubContext) error) error { for _, ctr := range containers { - err := format(&ContainerContext{trunc: ctx.Trunc, c: ctr}) - if err != nil { + if err := format(&ContainerContext{trunc: ctx.Trunc, c: ctr}); err != nil { return err } } return nil - } - return ctx.Write(NewContainerContext(), render) + }) } // ContainerContext is a struct used for rendering a list of containers in a Go template. @@ -256,6 +254,7 @@ func (c *ContainerContext) Labels() string { for k, v := range c.c.Labels { joinLabels = append(joinLabels, k+"="+v) } + sort.Strings(joinLabels) return strings.Join(joinLabels, ",") } diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 126991222a..584e29def8 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -371,9 +371,6 @@ size: 0B } func TestContainerContextWriteWithNoContainers(t *testing.T) { - out := bytes.NewBufferString("") - containers := []container.Summary{} - cases := []struct { context Context expected string @@ -381,40 +378,34 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) { { context: Context{ Format: "{{.Image}}", - Output: out, }, }, { context: Context{ Format: "table {{.Image}}", - Output: out, }, expected: "IMAGE\n", }, { context: Context{ Format: NewContainerFormat("{{.Image}}", false, true), - Output: out, }, }, { context: Context{ Format: NewContainerFormat("table {{.Image}}", false, true), - Output: out, }, expected: "IMAGE\n", }, { context: Context{ Format: "table {{.Image}}\t{{.Size}}", - Output: out, }, expected: "IMAGE SIZE\n", }, { context: Context{ Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true), - Output: out, }, expected: "IMAGE SIZE\n", }, @@ -422,11 +413,11 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) { for _, tc := range cases { t.Run(string(tc.context.Format), func(t *testing.T) { - err := ContainerWrite(tc.context, containers) + out := new(bytes.Buffer) + tc.context.Output = out + err := ContainerWrite(tc.context, nil) assert.NilError(t, err) assert.Equal(t, out.String(), tc.expected) - // Clean buffer - out.Reset() }) } } @@ -506,28 +497,59 @@ func TestContainerContextWriteJSONField(t *testing.T) { } func TestContainerBackCompat(t *testing.T) { - containers := []container.Summary{{ID: "brewhaha"}} - cases := []string{ - "ID", - "Names", - "Image", - "Command", - "CreatedAt", - "RunningFor", - "Ports", - "Status", - "Size", - "Labels", - "Mounts", + createdAtTime := time.Now().AddDate(-1, 0, 0) // 1 year ago + + ctrContext := container.Summary{ + ID: "aabbccddeeff", + Names: []string{"/foobar_baz"}, + Image: "docker.io/library/ubuntu", // should this have canonical format or not? + ImageID: "sha256:a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", // should this have algo-prefix or not? + ImageManifestDescriptor: nil, + Command: "/bin/sh", + Created: createdAtTime.UTC().Unix(), + Ports: []container.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}, + SizeRw: 123, + SizeRootFs: 12345, + Labels: map[string]string{"label1": "value1", "label2": "value2"}, + State: "running", + Status: "running", + HostConfig: struct { + NetworkMode string `json:",omitempty"` + Annotations map[string]string `json:",omitempty"` + }{ + NetworkMode: "bridge", + Annotations: map[string]string{ + "com.example.annotation": "hello", + }, + }, + NetworkSettings: nil, + Mounts: nil, } - buf := bytes.NewBuffer(nil) - for _, c := range cases { - ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf} - if err := ContainerWrite(ctx, containers); err != nil { - t.Logf("could not render template for field '%s': %v", c, err) - t.Fail() - } - buf.Reset() + + tests := []struct { + field string + expected string + }{ + {field: "ID", expected: "aabbccddeeff"}, + {field: "Names", expected: "foobar_baz"}, + {field: "Image", expected: "docker.io/library/ubuntu"}, + {field: "Command", expected: `"/bin/sh"`}, + {field: "CreatedAt", expected: time.Unix(createdAtTime.Unix(), 0).String()}, + {field: "RunningFor", expected: "12 months ago"}, + {field: "Ports", expected: "8080/tcp"}, + {field: "Status", expected: "running"}, + {field: "Size", expected: "123B (virtual 12.3kB)"}, + {field: "Labels", expected: "label1=value1,label2=value2"}, + {field: "Mounts", expected: ""}, + } + + for _, tc := range tests { + t.Run(tc.field, func(t *testing.T) { + buf := new(bytes.Buffer) + ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", tc.field)), Output: buf} + assert.NilError(t, ContainerWrite(ctx, []container.Summary{ctrContext})) + assert.Check(t, is.Equal(strings.TrimSpace(buf.String()), tc.expected)) + }) } } diff --git a/cli/command/formatter/disk_usage.go b/cli/command/formatter/disk_usage.go index f0fb4e1f2c..e815890ca2 100644 --- a/cli/command/formatter/disk_usage.go +++ b/cli/command/formatter/disk_usage.go @@ -43,7 +43,7 @@ type DiskUsageContext struct { } func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) { - ctx.buffer = bytes.NewBufferString("") + ctx.buffer = &bytes.Buffer{} ctx.header = "" ctx.Format = Format(format) ctx.preFormat() @@ -87,7 +87,7 @@ func (ctx *DiskUsageContext) Write() (err error) { if ctx.Verbose { return ctx.verboseWrite() } - ctx.buffer = bytes.NewBufferString("") + ctx.buffer = &bytes.Buffer{} ctx.preFormat() tmpl, err := ctx.parseFormat() diff --git a/cli/command/formatter/formatter.go b/cli/command/formatter/formatter.go index 760f77556d..7803cabe45 100644 --- a/cli/command/formatter/formatter.go +++ b/cli/command/formatter/formatter.go @@ -82,6 +82,9 @@ func (c *Context) parseFormat() (*template.Template, error) { } func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) { + if c.Output == nil { + c.Output = io.Discard + } if c.Format.IsTable() { t := tabwriter.NewWriter(c.Output, 10, 1, 3, ' ', 0) buffer := bytes.NewBufferString("") @@ -111,7 +114,7 @@ type SubFormat func(func(SubContext) error) error // Write the template to the buffer using this Context func (c *Context) Write(sub SubContext, f SubFormat) error { - c.buffer = bytes.NewBufferString("") + c.buffer = &bytes.Buffer{} c.preFormat() tmpl, err := c.parseFormat() diff --git a/cli/command/inspect/inspector.go b/cli/command/inspect/inspector.go index c7be1c85ba..dab48e6f3c 100644 --- a/cli/command/inspect/inspector.go +++ b/cli/command/inspect/inspector.go @@ -26,23 +26,29 @@ type Inspector interface { // TemplateInspector uses a text template to inspect elements. type TemplateInspector struct { - outputStream io.Writer - buffer *bytes.Buffer - tmpl *template.Template + out io.Writer + buffer *bytes.Buffer + tmpl *template.Template } // NewTemplateInspector creates a new inspector with a template. -func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector { +func NewTemplateInspector(out io.Writer, tmpl *template.Template) *TemplateInspector { + if out == nil { + out = io.Discard + } return &TemplateInspector{ - outputStream: outputStream, - buffer: new(bytes.Buffer), - tmpl: tmpl, + out: out, + buffer: new(bytes.Buffer), + tmpl: tmpl, } } // NewTemplateInspectorFromString creates a new TemplateInspector from a string // which is compiled into a template. func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) { + if out == nil { + return nil, errors.New("no output stream") + } if tmplStr == "" { return NewIndentedInspector(out), nil } @@ -65,6 +71,9 @@ type GetRefFunc func(ref string) (any, []byte, error) // Inspect fetches objects by reference using GetRefFunc and writes the json // representation to the output writer. func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error { + if out == nil { + return errors.New("no output stream") + } inspector, err := NewTemplateInspectorFromString(out, tmplStr) if err != nil { return cli.StatusError{StatusCode: 64, Status: err.Error()} @@ -138,18 +147,21 @@ func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { // Flush writes the result of inspecting all elements into the output stream. func (i *TemplateInspector) Flush() error { if i.buffer.Len() == 0 { - _, err := io.WriteString(i.outputStream, "\n") + _, err := io.WriteString(i.out, "\n") return err } - _, err := io.Copy(i.outputStream, i.buffer) + _, err := io.Copy(i.out, i.buffer) return err } // NewIndentedInspector generates a new inspector with an indented representation // of elements. -func NewIndentedInspector(outputStream io.Writer) Inspector { - return &elementsInspector{ - outputStream: outputStream, +func NewIndentedInspector(out io.Writer) Inspector { + if out == nil { + out = io.Discard + } + return &jsonInspector{ + out: out, raw: func(dst *bytes.Buffer, src []byte) error { return json.Indent(dst, src, "", " ") }, @@ -161,23 +173,26 @@ func NewIndentedInspector(outputStream io.Writer) Inspector { // NewJSONInspector generates a new inspector with a compact representation // of elements. -func NewJSONInspector(outputStream io.Writer) Inspector { - return &elementsInspector{ - outputStream: outputStream, - raw: json.Compact, - el: json.Marshal, +func NewJSONInspector(out io.Writer) Inspector { + if out == nil { + out = io.Discard + } + return &jsonInspector{ + out: out, + raw: json.Compact, + el: json.Marshal, } } -type elementsInspector struct { - outputStream io.Writer - elements []any - rawElements [][]byte - raw func(dst *bytes.Buffer, src []byte) error - el func(v any) ([]byte, error) +type jsonInspector struct { + out io.Writer + elements []any + rawElements [][]byte + raw func(dst *bytes.Buffer, src []byte) error + el func(v any) ([]byte, error) } -func (e *elementsInspector) Inspect(typedElement any, rawElement []byte) error { +func (e *jsonInspector) Inspect(typedElement any, rawElement []byte) error { if rawElement != nil { e.rawElements = append(e.rawElements, rawElement) } else { @@ -186,9 +201,9 @@ func (e *elementsInspector) Inspect(typedElement any, rawElement []byte) error { return nil } -func (e *elementsInspector) Flush() error { +func (e *jsonInspector) Flush() error { if len(e.elements) == 0 && len(e.rawElements) == 0 { - _, err := io.WriteString(e.outputStream, "[]\n") + _, err := io.WriteString(e.out, "[]\n") return err } @@ -216,9 +231,9 @@ func (e *elementsInspector) Flush() error { buffer = bytes.NewReader(b) } - if _, err := io.Copy(e.outputStream, buffer); err != nil { + if _, err := io.Copy(e.out, buffer); err != nil { return err } - _, err := io.WriteString(e.outputStream, "\n") + _, err := io.WriteString(e.out, "\n") return err }