Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions cli/command/formatter/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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, ",")
}

Expand Down
88 changes: 55 additions & 33 deletions cli/command/formatter/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,62 +371,53 @@ size: 0B
}

func TestContainerContextWriteWithNoContainers(t *testing.T) {
out := bytes.NewBufferString("")
containers := []container.Summary{}

cases := []struct {
context Context
expected string
}{
{
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",
},
}

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()
})
}
}
Expand Down Expand Up @@ -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))
})
}
}

Expand Down
4 changes: 2 additions & 2 deletions cli/command/formatter/disk_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion cli/command/formatter/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand Down Expand Up @@ -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()
Expand Down
71 changes: 43 additions & 28 deletions cli/command/inspect/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()}
Expand Down Expand Up @@ -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, "", " ")
},
Expand All @@ -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 {
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Loading