diff --git a/command/formatter/container.go b/command/formatter/container.go index e31611c1e7..9b5c24636c 100644 --- a/command/formatter/container.go +++ b/command/formatter/container.go @@ -15,7 +15,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" @@ -72,7 +72,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 { @@ -81,12 +91,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) } @@ -94,7 +123,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 { @@ -108,7 +136,6 @@ func (c *containerContext) Names() string { } func (c *containerContext) Image() string { - c.AddHeader(imageHeader) if c.c.Image == "" { return "" } @@ -136,7 +163,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) @@ -145,28 +171,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) @@ -178,7 +199,6 @@ func (c *containerContext) Size() string { } func (c *containerContext) Labels() string { - c.AddHeader(labelsHeader) if c.c.Labels == nil { return "" } @@ -191,12 +211,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 "" } @@ -204,8 +218,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 { @@ -223,8 +235,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" { @@ -236,8 +246,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..a5615d1768 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) { @@ -247,6 +226,14 @@ size: 0B Context{Format: NewContainerFormat("{{.Image}}", false, true)}, "ubuntu\nubuntu\n", }, + // Special headers for customerized table format + { + Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)}, + `CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS +conta "ubuntu" 24 hours ago//.FOOBAR_BAZ +conta "ubuntu" 24 hours ago//.FOOBAR_BAR +`, + }, } for _, testcase := range cases { @@ -333,8 +320,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..318e1692be --- /dev/null +++ b/command/formatter/disk_usage_test.go @@ -0,0 +1,47 @@ +package formatter + +import ( + "bytes" + "testing" + + "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 { + 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..a151e9c283 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.Funcs(templates.HeaderFunctions).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..3aae34ea11 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,7 @@ 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) + return ctx.Write(newImageContext(), render) } func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { @@ -192,12 +192,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 +222,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 +254,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 +265,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 b8b476dd66..421728976f 100644 --- a/command/formatter/service.go +++ b/command/formatter/service.go @@ -388,7 +388,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 { @@ -403,27 +411,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) - } } }