Browse Source

Add volume --format flag to ls

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Vincent Demeester 9 years ago
parent
commit
a488ad1a09

+ 6 - 0
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 {

+ 114 - 0
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]
+}

+ 183 - 0
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>: 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()
+	}
+}

+ 20 - 15
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
 }
 

+ 1 - 0
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

+ 31 - 0
docs/reference/commandline/volume_ls.md

@@ -23,6 +23,7 @@ Options:
                        - dangling=<boolean> a volume if referenced or not
                        - driver=<string> a volume's driver name
                        - name=<string> 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)

+ 47 - 2
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