diff --git a/api/client/cli.go b/api/client/cli.go index 4f02d0bae2..1686e28751 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -118,18 +118,6 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { return nil } -// PsFormat returns the format string specified in the configuration. -// String contains columns and format specification, for example {{ID}}\t{{Name}}. -func (cli *DockerCli) PsFormat() string { - return cli.configFile.PsFormat -} - -// ImagesFormat returns the format string specified in the configuration. -// String contains columns and format specification, for example {{ID}}\t{{Name}}. -func (cli *DockerCli) ImagesFormat() string { - return cli.configFile.ImagesFormat -} - func (cli *DockerCli) setRawTerminal() error { if os.Getenv("NORAW") == "" { if cli.isTerminalIn { diff --git a/api/client/container/ps.go b/api/client/container/ps.go index e4624b17d8..aab910f7bc 100644 --- a/api/client/container/ps.go +++ b/api/client/container/ps.go @@ -101,8 +101,8 @@ func runPs(dockerCli *client.DockerCli, opts *psOptions) error { f := opts.format if len(f) == 0 { - if len(dockerCli.PsFormat()) > 0 && !opts.quiet { - f = dockerCli.PsFormat() + if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().PsFormat } else { f = "table" } diff --git a/api/client/formatter/container.go b/api/client/formatter/container.go new file mode 100644 index 0000000000..4903b4ef80 --- /dev/null +++ b/api/client/formatter/container.go @@ -0,0 +1,208 @@ +package formatter + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/engine-api/types" + "github.com/docker/go-units" +) + +const ( + defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" + + containerIDHeader = "CONTAINER ID" + namesHeader = "NAMES" + commandHeader = "COMMAND" + runningForHeader = "CREATED" + statusHeader = "STATUS" + portsHeader = "PORTS" + mountsHeader = "MOUNTS" +) + +// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. +type ContainerContext struct { + Context + // Size when set to true will display the size of the output. + Size bool + // Containers + Containers []types.Container +} + +func (ctx ContainerContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } else { + ctx.Format = defaultContainerTableFormat + if ctx.Size { + ctx.Format += `\t{{.Size}}` + } + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `container_id: {{.ID}}` + } else { + ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` + if ctx.Size { + ctx.Format += `size: {{.Size}}\n` + } + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, container := range ctx.Containers { + containerCtx := &containerContext{ + trunc: ctx.Trunc, + c: container, + } + err = ctx.contextFormat(tmpl, containerCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &containerContext{}) +} + +type containerContext struct { + baseSubContext + trunc bool + c types.Container +} + +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 { + 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 "" + } + if c.trunc { + if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { + return trunc + } + } + 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) Mounts() string { + c.addHeader(mountsHeader) + + var name string + var mounts []string + for _, m := range c.c.Mounts { + if m.Name == "" { + name = m.Source + } else { + name = m.Name + } + if c.trunc { + name = stringutils.Truncate(name, 15) + } + mounts = append(mounts, name) + } + return strings.Join(mounts, ",") +} diff --git a/api/client/formatter/container_test.go b/api/client/formatter/container_test.go new file mode 100644 index 0000000000..c5f45ef6ff --- /dev/null +++ b/api/client/formatter/container_test.go @@ -0,0 +1,404 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + "testing" + "time" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" +) + +func TestContainerPsContext(t *testing.T) { + containerID := stringid.GenerateRandomID() + unix := time.Now().Add(-65 * time.Second).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), 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{ + Image: "a5a665ff33eced1e0803148700880edab4", + ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", + }, + true, + "a5a665ff33ec", + imageHeader, + ctx.Image, + }, + {types.Container{ + Image: "a5a665ff33eced1e0803148700880edab4", + ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", + }, + false, + "a5a665ff33eced1e0803148700880edab4", + imageHeader, + ctx.Image, + }, + {types.Container{Image: ""}, true, "", 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, "10 B", sizeHeader, ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", 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{ + Mounts: []types.MountPoint{ + { + Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", + Driver: "local", + Source: "/a/path", + }, + }, + }, true, "733908409c91817", mountsHeader, ctx.Mounts}, + {types.Container{ + Mounts: []types.MountPoint{ + { + Driver: "local", + Source: "/a/path", + }, + }, + }, false, "/a/path", mountsHeader, ctx.Mounts}, + {types.Container{ + Mounts: []types.MountPoint{ + { + Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", + Driver: "local", + Source: "/a/path", + }, + }, + }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts}, + } + + for _, c := range cases { + ctx = containerContext{c: c.container, trunc: c.trunc} + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } 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"}} + ctx = containerContext{c: c1, 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) + + } + + 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) { + unixTime := time.Now().AddDate(0, 0, -1).Unix() + expectedTime := time.Unix(unixTime, 0).String() + + contexts := []struct { + context ContainerContext + expected string + }{ + // Errors + { + ContainerContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table Format + { + ContainerContext{ + Context: Context{ + Format: "table", + }, + Size: true, + }, + `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE +containerID1 ubuntu "" 24 hours ago foobar_baz 0 B +containerID2 ubuntu "" 24 hours ago foobar_bar 0 B +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "table", + }, + }, + `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +containerID1 ubuntu "" 24 hours ago foobar_baz +containerID2 ubuntu "" 24 hours ago foobar_bar +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + }, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + }, + Size: true, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Quiet: true, + }, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + "containerID1\ncontainerID2\n", + }, + // Raw Format + { + ContainerContext{ + Context: Context{ + Format: "raw", + }, + }, + fmt.Sprintf(`container_id: containerID1 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_baz +labels: +ports: + +container_id: containerID2 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_bar +labels: +ports: + +`, expectedTime, expectedTime), + }, + { + ContainerContext{ + Context: Context{ + Format: "raw", + }, + Size: true, + }, + fmt.Sprintf(`container_id: containerID1 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_baz +labels: +ports: +size: 0 B + +container_id: containerID2 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_bar +labels: +ports: +size: 0 B + +`, expectedTime, expectedTime), + }, + { + ContainerContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + "container_id: containerID1\ncontainer_id: containerID2\n", + }, + // Custom Format + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + }, + }, + "ubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + }, + Size: true, + }, + "ubuntu\nubuntu\n", + }, + } + + for _, context := range contexts { + containers := []types.Container{ + {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime}, + {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Containers = containers + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} + +func TestContainerContextWriteWithNoContainers(t *testing.T) { + out := bytes.NewBufferString("") + containers := []types.Container{} + + contexts := []struct { + context ContainerContext + expected string + }{ + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + Output: out, + }, + }, + "", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Output: out, + }, + }, + "IMAGE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + Output: out, + }, + Size: true, + }, + "", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Output: out, + }, + Size: true, + }, + "IMAGE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}\t{{.Size}}", + Output: out, + }, + }, + "IMAGE SIZE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}\t{{.Size}}", + Output: out, + }, + Size: true, + }, + "IMAGE SIZE\n", + }, + } + + for _, context := range contexts { + context.context.Containers = containers + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/api/client/formatter/custom.go b/api/client/formatter/custom.go index 079a71cff8..2aa2e7b554 100644 --- a/api/client/formatter/custom.go +++ b/api/client/formatter/custom.go @@ -1,215 +1,22 @@ package formatter import ( - "fmt" - "strconv" "strings" - "time" - - "github.com/docker/docker/api" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - "github.com/docker/engine-api/types" - "github.com/docker/go-units" ) const ( tableKey = "table" - containerIDHeader = "CONTAINER ID" imageHeader = "IMAGE" - namesHeader = "NAMES" - commandHeader = "COMMAND" createdSinceHeader = "CREATED" createdAtHeader = "CREATED AT" - runningForHeader = "CREATED" - statusHeader = "STATUS" - portsHeader = "PORTS" sizeHeader = "SIZE" labelsHeader = "LABELS" - imageIDHeader = "IMAGE ID" - repositoryHeader = "REPOSITORY" - tagHeader = "TAG" - digestHeader = "DIGEST" - mountsHeader = "MOUNTS" + nameHeader = "NAME" + driverHeader = "DRIVER" + scopeHeader = "SCOPE" ) -type containerContext struct { - baseSubContext - trunc bool - c types.Container -} - -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 { - 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 "" - } - if c.trunc { - if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { - return trunc - } - } - 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) Mounts() string { - c.addHeader(mountsHeader) - - var name string - var mounts []string - for _, m := range c.c.Mounts { - if m.Name == "" { - name = m.Source - } else { - name = m.Name - } - if c.trunc { - name = stringutils.Truncate(name, 15) - } - mounts = append(mounts, name) - } - return strings.Join(mounts, ",") -} - -type imageContext struct { - baseSubContext - trunc bool - i types.Image - repo string - tag string - digest string -} - -func (c *imageContext) ID() string { - c.addHeader(imageIDHeader) - if c.trunc { - return stringid.TruncateID(c.i.ID) - } - return c.i.ID -} - -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)) -} - -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.HumanSize(float64(c.i.Size)) -} - type subContext interface { fullHeader() string addHeader(header string) diff --git a/api/client/formatter/custom_test.go b/api/client/formatter/custom_test.go index 6a21f2bcd4..da42039dca 100644 --- a/api/client/formatter/custom_test.go +++ b/api/client/formatter/custom_test.go @@ -4,172 +4,8 @@ import ( "reflect" "strings" "testing" - "time" - - "github.com/docker/docker/pkg/stringid" - "github.com/docker/engine-api/types" ) -func TestContainerPsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() - unix := time.Now().Add(-65 * time.Second).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), 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{ - Image: "a5a665ff33eced1e0803148700880edab4", - ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", - }, - true, - "a5a665ff33ec", - imageHeader, - ctx.Image, - }, - {types.Container{ - Image: "a5a665ff33eced1e0803148700880edab4", - ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", - }, - false, - "a5a665ff33eced1e0803148700880edab4", - imageHeader, - ctx.Image, - }, - {types.Container{Image: ""}, true, "", 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, "10 B", sizeHeader, ctx.Size}, - {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", 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}, - } - - for _, c := range cases { - ctx = containerContext{c: c.container, trunc: c.trunc} - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } 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"}} - ctx = containerContext{c: c1, 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) - - } - - 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 TestImagesContext(t *testing.T) { - imageID := stringid.GenerateRandomID() - unix := time.Now().Unix() - - var ctx imageContext - cases := []struct { - imageCtx imageContext - expValue string - expHeader string - call func() string - }{ - {imageContext{ - i: types.Image{ID: imageID}, - trunc: true, - }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, - {imageContext{ - i: types.Image{ID: imageID}, - trunc: false, - }, imageID, imageIDHeader, ctx.ID}, - {imageContext{ - i: types.Image{Size: 10}, - trunc: true, - }, "10 B", sizeHeader, ctx.Size}, - {imageContext{ - i: types.Image{Created: unix}, - trunc: true, - }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, - // FIXME - // {imageContext{ - // i: types.Image{Created: unix}, - // trunc: true, - // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, - {imageContext{ - i: types.Image{}, - repo: "busybox", - }, "busybox", repositoryHeader, ctx.Repository}, - {imageContext{ - i: types.Image{}, - tag: "latest", - }, "latest", tagHeader, ctx.Tag}, - {imageContext{ - i: types.Image{}, - digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", - }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, - } - - for _, c := range cases { - ctx = c.imageCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } 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) - } - } -} - func compareMultipleValues(t *testing.T, value, expected string) { // comma-separated values means probably a map input, which won't // be guaranteed to have the same order as our expected value diff --git a/api/client/formatter/formatter.go b/api/client/formatter/formatter.go index 1e250a2522..de71c3cdd4 100644 --- a/api/client/formatter/formatter.go +++ b/api/client/formatter/formatter.go @@ -8,19 +8,14 @@ import ( "text/tabwriter" "text/template" - "github.com/docker/docker/reference" "github.com/docker/docker/utils/templates" - "github.com/docker/engine-api/types" ) const ( tableFormatKey = "table" rawFormatKey = "raw" - defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" - 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}}" - defaultQuietFormat = "{{.ID}}" + defaultQuietFormat = "{{.ID}}" ) // Context contains information required by the formatter to print the output as desired. @@ -93,215 +88,3 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) c.buffer.WriteString("\n") return nil } - -// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. -type ContainerContext struct { - Context - // Size when set to true will display the size of the output. - Size bool - // Containers - Containers []types.Container -} - -// ImageContext contains image specific information required by the formater, encapsulate a Context struct. -type ImageContext struct { - Context - Digest bool - // Images - Images []types.Image -} - -func (ctx ContainerContext) Write() { - switch ctx.Format { - case tableFormatKey: - if ctx.Quiet { - ctx.Format = defaultQuietFormat - } else { - ctx.Format = defaultContainerTableFormat - if ctx.Size { - ctx.Format += `\t{{.Size}}` - } - } - case rawFormatKey: - if ctx.Quiet { - ctx.Format = `container_id: {{.ID}}` - } else { - ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` - if ctx.Size { - ctx.Format += `size: {{.Size}}\n` - } - } - } - - ctx.buffer = bytes.NewBufferString("") - ctx.preformat() - - tmpl, err := ctx.parseFormat() - if err != nil { - return - } - - for _, container := range ctx.Containers { - containerCtx := &containerContext{ - trunc: ctx.Trunc, - c: container, - } - err = ctx.contextFormat(tmpl, containerCtx) - if err != nil { - return - } - } - - ctx.postformat(tmpl, &containerContext{}) -} - -func isDangling(image types.Image) bool { - return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" -} - -func (ctx ImageContext) Write() { - switch ctx.Format { - case tableFormatKey: - ctx.Format = defaultImageTableFormat - if ctx.Digest { - ctx.Format = defaultImageTableFormatWithDigest - } - if ctx.Quiet { - ctx.Format = defaultQuietFormat - } - case rawFormatKey: - if ctx.Quiet { - ctx.Format = `image_id: {{.ID}}` - } else { - if ctx.Digest { - ctx.Format = `repository: {{ .Repository }} -tag: {{.Tag}} -digest: {{.Digest}} -image_id: {{.ID}} -created_at: {{.CreatedAt}} -virtual_size: {{.Size}} -` - } else { - ctx.Format = `repository: {{ .Repository }} -tag: {{.Tag}} -image_id: {{.ID}} -created_at: {{.CreatedAt}} -virtual_size: {{.Size}} -` - } - } - } - - ctx.buffer = bytes.NewBufferString("") - ctx.preformat() - if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") { - ctx.finalFormat += "\t{{.Digest}}" - } - - tmpl, err := ctx.parseFormat() - if err != nil { - return - } - - for _, image := range ctx.Images { - images := []*imageContext{} - if isDangling(image) { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: "", - tag: "", - digest: "", - }) - } else { - repoTags := map[string][]string{} - repoDigests := map[string][]string{} - - for _, refString := range append(image.RepoTags) { - ref, err := reference.ParseNamed(refString) - if err != nil { - continue - } - if nt, ok := ref.(reference.NamedTagged); ok { - repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) - } - } - for _, refString := range append(image.RepoDigests) { - ref, err := reference.ParseNamed(refString) - if err != nil { - continue - } - if c, ok := ref.(reference.Canonical); ok { - repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) - } - } - - for repo, tags := range repoTags { - digests := repoDigests[repo] - - // Do not display digests as their own row - delete(repoDigests, repo) - - if !ctx.Digest { - // Ignore digest references, just show tag once - digests = nil - } - - for _, tag := range tags { - if len(digests) == 0 { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: "", - }) - continue - } - // Display the digests for each tag - for _, dgst := range digests { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: dgst, - }) - } - - } - } - - // Show rows for remaining digest only references - for repo, digests := range repoDigests { - // If digests are displayed, show row per digest - if ctx.Digest { - for _, dgst := range digests { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: "", - digest: dgst, - }) - } - } else { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: "", - }) - } - } - } - for _, imageCtx := range images { - err = ctx.contextFormat(tmpl, imageCtx) - if err != nil { - return - } - } - } - - ctx.postformat(tmpl, &imageContext{}) -} diff --git a/api/client/formatter/image.go b/api/client/formatter/image.go new file mode 100644 index 0000000000..04cd4b75fd --- /dev/null +++ b/api/client/formatter/image.go @@ -0,0 +1,229 @@ +package formatter + +import ( + "bytes" + "strings" + "time" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + "github.com/docker/go-units" +) + +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}}" + + imageIDHeader = "IMAGE ID" + repositoryHeader = "REPOSITORY" + tagHeader = "TAG" + digestHeader = "DIGEST" +) + +// ImageContext contains image specific information required by the formater, encapsulate a Context struct. +type ImageContext struct { + Context + Digest bool + // Images + Images []types.Image +} + +func isDangling(image types.Image) bool { + return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" +} + +func (ctx ImageContext) Write() { + switch ctx.Format { + case tableFormatKey: + ctx.Format = defaultImageTableFormat + if ctx.Digest { + ctx.Format = defaultImageTableFormatWithDigest + } + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `image_id: {{.ID}}` + } else { + if ctx.Digest { + ctx.Format = `repository: {{ .Repository }} +tag: {{.Tag}} +digest: {{.Digest}} +image_id: {{.ID}} +created_at: {{.CreatedAt}} +virtual_size: {{.Size}} +` + } else { + ctx.Format = `repository: {{ .Repository }} +tag: {{.Tag}} +image_id: {{.ID}} +created_at: {{.CreatedAt}} +virtual_size: {{.Size}} +` + } + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") { + ctx.finalFormat += "\t{{.Digest}}" + } + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, image := range ctx.Images { + images := []*imageContext{} + if isDangling(image) { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: "", + tag: "", + digest: "", + }) + } else { + repoTags := map[string][]string{} + repoDigests := map[string][]string{} + + for _, refString := range append(image.RepoTags) { + ref, err := reference.ParseNamed(refString) + if err != nil { + continue + } + if nt, ok := ref.(reference.NamedTagged); ok { + repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) + } + } + for _, refString := range append(image.RepoDigests) { + ref, err := reference.ParseNamed(refString) + if err != nil { + continue + } + if c, ok := ref.(reference.Canonical); ok { + repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) + } + } + + for repo, tags := range repoTags { + digests := repoDigests[repo] + + // Do not display digests as their own row + delete(repoDigests, repo) + + if !ctx.Digest { + // Ignore digest references, just show tag once + digests = nil + } + + for _, tag := range tags { + if len(digests) == 0 { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: "", + }) + continue + } + // Display the digests for each tag + for _, dgst := range digests { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: dgst, + }) + } + + } + } + + // Show rows for remaining digest only references + for repo, digests := range repoDigests { + // If digests are displayed, show row per digest + if ctx.Digest { + for _, dgst := range digests { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: "", + digest: dgst, + }) + } + } else { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: "", + }) + } + } + } + for _, imageCtx := range images { + err = ctx.contextFormat(tmpl, imageCtx) + if err != nil { + return + } + } + } + + ctx.postformat(tmpl, &imageContext{}) +} + +type imageContext struct { + baseSubContext + trunc bool + i types.Image + repo string + tag string + digest string +} + +func (c *imageContext) ID() string { + c.addHeader(imageIDHeader) + if c.trunc { + return stringid.TruncateID(c.i.ID) + } + return c.i.ID +} + +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)) +} + +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.HumanSize(float64(c.i.Size)) +} diff --git a/api/client/formatter/formatter_test.go b/api/client/formatter/image_test.go similarity index 55% rename from api/client/formatter/formatter_test.go rename to api/client/formatter/image_test.go index 07cde63f95..64fda18bf5 100644 --- a/api/client/formatter/formatter_test.go +++ b/api/client/formatter/image_test.go @@ -3,265 +3,73 @@ package formatter import ( "bytes" "fmt" + "strings" "testing" "time" + "github.com/docker/docker/pkg/stringid" "github.com/docker/engine-api/types" ) -func TestContainerContextWrite(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -1).Unix() - expectedTime := time.Unix(unixTime, 0).String() +func TestImageContext(t *testing.T) { + imageID := stringid.GenerateRandomID() + unix := time.Now().Unix() - contexts := []struct { - context ContainerContext - expected string + var ctx imageContext + cases := []struct { + imageCtx imageContext + expValue string + expHeader string + call func() string }{ - // Errors - { - ContainerContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - ContainerContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - ContainerContext{ - Context: Context{ - Format: "table", - }, - }, - `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -containerID1 ubuntu "" 24 hours ago foobar_baz -containerID2 ubuntu "" 24 hours ago foobar_bar -`, - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - }, - }, - "IMAGE\nubuntu\nubuntu\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - }, - Size: true, - }, - "IMAGE\nubuntu\nubuntu\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - Quiet: true, - }, - }, - "IMAGE\nubuntu\nubuntu\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "table", - Quiet: true, - }, - }, - "containerID1\ncontainerID2\n", - }, - // Raw Format - { - ContainerContext{ - Context: Context{ - Format: "raw", - }, - }, - fmt.Sprintf(`container_id: containerID1 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_baz -labels: -ports: - -container_id: containerID2 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_bar -labels: -ports: - -`, expectedTime, expectedTime), - }, - { - ContainerContext{ - Context: Context{ - Format: "raw", - }, - Size: true, - }, - fmt.Sprintf(`container_id: containerID1 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_baz -labels: -ports: -size: 0 B - -container_id: containerID2 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_bar -labels: -ports: -size: 0 B - -`, expectedTime, expectedTime), - }, - { - ContainerContext{ - Context: Context{ - Format: "raw", - Quiet: true, - }, - }, - "container_id: containerID1\ncontainer_id: containerID2\n", - }, - // Custom Format - { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - }, - }, - "ubuntu\nubuntu\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - }, - Size: true, - }, - "ubuntu\nubuntu\n", - }, + {imageContext{ + i: types.Image{ID: imageID}, + trunc: true, + }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, + {imageContext{ + i: types.Image{ID: imageID}, + trunc: false, + }, imageID, imageIDHeader, ctx.ID}, + {imageContext{ + i: types.Image{Size: 10}, + trunc: true, + }, "10 B", sizeHeader, ctx.Size}, + {imageContext{ + i: types.Image{Created: unix}, + trunc: true, + }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + // FIXME + // {imageContext{ + // i: types.Image{Created: unix}, + // trunc: true, + // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, + {imageContext{ + i: types.Image{}, + repo: "busybox", + }, "busybox", repositoryHeader, ctx.Repository}, + {imageContext{ + i: types.Image{}, + tag: "latest", + }, "latest", tagHeader, ctx.Tag}, + {imageContext{ + i: types.Image{}, + digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", + }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, } - for _, context := range contexts { - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime}, + for _, c := range cases { + ctx = c.imageCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - out := bytes.NewBufferString("") - context.context.Output = out - context.context.Containers = containers - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) } - // Clean buffer - out.Reset() - } -} - -func TestContainerContextWriteWithNoContainers(t *testing.T) { - out := bytes.NewBufferString("") - containers := []types.Container{} - - contexts := []struct { - context ContainerContext - expected string - }{ - { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - Output: out, - }, - }, - "", - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - Output: out, - }, - }, - "IMAGE\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - Output: out, - }, - Size: true, - }, - "", - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - Output: out, - }, - Size: true, - }, - "IMAGE\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}\t{{.Size}}", - Output: out, - }, - }, - "IMAGE SIZE\n", - }, - { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}\t{{.Size}}", - Output: out, - }, - Size: true, - }, - "IMAGE SIZE\n", - }, - } - - for _, context := range contexts { - context.context.Containers = containers - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) - } - // Clean buffer - out.Reset() } } diff --git a/api/client/formatter/network.go b/api/client/formatter/network.go new file mode 100644 index 0000000000..9bd3d3b180 --- /dev/null +++ b/api/client/formatter/network.go @@ -0,0 +1,129 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" +) + +const ( + defaultNetworkTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}" + + networkIDHeader = "NETWORK ID" + ipv6Header = "IPV6" + internalHeader = "INTERNAL" +) + +// NetworkContext contains network specific information required by the formatter, +// encapsulate a Context struct. +type NetworkContext struct { + Context + // Networks + Networks []types.NetworkResource +} + +func (ctx NetworkContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } else { + ctx.Format = defaultNetworkTableFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `network_id: {{.ID}}` + } else { + ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, network := range ctx.Networks { + networkCtx := &networkContext{ + trunc: ctx.Trunc, + n: network, + } + err = ctx.contextFormat(tmpl, networkCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &networkContext{}) +} + +type networkContext struct { + baseSubContext + trunc bool + n types.NetworkResource +} + +func (c *networkContext) ID() string { + c.addHeader(networkIDHeader) + if c.trunc { + return stringid.TruncateID(c.n.ID) + } + return c.n.ID +} + +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 "" + } + + var joinLabels []string + for k, v := range c.n.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +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 "" + } + return c.n.Labels[name] +} diff --git a/api/client/formatter/network_test.go b/api/client/formatter/network_test.go new file mode 100644 index 0000000000..b760bfab1d --- /dev/null +++ b/api/client/formatter/network_test.go @@ -0,0 +1,201 @@ +package formatter + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" +) + +func TestNetworkContext(t *testing.T) { + networkID := stringid.GenerateRandomID() + + var ctx networkContext + cases := []struct { + networkCtx networkContext + expValue string + expHeader string + call func() string + }{ + {networkContext{ + n: types.NetworkResource{ID: networkID}, + trunc: false, + }, networkID, networkIDHeader, ctx.ID}, + {networkContext{ + n: types.NetworkResource{ID: networkID}, + trunc: true, + }, stringid.TruncateID(networkID), networkIDHeader, ctx.ID}, + {networkContext{ + n: types.NetworkResource{Name: "network_name"}, + }, "network_name", nameHeader, ctx.Name}, + {networkContext{ + n: types.NetworkResource{Driver: "driver_name"}, + }, "driver_name", driverHeader, ctx.Driver}, + {networkContext{ + n: types.NetworkResource{EnableIPv6: true}, + }, "true", ipv6Header, ctx.IPv6}, + {networkContext{ + n: types.NetworkResource{EnableIPv6: false}, + }, "false", ipv6Header, ctx.IPv6}, + {networkContext{ + n: types.NetworkResource{Internal: true}, + }, "true", internalHeader, ctx.Internal}, + {networkContext{ + n: types.NetworkResource{Internal: false}, + }, "false", internalHeader, ctx.Internal}, + {networkContext{ + n: types.NetworkResource{}, + }, "", labelsHeader, ctx.Labels}, + {networkContext{ + n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + } + + for _, c := range cases { + ctx = c.networkCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } 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) + } + } +} + +func TestNetworkContextWrite(t *testing.T) { + contexts := []struct { + context NetworkContext + expected string + }{ + + // Errors + { + NetworkContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + NetworkContext{ + Context: Context{ + Format: "table", + }, + }, + `NETWORK ID NAME DRIVER SCOPE +networkID1 foobar_baz foo local +networkID2 foobar_bar bar local +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + `networkID1 +networkID2 +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "table {{.Name}}", + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "table {{.Name}}", + Quiet: true, + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + NetworkContext{ + Context: Context{ + Format: "raw", + }, + }, `network_id: networkID1 +name: foobar_baz +driver: foo +scope: local + +network_id: networkID2 +name: foobar_bar +driver: bar +scope: local + +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `network_id: networkID1 +network_id: networkID2 +`, + }, + // Custom Format + { + NetworkContext{ + Context: Context{ + Format: "{{.Name}}", + }, + }, + `foobar_baz +foobar_bar +`, + }, + } + + for _, context := range contexts { + networks := []types.NetworkResource{ + {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"}, + {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Networks = networks + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/api/client/formatter/volume.go b/api/client/formatter/volume.go new file mode 100644 index 0000000000..00ebada364 --- /dev/null +++ b/api/client/formatter/volume.go @@ -0,0 +1,114 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + + "github.com/docker/engine-api/types" +) + +const ( + defaultVolumeQuietFormat = "{{.Name}}" + defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" + + mountpointHeader = "MOUNTPOINT" + // Status header ? +) + +// VolumeContext contains volume specific information required by the formatter, +// encapsulate a Context struct. +type VolumeContext struct { + Context + // Volumes + Volumes []*types.Volume +} + +func (ctx VolumeContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultVolumeQuietFormat + } else { + ctx.Format = defaultVolumeTableFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `name: {{.Name}}` + } else { + ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n` + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, volume := range ctx.Volumes { + volumeCtx := &volumeContext{ + v: volume, + } + err = ctx.contextFormat(tmpl, volumeCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &networkContext{}) +} + +type volumeContext struct { + baseSubContext + v *types.Volume +} + +func (c *volumeContext) Name() string { + c.addHeader(nameHeader) + 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 "" + } + + var joinLabels []string + for k, v := range c.v.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +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 "" + } + return c.v.Labels[name] +} diff --git a/api/client/formatter/volume_test.go b/api/client/formatter/volume_test.go new file mode 100644 index 0000000000..5562446a22 --- /dev/null +++ b/api/client/formatter/volume_test.go @@ -0,0 +1,183 @@ +package formatter + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" +) + +func TestVolumeContext(t *testing.T) { + volumeName := stringid.GenerateRandomID() + + var ctx volumeContext + cases := []struct { + volumeCtx volumeContext + expValue string + expHeader string + call func() string + }{ + {volumeContext{ + v: &types.Volume{Name: volumeName}, + }, volumeName, nameHeader, ctx.Name}, + {volumeContext{ + v: &types.Volume{Driver: "driver_name"}, + }, "driver_name", driverHeader, ctx.Driver}, + {volumeContext{ + v: &types.Volume{Scope: "local"}, + }, "local", scopeHeader, ctx.Scope}, + {volumeContext{ + v: &types.Volume{Mountpoint: "mountpoint"}, + }, "mountpoint", mountpointHeader, ctx.Mountpoint}, + {volumeContext{ + v: &types.Volume{}, + }, "", labelsHeader, ctx.Labels}, + {volumeContext{ + v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + } + + for _, c := range cases { + ctx = c.volumeCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } 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) + } + } +} + +func TestVolumeContextWrite(t *testing.T) { + contexts := []struct { + context VolumeContext + expected string + }{ + + // Errors + { + VolumeContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + VolumeContext{ + Context: Context{ + Format: "table", + }, + }, + `DRIVER NAME +foo foobar_baz +bar foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + `foobar_baz +foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table {{.Name}}", + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table {{.Name}}", + Quiet: true, + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + VolumeContext{ + Context: Context{ + Format: "raw", + }, + }, `name: foobar_baz +driver: foo + +name: foobar_bar +driver: bar + +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `name: foobar_baz +name: foobar_bar +`, + }, + // Custom Format + { + VolumeContext{ + Context: Context{ + Format: "{{.Name}}", + }, + }, + `foobar_baz +foobar_bar +`, + }, + } + + for _, context := range contexts { + volumes := []*types.Volume{ + {Name: "foobar_baz", Driver: "foo"}, + {Name: "foobar_bar", Driver: "bar"}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Volumes = volumes + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/api/client/image/images.go b/api/client/image/images.go index 460fb79801..78458871c5 100644 --- a/api/client/image/images.go +++ b/api/client/image/images.go @@ -79,8 +79,8 @@ func runImages(dockerCli *client.DockerCli, opts imagesOptions) error { f := opts.format if len(f) == 0 { - if len(dockerCli.ImagesFormat()) > 0 && !opts.quiet { - f = dockerCli.ImagesFormat() + if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().ImagesFormat } else { f = "table" } diff --git a/api/client/network/list.go b/api/client/network/list.go index c149b985c9..3b6622f83e 100644 --- a/api/client/network/list.go +++ b/api/client/network/list.go @@ -1,15 +1,13 @@ package network import ( - "fmt" "sort" - "text/tabwriter" "golang.org/x/net/context" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/formatter" "github.com/docker/docker/cli" - "github.com/docker/docker/pkg/stringid" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/filters" "github.com/spf13/cobra" @@ -24,6 +22,7 @@ func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name } type listOptions struct { quiet bool noTrunc bool + format string filter []string } @@ -43,6 +42,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") + flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')") return cmd @@ -69,32 +69,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error { return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - if !opts.quiet { - fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE") - fmt.Fprintf(w, "\n") + f := opts.format + if len(f) == 0 { + if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().NetworksFormat + } else { + f = "table" + } } sort.Sort(byNetworkName(networkResources)) - for _, networkResource := range networkResources { - ID := networkResource.ID - netName := networkResource.Name - driver := networkResource.Driver - scope := networkResource.Scope - if !opts.noTrunc { - ID = stringid.TruncateID(ID) - } - if opts.quiet { - fmt.Fprintln(w, ID) - continue - } - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t", - ID, - netName, - driver, - scope) - fmt.Fprint(w, "\n") + + networksCtx := formatter.NetworkContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + Trunc: !opts.noTrunc, + }, + Networks: networkResources, } - w.Flush() + + networksCtx.Write() + return nil } diff --git a/api/client/volume/list.go b/api/client/volume/list.go index 0cebe4d556..c0584ea8a2 100644 --- a/api/client/volume/list.go +++ b/api/client/volume/list.go @@ -1,13 +1,12 @@ package volume import ( - "fmt" "sort" - "text/tabwriter" "golang.org/x/net/context" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/formatter" "github.com/docker/docker/cli" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/filters" @@ -24,6 +23,7 @@ func (r byVolumeName) Less(i, j int) bool { type listOptions struct { quiet bool + format string filter []string } @@ -43,6 +43,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") + flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')") return cmd @@ -65,24 +66,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error { return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - if !opts.quiet { - for _, warn := range volumes.Warnings { - fmt.Fprintln(dockerCli.Err(), warn) + f := opts.format + if len(f) == 0 { + if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().VolumesFormat + } else { + f = "table" } - fmt.Fprintf(w, "DRIVER \tVOLUME NAME") - fmt.Fprintf(w, "\n") } sort.Sort(byVolumeName(volumes.Volumes)) - for _, vol := range volumes.Volumes { - if opts.quiet { - fmt.Fprintln(w, vol.Name) - continue - } - fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name) + + volumeCtx := formatter.VolumeContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + }, + Volumes: volumes.Volumes, } - w.Flush() + + volumeCtx.Write() + return nil } diff --git a/cliconfig/configfile/file.go b/cliconfig/configfile/file.go index 7c94e27dce..9ac952042a 100644 --- a/cliconfig/configfile/file.go +++ b/cliconfig/configfile/file.go @@ -26,6 +26,8 @@ type ConfigFile struct { HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` PsFormat string `json:"psFormat,omitempty"` ImagesFormat string `json:"imagesFormat,omitempty"` + NetworksFormat string `json:"networksFormat,omitempty"` + VolumesFormat string `json:"volumesFormat,omitempty"` DetachKeys string `json:"detachKeys,omitempty"` CredentialsStore string `json:"credsStore,omitempty"` Filename string `json:"-"` // Note: for internal use only diff --git a/docs/reference/commandline/network_ls.md b/docs/reference/commandline/network_ls.md index 49614bdaf4..e8ba6d97fb 100644 --- a/docs/reference/commandline/network_ls.md +++ b/docs/reference/commandline/network_ls.md @@ -20,6 +20,7 @@ Aliases: Options: -f, --filter value Provide filter values (i.e. 'dangling=true') (default []) + --format string Pretty-print networks using a Go template --help Print usage --no-trunc Do not truncate the output -q, --quiet Only display volume names @@ -169,6 +170,38 @@ $ docker network rm `docker network ls --filter type=custom -q` A warning will be issued when trying to remove a network that has containers attached. +## Formatting + +The formatting options (`--format`) pretty-prints networks output +using a Go template. + +Valid placeholders for the Go template are listed below: + +Placeholder | Description +------------|------------------------------------------------------------------------------------------ +`.ID` | Network ID +`.Name` | Network name +`.Driver` | Network driver +`.Scope` | Network scope (local, global) +`.IPv6` | Whether IPv6 is enabled on the network or not. +`.Internal` | Whether the network is internal or not. +`.Labels` | All labels assigned to the network. +`.Label` | Value of a specific label for this network. For example `{{.Label "project.version"}}` + +When using the `--format` option, the `network ls` command will either +output the data exactly as the template declares or, when using the +`table` directive, includes column headers as well. + +The following example uses a template without headers and outputs the +`ID` and `Driver` entries separated by a colon for all networks: + +```bash +$ docker network ls --format "{{.ID}}: {{.Driver}}" +afaaab448eb2: bridge +d1584f8dc718: host +391df270dc66: null +``` + ## Related information * [network disconnect ](network_disconnect.md) diff --git a/docs/reference/commandline/volume_ls.md b/docs/reference/commandline/volume_ls.md index cdeea42e8c..df2430a35f 100644 --- a/docs/reference/commandline/volume_ls.md +++ b/docs/reference/commandline/volume_ls.md @@ -23,6 +23,7 @@ Options: - dangling= a volume if referenced or not - driver= a volume's driver name - name= a volume's name + --format string Pretty-print volumes using a Go template --help Print usage -q, --quiet Only display volume names ``` @@ -82,6 +83,36 @@ The following filter matches all volumes with a name containing the `rose` strin DRIVER VOLUME NAME local rosemary +## Formatting + +The formatting options (`--format`) pretty-prints volumes output +using a Go template. + +Valid placeholders for the Go template are listed below: + +Placeholder | Description +--------------|------------------------------------------------------------------------------------------ +`.Name` | Network name +`.Driver` | Network driver +`.Scope` | Network scope (local, global) +`.Mountpoint` | Whether the network is internal or not. +`.Labels` | All labels assigned to the volume. +`.Label` | Value of a specific label for this volume. For example `{{.Label "project.version"}}` + +When using the `--format` option, the `volume ls` command will either +output the data exactly as the template declares or, when using the +`table` directive, includes column headers as well. + +The following example uses a template without headers and outputs the +`Name` and `Driver` entries separated by a colon for all volumes: + +```bash +$ docker volume ls --format "{{.Name}}: {{.Driver}}" +vol1: local +vol2: local +vol3: local +``` + ## Related information * [volume create](volume_create.md) diff --git a/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go index 0611b2c089..faf798782f 100644 --- a/integration-cli/docker_cli_network_unix_test.go +++ b/integration-cli/docker_cli_network_unix_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "time" @@ -279,6 +280,43 @@ func (s *DockerNetworkSuite) TestDockerNetworkLsDefault(c *check.C) { } } +func (s *DockerSuite) TestNetworkLsFormat(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "network", "ls", "--format", "{{.Name}}") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"bridge", "host", "none"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + +func (s *DockerSuite) TestNetworkLsFormatDefaultFormat(c *check.C) { + testRequires(c, DaemonIsLinux) + + config := `{ + "networksFormat": "{{ .Name }} default" +}` + d, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(d) + + err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "--config", d, "network", "ls") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"bridge default", "host default", "none default"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + func (s *DockerNetworkSuite) TestDockerNetworkCreatePredefined(c *check.C) { predefined := []string{"bridge", "host", "none", "default"} for _, net := range predefined { diff --git a/integration-cli/docker_cli_volume_test.go b/integration-cli/docker_cli_volume_test.go index 8835e1d60f..9e62fc731b 100644 --- a/integration-cli/docker_cli_volume_test.go +++ b/integration-cli/docker_cli_volume_test.go @@ -1,7 +1,10 @@ package main import ( + "io/ioutil" + "os" "os/exec" + "path/filepath" "strings" "github.com/docker/docker/pkg/integration/checker" @@ -65,20 +68,62 @@ func (s *DockerSuite) TestVolumeCliInspectMulti(c *check.C) { func (s *DockerSuite) TestVolumeCliLs(c *check.C) { prefix, _ := getPrefixAndSlashFromDaemonPlatform() - out, _ := dockerCmd(c, "volume", "create", "--name", "aaa") + dockerCmd(c, "volume", "create", "--name", "aaa") dockerCmd(c, "volume", "create", "--name", "test") dockerCmd(c, "volume", "create", "--name", "soo") dockerCmd(c, "run", "-v", "soo:"+prefix+"/foo", "busybox", "ls", "/") - out, _ = dockerCmd(c, "volume", "ls") + out, _ := dockerCmd(c, "volume", "ls") outArr := strings.Split(strings.TrimSpace(out), "\n") c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out)) assertVolList(c, out, []string{"aaa", "soo", "test"}) } +func (s *DockerSuite) TestVolumeLsFormat(c *check.C) { + dockerCmd(c, "volume", "create", "--name", "aaa") + dockerCmd(c, "volume", "create", "--name", "test") + dockerCmd(c, "volume", "create", "--name", "soo") + + out, _ := dockerCmd(c, "volume", "ls", "--format", "{{.Name}}") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"aaa", "soo", "test"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + +func (s *DockerSuite) TestVolumeLsFormatDefaultFormat(c *check.C) { + dockerCmd(c, "volume", "create", "--name", "aaa") + dockerCmd(c, "volume", "create", "--name", "test") + dockerCmd(c, "volume", "create", "--name", "soo") + + config := `{ + "volumesFormat": "{{ .Name }} default" +}` + d, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(d) + + err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "--config", d, "volume", "ls") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"aaa default", "soo default", "test default"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + // assertVolList checks volume retrieved with ls command // equals to expected volume list // note: out should be `volume ls [option]` result diff --git a/man/docker-network-ls.1.md b/man/docker-network-ls.1.md index 3eeff05993..d5399d7a59 100644 --- a/man/docker-network-ls.1.md +++ b/man/docker-network-ls.1.md @@ -7,6 +7,7 @@ docker-network-ls - list networks # SYNOPSIS **docker network ls** [**-f**|**--filter**[=*[]*]] +[**--format**=*"TEMPLATE"*] [**--no-trunc**[=*true*|*false*]] [**-q**|**--quiet**[=*true*|*false*]] [**--help**] @@ -162,6 +163,18 @@ attached. **-f**, **--filter**=*[]* filter output based on conditions provided. +**--format**="*TEMPLATE*" + Pretty-print networks using a Go template. + Valid placeholders: + .ID - Network ID + .Name - Network name + .Driver - Network driver + .Scope - Network scope (local, global) + .IPv6 - Whether IPv6 is enabled on the network or not + .Internal - Whether the network is internal or not + .Labels - All labels assigned to the network + .Label - Value of a specific label for this network. For example `{{.Label "project.version"}}` + **--no-trunc**=*true*|*false* Do not truncate the output