Merge pull request #14699 from estesp/docker-ps-format

Carry #10255: Docker ps format
This commit is contained in:
David Calavera 2015-07-22 10:58:34 -07:00
commit 40b922418c
11 changed files with 534 additions and 86 deletions

View file

@ -179,6 +179,10 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
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.
// 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.

View file

@ -2,21 +2,14 @@ package client
import (
"encoding/json"
"fmt"
"net/url"
"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/opts"
flag "github.com/docker/docker/pkg/mflag"
"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.
@ -38,6 +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")
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")
format = cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template")
flFilter = opts.NewListOpts(nil)
)
cmd.Require(flag.Exact, 0)
@ -98,87 +92,24 @@ func (cli *DockerCli) CmdPs(args ...string) error {
return err
}
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
if !*quiet {
fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
if *size {
fmt.Fprintln(w, "\tSIZE")
f := *format
if len(f) == 0 {
if len(cli.PsFormat()) > 0 {
f = cli.PsFormat()
} else {
fmt.Fprint(w, "\n")
f = "table"
}
}
stripNamePrefix := func(ss []string) []string {
for i, s := range ss {
ss[i] = s[1:]
}
return ss
psCtx := ps.Context{
Output: cli.out,
Format: f,
Quiet: *quiet,
Size: *size,
Trunc: !*noTrunc,
}
for _, container := range containers {
ID := container.ID
if !*noTrunc {
ID = stringid.TruncateID(ID)
}
if *quiet {
fmt.Fprintln(w, ID)
continue
}
var (
names = stripNamePrefix(container.Names)
command = strconv.Quote(container.Command)
displayPort string
)
if !*noTrunc {
command = stringutils.Truncate(command, 20)
// only display the default name for the container with notrunc is passed
for _, name := range names {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
}
}
}
image := container.Image
if image == "" {
image = "<no image>"
}
if container.HostConfig.NetworkMode == "host" {
displayPort = "*/tcp, */udp"
} else {
displayPort = api.DisplayablePorts(container.Ports)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", ID, image, command,
units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))),
container.Status, displayPort, strings.Join(names, ","))
if *size {
if container.SizeRootFs > 0 {
fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(float64(container.SizeRw)), units.HumanSize(float64(container.SizeRootFs)))
} else {
fmt.Fprintf(w, "%s\n", units.HumanSize(float64(container.SizeRw)))
}
continue
}
fmt.Fprint(w, "\n")
}
if !*quiet {
w.Flush()
}
ps.Format(psCtx, containers)
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,88 @@
package ps
import (
"reflect"
"strings"
"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{{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 strings.Contains(v, ",") {
// comma-separated values means probably a map input, which won't
// be guaranteed to have the same order as our expected value
// We'll create maps and use reflect.DeepEquals to check instead:
entriesMap := make(map[string]string)
expMap := make(map[string]string)
entries := strings.Split(v, ",")
expectedEntries := strings.Split(c.expValue, ",")
for _, entry := range entries {
keyval := strings.Split(entry, "=")
entriesMap[keyval[0]] = keyval[1]
}
for _, expected := range expectedEntries {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
if !reflect.DeepEqual(expMap, entriesMap) {
t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
}
} 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)
}
}
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.Fatalf("Expected 33, was %s\n", sid)
}
if node != "ubuntu" {
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)
}
}

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 {
AuthConfigs map[string]AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
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))
}
}
func TestJsonWithPsFormat(t *testing.T) {
tmpHome, _ := ioutil.TempDir("", "config-test")
fn := filepath.Join(tmpHome, ConfigFileName)
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, ConfigFileName))
if !strings.Contains(string(buf), `"psFormat":`) ||
!strings.Contains(string(buf), "{{.ID}}") {
t.Fatalf("Should have save in new form: %s", string(buf))
}
}

View file

@ -85,18 +85,26 @@ mechanisms, you must keep in mind the order of precedence among them. Command
line options override environment variables and environment variables override
properties you specify in a `config.json` file.
The `config.json` file stores a JSON encoding of a single `HttpHeaders`
property. The property specifies a set of headers to include in all messages
The `config.json` file stores a JSON encoding of several properties:
The property `HttpHeaders` specifies a set of headers to include in all messages
sent from the Docker client to the daemon. Docker does not try to interpret or
understand these header; it simply puts them into the messages. Docker does
not allow these headers to change any headers it sets for itself.
The property `psFormat` specifies the default format for `docker ps` output.
When the `--format` flag is not provided with the `docker ps` command,
Docker's client uses this property. If this property is not set, the client
falls back to the default table format. For a list of supported formatting
directives, see the [**Formatting** section in the `docker ps` documentation](../ps)
Following is a sample `config.json` file:
{
"HttpHeaders: {
"MyHeader": "MyValue"
}
},
"psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
}
## Help

View file

@ -24,6 +24,7 @@ weight=1
-q, --quiet=false Only display numeric IDs
-s, --size=false Display total file sizes
--since="" Show created since Id or Name, include non-running
--format=[] Pretty-print containers using a Go template
Running `docker ps --no-trunc` showing 2 linked containers.
@ -60,5 +61,42 @@ The currently supported filters are:
This shows all the containers that have exited with status of '0'
## Formatting
The formatting option (`--format`) will pretty-print container output using a Go template.
Valid placeholders for the Go template are listed below:
Placeholder | Description
---- | ----
`.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"}}`
When using the `--format` option, the `ps` command will either output the data exactly as the template
declares or, when using the `table` directive, will include column headers as well.
The following example uses a template without headers and outputs the `ID` and `Command`
entries separated by a colon for all running containers:
$ 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
To list all running containers with their labels in a table format you can use:
$ 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

View file

@ -508,3 +508,34 @@ func (s *DockerSuite) TestPsListContainersFilterCreated(c *check.C) {
c.Fatalf("Expected id %s, got %s for filter, out: %s", cID, containerOut, out)
}
}
func (s *DockerSuite) TestPsFormatMultiNames(c *check.C) {
//create 2 containers and link them
dockerCmd(c, "run", "--name=child", "-d", "busybox", "top")
dockerCmd(c, "run", "--name=parent", "--link=child:linkedone", "-d", "busybox", "top")
//use the new format capabilities to only list the names and --no-trunc to get all names
out, _ := dockerCmd(c, "ps", "--format", "{{.Names}}", "--no-trunc")
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
expected := []string{"parent", "child,parent/linkedone"}
var names []string
for _, l := range lines {
names = append(names, l)
}
if !reflect.DeepEqual(expected, names) {
c.Fatalf("Expected array with non-truncated names: %v, got: %v", expected, names)
}
//now list without turning off truncation and make sure we only get the non-link names
out, _ = dockerCmd(c, "ps", "--format", "{{.Names}}")
lines = strings.Split(strings.TrimSpace(string(out)), "\n")
expected = []string{"parent", "child"}
var truncNames []string
for _, l := range lines {
truncNames = append(truncNames, l)
}
if !reflect.DeepEqual(expected, truncNames) {
c.Fatalf("Expected array with truncated names: %v, got: %v", expected, truncNames)
}
}

View file

@ -16,6 +16,7 @@ docker-ps - List containers
[**-q**|**--quiet**[=*false*]]
[**-s**|**--size**[=*false*]]
[**--since**[=*SINCE*]]
[**--format**=*"TEMPLATE"*]
# DESCRIPTION
@ -59,6 +60,20 @@ the running containers.
**--since**=""
Show only containers created since Id or Name, include non-running ones.
**--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
# Display all containers, including non-running
@ -82,6 +97,32 @@ the running containers.
# docker ps -a -q --filter=name=determined_torvalds
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
April 2014, Originally compiled by William Henry (whenry at redhat dot com)
based on docker.com source material and internal work.