Browse code

Add network --format flag to ls

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

Vincent Demeester authored on 2016/08/04 21:59:51
Showing 9 changed files
... ...
@@ -130,6 +130,12 @@ func (cli *DockerCli) ImagesFormat() string {
130 130
 	return cli.configFile.ImagesFormat
131 131
 }
132 132
 
133
+// NetworksFormat returns the format string specified in the configuration.
134
+// String contains columns and format specification, for example {{ID}}\t{{Name}}
135
+func (cli *DockerCli) NetworksFormat() string {
136
+	return cli.configFile.NetworksFormat
137
+}
138
+
133 139
 func (cli *DockerCli) setRawTerminal() error {
134 140
 	if os.Getenv("NORAW") == "" {
135 141
 		if cli.isTerminalIn {
... ...
@@ -14,6 +14,7 @@ const (
14 14
 	labelsHeader       = "LABELS"
15 15
 	nameHeader         = "NAME"
16 16
 	driverHeader       = "DRIVER"
17
+	scopeHeader        = "SCOPE"
17 18
 )
18 19
 
19 20
 type subContext interface {
20 21
new file mode 100644
... ...
@@ -0,0 +1,129 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strings"
6
+
7
+	"github.com/docker/docker/pkg/stringid"
8
+	"github.com/docker/engine-api/types"
9
+)
10
+
11
+const (
12
+	defaultNetworkTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}"
13
+
14
+	networkIDHeader = "NETWORK ID"
15
+	ipv6Header      = "IPV6"
16
+	internalHeader  = "INTERNAL"
17
+)
18
+
19
+// NetworkContext contains network specific information required by the formatter,
20
+// encapsulate a Context struct.
21
+type NetworkContext struct {
22
+	Context
23
+	// Networks
24
+	Networks []types.NetworkResource
25
+}
26
+
27
+func (ctx NetworkContext) Write() {
28
+	switch ctx.Format {
29
+	case tableFormatKey:
30
+		if ctx.Quiet {
31
+			ctx.Format = defaultQuietFormat
32
+		} else {
33
+			ctx.Format = defaultNetworkTableFormat
34
+		}
35
+	case rawFormatKey:
36
+		if ctx.Quiet {
37
+			ctx.Format = `network_id: {{.ID}}`
38
+		} else {
39
+			ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
40
+		}
41
+	}
42
+
43
+	ctx.buffer = bytes.NewBufferString("")
44
+	ctx.preformat()
45
+
46
+	tmpl, err := ctx.parseFormat()
47
+	if err != nil {
48
+		return
49
+	}
50
+
51
+	for _, network := range ctx.Networks {
52
+		networkCtx := &networkContext{
53
+			trunc: ctx.Trunc,
54
+			n:     network,
55
+		}
56
+		err = ctx.contextFormat(tmpl, networkCtx)
57
+		if err != nil {
58
+			return
59
+		}
60
+	}
61
+
62
+	ctx.postformat(tmpl, &networkContext{})
63
+}
64
+
65
+type networkContext struct {
66
+	baseSubContext
67
+	trunc bool
68
+	n     types.NetworkResource
69
+}
70
+
71
+func (c *networkContext) ID() string {
72
+	c.addHeader(networkIDHeader)
73
+	if c.trunc {
74
+		return stringid.TruncateID(c.n.ID)
75
+	}
76
+	return c.n.ID
77
+}
78
+
79
+func (c *networkContext) Name() string {
80
+	c.addHeader(nameHeader)
81
+	return c.n.Name
82
+}
83
+
84
+func (c *networkContext) Driver() string {
85
+	c.addHeader(driverHeader)
86
+	return c.n.Driver
87
+}
88
+
89
+func (c *networkContext) Scope() string {
90
+	c.addHeader(scopeHeader)
91
+	return c.n.Scope
92
+}
93
+
94
+func (c *networkContext) IPv6() string {
95
+	c.addHeader(ipv6Header)
96
+	return fmt.Sprintf("%v", c.n.EnableIPv6)
97
+}
98
+
99
+func (c *networkContext) Internal() string {
100
+	c.addHeader(internalHeader)
101
+	return fmt.Sprintf("%v", c.n.Internal)
102
+}
103
+
104
+func (c *networkContext) Labels() string {
105
+	c.addHeader(labelsHeader)
106
+	if c.n.Labels == nil {
107
+		return ""
108
+	}
109
+
110
+	var joinLabels []string
111
+	for k, v := range c.n.Labels {
112
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
113
+	}
114
+	return strings.Join(joinLabels, ",")
115
+}
116
+
117
+func (c *networkContext) Label(name string) string {
118
+	n := strings.Split(name, ".")
119
+	r := strings.NewReplacer("-", " ", "_", " ")
120
+	h := r.Replace(n[len(n)-1])
121
+
122
+	c.addHeader(h)
123
+
124
+	if c.n.Labels == nil {
125
+		return ""
126
+	}
127
+	return c.n.Labels[name]
128
+}
0 129
new file mode 100644
... ...
@@ -0,0 +1,201 @@
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 TestNetworkContext(t *testing.T) {
12
+	networkID := stringid.GenerateRandomID()
13
+
14
+	var ctx networkContext
15
+	cases := []struct {
16
+		networkCtx networkContext
17
+		expValue   string
18
+		expHeader  string
19
+		call       func() string
20
+	}{
21
+		{networkContext{
22
+			n:     types.NetworkResource{ID: networkID},
23
+			trunc: false,
24
+		}, networkID, networkIDHeader, ctx.ID},
25
+		{networkContext{
26
+			n:     types.NetworkResource{ID: networkID},
27
+			trunc: true,
28
+		}, stringid.TruncateID(networkID), networkIDHeader, ctx.ID},
29
+		{networkContext{
30
+			n: types.NetworkResource{Name: "network_name"},
31
+		}, "network_name", nameHeader, ctx.Name},
32
+		{networkContext{
33
+			n: types.NetworkResource{Driver: "driver_name"},
34
+		}, "driver_name", driverHeader, ctx.Driver},
35
+		{networkContext{
36
+			n: types.NetworkResource{EnableIPv6: true},
37
+		}, "true", ipv6Header, ctx.IPv6},
38
+		{networkContext{
39
+			n: types.NetworkResource{EnableIPv6: false},
40
+		}, "false", ipv6Header, ctx.IPv6},
41
+		{networkContext{
42
+			n: types.NetworkResource{Internal: true},
43
+		}, "true", internalHeader, ctx.Internal},
44
+		{networkContext{
45
+			n: types.NetworkResource{Internal: false},
46
+		}, "false", internalHeader, ctx.Internal},
47
+		{networkContext{
48
+			n: types.NetworkResource{},
49
+		}, "", labelsHeader, ctx.Labels},
50
+		{networkContext{
51
+			n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
52
+		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
53
+	}
54
+
55
+	for _, c := range cases {
56
+		ctx = c.networkCtx
57
+		v := c.call()
58
+		if strings.Contains(v, ",") {
59
+			compareMultipleValues(t, v, c.expValue)
60
+		} else if v != c.expValue {
61
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
62
+		}
63
+
64
+		h := ctx.fullHeader()
65
+		if h != c.expHeader {
66
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
67
+		}
68
+	}
69
+}
70
+
71
+func TestNetworkContextWrite(t *testing.T) {
72
+	contexts := []struct {
73
+		context  NetworkContext
74
+		expected string
75
+	}{
76
+
77
+		// Errors
78
+		{
79
+			NetworkContext{
80
+				Context: Context{
81
+					Format: "{{InvalidFunction}}",
82
+				},
83
+			},
84
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
85
+`,
86
+		},
87
+		{
88
+			NetworkContext{
89
+				Context: Context{
90
+					Format: "{{nil}}",
91
+				},
92
+			},
93
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
94
+`,
95
+		},
96
+		// Table format
97
+		{
98
+			NetworkContext{
99
+				Context: Context{
100
+					Format: "table",
101
+				},
102
+			},
103
+			`NETWORK ID          NAME                DRIVER              SCOPE
104
+networkID1          foobar_baz          foo                 local
105
+networkID2          foobar_bar          bar                 local
106
+`,
107
+		},
108
+		{
109
+			NetworkContext{
110
+				Context: Context{
111
+					Format: "table",
112
+					Quiet:  true,
113
+				},
114
+			},
115
+			`networkID1
116
+networkID2
117
+`,
118
+		},
119
+		{
120
+			NetworkContext{
121
+				Context: Context{
122
+					Format: "table {{.Name}}",
123
+				},
124
+			},
125
+			`NAME
126
+foobar_baz
127
+foobar_bar
128
+`,
129
+		},
130
+		{
131
+			NetworkContext{
132
+				Context: Context{
133
+					Format: "table {{.Name}}",
134
+					Quiet:  true,
135
+				},
136
+			},
137
+			`NAME
138
+foobar_baz
139
+foobar_bar
140
+`,
141
+		},
142
+		// Raw Format
143
+		{
144
+			NetworkContext{
145
+				Context: Context{
146
+					Format: "raw",
147
+				},
148
+			}, `network_id: networkID1
149
+name: foobar_baz
150
+driver: foo
151
+scope: local
152
+
153
+network_id: networkID2
154
+name: foobar_bar
155
+driver: bar
156
+scope: local
157
+
158
+`,
159
+		},
160
+		{
161
+			NetworkContext{
162
+				Context: Context{
163
+					Format: "raw",
164
+					Quiet:  true,
165
+				},
166
+			},
167
+			`network_id: networkID1
168
+network_id: networkID2
169
+`,
170
+		},
171
+		// Custom Format
172
+		{
173
+			NetworkContext{
174
+				Context: Context{
175
+					Format: "{{.Name}}",
176
+				},
177
+			},
178
+			`foobar_baz
179
+foobar_bar
180
+`,
181
+		},
182
+	}
183
+
184
+	for _, context := range contexts {
185
+		networks := []types.NetworkResource{
186
+			{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
187
+			{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
188
+		}
189
+		out := bytes.NewBufferString("")
190
+		context.context.Output = out
191
+		context.context.Networks = networks
192
+		context.context.Write()
193
+		actual := out.String()
194
+		if actual != context.expected {
195
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
196
+		}
197
+		// Clean buffer
198
+		out.Reset()
199
+	}
200
+}
... ...
@@ -1,15 +1,13 @@
1 1
 package network
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
-	"github.com/docker/docker/pkg/stringid"
13 11
 	"github.com/docker/engine-api/types"
14 12
 	"github.com/docker/engine-api/types/filters"
15 13
 	"github.com/spf13/cobra"
... ...
@@ -24,6 +22,7 @@ func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name }
24 24
 type listOptions struct {
25 25
 	quiet   bool
26 26
 	noTrunc bool
27
+	format  string
27 28
 	filter  []string
28 29
 }
29 30
 
... ...
@@ -43,6 +42,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
43 43
 	flags := cmd.Flags()
44 44
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
45 45
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output")
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
... ...
@@ -69,32 +69,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
69 69
 		return err
70 70
 	}
71 71
 
72
-	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
73
-	if !opts.quiet {
74
-		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
75
-		fmt.Fprintf(w, "\n")
72
+	f := opts.format
73
+	if len(f) == 0 {
74
+		if len(dockerCli.NetworksFormat()) > 0 && !opts.quiet {
75
+			f = dockerCli.NetworksFormat()
76
+		} else {
77
+			f = "table"
78
+		}
76 79
 	}
77 80
 
78 81
 	sort.Sort(byNetworkName(networkResources))
79
-	for _, networkResource := range networkResources {
80
-		ID := networkResource.ID
81
-		netName := networkResource.Name
82
-		driver := networkResource.Driver
83
-		scope := networkResource.Scope
84
-		if !opts.noTrunc {
85
-			ID = stringid.TruncateID(ID)
86
-		}
87
-		if opts.quiet {
88
-			fmt.Fprintln(w, ID)
89
-			continue
90
-		}
91
-		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t",
92
-			ID,
93
-			netName,
94
-			driver,
95
-			scope)
96
-		fmt.Fprint(w, "\n")
82
+
83
+	networksCtx := formatter.NetworkContext{
84
+		Context: formatter.Context{
85
+			Output: dockerCli.Out(),
86
+			Format: f,
87
+			Quiet:  opts.quiet,
88
+			Trunc:  !opts.noTrunc,
89
+		},
90
+		Networks: networkResources,
97 91
 	}
98
-	w.Flush()
92
+
93
+	networksCtx.Write()
94
+
99 95
 	return nil
100 96
 }
... ...
@@ -26,6 +26,7 @@ type ConfigFile struct {
26 26
 	HTTPHeaders      map[string]string           `json:"HttpHeaders,omitempty"`
27 27
 	PsFormat         string                      `json:"psFormat,omitempty"`
28 28
 	ImagesFormat     string                      `json:"imagesFormat,omitempty"`
29
+	NetworksFormat   string                      `json:"networksFormat,omitempty"`
29 30
 	DetachKeys       string                      `json:"detachKeys,omitempty"`
30 31
 	CredentialsStore string                      `json:"credsStore,omitempty"`
31 32
 	Filename         string                      `json:"-"` // Note: for internal use only
... ...
@@ -20,6 +20,7 @@ Aliases:
20 20
 
21 21
 Options:
22 22
   -f, --filter value   Provide filter values (i.e. 'dangling=true') (default [])
23
+      --format string  Pretty-print networks using a Go template
23 24
       --help           Print usage
24 25
       --no-trunc       Do not truncate the output
25 26
   -q, --quiet          Only display volume names
... ...
@@ -169,6 +170,38 @@ $ docker network rm `docker network ls --filter type=custom -q`
169 169
 A warning will be issued when trying to remove a network that has containers
170 170
 attached.
171 171
 
172
+## Formatting
173
+
174
+The formatting options (`--format`) pretty-prints networks output
175
+using a Go template.
176
+
177
+Valid placeholders for the Go template are listed below:
178
+
179
+Placeholder | Description
180
+------------|------------------------------------------------------------------------------------------
181
+`.ID`       | Network ID 
182
+`.Name`     | Network name
183
+`.Driver`   | Network driver
184
+`.Scope`    | Network scope (local, global)
185
+`.IPv6`     | Whether IPv6 is enabled on the network or not.
186
+`.Internal` | Whether the network is internal or not.
187
+`.Labels`   | All labels assigned to the network.
188
+`.Label`    | Value of a specific label for this network. For example `{{.Label "project.version"}}`
189
+
190
+When using the `--format` option, the `network ls` command will either
191
+output the data exactly as the template declares or, when using the
192
+`table` directive, includes column headers as well.
193
+
194
+The following example uses a template without headers and outputs the
195
+`ID` and `Driver` entries separated by a colon for all networks:
196
+
197
+```bash
198
+$ docker network ls --format "{{.ID}}: {{.Driver}}"
199
+afaaab448eb2: bridge
200
+d1584f8dc718: host
201
+391df270dc66: null
202
+```
203
+
172 204
 ## Related information
173 205
 
174 206
 * [network disconnect ](network_disconnect.md)
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"net/http"
11 11
 	"net/http/httptest"
12 12
 	"os"
13
+	"path/filepath"
13 14
 	"strings"
14 15
 	"time"
15 16
 
... ...
@@ -279,6 +280,43 @@ func (s *DockerNetworkSuite) TestDockerNetworkLsDefault(c *check.C) {
279 279
 	}
280 280
 }
281 281
 
282
+func (s *DockerSuite) TestNetworkLsFormat(c *check.C) {
283
+	testRequires(c, DaemonIsLinux)
284
+	out, _ := dockerCmd(c, "network", "ls", "--format", "{{.Name}}")
285
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
286
+
287
+	expected := []string{"bridge", "host", "none"}
288
+	var names []string
289
+	for _, l := range lines {
290
+		names = append(names, l)
291
+	}
292
+	c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
293
+}
294
+
295
+func (s *DockerSuite) TestNetworkLsFormatDefaultFormat(c *check.C) {
296
+	testRequires(c, DaemonIsLinux)
297
+
298
+	config := `{
299
+		"networksFormat": "{{ .Name }} default"
300
+}`
301
+	d, err := ioutil.TempDir("", "integration-cli-")
302
+	c.Assert(err, checker.IsNil)
303
+	defer os.RemoveAll(d)
304
+
305
+	err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644)
306
+	c.Assert(err, checker.IsNil)
307
+
308
+	out, _ := dockerCmd(c, "--config", d, "network", "ls")
309
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
310
+
311
+	expected := []string{"bridge default", "host default", "none default"}
312
+	var names []string
313
+	for _, l := range lines {
314
+		names = append(names, l)
315
+	}
316
+	c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
317
+}
318
+
282 319
 func (s *DockerNetworkSuite) TestDockerNetworkCreatePredefined(c *check.C) {
283 320
 	predefined := []string{"bridge", "host", "none", "default"}
284 321
 	for _, net := range predefined {
... ...
@@ -7,6 +7,7 @@ docker-network-ls - list networks
7 7
 # SYNOPSIS
8 8
 **docker network ls**
9 9
 [**-f**|**--filter**[=*[]*]]
10
+[**--format**=*"TEMPLATE"*]
10 11
 [**--no-trunc**[=*true*|*false*]]
11 12
 [**-q**|**--quiet**[=*true*|*false*]]
12 13
 [**--help**]
... ...
@@ -162,6 +163,18 @@ attached.
162 162
 **-f**, **--filter**=*[]*
163 163
   filter output based on conditions provided. 
164 164
 
165
+**--format**="*TEMPLATE*"
166
+  Pretty-print networks using a Go template.
167
+  Valid placeholders:
168
+     .ID - Network ID
169
+     .Name - Network name
170
+     .Driver - Network driver
171
+     .Scope - Network scope (local, global)
172
+     .IPv6 - Whether IPv6 is enabled on the network or not
173
+     .Internal - Whether the network is internal or not
174
+     .Labels - All labels assigned to the network
175
+     .Label - Value of a specific label for this network. For example `{{.Label "project.version"}}`
176
+
165 177
 **--no-trunc**=*true*|*false*
166 178
   Do not truncate the output
167 179