From a488ad1a090eb0b6069f79cd045badaaa8ec1bda Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 4 Aug 2016 14:59:55 +0200 Subject: [PATCH] Add volume --format flag to ls Signed-off-by: Vincent Demeester --- api/client/cli.go | 6 + api/client/formatter/volume.go | 114 ++++++++++++++ api/client/formatter/volume_test.go | 183 ++++++++++++++++++++++ api/client/volume/list.go | 35 +++-- cliconfig/configfile/file.go | 1 + docs/reference/commandline/volume_ls.md | 31 ++++ integration-cli/docker_cli_volume_test.go | 49 +++++- 7 files changed, 402 insertions(+), 17 deletions(-) create mode 100644 api/client/formatter/volume.go create mode 100644 api/client/formatter/volume_test.go diff --git a/api/client/cli.go b/api/client/cli.go index 951023eb5b..d5c2adc724 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -136,6 +136,12 @@ func (cli *DockerCli) NetworksFormat() string { return cli.configFile.NetworksFormat } +// VolumesFormat returns the format string specified in the configuration. +// String contains columns and format specification, for example {{ID}}\t{{Name}} +func (cli *DockerCli) VolumesFormat() string { + return cli.configFile.VolumesFormat +} + func (cli *DockerCli) setRawTerminal() error { if os.Getenv("NORAW") == "" { if cli.isTerminalIn { diff --git a/api/client/formatter/volume.go b/api/client/formatter/volume.go new file mode 100644 index 0000000000..00ebada364 --- /dev/null +++ b/api/client/formatter/volume.go @@ -0,0 +1,114 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + + "github.com/docker/engine-api/types" +) + +const ( + defaultVolumeQuietFormat = "{{.Name}}" + defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" + + mountpointHeader = "MOUNTPOINT" + // Status header ? +) + +// VolumeContext contains volume specific information required by the formatter, +// encapsulate a Context struct. +type VolumeContext struct { + Context + // Volumes + Volumes []*types.Volume +} + +func (ctx VolumeContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultVolumeQuietFormat + } else { + ctx.Format = defaultVolumeTableFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `name: {{.Name}}` + } else { + ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n` + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, volume := range ctx.Volumes { + volumeCtx := &volumeContext{ + v: volume, + } + err = ctx.contextFormat(tmpl, volumeCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &networkContext{}) +} + +type volumeContext struct { + baseSubContext + v *types.Volume +} + +func (c *volumeContext) Name() string { + c.addHeader(nameHeader) + return c.v.Name +} + +func (c *volumeContext) Driver() string { + c.addHeader(driverHeader) + return c.v.Driver +} + +func (c *volumeContext) Scope() string { + c.addHeader(scopeHeader) + return c.v.Scope +} + +func (c *volumeContext) Mountpoint() string { + c.addHeader(mountpointHeader) + return c.v.Mountpoint +} + +func (c *volumeContext) Labels() string { + c.addHeader(labelsHeader) + if c.v.Labels == nil { + return "" + } + + var joinLabels []string + for k, v := range c.v.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *volumeContext) Label(name string) string { + + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + c.addHeader(h) + + if c.v.Labels == nil { + return "" + } + return c.v.Labels[name] +} diff --git a/api/client/formatter/volume_test.go b/api/client/formatter/volume_test.go new file mode 100644 index 0000000000..5562446a22 --- /dev/null +++ b/api/client/formatter/volume_test.go @@ -0,0 +1,183 @@ +package formatter + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" +) + +func TestVolumeContext(t *testing.T) { + volumeName := stringid.GenerateRandomID() + + var ctx volumeContext + cases := []struct { + volumeCtx volumeContext + expValue string + expHeader string + call func() string + }{ + {volumeContext{ + v: &types.Volume{Name: volumeName}, + }, volumeName, nameHeader, ctx.Name}, + {volumeContext{ + v: &types.Volume{Driver: "driver_name"}, + }, "driver_name", driverHeader, ctx.Driver}, + {volumeContext{ + v: &types.Volume{Scope: "local"}, + }, "local", scopeHeader, ctx.Scope}, + {volumeContext{ + v: &types.Volume{Mountpoint: "mountpoint"}, + }, "mountpoint", mountpointHeader, ctx.Mountpoint}, + {volumeContext{ + v: &types.Volume{}, + }, "", labelsHeader, ctx.Labels}, + {volumeContext{ + v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + } + + for _, c := range cases { + ctx = c.volumeCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +func TestVolumeContextWrite(t *testing.T) { + contexts := []struct { + context VolumeContext + expected string + }{ + + // Errors + { + VolumeContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + VolumeContext{ + Context: Context{ + Format: "table", + }, + }, + `DRIVER NAME +foo foobar_baz +bar foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + `foobar_baz +foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table {{.Name}}", + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table {{.Name}}", + Quiet: true, + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + VolumeContext{ + Context: Context{ + Format: "raw", + }, + }, `name: foobar_baz +driver: foo + +name: foobar_bar +driver: bar + +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `name: foobar_baz +name: foobar_bar +`, + }, + // Custom Format + { + VolumeContext{ + Context: Context{ + Format: "{{.Name}}", + }, + }, + `foobar_baz +foobar_bar +`, + }, + } + + for _, context := range contexts { + volumes := []*types.Volume{ + {Name: "foobar_baz", Driver: "foo"}, + {Name: "foobar_bar", Driver: "bar"}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Volumes = volumes + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/api/client/volume/list.go b/api/client/volume/list.go index 0cebe4d556..1329803ae9 100644 --- a/api/client/volume/list.go +++ b/api/client/volume/list.go @@ -1,13 +1,12 @@ package volume import ( - "fmt" "sort" - "text/tabwriter" "golang.org/x/net/context" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/formatter" "github.com/docker/docker/cli" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/filters" @@ -24,6 +23,7 @@ func (r byVolumeName) Less(i, j int) bool { type listOptions struct { quiet bool + format string filter []string } @@ -43,6 +43,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") + flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')") return cmd @@ -65,24 +66,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error { return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - if !opts.quiet { - for _, warn := range volumes.Warnings { - fmt.Fprintln(dockerCli.Err(), warn) + f := opts.format + if len(f) == 0 { + if len(dockerCli.VolumesFormat()) > 0 && !opts.quiet { + f = dockerCli.VolumesFormat() + } else { + f = "table" } - fmt.Fprintf(w, "DRIVER \tVOLUME NAME") - fmt.Fprintf(w, "\n") } sort.Sort(byVolumeName(volumes.Volumes)) - for _, vol := range volumes.Volumes { - if opts.quiet { - fmt.Fprintln(w, vol.Name) - continue - } - fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name) + + volumeCtx := formatter.VolumeContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + }, + Volumes: volumes.Volumes, } - w.Flush() + + volumeCtx.Write() + return nil } diff --git a/cliconfig/configfile/file.go b/cliconfig/configfile/file.go index 201784c7b7..9ac952042a 100644 --- a/cliconfig/configfile/file.go +++ b/cliconfig/configfile/file.go @@ -27,6 +27,7 @@ type ConfigFile struct { PsFormat string `json:"psFormat,omitempty"` ImagesFormat string `json:"imagesFormat,omitempty"` NetworksFormat string `json:"networksFormat,omitempty"` + VolumesFormat string `json:"volumesFormat,omitempty"` DetachKeys string `json:"detachKeys,omitempty"` CredentialsStore string `json:"credsStore,omitempty"` Filename string `json:"-"` // Note: for internal use only diff --git a/docs/reference/commandline/volume_ls.md b/docs/reference/commandline/volume_ls.md index cdeea42e8c..df2430a35f 100644 --- a/docs/reference/commandline/volume_ls.md +++ b/docs/reference/commandline/volume_ls.md @@ -23,6 +23,7 @@ Options: - dangling= a volume if referenced or not - driver= a volume's driver name - name= a volume's name + --format string Pretty-print volumes using a Go template --help Print usage -q, --quiet Only display volume names ``` @@ -82,6 +83,36 @@ The following filter matches all volumes with a name containing the `rose` strin DRIVER VOLUME NAME local rosemary +## Formatting + +The formatting options (`--format`) pretty-prints volumes output +using a Go template. + +Valid placeholders for the Go template are listed below: + +Placeholder | Description +--------------|------------------------------------------------------------------------------------------ +`.Name` | Network name +`.Driver` | Network driver +`.Scope` | Network scope (local, global) +`.Mountpoint` | Whether the network is internal or not. +`.Labels` | All labels assigned to the volume. +`.Label` | Value of a specific label for this volume. For example `{{.Label "project.version"}}` + +When using the `--format` option, the `volume ls` command will either +output the data exactly as the template declares or, when using the +`table` directive, includes column headers as well. + +The following example uses a template without headers and outputs the +`Name` and `Driver` entries separated by a colon for all volumes: + +```bash +$ docker volume ls --format "{{.Name}}: {{.Driver}}" +vol1: local +vol2: local +vol3: local +``` + ## Related information * [volume create](volume_create.md) diff --git a/integration-cli/docker_cli_volume_test.go b/integration-cli/docker_cli_volume_test.go index 8835e1d60f..9e62fc731b 100644 --- a/integration-cli/docker_cli_volume_test.go +++ b/integration-cli/docker_cli_volume_test.go @@ -1,7 +1,10 @@ package main import ( + "io/ioutil" + "os" "os/exec" + "path/filepath" "strings" "github.com/docker/docker/pkg/integration/checker" @@ -65,20 +68,62 @@ func (s *DockerSuite) TestVolumeCliInspectMulti(c *check.C) { func (s *DockerSuite) TestVolumeCliLs(c *check.C) { prefix, _ := getPrefixAndSlashFromDaemonPlatform() - out, _ := dockerCmd(c, "volume", "create", "--name", "aaa") + dockerCmd(c, "volume", "create", "--name", "aaa") dockerCmd(c, "volume", "create", "--name", "test") dockerCmd(c, "volume", "create", "--name", "soo") dockerCmd(c, "run", "-v", "soo:"+prefix+"/foo", "busybox", "ls", "/") - out, _ = dockerCmd(c, "volume", "ls") + out, _ := dockerCmd(c, "volume", "ls") outArr := strings.Split(strings.TrimSpace(out), "\n") c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out)) assertVolList(c, out, []string{"aaa", "soo", "test"}) } +func (s *DockerSuite) TestVolumeLsFormat(c *check.C) { + dockerCmd(c, "volume", "create", "--name", "aaa") + dockerCmd(c, "volume", "create", "--name", "test") + dockerCmd(c, "volume", "create", "--name", "soo") + + out, _ := dockerCmd(c, "volume", "ls", "--format", "{{.Name}}") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"aaa", "soo", "test"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + +func (s *DockerSuite) TestVolumeLsFormatDefaultFormat(c *check.C) { + dockerCmd(c, "volume", "create", "--name", "aaa") + dockerCmd(c, "volume", "create", "--name", "test") + dockerCmd(c, "volume", "create", "--name", "soo") + + config := `{ + "volumesFormat": "{{ .Name }} default" +}` + d, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(d) + + err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "--config", d, "volume", "ls") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"aaa default", "soo default", "test default"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + // assertVolList checks volume retrieved with ls command // equals to expected volume list // note: out should be `volume ls [option]` result