Skip to content

Commit

Permalink
Merge pull request moby#30733 from yongtang/02022017-formatter-header
Browse files Browse the repository at this point in the history
Allow `--format` to use different delim in `table` format
  • Loading branch information
mlaventure authored Mar 3, 2017
2 parents cddb898 + 8b165ca commit 9369acc
Show file tree
Hide file tree
Showing 18 changed files with 288 additions and 247 deletions.
58 changes: 33 additions & 25 deletions cli/command/formatter/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -81,20 +91,38 @@ 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)
}
return c.c.ID
}

func (c *containerContext) Names() string {
c.AddHeader(namesHeader)
names := stripNamePrefix(c.c.Names)
if c.trunc {
for _, name := range names {
Expand All @@ -108,7 +136,6 @@ func (c *containerContext) Names() string {
}

func (c *containerContext) Image() string {
c.AddHeader(imageHeader)
if c.c.Image == "" {
return "<no image>"
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand All @@ -178,7 +199,6 @@ func (c *containerContext) Size() string {
}

func (c *containerContext) Labels() string {
c.AddHeader(labelsHeader)
if c.c.Labels == nil {
return ""
}
Expand All @@ -191,21 +211,13 @@ 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 ""
}
return c.c.Labels[name]
}

func (c *containerContext) Mounts() string {
c.AddHeader(mountsHeader)

var name string
var mounts []string
for _, m := range c.c.Mounts {
Expand All @@ -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" {
Expand All @@ -236,8 +246,6 @@ func (c *containerContext) LocalVolumes() string {
}

func (c *containerContext) Networks() string {
c.AddHeader(networksHeader)

if c.c.NetworkSettings == nil {
return ""
}
Expand Down
71 changes: 29 additions & 42 deletions cli/command/formatter/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
{
Expand All @@ -67,15 +64,15 @@ 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{
{
Driver: "local",
Source: "/a/path",
},
},
}, false, "/a/path", mountsHeader, ctx.Mounts},
}, false, "/a/path", ctx.Mounts},
{types.Container{
Mounts: []types.MountPoint{
{
Expand All @@ -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 {
Expand All @@ -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"}}
Expand All @@ -115,26 +107,13 @@ 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}

label := ctx.Label("anything.really")
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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 5 additions & 21 deletions cli/command/formatter/custom.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package formatter

import (
"strings"
)

const (
imageHeader = "IMAGE"
createdSinceHeader = "CREATED"
Expand All @@ -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
}

// FullHeader returns the header as a string
func (c *HeaderContext) FullHeader() string {
if c.header == nil {
return ""
}
return strings.Join(c.header, "\t")
header interface{}
}

// 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 {
Expand Down
Loading

0 comments on commit 9369acc

Please sign in to comment.