mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #23475 from vdemeester/add-format-flag-to-network-and-volume-ls
Add format flag to network and volume ls
This commit is contained in:
commit
1ccd9d3137
22 changed files with 1737 additions and 884 deletions
|
@ -118,18 +118,6 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
|
||||||
return nil
|
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 {
|
func (cli *DockerCli) setRawTerminal() error {
|
||||||
if os.Getenv("NORAW") == "" {
|
if os.Getenv("NORAW") == "" {
|
||||||
if cli.isTerminalIn {
|
if cli.isTerminalIn {
|
||||||
|
|
|
@ -101,8 +101,8 @@ func runPs(dockerCli *client.DockerCli, opts *psOptions) error {
|
||||||
|
|
||||||
f := opts.format
|
f := opts.format
|
||||||
if len(f) == 0 {
|
if len(f) == 0 {
|
||||||
if len(dockerCli.PsFormat()) > 0 && !opts.quiet {
|
if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
|
||||||
f = dockerCli.PsFormat()
|
f = dockerCli.ConfigFile().PsFormat
|
||||||
} else {
|
} else {
|
||||||
f = "table"
|
f = "table"
|
||||||
}
|
}
|
||||||
|
|
208
api/client/formatter/container.go
Normal file
208
api/client/formatter/container.go
Normal file
|
@ -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 "<no image>"
|
||||||
|
}
|
||||||
|
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, ",")
|
||||||
|
}
|
404
api/client/formatter/container_test.go
Normal file
404
api/client/formatter/container_test.go
Normal file
|
@ -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, "<no image>", imageHeader, ctx.Image},
|
||||||
|
{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
|
||||||
|
{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
||||||
|
{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
|
||||||
|
{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
|
||||||
|
{types.Container{SizeRw: 10}, true, "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>: 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,215 +1,22 @@
|
||||||
package formatter
|
package formatter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"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 (
|
const (
|
||||||
tableKey = "table"
|
tableKey = "table"
|
||||||
|
|
||||||
containerIDHeader = "CONTAINER ID"
|
|
||||||
imageHeader = "IMAGE"
|
imageHeader = "IMAGE"
|
||||||
namesHeader = "NAMES"
|
|
||||||
commandHeader = "COMMAND"
|
|
||||||
createdSinceHeader = "CREATED"
|
createdSinceHeader = "CREATED"
|
||||||
createdAtHeader = "CREATED AT"
|
createdAtHeader = "CREATED AT"
|
||||||
runningForHeader = "CREATED"
|
|
||||||
statusHeader = "STATUS"
|
|
||||||
portsHeader = "PORTS"
|
|
||||||
sizeHeader = "SIZE"
|
sizeHeader = "SIZE"
|
||||||
labelsHeader = "LABELS"
|
labelsHeader = "LABELS"
|
||||||
imageIDHeader = "IMAGE ID"
|
nameHeader = "NAME"
|
||||||
repositoryHeader = "REPOSITORY"
|
driverHeader = "DRIVER"
|
||||||
tagHeader = "TAG"
|
scopeHeader = "SCOPE"
|
||||||
digestHeader = "DIGEST"
|
|
||||||
mountsHeader = "MOUNTS"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 "<no image>"
|
|
||||||
}
|
|
||||||
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 {
|
type subContext interface {
|
||||||
fullHeader() string
|
fullHeader() string
|
||||||
addHeader(header string)
|
addHeader(header string)
|
||||||
|
|
|
@ -4,172 +4,8 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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, "<no image>", imageHeader, ctx.Image},
|
|
||||||
{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
|
|
||||||
{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
|
||||||
{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
|
|
||||||
{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
|
|
||||||
{types.Container{SizeRw: 10}, true, "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) {
|
func compareMultipleValues(t *testing.T, value, expected string) {
|
||||||
// comma-separated values means probably a map input, which won't
|
// comma-separated values means probably a map input, which won't
|
||||||
// be guaranteed to have the same order as our expected value
|
// be guaranteed to have the same order as our expected value
|
||||||
|
|
|
@ -8,19 +8,14 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
"github.com/docker/docker/utils/templates"
|
"github.com/docker/docker/utils/templates"
|
||||||
"github.com/docker/engine-api/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tableFormatKey = "table"
|
tableFormatKey = "table"
|
||||||
rawFormatKey = "raw"
|
rawFormatKey = "raw"
|
||||||
|
|
||||||
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
defaultQuietFormat = "{{.ID}}"
|
||||||
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}}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context contains information required by the formatter to print the output as desired.
|
// 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")
|
c.buffer.WriteString("\n")
|
||||||
return nil
|
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] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
|
|
||||||
}
|
|
||||||
|
|
||||||
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: "<none>",
|
|
||||||
tag: "<none>",
|
|
||||||
digest: "<none>",
|
|
||||||
})
|
|
||||||
} 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: "<none>",
|
|
||||||
})
|
|
||||||
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: "<none>",
|
|
||||||
digest: dgst,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
images = append(images, &imageContext{
|
|
||||||
trunc: ctx.Trunc,
|
|
||||||
i: image,
|
|
||||||
repo: repo,
|
|
||||||
tag: "<none>",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, imageCtx := range images {
|
|
||||||
err = ctx.contextFormat(tmpl, imageCtx)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.postformat(tmpl, &imageContext{})
|
|
||||||
}
|
|
||||||
|
|
229
api/client/formatter/image.go
Normal file
229
api/client/formatter/image.go
Normal file
|
@ -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] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
|
||||||
|
}
|
||||||
|
|
||||||
|
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: "<none>",
|
||||||
|
tag: "<none>",
|
||||||
|
digest: "<none>",
|
||||||
|
})
|
||||||
|
} 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: "<none>",
|
||||||
|
})
|
||||||
|
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: "<none>",
|
||||||
|
digest: dgst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
images = append(images, &imageContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
i: image,
|
||||||
|
repo: repo,
|
||||||
|
tag: "<none>",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
|
@ -3,265 +3,73 @@ package formatter
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContainerContextWrite(t *testing.T) {
|
func TestImageContext(t *testing.T) {
|
||||||
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
imageID := stringid.GenerateRandomID()
|
||||||
expectedTime := time.Unix(unixTime, 0).String()
|
unix := time.Now().Unix()
|
||||||
|
|
||||||
contexts := []struct {
|
var ctx imageContext
|
||||||
context ContainerContext
|
cases := []struct {
|
||||||
expected string
|
imageCtx imageContext
|
||||||
|
expValue string
|
||||||
|
expHeader string
|
||||||
|
call func() string
|
||||||
}{
|
}{
|
||||||
// Errors
|
{imageContext{
|
||||||
{
|
i: types.Image{ID: imageID},
|
||||||
ContainerContext{
|
trunc: true,
|
||||||
Context: Context{
|
}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
|
||||||
Format: "{{InvalidFunction}}",
|
{imageContext{
|
||||||
},
|
i: types.Image{ID: imageID},
|
||||||
},
|
trunc: false,
|
||||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
}, imageID, imageIDHeader, ctx.ID},
|
||||||
`,
|
{imageContext{
|
||||||
},
|
i: types.Image{Size: 10},
|
||||||
{
|
trunc: true,
|
||||||
ContainerContext{
|
}, "10 B", sizeHeader, ctx.Size},
|
||||||
Context: Context{
|
{imageContext{
|
||||||
Format: "{{nil}}",
|
i: types.Image{Created: unix},
|
||||||
},
|
trunc: true,
|
||||||
},
|
}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
||||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
// FIXME
|
||||||
`,
|
// {imageContext{
|
||||||
},
|
// i: types.Image{Created: unix},
|
||||||
// Table Format
|
// trunc: true,
|
||||||
{
|
// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
|
||||||
ContainerContext{
|
{imageContext{
|
||||||
Context: Context{
|
i: types.Image{},
|
||||||
Format: "table",
|
repo: "busybox",
|
||||||
},
|
}, "busybox", repositoryHeader, ctx.Repository},
|
||||||
},
|
{imageContext{
|
||||||
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
i: types.Image{},
|
||||||
containerID1 ubuntu "" 24 hours ago foobar_baz
|
tag: "latest",
|
||||||
containerID2 ubuntu "" 24 hours ago foobar_bar
|
}, "latest", tagHeader, ctx.Tag},
|
||||||
`,
|
{imageContext{
|
||||||
},
|
i: types.Image{},
|
||||||
{
|
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
|
||||||
ContainerContext{
|
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
|
||||||
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 {
|
for _, c := range cases {
|
||||||
containers := []types.Container{
|
ctx = c.imageCtx
|
||||||
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
v := c.call()
|
||||||
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
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
|
h := ctx.fullHeader()
|
||||||
context.context.Containers = containers
|
if h != c.expHeader {
|
||||||
context.context.Write()
|
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
129
api/client/formatter/network.go
Normal file
129
api/client/formatter/network.go
Normal file
|
@ -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]
|
||||||
|
}
|
201
api/client/formatter/network_test.go
Normal file
201
api/client/formatter/network_test.go
Normal file
|
@ -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>: 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()
|
||||||
|
}
|
||||||
|
}
|
114
api/client/formatter/volume.go
Normal file
114
api/client/formatter/volume.go
Normal file
|
@ -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]
|
||||||
|
}
|
183
api/client/formatter/volume_test.go
Normal file
183
api/client/formatter/volume_test.go
Normal file
|
@ -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>: 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,8 +79,8 @@ func runImages(dockerCli *client.DockerCli, opts imagesOptions) error {
|
||||||
|
|
||||||
f := opts.format
|
f := opts.format
|
||||||
if len(f) == 0 {
|
if len(f) == 0 {
|
||||||
if len(dockerCli.ImagesFormat()) > 0 && !opts.quiet {
|
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
|
||||||
f = dockerCli.ImagesFormat()
|
f = dockerCli.ConfigFile().ImagesFormat
|
||||||
} else {
|
} else {
|
||||||
f = "table"
|
f = "table"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/docker/docker/api/client"
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/api/client/formatter"
|
||||||
"github.com/docker/docker/cli"
|
"github.com/docker/docker/cli"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
"github.com/docker/engine-api/types/filters"
|
"github.com/docker/engine-api/types/filters"
|
||||||
"github.com/spf13/cobra"
|
"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 {
|
type listOptions struct {
|
||||||
quiet bool
|
quiet bool
|
||||||
noTrunc bool
|
noTrunc bool
|
||||||
|
format string
|
||||||
filter []string
|
filter []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +42,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
|
||||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output")
|
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')")
|
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -69,32 +69,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
f := opts.format
|
||||||
if !opts.quiet {
|
if len(f) == 0 {
|
||||||
fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
|
if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
|
||||||
fmt.Fprintf(w, "\n")
|
f = dockerCli.ConfigFile().NetworksFormat
|
||||||
|
} else {
|
||||||
|
f = "table"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(byNetworkName(networkResources))
|
sort.Sort(byNetworkName(networkResources))
|
||||||
for _, networkResource := range networkResources {
|
|
||||||
ID := networkResource.ID
|
networksCtx := formatter.NetworkContext{
|
||||||
netName := networkResource.Name
|
Context: formatter.Context{
|
||||||
driver := networkResource.Driver
|
Output: dockerCli.Out(),
|
||||||
scope := networkResource.Scope
|
Format: f,
|
||||||
if !opts.noTrunc {
|
Quiet: opts.quiet,
|
||||||
ID = stringid.TruncateID(ID)
|
Trunc: !opts.noTrunc,
|
||||||
}
|
},
|
||||||
if opts.quiet {
|
Networks: networkResources,
|
||||||
fmt.Fprintln(w, ID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t",
|
|
||||||
ID,
|
|
||||||
netName,
|
|
||||||
driver,
|
|
||||||
scope)
|
|
||||||
fmt.Fprint(w, "\n")
|
|
||||||
}
|
}
|
||||||
w.Flush()
|
|
||||||
|
networksCtx.Write()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package volume
|
package volume
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/docker/docker/api/client"
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/api/client/formatter"
|
||||||
"github.com/docker/docker/cli"
|
"github.com/docker/docker/cli"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
"github.com/docker/engine-api/types/filters"
|
"github.com/docker/engine-api/types/filters"
|
||||||
|
@ -24,6 +23,7 @@ func (r byVolumeName) Less(i, j int) bool {
|
||||||
|
|
||||||
type listOptions struct {
|
type listOptions struct {
|
||||||
quiet bool
|
quiet bool
|
||||||
|
format string
|
||||||
filter []string
|
filter []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
|
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')")
|
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -65,24 +66,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
f := opts.format
|
||||||
if !opts.quiet {
|
if len(f) == 0 {
|
||||||
for _, warn := range volumes.Warnings {
|
if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet {
|
||||||
fmt.Fprintln(dockerCli.Err(), warn)
|
f = dockerCli.ConfigFile().VolumesFormat
|
||||||
|
} else {
|
||||||
|
f = "table"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "DRIVER \tVOLUME NAME")
|
|
||||||
fmt.Fprintf(w, "\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(byVolumeName(volumes.Volumes))
|
sort.Sort(byVolumeName(volumes.Volumes))
|
||||||
for _, vol := range volumes.Volumes {
|
|
||||||
if opts.quiet {
|
volumeCtx := formatter.VolumeContext{
|
||||||
fmt.Fprintln(w, vol.Name)
|
Context: formatter.Context{
|
||||||
continue
|
Output: dockerCli.Out(),
|
||||||
}
|
Format: f,
|
||||||
fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name)
|
Quiet: opts.quiet,
|
||||||
|
},
|
||||||
|
Volumes: volumes.Volumes,
|
||||||
}
|
}
|
||||||
w.Flush()
|
|
||||||
|
volumeCtx.Write()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ type ConfigFile struct {
|
||||||
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
|
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
|
||||||
PsFormat string `json:"psFormat,omitempty"`
|
PsFormat string `json:"psFormat,omitempty"`
|
||||||
ImagesFormat string `json:"imagesFormat,omitempty"`
|
ImagesFormat string `json:"imagesFormat,omitempty"`
|
||||||
|
NetworksFormat string `json:"networksFormat,omitempty"`
|
||||||
|
VolumesFormat string `json:"volumesFormat,omitempty"`
|
||||||
DetachKeys string `json:"detachKeys,omitempty"`
|
DetachKeys string `json:"detachKeys,omitempty"`
|
||||||
CredentialsStore string `json:"credsStore,omitempty"`
|
CredentialsStore string `json:"credsStore,omitempty"`
|
||||||
Filename string `json:"-"` // Note: for internal use only
|
Filename string `json:"-"` // Note: for internal use only
|
||||||
|
|
|
@ -20,6 +20,7 @@ Aliases:
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-f, --filter value Provide filter values (i.e. 'dangling=true') (default [])
|
-f, --filter value Provide filter values (i.e. 'dangling=true') (default [])
|
||||||
|
--format string Pretty-print networks using a Go template
|
||||||
--help Print usage
|
--help Print usage
|
||||||
--no-trunc Do not truncate the output
|
--no-trunc Do not truncate the output
|
||||||
-q, --quiet Only display volume names
|
-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
|
A warning will be issued when trying to remove a network that has containers
|
||||||
attached.
|
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
|
## Related information
|
||||||
|
|
||||||
* [network disconnect ](network_disconnect.md)
|
* [network disconnect ](network_disconnect.md)
|
||||||
|
|
|
@ -23,6 +23,7 @@ Options:
|
||||||
- dangling=<boolean> a volume if referenced or not
|
- dangling=<boolean> a volume if referenced or not
|
||||||
- driver=<string> a volume's driver name
|
- driver=<string> a volume's driver name
|
||||||
- name=<string> a volume's name
|
- name=<string> a volume's name
|
||||||
|
--format string Pretty-print volumes using a Go template
|
||||||
--help Print usage
|
--help Print usage
|
||||||
-q, --quiet Only display volume names
|
-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
|
DRIVER VOLUME NAME
|
||||||
local rosemary
|
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
|
## Related information
|
||||||
|
|
||||||
* [volume create](volume_create.md)
|
* [volume create](volume_create.md)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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) {
|
func (s *DockerNetworkSuite) TestDockerNetworkCreatePredefined(c *check.C) {
|
||||||
predefined := []string{"bridge", "host", "none", "default"}
|
predefined := []string{"bridge", "host", "none", "default"}
|
||||||
for _, net := range predefined {
|
for _, net := range predefined {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/integration/checker"
|
"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) {
|
func (s *DockerSuite) TestVolumeCliLs(c *check.C) {
|
||||||
prefix, _ := getPrefixAndSlashFromDaemonPlatform()
|
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", "test")
|
||||||
|
|
||||||
dockerCmd(c, "volume", "create", "--name", "soo")
|
dockerCmd(c, "volume", "create", "--name", "soo")
|
||||||
dockerCmd(c, "run", "-v", "soo:"+prefix+"/foo", "busybox", "ls", "/")
|
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")
|
outArr := strings.Split(strings.TrimSpace(out), "\n")
|
||||||
c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out))
|
c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out))
|
||||||
|
|
||||||
assertVolList(c, out, []string{"aaa", "soo", "test"})
|
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
|
// assertVolList checks volume retrieved with ls command
|
||||||
// equals to expected volume list
|
// equals to expected volume list
|
||||||
// note: out should be `volume ls [option]` result
|
// note: out should be `volume ls [option]` result
|
||||||
|
|
|
@ -7,6 +7,7 @@ docker-network-ls - list networks
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
**docker network ls**
|
**docker network ls**
|
||||||
[**-f**|**--filter**[=*[]*]]
|
[**-f**|**--filter**[=*[]*]]
|
||||||
|
[**--format**=*"TEMPLATE"*]
|
||||||
[**--no-trunc**[=*true*|*false*]]
|
[**--no-trunc**[=*true*|*false*]]
|
||||||
[**-q**|**--quiet**[=*true*|*false*]]
|
[**-q**|**--quiet**[=*true*|*false*]]
|
||||||
[**--help**]
|
[**--help**]
|
||||||
|
@ -162,6 +163,18 @@ attached.
|
||||||
**-f**, **--filter**=*[]*
|
**-f**, **--filter**=*[]*
|
||||||
filter output based on conditions provided.
|
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*
|
**--no-trunc**=*true*|*false*
|
||||||
Do not truncate the output
|
Do not truncate the output
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue