Docker ps custom formatting.

Docker-DCO-1.1-Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
David Calavera 2015-05-01 14:23:27 -07:00 committed by Phil Estes
parent de0e883331
commit 37209190c7
8 changed files with 435 additions and 134 deletions

View File

@ -179,6 +179,10 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
return nil return nil
} }
func (cli *DockerCli) PsFormat() string {
return cli.configFile.PsFormat
}
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
// The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config // The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config
// is set the client scheme will be set to https. // is set the client scheme will be set to https.

View File

@ -2,21 +2,14 @@ package client
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"text/tabwriter"
"time"
"github.com/docker/docker/api" "github.com/docker/docker/api/client/ps"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/parsers/filters" "github.com/docker/docker/pkg/parsers/filters"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/pkg/units"
) )
// CmdPs outputs a list of Docker containers. // CmdPs outputs a list of Docker containers.
@ -38,7 +31,7 @@ func (cli *DockerCli) CmdPs(args ...string) error {
since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running") since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running")
before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name") before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name")
last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running") last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running")
fields = cmd.String([]string{"-fields"}, "cimtspn", "Choose fields to print, and order (c,i,m,t,s,p,n,z)") format = cmd.String([]string{"F", "-format"}, "", "Pretty-print containers using a Go template")
flFilter = opts.NewListOpts(nil) flFilter = opts.NewListOpts(nil)
) )
cmd.Require(flag.Exact, 0) cmd.Require(flag.Exact, 0)
@ -99,136 +92,24 @@ func (cli *DockerCli) CmdPs(args ...string) error {
return err return err
} }
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) f := *format
if *quiet { if len(f) == 0 {
*fields = "c" if len(cli.PsFormat()) > 0 {
} f = cli.PsFormat()
} else {
if *size { f = "table"
*fields = *fields + "z"
}
if !*quiet {
headermap := map[rune]string{
'c': "CONTAINER ID",
'i': "IMAGE",
'm': "COMMAND",
's': "STATUS",
't': "CREATED",
'p': "PORTS",
'n': "NAMES",
'z': "SIZE",
}
headers := make([]string, 0)
for _, v := range *fields {
if title, ok := headermap[v]; ok {
headers = append(headers, title)
}
}
if len(headers) > 0 {
fmt.Fprint(w, strings.Join(headers, "\t")+"\n")
} }
} }
stripNamePrefix := func(ss []string) []string { psCtx := ps.Context{
for i, s := range ss { Output: cli.out,
ss[i] = s[1:] Format: f,
} Quiet: *quiet,
Size: *size,
return ss Trunc: !*noTrunc,
} }
type containerMeta struct { ps.Format(psCtx, containers)
c string
i string
m string
t string
s string
p string
n string
z string
}
var displayPort string
if container.HostConfig.NetworkMode == "host" {
displayPort = "*/tcp, */udp"
} else {
displayPort = api.DisplayablePorts(container.Ports)
}
outp := make([]containerMeta, 0)
for _, container := range containers {
next := containerMeta{
c: container.ID,
n: "",
m: strconv.Quote(container.Command),
i: container.Image,
t: units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))) + " ago",
s: container.Status,
p: displayPort,
z: fmt.Sprintf("%s", units.HumanSize(float64(container.SizeRw))),
}
// handle truncation
outNames := stripNamePrefix(container.Names)
if !*noTrunc {
next.c = stringid.TruncateID(next.c)
next.m = stringutils.Truncate(next.m, 20)
// only display the default name for the container with notrunc is passed
for _, name := range outNames {
if len(strings.Split(name, "/")) == 1 {
outNames = []string{name}
break
}
}
}
next.n = strings.Join(outNames, ",")
if next.i == "" {
next.i = "<no image>"
}
// handle rootfs sizing
if container.SizeRootFs > 0 {
next.z = next.z + fmt.Sprintf(" (virtual %s)", units.HumanSize(float64(container.SizeRootFs)))
}
outp = append(outp, next)
}
for _, out := range outp {
of := make([]string, 0)
for _, v := range *fields {
switch v {
case 'c':
of = append(of, out.c)
case 'i':
of = append(of, out.i)
case 'm':
of = append(of, out.m)
case 't':
of = append(of, out.t)
case 's':
of = append(of, out.s)
case 'p':
of = append(of, out.p)
case 'n':
of = append(of, out.n)
case 'z':
of = append(of, out.z)
}
}
if len(of) > 0 {
fmt.Fprintf(w, "%s\n", strings.Join(of, "\t"))
}
}
if !*quiet {
w.Flush()
}
return nil return nil
} }

210
api/client/ps/custom.go Normal file
View File

@ -0,0 +1,210 @@
package ps
import (
"bytes"
"fmt"
"strconv"
"strings"
"text/tabwriter"
"text/template"
"time"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/pkg/units"
)
const (
tableKey = "table"
idHeader = "CONTAINER ID"
imageHeader = "IMAGE"
namesHeader = "NAMES"
commandHeader = "COMMAND"
createdAtHeader = "CREATED AT"
runningForHeader = "CREATED"
statusHeader = "STATUS"
portsHeader = "PORTS"
sizeHeader = "SIZE"
labelsHeader = "LABELS"
)
type containerContext struct {
trunc bool
header []string
c types.Container
}
func (c *containerContext) ID() string {
c.addHeader(idHeader)
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 {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
}
}
}
return strings.Join(names, ",")
}
func (c *containerContext) Image() string {
c.addHeader(imageHeader)
if c.c.Image == "" {
return "<no image>"
}
return c.c.Image
}
func (c *containerContext) Command() string {
c.addHeader(commandHeader)
command := c.c.Command
if c.trunc {
command = stringutils.Truncate(command, 20)
}
return strconv.Quote(command)
}
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))
}
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.HumanSize(float64(c.c.SizeRw))
sv := units.HumanSize(float64(c.c.SizeRootFs))
sf := srw
if c.c.SizeRootFs > 0 {
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
}
return sf
}
func (c *containerContext) Labels() string {
c.addHeader(labelsHeader)
if c.c.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
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) fullHeader() string {
if c.header == nil {
return ""
}
return strings.Join(c.header, "\t")
}
func (c *containerContext) addHeader(header string) {
if c.header == nil {
c.header = []string{}
}
c.header = append(c.header, strings.ToUpper(header))
}
func customFormat(ctx Context, containers []types.Container) {
var (
table bool
header string
format = ctx.Format
buffer = bytes.NewBufferString("")
)
if strings.HasPrefix(ctx.Format, tableKey) {
table = true
format = format[len(tableKey):]
}
format = strings.Trim(format, " ")
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
format = r.Replace(format)
if table && ctx.Size {
format += "\t{{.Size}}"
}
tmpl, err := template.New("ps template").Parse(format)
if err != nil {
buffer.WriteString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err))
}
for _, container := range containers {
containerCtx := &containerContext{
trunc: ctx.Trunc,
c: container,
}
if err := tmpl.Execute(buffer, containerCtx); err != nil {
buffer = bytes.NewBufferString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err))
break
}
if table && len(header) == 0 {
header = containerCtx.fullHeader()
}
buffer.WriteString("\n")
}
if table {
t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0)
t.Write([]byte(header))
t.Write([]byte("\n"))
buffer.WriteTo(t)
t.Flush()
} else {
buffer.WriteTo(ctx.Output)
}
}
func stripNamePrefix(ss []string) []string {
for i, s := range ss {
ss[i] = s[1:]
}
return ss
}

View File

@ -0,0 +1,68 @@
package ps
import (
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
)
func TestContainerContextID(t *testing.T) {
containerId := stringid.GenerateRandomID()
unix := time.Now().Unix()
var ctx containerContext
cases := []struct {
container types.Container
trunc bool
expValue string
expHeader string
call func() string
}{
{types.Container{ID: containerId}, true, stringid.TruncateID(containerId), idHeader, 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: ""}, true, "<no image>", imageHeader, ctx.Image},
{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
{types.Container{Created: int(unix)}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
{types.Container{Ports: []types.Port{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, "10 B", sizeHeader, ctx.Size},
{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
}
for _, c := range cases {
ctx = containerContext{c: c.container, trunc: c.trunc}
v := c.call()
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)
}
}
c := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
ctx = containerContext{c: c, trunc: true}
sid := ctx.Label("com.docker.swarm.swarm-id")
node := ctx.Label("com.docker.swarm.node_name")
if sid != "33" {
t.Fatal("Expected 33, was %s\n", sid)
}
if node != "ubuntu" {
t.Fatal("Expected ubuntu, was %s\n", node)
}
h := ctx.fullHeader()
if h != "SWARM ID\tNODE NAME" {
t.Fatal("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
}
}

View File

@ -0,0 +1,65 @@
package ps
import (
"io"
"github.com/docker/docker/api/types"
)
const (
tableFormatKey = "table"
rawFormatKey = "raw"
defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
defaultQuietFormat = "{{.ID}}"
)
type Context struct {
Output io.Writer
Format string
Size bool
Quiet bool
Trunc bool
}
func Format(ctx Context, containers []types.Container) {
switch ctx.Format {
case tableFormatKey:
tableFormat(ctx, containers)
case rawFormatKey:
rawFormat(ctx, containers)
default:
customFormat(ctx, containers)
}
}
func rawFormat(ctx Context, containers []types.Container) {
if ctx.Quiet {
ctx.Format = `container_id: {{.ID}}`
} else {
ctx.Format = `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
status: {{.Status}}
names: {{.Names}}
labels: {{.Labels}}
ports: {{.Ports}}
`
if ctx.Size {
ctx.Format += `size: {{.Size}}
`
}
}
customFormat(ctx, containers)
}
func tableFormat(ctx Context, containers []types.Container) {
ctx.Format = defaultTableFormat
if ctx.Quiet {
ctx.Format = defaultQuietFormat
}
customFormat(ctx, containers)
}

View File

@ -57,6 +57,7 @@ type AuthConfig struct {
type ConfigFile struct { type ConfigFile struct {
AuthConfigs map[string]AuthConfig `json:"auths"` AuthConfigs map[string]AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
filename string // Note: not serialized - for internal use only filename string // Note: not serialized - for internal use only
} }

View File

@ -155,3 +155,34 @@ func TestNewJson(t *testing.T) {
t.Fatalf("Should have save in new form: %s", string(buf)) t.Fatalf("Should have save in new form: %s", string(buf))
} }
} }
func TestJsonWithPsFormat(t *testing.T) {
tmpHome, _ := ioutil.TempDir("", "config-test")
fn := filepath.Join(tmpHome, CONFIGFILE)
js := `{
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
}`
ioutil.WriteFile(fn, []byte(js), 0600)
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
}
// Now save it and make sure it shows up in new form
err = config.Save()
if err != nil {
t.Fatalf("Failed to save: %q", err)
}
buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
if !strings.Contains(string(buf), `"psFormat":`) ||
!strings.Contains(string(buf), "{{.ID}}") {
t.Fatalf("Should have save in new form: %s", string(buf))
}
}

View File

@ -16,6 +16,7 @@ docker-ps - List containers
[**-q**|**--quiet**[=*false*]] [**-q**|**--quiet**[=*false*]]
[**-s**|**--size**[=*false*]] [**-s**|**--size**[=*false*]]
[**--since**[=*SINCE*]] [**--since**[=*SINCE*]]
[**-F**|**--format**=*"TEMPLATE"*]
# DESCRIPTION # DESCRIPTION
@ -59,6 +60,20 @@ the running containers.
**--since**="" **--since**=""
Show only containers created since Id or Name, include non-running ones. Show only containers created since Id or Name, include non-running ones.
**-F**, **--format**=*"TEMPLATE"*
Pretty-print containers using a Go template.
Valid placeholders:
.ID - Container ID
.Image - Image ID
.Command - Quoted command
.CreatedAt - Time when the container was created.
.RunningFor - Elapsed time since the container was started.
.Ports - Exposed ports.
.Status - Container status.
.Size - Container disk size.
.Labels - All labels asigned to the container.
.Label - Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}`
# EXAMPLES # EXAMPLES
# Display all containers, including non-running # Display all containers, including non-running
@ -82,6 +97,32 @@ the running containers.
# docker ps -a -q --filter=name=determined_torvalds # docker ps -a -q --filter=name=determined_torvalds
c1d3b0166030 c1d3b0166030
# Display containers with their commands
# docker ps --format "{{.ID}}: {{.Command}}"
a87ecb4f327c: /bin/sh -c #(nop) MA
01946d9d34d8: /bin/sh -c #(nop) MA
c1d3b0166030: /bin/sh -c yum -y up
41d50ecd2f57: /bin/sh -c #(nop) MA
# Display containers with their labels in a table
# docker ps --format "table {{.ID}}\t{{.Labels}}"
CONTAINER ID LABELS
a87ecb4f327c com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd
01946d9d34d8
c1d3b0166030 com.docker.swarm.node=debian,com.docker.swarm.cpu=6
41d50ecd2f57 com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd
# Display containers with their node label in a table
# docker ps --format 'table {{.ID}}\t{{(.Label "com.docker.swarm.node")}}'
CONTAINER ID NODE
a87ecb4f327c ubuntu
01946d9d34d8
c1d3b0166030 debian
41d50ecd2f57 fedora
# HISTORY # HISTORY
April 2014, Originally compiled by William Henry (whenry at redhat dot com) April 2014, Originally compiled by William Henry (whenry at redhat dot com)
based on docker.com source material and internal work. based on docker.com source material and internal work.