Browse code

Add volume --format flag to ls

Signed-off-by: Vincent Demeester <vincent@sbr.pm>

Vincent Demeester authored on 2016/08/04 21:59:55
Showing 7 changed files
... ...
@@ -136,6 +136,12 @@ func (cli *DockerCli) NetworksFormat() string {
136 136
 	return cli.configFile.NetworksFormat
137 137
 }
138 138
 
139
+// VolumesFormat returns the format string specified in the configuration.
140
+// String contains columns and format specification, for example {{ID}}\t{{Name}}
141
+func (cli *DockerCli) VolumesFormat() string {
142
+	return cli.configFile.VolumesFormat
143
+}
144
+
139 145
 func (cli *DockerCli) setRawTerminal() error {
140 146
 	if os.Getenv("NORAW") == "" {
141 147
 		if cli.isTerminalIn {
142 148
new file mode 100644
... ...
@@ -0,0 +1,114 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strings"
6
+
7
+	"github.com/docker/engine-api/types"
8
+)
9
+
10
+const (
11
+	defaultVolumeQuietFormat = "{{.Name}}"
12
+	defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
13
+
14
+	mountpointHeader = "MOUNTPOINT"
15
+	// Status header ?
16
+)
17
+
18
+// VolumeContext contains volume specific information required by the formatter,
19
+// encapsulate a Context struct.
20
+type VolumeContext struct {
21
+	Context
22
+	// Volumes
23
+	Volumes []*types.Volume
24
+}
25
+
26
+func (ctx VolumeContext) Write() {
27
+	switch ctx.Format {
28
+	case tableFormatKey:
29
+		if ctx.Quiet {
30
+			ctx.Format = defaultVolumeQuietFormat
31
+		} else {
32
+			ctx.Format = defaultVolumeTableFormat
33
+		}
34
+	case rawFormatKey:
35
+		if ctx.Quiet {
36
+			ctx.Format = `name: {{.Name}}`
37
+		} else {
38
+			ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
39
+		}
40
+	}
41
+
42
+	ctx.buffer = bytes.NewBufferString("")
43
+	ctx.preformat()
44
+
45
+	tmpl, err := ctx.parseFormat()
46
+	if err != nil {
47
+		return
48
+	}
49
+
50
+	for _, volume := range ctx.Volumes {
51
+		volumeCtx := &volumeContext{
52
+			v: volume,
53
+		}
54
+		err = ctx.contextFormat(tmpl, volumeCtx)
55
+		if err != nil {
56
+			return
57
+		}
58
+	}
59
+
60
+	ctx.postformat(tmpl, &networkContext{})
61
+}
62
+
63
+type volumeContext struct {
64
+	baseSubContext
65
+	v *types.Volume
66
+}
67
+
68
+func (c *volumeContext) Name() string {
69
+	c.addHeader(nameHeader)
70
+	return c.v.Name
71
+}
72
+
73
+func (c *volumeContext) Driver() string {
74
+	c.addHeader(driverHeader)
75
+	return c.v.Driver
76
+}
77
+
78
+func (c *volumeContext) Scope() string {
79
+	c.addHeader(scopeHeader)
80
+	return c.v.Scope
81
+}
82
+
83
+func (c *volumeContext) Mountpoint() string {
84
+	c.addHeader(mountpointHeader)
85
+	return c.v.Mountpoint
86
+}
87
+
88
+func (c *volumeContext) Labels() string {
89
+	c.addHeader(labelsHeader)
90
+	if c.v.Labels == nil {
91
+		return ""
92
+	}
93
+
94
+	var joinLabels []string
95
+	for k, v := range c.v.Labels {
96
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
97
+	}
98
+	return strings.Join(joinLabels, ",")
99
+}
100
+
101
+func (c *volumeContext) Label(name string) string {
102
+
103
+	n := strings.Split(name, ".")
104
+	r := strings.NewReplacer("-", " ", "_", " ")
105
+	h := r.Replace(n[len(n)-1])
106
+
107
+	c.addHeader(h)
108
+
109
+	if c.v.Labels == nil {
110
+		return ""
111
+	}
112
+	return c.v.Labels[name]
113
+}
0 114
new file mode 100644
... ...
@@ -0,0 +1,183 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"strings"
5
+	"testing"
6
+
7
+	"github.com/docker/docker/pkg/stringid"
8
+	"github.com/docker/engine-api/types"
9
+)
10
+
11
+func TestVolumeContext(t *testing.T) {
12
+	volumeName := stringid.GenerateRandomID()
13
+
14
+	var ctx volumeContext
15
+	cases := []struct {
16
+		volumeCtx volumeContext
17
+		expValue  string
18
+		expHeader string
19
+		call      func() string
20
+	}{
21
+		{volumeContext{
22
+			v: &types.Volume{Name: volumeName},
23
+		}, volumeName, nameHeader, ctx.Name},
24
+		{volumeContext{
25
+			v: &types.Volume{Driver: "driver_name"},
26
+		}, "driver_name", driverHeader, ctx.Driver},
27
+		{volumeContext{
28
+			v: &types.Volume{Scope: "local"},
29
+		}, "local", scopeHeader, ctx.Scope},
30
+		{volumeContext{
31
+			v: &types.Volume{Mountpoint: "mountpoint"},
32
+		}, "mountpoint", mountpointHeader, ctx.Mountpoint},
33
+		{volumeContext{
34
+			v: &types.Volume{},
35
+		}, "", labelsHeader, ctx.Labels},
36
+		{volumeContext{
37
+			v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
38
+		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
39
+	}
40
+
41
+	for _, c := range cases {
42
+		ctx = c.volumeCtx
43
+		v := c.call()
44
+		if strings.Contains(v, ",") {
45
+			compareMultipleValues(t, v, c.expValue)
46
+		} else if v != c.expValue {
47
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
48
+		}
49
+
50
+		h := ctx.fullHeader()
51
+		if h != c.expHeader {
52
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
53
+		}
54
+	}
55
+}
56
+
57
+func TestVolumeContextWrite(t *testing.T) {
58
+	contexts := []struct {
59
+		context  VolumeContext
60
+		expected string
61
+	}{
62
+
63
+		// Errors
64
+		{
65
+			VolumeContext{
66
+				Context: Context{
67
+					Format: "{{InvalidFunction}}",
68
+				},
69
+			},
70
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
71
+`,
72
+		},
73
+		{
74
+			VolumeContext{
75
+				Context: Context{
76
+					Format: "{{nil}}",
77
+				},
78
+			},
79
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
80
+`,
81
+		},
82
+		// Table format
83
+		{
84
+			VolumeContext{
85
+				Context: Context{
86
+					Format: "table",
87
+				},
88
+			},
89
+			`DRIVER              NAME
90
+foo                 foobar_baz
91
+bar                 foobar_bar
92
+`,
93
+		},
94
+		{
95
+			VolumeContext{
96
+				Context: Context{
97
+					Format: "table",
98
+					Quiet:  true,
99
+				},
100
+			},
101
+			`foobar_baz
102
+foobar_bar
103
+`,
104
+		},
105
+		{
106
+			VolumeContext{
107
+				Context: Context{
108
+					Format: "table {{.Name}}",
109
+				},
110
+			},
111
+			`NAME
112
+foobar_baz
113
+foobar_bar
114
+`,
115
+		},
116
+		{
117
+			VolumeContext{
118
+				Context: Context{
119
+					Format: "table {{.Name}}",
120
+					Quiet:  true,
121
+				},
122
+			},
123
+			`NAME
124
+foobar_baz
125
+foobar_bar
126
+`,
127
+		},
128
+		// Raw Format
129
+		{
130
+			VolumeContext{
131
+				Context: Context{
132
+					Format: "raw",
133
+				},
134
+			}, `name: foobar_baz
135
+driver: foo
136
+
137
+name: foobar_bar
138
+driver: bar
139
+
140
+`,
141
+		},
142
+		{
143
+			VolumeContext{
144
+				Context: Context{
145
+					Format: "raw",
146
+					Quiet:  true,
147
+				},
148
+			},
149
+			`name: foobar_baz
150
+name: foobar_bar
151
+`,
152
+		},
153
+		// Custom Format
154
+		{
155
+			VolumeContext{
156
+				Context: Context{
157
+					Format: "{{.Name}}",
158
+				},
159
+			},
160
+			`foobar_baz
161
+foobar_bar
162
+`,
163
+		},
164
+	}
165
+
166
+	for _, context := range contexts {
167
+		volumes := []*types.Volume{
168
+			{Name: "foobar_baz", Driver: "foo"},
169
+			{Name: "foobar_bar", Driver: "bar"},
170
+		}
171
+		out := bytes.NewBufferString("")
172
+		context.context.Output = out
173
+		context.context.Volumes = volumes
174
+		context.context.Write()
175
+		actual := out.String()
176
+		if actual != context.expected {
177
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
178
+		}
179
+		// Clean buffer
180
+		out.Reset()
181
+	}
182
+}
... ...
@@ -1,13 +1,12 @@
1 1
 package volume
2 2
 
3 3
 import (
4
-	"fmt"
5 4
 	"sort"
6
-	"text/tabwriter"
7 5
 
8 6
 	"golang.org/x/net/context"
9 7
 
10 8
 	"github.com/docker/docker/api/client"
9
+	"github.com/docker/docker/api/client/formatter"
11 10
 	"github.com/docker/docker/cli"
12 11
 	"github.com/docker/engine-api/types"
13 12
 	"github.com/docker/engine-api/types/filters"
... ...
@@ -24,6 +23,7 @@ func (r byVolumeName) Less(i, j int) bool {
24 24
 
25 25
 type listOptions struct {
26 26
 	quiet  bool
27
+	format string
27 28
 	filter []string
28 29
 }
29 30
 
... ...
@@ -43,6 +43,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
43 43
 
44 44
 	flags := cmd.Flags()
45 45
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
46
+	flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template")
46 47
 	flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')")
47 48
 
48 49
 	return cmd
... ...
@@ -65,24 +66,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
65 65
 		return err
66 66
 	}
67 67
 
68
-	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
69
-	if !opts.quiet {
70
-		for _, warn := range volumes.Warnings {
71
-			fmt.Fprintln(dockerCli.Err(), warn)
68
+	f := opts.format
69
+	if len(f) == 0 {
70
+		if len(dockerCli.VolumesFormat()) > 0 && !opts.quiet {
71
+			f = dockerCli.VolumesFormat()
72
+		} else {
73
+			f = "table"
72 74
 		}
73
-		fmt.Fprintf(w, "DRIVER \tVOLUME NAME")
74
-		fmt.Fprintf(w, "\n")
75 75
 	}
76 76
 
77 77
 	sort.Sort(byVolumeName(volumes.Volumes))
78
-	for _, vol := range volumes.Volumes {
79
-		if opts.quiet {
80
-			fmt.Fprintln(w, vol.Name)
81
-			continue
82
-		}
83
-		fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name)
78
+
79
+	volumeCtx := formatter.VolumeContext{
80
+		Context: formatter.Context{
81
+			Output: dockerCli.Out(),
82
+			Format: f,
83
+			Quiet:  opts.quiet,
84
+		},
85
+		Volumes: volumes.Volumes,
84 86
 	}
85
-	w.Flush()
87
+
88
+	volumeCtx.Write()
89
+
86 90
 	return nil
87 91
 }
88 92
 
... ...
@@ -27,6 +27,7 @@ type ConfigFile struct {
27 27
 	PsFormat         string                      `json:"psFormat,omitempty"`
28 28
 	ImagesFormat     string                      `json:"imagesFormat,omitempty"`
29 29
 	NetworksFormat   string                      `json:"networksFormat,omitempty"`
30
+	VolumesFormat    string                      `json:"volumesFormat,omitempty"`
30 31
 	DetachKeys       string                      `json:"detachKeys,omitempty"`
31 32
 	CredentialsStore string                      `json:"credsStore,omitempty"`
32 33
 	Filename         string                      `json:"-"` // Note: for internal use only
... ...
@@ -23,6 +23,7 @@ Options:
23 23
                        - dangling=<boolean> a volume if referenced or not
24 24
                        - driver=<string> a volume's driver name
25 25
                        - name=<string> a volume's name
26
+      --format string  Pretty-print volumes using a Go template
26 27
       --help           Print usage
27 28
   -q, --quiet          Only display volume names
28 29
 ```
... ...
@@ -82,6 +83,36 @@ The following filter matches all volumes with a name containing the `rose` strin
82 82
     DRIVER              VOLUME NAME
83 83
     local               rosemary
84 84
 
85
+## Formatting
86
+
87
+The formatting options (`--format`) pretty-prints volumes output
88
+using a Go template.
89
+
90
+Valid placeholders for the Go template are listed below:
91
+
92
+Placeholder   | Description
93
+--------------|------------------------------------------------------------------------------------------
94
+`.Name`       | Network name
95
+`.Driver`     | Network driver
96
+`.Scope`      | Network scope (local, global)
97
+`.Mountpoint` | Whether the network is internal or not.
98
+`.Labels`     | All labels assigned to the volume.
99
+`.Label`      | Value of a specific label for this volume. For example `{{.Label "project.version"}}`
100
+
101
+When using the `--format` option, the `volume ls` command will either
102
+output the data exactly as the template declares or, when using the
103
+`table` directive, includes column headers as well.
104
+
105
+The following example uses a template without headers and outputs the
106
+`Name` and `Driver` entries separated by a colon for all volumes:
107
+
108
+```bash
109
+$ docker volume ls --format "{{.Name}}: {{.Driver}}"
110
+vol1: local
111
+vol2: local
112
+vol3: local
113
+```
114
+
85 115
 ## Related information
86 116
 
87 117
 * [volume create](volume_create.md)
... ...
@@ -1,7 +1,10 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"io/ioutil"
5
+	"os"
4 6
 	"os/exec"
7
+	"path/filepath"
5 8
 	"strings"
6 9
 
7 10
 	"github.com/docker/docker/pkg/integration/checker"
... ...
@@ -65,20 +68,62 @@ func (s *DockerSuite) TestVolumeCliInspectMulti(c *check.C) {
65 65
 
66 66
 func (s *DockerSuite) TestVolumeCliLs(c *check.C) {
67 67
 	prefix, _ := getPrefixAndSlashFromDaemonPlatform()
68
-	out, _ := dockerCmd(c, "volume", "create", "--name", "aaa")
68
+	dockerCmd(c, "volume", "create", "--name", "aaa")
69 69
 
70 70
 	dockerCmd(c, "volume", "create", "--name", "test")
71 71
 
72 72
 	dockerCmd(c, "volume", "create", "--name", "soo")
73 73
 	dockerCmd(c, "run", "-v", "soo:"+prefix+"/foo", "busybox", "ls", "/")
74 74
 
75
-	out, _ = dockerCmd(c, "volume", "ls")
75
+	out, _ := dockerCmd(c, "volume", "ls")
76 76
 	outArr := strings.Split(strings.TrimSpace(out), "\n")
77 77
 	c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out))
78 78
 
79 79
 	assertVolList(c, out, []string{"aaa", "soo", "test"})
80 80
 }
81 81
 
82
+func (s *DockerSuite) TestVolumeLsFormat(c *check.C) {
83
+	dockerCmd(c, "volume", "create", "--name", "aaa")
84
+	dockerCmd(c, "volume", "create", "--name", "test")
85
+	dockerCmd(c, "volume", "create", "--name", "soo")
86
+
87
+	out, _ := dockerCmd(c, "volume", "ls", "--format", "{{.Name}}")
88
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
89
+
90
+	expected := []string{"aaa", "soo", "test"}
91
+	var names []string
92
+	for _, l := range lines {
93
+		names = append(names, l)
94
+	}
95
+	c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
96
+}
97
+
98
+func (s *DockerSuite) TestVolumeLsFormatDefaultFormat(c *check.C) {
99
+	dockerCmd(c, "volume", "create", "--name", "aaa")
100
+	dockerCmd(c, "volume", "create", "--name", "test")
101
+	dockerCmd(c, "volume", "create", "--name", "soo")
102
+
103
+	config := `{
104
+		"volumesFormat": "{{ .Name }} default"
105
+}`
106
+	d, err := ioutil.TempDir("", "integration-cli-")
107
+	c.Assert(err, checker.IsNil)
108
+	defer os.RemoveAll(d)
109
+
110
+	err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644)
111
+	c.Assert(err, checker.IsNil)
112
+
113
+	out, _ := dockerCmd(c, "--config", d, "volume", "ls")
114
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
115
+
116
+	expected := []string{"aaa default", "soo default", "test default"}
117
+	var names []string
118
+	for _, l := range lines {
119
+		names = append(names, l)
120
+	}
121
+	c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
122
+}
123
+
82 124
 // assertVolList checks volume retrieved with ls command
83 125
 // equals to expected volume list
84 126
 // note: out should be `volume ls [option]` result