Quellcode durchsuchen

Add network --format flag to ls

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Vincent Demeester vor 9 Jahren
Ursprung
Commit
a8aaafc4a3

+ 6 - 0
api/client/cli.go

@@ -130,6 +130,12 @@ func (cli *DockerCli) ImagesFormat() string {
 	return cli.configFile.ImagesFormat
 }
 
+// NetworksFormat returns the format string specified in the configuration.
+// String contains columns and format specification, for example {{ID}}\t{{Name}}
+func (cli *DockerCli) NetworksFormat() string {
+	return cli.configFile.NetworksFormat
+}
+
 func (cli *DockerCli) setRawTerminal() error {
 	if os.Getenv("NORAW") == "" {
 		if cli.isTerminalIn {

+ 1 - 0
api/client/formatter/custom.go

@@ -14,6 +14,7 @@ const (
 	labelsHeader       = "LABELS"
 	nameHeader         = "NAME"
 	driverHeader       = "DRIVER"
+	scopeHeader        = "SCOPE"
 )
 
 type subContext interface {

+ 129 - 0
api/client/formatter/network.go

@@ -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 - 0
api/client/formatter/network_test.go

@@ -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()
+	}
+}

+ 22 - 26
api/client/network/list.go

@@ -1,15 +1,13 @@
 package network
 
 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/docker/pkg/stringid"
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types/filters"
 	"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 {
 	quiet   bool
 	noTrunc bool
+	format  string
 	filter  []string
 }
 
@@ -43,6 +42,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
 	flags := cmd.Flags()
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
 	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')")
 
 	return cmd
@@ -69,32 +69,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
 		return err
 	}
 
-	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
-	if !opts.quiet {
-		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
-		fmt.Fprintf(w, "\n")
+	f := opts.format
+	if len(f) == 0 {
+		if len(dockerCli.NetworksFormat()) > 0 && !opts.quiet {
+			f = dockerCli.NetworksFormat()
+		} else {
+			f = "table"
+		}
 	}
 
 	sort.Sort(byNetworkName(networkResources))
-	for _, networkResource := range networkResources {
-		ID := networkResource.ID
-		netName := networkResource.Name
-		driver := networkResource.Driver
-		scope := networkResource.Scope
-		if !opts.noTrunc {
-			ID = stringid.TruncateID(ID)
-		}
-		if opts.quiet {
-			fmt.Fprintln(w, ID)
-			continue
-		}
-		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t",
-			ID,
-			netName,
-			driver,
-			scope)
-		fmt.Fprint(w, "\n")
+
+	networksCtx := formatter.NetworkContext{
+		Context: formatter.Context{
+			Output: dockerCli.Out(),
+			Format: f,
+			Quiet:  opts.quiet,
+			Trunc:  !opts.noTrunc,
+		},
+		Networks: networkResources,
 	}
-	w.Flush()
+
+	networksCtx.Write()
+
 	return nil
 }

+ 1 - 0
cliconfig/configfile/file.go

@@ -26,6 +26,7 @@ type ConfigFile struct {
 	HTTPHeaders      map[string]string           `json:"HttpHeaders,omitempty"`
 	PsFormat         string                      `json:"psFormat,omitempty"`
 	ImagesFormat     string                      `json:"imagesFormat,omitempty"`
+	NetworksFormat   string                      `json:"networksFormat,omitempty"`
 	DetachKeys       string                      `json:"detachKeys,omitempty"`
 	CredentialsStore string                      `json:"credsStore,omitempty"`
 	Filename         string                      `json:"-"` // Note: for internal use only

+ 33 - 0
docs/reference/commandline/network_ls.md

@@ -20,6 +20,7 @@ Aliases:
 
 Options:
   -f, --filter value   Provide filter values (i.e. 'dangling=true') (default [])
+      --format string  Pretty-print networks using a Go template
       --help           Print usage
       --no-trunc       Do not truncate the output
   -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
 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
 
 * [network disconnect ](network_disconnect.md)

+ 38 - 0
integration-cli/docker_cli_network_unix_test.go

@@ -10,6 +10,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"path/filepath"
 	"strings"
 	"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) {
 	predefined := []string{"bridge", "host", "none", "default"}
 	for _, net := range predefined {

+ 13 - 0
man/docker-network-ls.1.md

@@ -7,6 +7,7 @@ docker-network-ls - list networks
 # SYNOPSIS
 **docker network ls**
 [**-f**|**--filter**[=*[]*]]
+[**--format**=*"TEMPLATE"*]
 [**--no-trunc**[=*true*|*false*]]
 [**-q**|**--quiet**[=*true*|*false*]]
 [**--help**]
@@ -162,6 +163,18 @@ attached.
 **-f**, **--filter**=*[]*
   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*
   Do not truncate the output