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 <yong.tang.github@outlook.com>
This commit is contained in:
Yong Tang 2017-02-03 16:48:46 -08:00
parent 584e399d96
commit ea61dac9e6
17 changed files with 281 additions and 247 deletions

View file

@ -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 "<no image>"
}
@ -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 ""
}

View file

@ -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, "<no image>", 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, "<no image>", 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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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")

View file

@ -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"
}

View file

@ -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)
}
}
}

View file

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

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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("--")
}

View file

@ -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)
}
}
}

View file

@ -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"
}

View file

@ -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)
}
}
}