Преглед на файлове

Docker ps custom formatting.

Docker-DCO-1.1-Signed-off-by: David Calavera <david.calavera@gmail.com>
David Calavera преди 10 години
родител
ревизия
37209190c7
променени са 8 файла, в които са добавени 435 реда и са изтрити 134 реда
  1. 4 0
      api/client/cli.go
  2. 15 134
      api/client/ps.go
  3. 210 0
      api/client/ps/custom.go
  4. 68 0
      api/client/ps/custom_test.go
  5. 65 0
      api/client/ps/formatter.go
  6. 1 0
      cliconfig/config.go
  7. 31 0
      cliconfig/config_test.go
  8. 41 0
      man/docker-ps.1.md

+ 4 - 0
api/client/cli.go

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

+ 15 - 134
api/client/ps.go

@@ -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)
-	if *quiet {
-		*fields = "c"
-	}
-
-	if *size {
-		*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 {
-		for i, s := range ss {
-			ss[i] = s[1:]
-		}
-
-		return ss
-	}
-
-	type containerMeta struct {
-		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
-				}
-			}
+	f := *format
+	if len(f) == 0 {
+		if len(cli.PsFormat()) > 0 {
+			f = cli.PsFormat()
+		} else {
+			f = "table"
 		}
 		}
-		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"))
-		}
+	psCtx := ps.Context{
+		Output: cli.out,
+		Format: f,
+		Quiet:  *quiet,
+		Size:   *size,
+		Trunc:  !*noTrunc,
 	}
 	}
 
 
-	if !*quiet {
-		w.Flush()
-	}
+	ps.Format(psCtx, containers)
 
 
 	return nil
 	return nil
 }
 }

+ 210 - 0
api/client/ps/custom.go

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

+ 68 - 0
api/client/ps/custom_test.go

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

+ 65 - 0
api/client/ps/formatter.go

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

+ 1 - 0
cliconfig/config.go

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

+ 31 - 0
cliconfig/config_test.go

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

+ 41 - 0
man/docker-ps.1.md

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