From 9dda1155f3acc1d6e7f2532d569758f6c11228d6 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Feb 2017 16:48:46 -0800 Subject: [PATCH] Allow `--format` to use different delim in `table` format This fix is an attempt to address https://github.com/docker/docker/pull/28213#issuecomment-273840405 Currently when specify table format with table `--format "table {{.ID}}..."`, the delimiter in the header section of the table is always `"\t"`. That is actually different from the content of the table as the delimiter could be anything (or even contatenated with `.`, for example): ``` $ docker service ps web --format 'table {{.Name}}.{{.ID}}' --no-trunc NAME ID web.1.inyhxhvjcijl0hdbu8lgrwwh7 \_ web.1.p9m4kx2srjqmfms4igam0uqlb ``` This fix is an attampt to address the skewness of the table when delimiter is not `"\t"`. The basic idea is that, when header consists of `table` key, the header section will be redendered the same way as content section. A map mapping each placeholder name to the HEADER entry name is used for the context of the header. Unit tests have been updated and added to cover the changes. This fix is related to #28313. Signed-off-by: Yong Tang --- command/formatter/container.go | 58 ++++++++++++++----------- command/formatter/container_test.go | 63 ++++++++++------------------ command/formatter/custom.go | 26 +++--------- command/formatter/disk_usage.go | 31 ++++++-------- command/formatter/disk_usage_test.go | 56 +++++++++++++++++++++++++ command/formatter/formatter.go | 14 +++---- command/formatter/image.go | 51 +++++++++++++++------- command/formatter/image_test.go | 26 +++++------- command/formatter/network.go | 37 +++++++++------- command/formatter/network_test.go | 26 +++++------- command/formatter/plugin.go | 15 ++++--- command/formatter/plugin_test.go | 14 ++----- command/formatter/service.go | 15 ++++--- command/formatter/stats.go | 33 ++++++++------- command/formatter/stats_test.go | 5 --- command/formatter/volume.go | 40 +++++++++++------- command/formatter/volume_test.go | 18 +++----- 17 files changed, 281 insertions(+), 247 deletions(-) create mode 100644 command/formatter/disk_usage_test.go diff --git a/command/formatter/container.go b/command/formatter/container.go index 6273453355..c8cb7b69e0 100644 --- a/command/formatter/container.go +++ b/command/formatter/container.go @@ -14,7 +14,7 @@ import ( ) const ( - defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" + defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" containerIDHeader = "CONTAINER ID" namesHeader = "NAMES" @@ -71,7 +71,17 @@ func ContainerWrite(ctx Context, containers []types.Container) error { } return nil } - return ctx.Write(&containerContext{}, render) + return ctx.Write(newContainerContext(), render) +} + +type containerHeaderContext map[string]string + +func (c containerHeaderContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + return h } type containerContext struct { @@ -80,12 +90,31 @@ type containerContext struct { c types.Container } +func newContainerContext() *containerContext { + containerCtx := containerContext{} + containerCtx.header = containerHeaderContext{ + "ID": containerIDHeader, + "Names": namesHeader, + "Image": imageHeader, + "Command": commandHeader, + "CreatedAt": createdAtHeader, + "RunningFor": runningForHeader, + "Ports": portsHeader, + "Status": statusHeader, + "Size": sizeHeader, + "Labels": labelsHeader, + "Mounts": mountsHeader, + "LocalVolumes": localVolumes, + "Networks": networksHeader, + } + return &containerCtx +} + func (c *containerContext) MarshalJSON() ([]byte, error) { return marshalJSON(c) } func (c *containerContext) ID() string { - c.AddHeader(containerIDHeader) if c.trunc { return stringid.TruncateID(c.c.ID) } @@ -93,7 +122,6 @@ func (c *containerContext) ID() string { } func (c *containerContext) Names() string { - c.AddHeader(namesHeader) names := stripNamePrefix(c.c.Names) if c.trunc { for _, name := range names { @@ -107,7 +135,6 @@ func (c *containerContext) Names() string { } func (c *containerContext) Image() string { - c.AddHeader(imageHeader) if c.c.Image == "" { return "" } @@ -120,7 +147,6 @@ func (c *containerContext) Image() string { } func (c *containerContext) Command() string { - c.AddHeader(commandHeader) command := c.c.Command if c.trunc { command = stringutils.Ellipsis(command, 20) @@ -129,28 +155,23 @@ func (c *containerContext) Command() string { } func (c *containerContext) CreatedAt() string { - c.AddHeader(createdAtHeader) return time.Unix(int64(c.c.Created), 0).String() } func (c *containerContext) RunningFor() string { - c.AddHeader(runningForHeader) createdAt := time.Unix(int64(c.c.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" } func (c *containerContext) Ports() string { - c.AddHeader(portsHeader) return api.DisplayablePorts(c.c.Ports) } func (c *containerContext) Status() string { - c.AddHeader(statusHeader) return c.c.Status } func (c *containerContext) Size() string { - c.AddHeader(sizeHeader) srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) @@ -162,7 +183,6 @@ func (c *containerContext) Size() string { } func (c *containerContext) Labels() string { - c.AddHeader(labelsHeader) if c.c.Labels == nil { return "" } @@ -175,12 +195,6 @@ func (c *containerContext) Labels() string { } func (c *containerContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - c.AddHeader(h) - if c.c.Labels == nil { return "" } @@ -188,8 +202,6 @@ func (c *containerContext) Label(name string) string { } func (c *containerContext) Mounts() string { - c.AddHeader(mountsHeader) - var name string var mounts []string for _, m := range c.c.Mounts { @@ -207,8 +219,6 @@ func (c *containerContext) Mounts() string { } func (c *containerContext) LocalVolumes() string { - c.AddHeader(localVolumes) - count := 0 for _, m := range c.c.Mounts { if m.Driver == "local" { @@ -220,8 +230,6 @@ func (c *containerContext) LocalVolumes() string { } func (c *containerContext) Networks() string { - c.AddHeader(networksHeader) - if c.c.NetworkSettings == nil { return "" } diff --git a/command/formatter/container_test.go b/command/formatter/container_test.go index f013328158..ef6e86c597 100644 --- a/command/formatter/container_test.go +++ b/command/formatter/container_test.go @@ -22,22 +22,20 @@ func TestContainerPsContext(t *testing.T) { container types.Container trunc bool expValue string - expHeader string call func() string }{ - {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID}, - {types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID}, - {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names}, - {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image}, - {types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image}, - {types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image}, + {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID}, + {types.Container{ID: containerID}, false, containerID, ctx.ID}, + {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names}, + {types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image}, + {types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image}, + {types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image}, {types.Container{ Image: "a5a665ff33eced1e0803148700880edab4", ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", }, true, "a5a665ff33ec", - imageHeader, ctx.Image, }, {types.Container{ @@ -46,19 +44,18 @@ func TestContainerPsContext(t *testing.T) { }, false, "a5a665ff33eced1e0803148700880edab4", - imageHeader, ctx.Image, }, - {types.Container{Image: ""}, true, "", imageHeader, ctx.Image}, - {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command}, - {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, - {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, - {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, - {types.Container{SizeRw: 10}, true, "10B", sizeHeader, ctx.Size}, - {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", sizeHeader, ctx.Size}, - {types.Container{}, true, "", labelsHeader, ctx.Labels}, - {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels}, - {types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor}, + {types.Container{Image: ""}, true, "", ctx.Image}, + {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command}, + {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt}, + {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports}, + {types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status}, + {types.Container{SizeRw: 10}, true, "10B", ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size}, + {types.Container{}, true, "", ctx.Labels}, + {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels}, + {types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor}, {types.Container{ Mounts: []types.MountPoint{ { @@ -67,7 +64,7 @@ func TestContainerPsContext(t *testing.T) { Source: "/a/path", }, }, - }, true, "this-is-a-lo...", mountsHeader, ctx.Mounts}, + }, true, "this-is-a-lo...", ctx.Mounts}, {types.Container{ Mounts: []types.MountPoint{ { @@ -75,7 +72,7 @@ func TestContainerPsContext(t *testing.T) { Source: "/a/path", }, }, - }, false, "/a/path", mountsHeader, ctx.Mounts}, + }, false, "/a/path", ctx.Mounts}, {types.Container{ Mounts: []types.MountPoint{ { @@ -84,7 +81,7 @@ func TestContainerPsContext(t *testing.T) { Source: "/a/path", }, }, - }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts}, + }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts}, } for _, c := range cases { @@ -95,11 +92,6 @@ func TestContainerPsContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} @@ -115,12 +107,6 @@ func TestContainerPsContext(t *testing.T) { t.Fatalf("Expected ubuntu, was %s\n", node) } - h := ctx.FullHeader() - if h != "SWARM ID\tNODE NAME" { - t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) - - } - c2 := types.Container{} ctx = containerContext{c: c2, trunc: true} @@ -128,13 +114,6 @@ func TestContainerPsContext(t *testing.T) { if label != "" { t.Fatalf("Expected an empty string, was %s", label) } - - ctx = containerContext{c: c2, trunc: true} - FullHeader := ctx.FullHeader() - if FullHeader != "" { - t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader) - } - } func TestContainerContextWrite(t *testing.T) { @@ -333,8 +312,8 @@ func TestContainerContextWriteJSON(t *testing.T) { } expectedCreated := time.Unix(unix, 0).String() expectedJSONs := []map[string]interface{}{ - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""}, - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, } out := bytes.NewBufferString("") err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) diff --git a/command/formatter/custom.go b/command/formatter/custom.go index df32684429..73487f63ef 100644 --- a/command/formatter/custom.go +++ b/command/formatter/custom.go @@ -1,9 +1,5 @@ package formatter -import ( - "strings" -) - const ( imageHeader = "IMAGE" createdSinceHeader = "CREATED" @@ -16,29 +12,17 @@ const ( ) type subContext interface { - FullHeader() string - AddHeader(header string) + FullHeader() interface{} } // HeaderContext provides the subContext interface for managing headers type HeaderContext struct { - header []string + header interface{} } -// FullHeader returns the header as a string -func (c *HeaderContext) FullHeader() string { - if c.header == nil { - return "" - } - return strings.Join(c.header, "\t") -} - -// AddHeader adds another column to the header -func (c *HeaderContext) AddHeader(header string) { - if c.header == nil { - c.header = []string{} - } - c.header = append(c.header, strings.ToUpper(header)) +// FullHeader returns the header as an interface +func (c *HeaderContext) FullHeader() interface{} { + return c.header } func stripNamePrefix(ss []string) []string { diff --git a/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index fd7aabc7c2..7170411e1b 100644 --- a/command/formatter/disk_usage.go +++ b/command/formatter/disk_usage.go @@ -77,7 +77,15 @@ func (ctx *DiskUsageContext) Write() { return } - ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}}) + diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}} + diskUsageContainersCtx.header = map[string]string{ + "Type": typeHeader, + "TotalCount": totalHeader, + "Active": activeHeader, + "Size": sizeHeader, + "Reclaimable": reclaimableHeader, + } + ctx.postFormat(tmpl, &diskUsageContainersCtx) return } @@ -114,7 +122,7 @@ func (ctx *DiskUsageContext) Write() { return } } - ctx.postFormat(tmpl, &imageContext{}) + ctx.postFormat(tmpl, newImageContext()) // Now containers ctx.Output.Write([]byte("\nContainers space usage:\n\n")) @@ -133,7 +141,7 @@ func (ctx *DiskUsageContext) Write() { return } } - ctx.postFormat(tmpl, &containerContext{}) + ctx.postFormat(tmpl, newContainerContext()) // And volumes ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n")) @@ -149,7 +157,7 @@ func (ctx *DiskUsageContext) Write() { return } } - ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}}) + ctx.postFormat(tmpl, newVolumeContext()) } type diskUsageImagesContext struct { @@ -163,17 +171,14 @@ func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { } func (c *diskUsageImagesContext) Type() string { - c.AddHeader(typeHeader) return "Images" } func (c *diskUsageImagesContext) TotalCount() string { - c.AddHeader(totalHeader) return fmt.Sprintf("%d", len(c.images)) } func (c *diskUsageImagesContext) Active() string { - c.AddHeader(activeHeader) used := 0 for _, i := range c.images { if i.Containers > 0 { @@ -185,7 +190,6 @@ func (c *diskUsageImagesContext) Active() string { } func (c *diskUsageImagesContext) Size() string { - c.AddHeader(sizeHeader) return units.HumanSize(float64(c.totalSize)) } @@ -193,7 +197,6 @@ func (c *diskUsageImagesContext) Size() string { func (c *diskUsageImagesContext) Reclaimable() string { var used int64 - c.AddHeader(reclaimableHeader) for _, i := range c.images { if i.Containers != 0 { if i.VirtualSize == -1 || i.SharedSize == -1 { @@ -221,12 +224,10 @@ func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { } func (c *diskUsageContainersContext) Type() string { - c.AddHeader(typeHeader) return "Containers" } func (c *diskUsageContainersContext) TotalCount() string { - c.AddHeader(totalHeader) return fmt.Sprintf("%d", len(c.containers)) } @@ -237,7 +238,6 @@ func (c *diskUsageContainersContext) isActive(container types.Container) bool { } func (c *diskUsageContainersContext) Active() string { - c.AddHeader(activeHeader) used := 0 for _, container := range c.containers { if c.isActive(*container) { @@ -251,7 +251,6 @@ func (c *diskUsageContainersContext) Active() string { func (c *diskUsageContainersContext) Size() string { var size int64 - c.AddHeader(sizeHeader) for _, container := range c.containers { size += container.SizeRw } @@ -263,7 +262,6 @@ func (c *diskUsageContainersContext) Reclaimable() string { var reclaimable int64 var totalSize int64 - c.AddHeader(reclaimableHeader) for _, container := range c.containers { if !c.isActive(*container) { reclaimable += container.SizeRw @@ -289,17 +287,14 @@ func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { } func (c *diskUsageVolumesContext) Type() string { - c.AddHeader(typeHeader) return "Local Volumes" } func (c *diskUsageVolumesContext) TotalCount() string { - c.AddHeader(totalHeader) return fmt.Sprintf("%d", len(c.volumes)) } func (c *diskUsageVolumesContext) Active() string { - c.AddHeader(activeHeader) used := 0 for _, v := range c.volumes { @@ -314,7 +309,6 @@ func (c *diskUsageVolumesContext) Active() string { func (c *diskUsageVolumesContext) Size() string { var size int64 - c.AddHeader(sizeHeader) for _, v := range c.volumes { if v.UsageData.Size != -1 { size += v.UsageData.Size @@ -328,7 +322,6 @@ func (c *diskUsageVolumesContext) Reclaimable() string { var reclaimable int64 var totalSize int64 - c.AddHeader(reclaimableHeader) for _, v := range c.volumes { if v.UsageData.Size != -1 { if v.UsageData.RefCount == 0 { diff --git a/command/formatter/disk_usage_test.go b/command/formatter/disk_usage_test.go new file mode 100644 index 0000000000..06d1c2c1fe --- /dev/null +++ b/command/formatter/disk_usage_test.go @@ -0,0 +1,56 @@ +package formatter + +import ( + "bytes" + //"encoding/json" + //"strings" + "testing" + //"time" + + //"github.com/docker/docker/api/types" + //"github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestDiskUsageContextFormatWrite(t *testing.T) { + // Check default output format (verbose and non-verbose mode) for table headers + cases := []struct { + context DiskUsageContext + expected string + }{ + { + DiskUsageContext{Verbose: false}, + `TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 0 0 0B 0B +Containers 0 0 0B 0B +Local Volumes 0 0 0B 0B +`, + }, + { + DiskUsageContext{Verbose: true}, + `Images space usage: + +REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS + +Containers space usage: + +CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES + +Local Volumes space usage: + +VOLUME NAME LINKS SIZE +`, + }, + } + + for _, testcase := range cases { + //networks := []types.NetworkResource{ + // {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, + // {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, + //} + out := bytes.NewBufferString("") + testcase.context.Output = out + testcase.context.Write() + assert.Equal(t, out.String(), testcase.expected) + } +} diff --git a/command/formatter/formatter.go b/command/formatter/formatter.go index 4345f7c3bc..16e8e6af2c 100644 --- a/command/formatter/formatter.go +++ b/command/formatter/formatter.go @@ -44,7 +44,7 @@ type Context struct { // internal element finalFormat string - header string + header interface{} buffer *bytes.Buffer } @@ -71,14 +71,10 @@ func (c *Context) parseFormat() (*template.Template, error) { func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { if c.Format.IsTable() { - 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)) + buffer := bytes.NewBufferString("") + tmpl.Execute(buffer, subContext.FullHeader()) + buffer.WriteTo(t) t.Write([]byte("\n")) c.buffer.WriteTo(t) t.Flush() @@ -91,7 +87,7 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) if err := tmpl.Execute(c.buffer, subContext); err != nil { return fmt.Errorf("Template parsing error: %v\n", err) } - if c.Format.IsTable() && len(c.header) == 0 { + if c.Format.IsTable() && c.header != nil { c.header = subContext.FullHeader() } c.buffer.WriteString("\n") diff --git a/command/formatter/image.go b/command/formatter/image.go index b6508224a3..8f18045c11 100644 --- a/command/formatter/image.go +++ b/command/formatter/image.go @@ -11,8 +11,8 @@ import ( ) const ( - 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}}" + defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" + defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" imageIDHeader = "IMAGE ID" repositoryHeader = "REPOSITORY" @@ -76,7 +76,21 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { render := func(format func(subContext subContext) error) error { return imageFormat(ctx, images, format) } - return ctx.Write(&imageContext{}, render) + imageCtx := imageContext{} + imageCtx.header = map[string]string{ + "ID": imageIDHeader, + "Repository": repositoryHeader, + "Tag": tagHeader, + "Digest": digestHeader, + "CreatedSince": createdSinceHeader, + "CreatedAt": createdAtHeader, + "Size": sizeHeader, + "Containers": containersHeader, + "VirtualSize": sizeHeader, + "SharedSize": sharedSizeHeader, + "UniqueSize": uniqueSizeHeader, + } + return ctx.Write(newImageContext(), render) } func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { @@ -192,12 +206,29 @@ type imageContext struct { digest string } +func newImageContext() *imageContext { + imageCtx := imageContext{} + imageCtx.header = map[string]string{ + "ID": imageIDHeader, + "Repository": repositoryHeader, + "Tag": tagHeader, + "Digest": digestHeader, + "CreatedSince": createdSinceHeader, + "CreatedAt": createdAtHeader, + "Size": sizeHeader, + "Containers": containersHeader, + "VirtualSize": sizeHeader, + "SharedSize": sharedSizeHeader, + "UniqueSize": uniqueSizeHeader, + } + return &imageCtx +} + func (c *imageContext) MarshalJSON() ([]byte, error) { return marshalJSON(c) } func (c *imageContext) ID() string { - c.AddHeader(imageIDHeader) if c.trunc { return stringid.TruncateID(c.i.ID) } @@ -205,38 +236,31 @@ func (c *imageContext) ID() string { } func (c *imageContext) Repository() string { - c.AddHeader(repositoryHeader) return c.repo } func (c *imageContext) Tag() string { - c.AddHeader(tagHeader) return c.tag } func (c *imageContext) Digest() string { - c.AddHeader(digestHeader) return c.digest } func (c *imageContext) CreatedSince() string { - c.AddHeader(createdSinceHeader) createdAt := time.Unix(int64(c.i.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" } func (c *imageContext) CreatedAt() string { - c.AddHeader(createdAtHeader) return time.Unix(int64(c.i.Created), 0).String() } func (c *imageContext) Size() string { - c.AddHeader(sizeHeader) return units.HumanSizeWithPrecision(float64(c.i.Size), 3) } func (c *imageContext) Containers() string { - c.AddHeader(containersHeader) if c.i.Containers == -1 { return "N/A" } @@ -244,12 +268,10 @@ func (c *imageContext) Containers() string { } func (c *imageContext) VirtualSize() string { - c.AddHeader(sizeHeader) return units.HumanSize(float64(c.i.VirtualSize)) } func (c *imageContext) SharedSize() string { - c.AddHeader(sharedSizeHeader) if c.i.SharedSize == -1 { return "N/A" } @@ -257,7 +279,6 @@ func (c *imageContext) SharedSize() string { } func (c *imageContext) UniqueSize() string { - c.AddHeader(uniqueSizeHeader) if c.i.VirtualSize == -1 || c.i.SharedSize == -1 { return "N/A" } diff --git a/command/formatter/image_test.go b/command/formatter/image_test.go index cf134300a1..e7c15dbf5a 100644 --- a/command/formatter/image_test.go +++ b/command/formatter/image_test.go @@ -18,27 +18,26 @@ func TestImageContext(t *testing.T) { var ctx imageContext cases := []struct { - imageCtx imageContext - expValue string - expHeader string - call func() string + imageCtx imageContext + expValue string + call func() string }{ {imageContext{ i: types.ImageSummary{ID: imageID}, trunc: true, - }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, + }, stringid.TruncateID(imageID), ctx.ID}, {imageContext{ i: types.ImageSummary{ID: imageID}, trunc: false, - }, imageID, imageIDHeader, ctx.ID}, + }, imageID, ctx.ID}, {imageContext{ i: types.ImageSummary{Size: 10, VirtualSize: 10}, trunc: true, - }, "10B", sizeHeader, ctx.Size}, + }, "10B", ctx.Size}, {imageContext{ i: types.ImageSummary{Created: unix}, trunc: true, - }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + }, time.Unix(unix, 0).String(), ctx.CreatedAt}, // FIXME // {imageContext{ // i: types.ImageSummary{Created: unix}, @@ -47,15 +46,15 @@ func TestImageContext(t *testing.T) { {imageContext{ i: types.ImageSummary{}, repo: "busybox", - }, "busybox", repositoryHeader, ctx.Repository}, + }, "busybox", ctx.Repository}, {imageContext{ i: types.ImageSummary{}, tag: "latest", - }, "latest", tagHeader, ctx.Tag}, + }, "latest", ctx.Tag}, {imageContext{ i: types.ImageSummary{}, digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", - }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, + }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest}, } for _, c := range cases { @@ -66,11 +65,6 @@ func TestImageContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } diff --git a/command/formatter/network.go b/command/formatter/network.go index c29be412aa..4aeebd1750 100644 --- a/command/formatter/network.go +++ b/command/formatter/network.go @@ -44,7 +44,28 @@ func NetworkWrite(ctx Context, networks []types.NetworkResource) error { } return nil } - return ctx.Write(&networkContext{}, render) + networkCtx := networkContext{} + networkCtx.header = networkHeaderContext{ + "ID": networkIDHeader, + "Name": nameHeader, + "Driver": driverHeader, + "Scope": scopeHeader, + "IPv6": ipv6Header, + "Internal": internalHeader, + "Labels": labelsHeader, + "CreatedAt": createdAtHeader, + } + return ctx.Write(&networkCtx, render) +} + +type networkHeaderContext map[string]string + +func (c networkHeaderContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + return h } type networkContext struct { @@ -58,7 +79,6 @@ func (c *networkContext) MarshalJSON() ([]byte, error) { } func (c *networkContext) ID() string { - c.AddHeader(networkIDHeader) if c.trunc { return stringid.TruncateID(c.n.ID) } @@ -66,32 +86,26 @@ func (c *networkContext) ID() string { } func (c *networkContext) Name() string { - c.AddHeader(nameHeader) return c.n.Name } func (c *networkContext) Driver() string { - c.AddHeader(driverHeader) return c.n.Driver } func (c *networkContext) Scope() string { - c.AddHeader(scopeHeader) return c.n.Scope } func (c *networkContext) IPv6() string { - c.AddHeader(ipv6Header) return fmt.Sprintf("%v", c.n.EnableIPv6) } func (c *networkContext) Internal() string { - c.AddHeader(internalHeader) return fmt.Sprintf("%v", c.n.Internal) } func (c *networkContext) Labels() string { - c.AddHeader(labelsHeader) if c.n.Labels == nil { return "" } @@ -104,12 +118,6 @@ func (c *networkContext) Labels() string { } func (c *networkContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - c.AddHeader(h) - if c.n.Labels == nil { return "" } @@ -117,6 +125,5 @@ func (c *networkContext) Label(name string) string { } func (c *networkContext) CreatedAt() string { - c.AddHeader(createdAtHeader) return c.n.Created.String() } diff --git a/command/formatter/network_test.go b/command/formatter/network_test.go index e105afbdf8..24bf46d256 100644 --- a/command/formatter/network_test.go +++ b/command/formatter/network_test.go @@ -19,41 +19,40 @@ func TestNetworkContext(t *testing.T) { cases := []struct { networkCtx networkContext expValue string - expHeader string call func() string }{ {networkContext{ n: types.NetworkResource{ID: networkID}, trunc: false, - }, networkID, networkIDHeader, ctx.ID}, + }, networkID, ctx.ID}, {networkContext{ n: types.NetworkResource{ID: networkID}, trunc: true, - }, stringid.TruncateID(networkID), networkIDHeader, ctx.ID}, + }, stringid.TruncateID(networkID), ctx.ID}, {networkContext{ n: types.NetworkResource{Name: "network_name"}, - }, "network_name", nameHeader, ctx.Name}, + }, "network_name", ctx.Name}, {networkContext{ n: types.NetworkResource{Driver: "driver_name"}, - }, "driver_name", driverHeader, ctx.Driver}, + }, "driver_name", ctx.Driver}, {networkContext{ n: types.NetworkResource{EnableIPv6: true}, - }, "true", ipv6Header, ctx.IPv6}, + }, "true", ctx.IPv6}, {networkContext{ n: types.NetworkResource{EnableIPv6: false}, - }, "false", ipv6Header, ctx.IPv6}, + }, "false", ctx.IPv6}, {networkContext{ n: types.NetworkResource{Internal: true}, - }, "true", internalHeader, ctx.Internal}, + }, "true", ctx.Internal}, {networkContext{ n: types.NetworkResource{Internal: false}, - }, "false", internalHeader, ctx.Internal}, + }, "false", ctx.Internal}, {networkContext{ n: types.NetworkResource{}, - }, "", labelsHeader, ctx.Labels}, + }, "", ctx.Labels}, {networkContext{ n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + }, "label1=value1,label2=value2", ctx.Labels}, } for _, c := range cases { @@ -64,11 +63,6 @@ func TestNetworkContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } diff --git a/command/formatter/plugin.go b/command/formatter/plugin.go index 00bdf3d0f4..2b71281a58 100644 --- a/command/formatter/plugin.go +++ b/command/formatter/plugin.go @@ -44,7 +44,15 @@ func PluginWrite(ctx Context, plugins []*types.Plugin) error { } return nil } - return ctx.Write(&pluginContext{}, render) + pluginCtx := pluginContext{} + pluginCtx.header = map[string]string{ + "ID": pluginIDHeader, + "Name": nameHeader, + "Description": descriptionHeader, + "Enabled": enabledHeader, + "PluginReference": imageHeader, + } + return ctx.Write(&pluginCtx, render) } type pluginContext struct { @@ -58,7 +66,6 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) { } func (c *pluginContext) ID() string { - c.AddHeader(pluginIDHeader) if c.trunc { return stringid.TruncateID(c.p.ID) } @@ -66,12 +73,10 @@ func (c *pluginContext) ID() string { } func (c *pluginContext) Name() string { - c.AddHeader(nameHeader) return c.p.Name } func (c *pluginContext) Description() string { - c.AddHeader(descriptionHeader) desc := strings.Replace(c.p.Config.Description, "\n", "", -1) desc = strings.Replace(desc, "\r", "", -1) if c.trunc { @@ -82,11 +87,9 @@ func (c *pluginContext) Description() string { } func (c *pluginContext) Enabled() bool { - c.AddHeader(enabledHeader) return c.p.Enabled } func (c *pluginContext) PluginReference() string { - c.AddHeader(imageHeader) return c.p.PluginReference } diff --git a/command/formatter/plugin_test.go b/command/formatter/plugin_test.go index a6c8f7e6c1..3cc0af8a3e 100644 --- a/command/formatter/plugin_test.go +++ b/command/formatter/plugin_test.go @@ -18,23 +18,22 @@ func TestPluginContext(t *testing.T) { cases := []struct { pluginCtx pluginContext expValue string - expHeader string call func() string }{ {pluginContext{ p: types.Plugin{ID: pluginID}, trunc: false, - }, pluginID, pluginIDHeader, ctx.ID}, + }, pluginID, ctx.ID}, {pluginContext{ p: types.Plugin{ID: pluginID}, trunc: true, - }, stringid.TruncateID(pluginID), pluginIDHeader, ctx.ID}, + }, stringid.TruncateID(pluginID), ctx.ID}, {pluginContext{ p: types.Plugin{Name: "plugin_name"}, - }, "plugin_name", nameHeader, ctx.Name}, + }, "plugin_name", ctx.Name}, {pluginContext{ p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, - }, "plugin_description", descriptionHeader, ctx.Description}, + }, "plugin_description", ctx.Description}, } for _, c := range cases { @@ -45,11 +44,6 @@ func TestPluginContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } diff --git a/command/formatter/service.go b/command/formatter/service.go index 8e38cb3a11..f7d78154ef 100644 --- a/command/formatter/service.go +++ b/command/formatter/service.go @@ -372,7 +372,15 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser } return nil } - return ctx.Write(&serviceContext{}, render) + serviceCtx := serviceContext{} + serviceCtx.header = map[string]string{ + "ID": serviceIDHeader, + "Name": nameHeader, + "Mode": modeHeader, + "Replicas": replicasHeader, + "Image": imageHeader, + } + return ctx.Write(&serviceCtx, render) } type serviceContext struct { @@ -387,27 +395,22 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) { } func (c *serviceContext) ID() string { - c.AddHeader(serviceIDHeader) return stringid.TruncateID(c.service.ID) } func (c *serviceContext) Name() string { - c.AddHeader(nameHeader) return c.service.Spec.Name } func (c *serviceContext) Mode() string { - c.AddHeader(modeHeader) return c.mode } func (c *serviceContext) Replicas() string { - c.AddHeader(replicasHeader) return c.replicas } func (c *serviceContext) Image() string { - c.AddHeader(imageHeader) image := c.service.Spec.TaskTemplate.ContainerSpec.Image if ref, err := reference.ParseNormalizedNamed(image); err == nil { // update image string for display, (strips any digest) diff --git a/command/formatter/stats.go b/command/formatter/stats.go index 750f57eb43..c0151101a0 100644 --- a/command/formatter/stats.go +++ b/command/formatter/stats.go @@ -129,7 +129,24 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string } return nil } - return ctx.Write(&containerStatsContext{os: osType}, render) + memUsage := memUseHeader + if osType == winOSType { + memUsage = winMemUseHeader + } + containerStatsCtx := containerStatsContext{} + containerStatsCtx.header = map[string]string{ + "Container": containerHeader, + "Name": nameHeader, + "ID": containerIDHeader, + "CPUPerc": cpuPercHeader, + "MemUsage": memUsage, + "MemPerc": memPercHeader, + "NetIO": netIOHeader, + "BlockIO": blockIOHeader, + "PIDs": pidsHeader, + } + containerStatsCtx.os = osType + return ctx.Write(&containerStatsCtx, render) } type containerStatsContext struct { @@ -143,12 +160,10 @@ func (c *containerStatsContext) MarshalJSON() ([]byte, error) { } func (c *containerStatsContext) Container() string { - c.AddHeader(containerHeader) return c.s.Container } func (c *containerStatsContext) Name() string { - c.AddHeader(nameHeader) if len(c.s.Name) > 1 { return c.s.Name[1:] } @@ -156,12 +171,10 @@ func (c *containerStatsContext) Name() string { } func (c *containerStatsContext) ID() string { - c.AddHeader(containerIDHeader) return c.s.ID } func (c *containerStatsContext) CPUPerc() string { - c.AddHeader(cpuPercHeader) if c.s.IsInvalid { return fmt.Sprintf("--") } @@ -169,11 +182,6 @@ func (c *containerStatsContext) CPUPerc() string { } func (c *containerStatsContext) MemUsage() string { - header := memUseHeader - if c.os == winOSType { - header = winMemUseHeader - } - c.AddHeader(header) if c.s.IsInvalid { return fmt.Sprintf("-- / --") } @@ -184,8 +192,6 @@ func (c *containerStatsContext) MemUsage() string { } func (c *containerStatsContext) MemPerc() string { - header := memPercHeader - c.AddHeader(header) if c.s.IsInvalid || c.os == winOSType { return fmt.Sprintf("--") } @@ -193,7 +199,6 @@ func (c *containerStatsContext) MemPerc() string { } func (c *containerStatsContext) NetIO() string { - c.AddHeader(netIOHeader) if c.s.IsInvalid { return fmt.Sprintf("--") } @@ -201,7 +206,6 @@ func (c *containerStatsContext) NetIO() string { } func (c *containerStatsContext) BlockIO() string { - c.AddHeader(blockIOHeader) if c.s.IsInvalid { return fmt.Sprintf("--") } @@ -209,7 +213,6 @@ func (c *containerStatsContext) BlockIO() string { } func (c *containerStatsContext) PIDs() string { - c.AddHeader(pidsHeader) if c.s.IsInvalid || c.os == winOSType { return fmt.Sprintf("--") } diff --git a/command/formatter/stats_test.go b/command/formatter/stats_test.go index 9f48862b2a..5d6a91e7c9 100644 --- a/command/formatter/stats_test.go +++ b/command/formatter/stats_test.go @@ -42,11 +42,6 @@ func TestContainerStatsContext(t *testing.T) { if v := te.call(); v != te.expValue { t.Fatalf("Expected %q, got %q", te.expValue, v) } - - h := ctx.FullHeader() - if h != te.expHeader { - t.Fatalf("Expected %q, got %q", te.expHeader, h) - } } } diff --git a/command/formatter/volume.go b/command/formatter/volume.go index 90c9b13536..342f2fb934 100644 --- a/command/formatter/volume.go +++ b/command/formatter/volume.go @@ -45,7 +45,17 @@ func VolumeWrite(ctx Context, volumes []*types.Volume) error { } return nil } - return ctx.Write(&volumeContext{}, render) + return ctx.Write(newVolumeContext(), render) +} + +type volumeHeaderContext map[string]string + +func (c volumeHeaderContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + return h } type volumeContext struct { @@ -53,32 +63,41 @@ type volumeContext struct { v types.Volume } +func newVolumeContext() *volumeContext { + volumeCtx := volumeContext{} + volumeCtx.header = volumeHeaderContext{ + "Name": volumeNameHeader, + "Driver": driverHeader, + "Scope": scopeHeader, + "Mountpoint": mountpointHeader, + "Labels": labelsHeader, + "Links": linksHeader, + "Size": sizeHeader, + } + return &volumeCtx +} + func (c *volumeContext) MarshalJSON() ([]byte, error) { return marshalJSON(c) } func (c *volumeContext) Name() string { - c.AddHeader(volumeNameHeader) return c.v.Name } func (c *volumeContext) Driver() string { - c.AddHeader(driverHeader) return c.v.Driver } func (c *volumeContext) Scope() string { - c.AddHeader(scopeHeader) return c.v.Scope } func (c *volumeContext) Mountpoint() string { - c.AddHeader(mountpointHeader) return c.v.Mountpoint } func (c *volumeContext) Labels() string { - c.AddHeader(labelsHeader) if c.v.Labels == nil { return "" } @@ -91,13 +110,6 @@ func (c *volumeContext) Labels() string { } func (c *volumeContext) Label(name string) string { - - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - c.AddHeader(h) - if c.v.Labels == nil { return "" } @@ -105,7 +117,6 @@ func (c *volumeContext) Label(name string) string { } func (c *volumeContext) Links() string { - c.AddHeader(linksHeader) if c.v.UsageData == nil { return "N/A" } @@ -113,7 +124,6 @@ func (c *volumeContext) Links() string { } func (c *volumeContext) Size() string { - c.AddHeader(sizeHeader) if c.v.UsageData == nil { return "N/A" } diff --git a/command/formatter/volume_test.go b/command/formatter/volume_test.go index 9ec18b6916..9c23ae447d 100644 --- a/command/formatter/volume_test.go +++ b/command/formatter/volume_test.go @@ -18,27 +18,26 @@ func TestVolumeContext(t *testing.T) { cases := []struct { volumeCtx volumeContext expValue string - expHeader string call func() string }{ {volumeContext{ v: types.Volume{Name: volumeName}, - }, volumeName, volumeNameHeader, ctx.Name}, + }, volumeName, ctx.Name}, {volumeContext{ v: types.Volume{Driver: "driver_name"}, - }, "driver_name", driverHeader, ctx.Driver}, + }, "driver_name", ctx.Driver}, {volumeContext{ v: types.Volume{Scope: "local"}, - }, "local", scopeHeader, ctx.Scope}, + }, "local", ctx.Scope}, {volumeContext{ v: types.Volume{Mountpoint: "mountpoint"}, - }, "mountpoint", mountpointHeader, ctx.Mountpoint}, + }, "mountpoint", ctx.Mountpoint}, {volumeContext{ v: types.Volume{}, - }, "", labelsHeader, ctx.Labels}, + }, "", ctx.Labels}, {volumeContext{ v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + }, "label1=value1,label2=value2", ctx.Labels}, } for _, c := range cases { @@ -49,11 +48,6 @@ func TestVolumeContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } }