Browse Source

Merge pull request #31557 from ripcurld0/add_stack_ls

Add the format option to the docker stack ls command
Sebastiaan van Stijn 8 years ago
parent
commit
6559abaf47

+ 67 - 0
cli/command/formatter/stack.go

@@ -0,0 +1,67 @@
+package formatter
+
+import (
+	"strconv"
+)
+
+const (
+	defaultStackTableFormat = "table {{.Name}}\t{{.Services}}"
+
+	stackServicesHeader = "SERVICES"
+)
+
+// Stack contains deployed stack information.
+type Stack struct {
+	// Name is the name of the stack
+	Name string
+	// Services is the number of the services
+	Services int
+}
+
+// NewStackFormat returns a format for use with a stack Context
+func NewStackFormat(source string) Format {
+	switch source {
+	case TableFormatKey:
+		return defaultStackTableFormat
+	}
+	return Format(source)
+}
+
+// StackWrite writes formatted stacks using the Context
+func StackWrite(ctx Context, stacks []*Stack) error {
+	render := func(format func(subContext subContext) error) error {
+		for _, stack := range stacks {
+			if err := format(&stackContext{s: stack}); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+	return ctx.Write(newStackContext(), render)
+}
+
+type stackContext struct {
+	HeaderContext
+	s *Stack
+}
+
+func newStackContext() *stackContext {
+	stackCtx := stackContext{}
+	stackCtx.header = map[string]string{
+		"Name":     nameHeader,
+		"Services": stackServicesHeader,
+	}
+	return &stackCtx
+}
+
+func (s *stackContext) MarshalJSON() ([]byte, error) {
+	return marshalJSON(s)
+}
+
+func (s *stackContext) Name() string {
+	return s.s.Name
+}
+
+func (s *stackContext) Services() string {
+	return strconv.Itoa(s.s.Services)
+}

+ 64 - 0
cli/command/formatter/stack_test.go

@@ -0,0 +1,64 @@
+package formatter
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestStackContextWrite(t *testing.T) {
+	cases := []struct {
+		context  Context
+		expected string
+	}{
+		// Errors
+		{
+			Context{Format: "{{InvalidFunction}}"},
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
+`,
+		},
+		{
+			Context{Format: "{{nil}}"},
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
+`,
+		},
+		// Table format
+		{
+			Context{Format: NewStackFormat("table")},
+			`NAME                SERVICES
+baz                 2
+bar                 1
+`,
+		},
+		{
+			Context{Format: NewStackFormat("table {{.Name}}")},
+			`NAME
+baz
+bar
+`,
+		},
+		// Custom Format
+		{
+			Context{Format: NewStackFormat("{{.Name}}")},
+			`baz
+bar
+`,
+		},
+	}
+
+	stacks := []*Stack{
+		{Name: "baz", Services: 2},
+		{Name: "bar", Services: 1},
+	}
+	for _, testcase := range cases {
+		out := bytes.NewBufferString("")
+		testcase.context.Output = out
+		err := StackWrite(testcase.context, stacks)
+		if err != nil {
+			assert.Error(t, err, testcase.expected)
+		} else {
+			assert.Equal(t, out.String(), testcase.expected)
+		}
+	}
+}

+ 19 - 46
cli/command/stack/list.go

@@ -1,15 +1,12 @@
 package stack
 
 import (
-	"fmt"
-	"io"
 	"sort"
-	"strconv"
-	"text/tabwriter"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/cli/compose/convert"
 	"github.com/docker/docker/client"
 	"github.com/pkg/errors"
@@ -17,11 +14,8 @@ import (
 	"golang.org/x/net/context"
 )
 
-const (
-	listItemFmt = "%s\t%s\n"
-)
-
 type listOptions struct {
+	format string
 }
 
 func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
@@ -37,6 +31,8 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
 		},
 	}
 
+	flags := cmd.Flags()
+	flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template")
 	return cmd
 }
 
@@ -48,55 +44,32 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
 	if err != nil {
 		return err
 	}
-
-	out := dockerCli.Out()
-	printTable(out, stacks)
-	return nil
+	format := opts.format
+	if len(format) == 0 {
+		format = formatter.TableFormatKey
+	}
+	stackCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewStackFormat(format),
+	}
+	sort.Sort(byName(stacks))
+	return formatter.StackWrite(stackCtx, stacks)
 }
 
-type byName []*stack
+type byName []*formatter.Stack
 
 func (n byName) Len() int           { return len(n) }
 func (n byName) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }
 func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
 
-func printTable(out io.Writer, stacks []*stack) {
-	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
-
-	// Ignore flushing errors
-	defer writer.Flush()
-
-	sort.Sort(byName(stacks))
-
-	fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES")
-	for _, stack := range stacks {
-		fmt.Fprintf(
-			writer,
-			listItemFmt,
-			stack.Name,
-			strconv.Itoa(stack.Services),
-		)
-	}
-}
-
-type stack struct {
-	// Name is the name of the stack
-	Name string
-	// Services is the number of the services
-	Services int
-}
-
-func getStacks(
-	ctx context.Context,
-	apiclient client.APIClient,
-) ([]*stack, error) {
+func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) {
 	services, err := apiclient.ServiceList(
 		ctx,
 		types.ServiceListOptions{Filters: getAllStacksFilter()})
 	if err != nil {
 		return nil, err
 	}
-	m := make(map[string]*stack, 0)
+	m := make(map[string]*formatter.Stack, 0)
 	for _, service := range services {
 		labels := service.Spec.Labels
 		name, ok := labels[convert.LabelNamespace]
@@ -106,7 +79,7 @@ func getStacks(
 		}
 		ztack, ok := m[name]
 		if !ok {
-			m[name] = &stack{
+			m[name] = &formatter.Stack{
 				Name:     name,
 				Services: 1,
 			}
@@ -114,7 +87,7 @@ func getStacks(
 			ztack.Services++
 		}
 	}
-	var stacks []*stack
+	var stacks []*formatter.Stack
 	for _, stack := range m {
 		stacks = append(stacks, stack)
 	}

+ 26 - 1
docs/reference/commandline/stack_ls.md

@@ -24,7 +24,8 @@ Aliases:
   ls, list
 
 Options:
-      --help   Print usage
+      --help            Print usage
+      --format string   Pretty-print stacks using a Go template
 ```
 
 ## Description
@@ -43,6 +44,30 @@ vossibility-stack  6
 myapp              2
 ```
 
+### Formatting
+
+The formatting option (`--format`) pretty-prints stacks using a Go template.
+
+Valid placeholders for the Go template are listed below:
+
+| Placeholder | Description        |
+| ----------- | ------------------ |
+| `.Name`     | Stack name         |
+| `.Services` | Number of services |
+
+When using the `--format` option, the `stack ls` command either outputs
+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 `Services` entries separated by a colon for all stacks:
+
+```bash
+$ docker stack ls --format "{{.Name}}: {{.Services}}"
+web-server: 1
+web-cache: 4
+```
+
 ## Related commands
 
 * [stack deploy](stack_deploy.md)

+ 15 - 4
integration-cli/docker_cli_stack_test.go

@@ -15,6 +15,17 @@ import (
 	"github.com/go-check/check"
 )
 
+var cleanSpaces = func(s string) string {
+	lines := strings.Split(s, "\n")
+	for i, line := range lines {
+		spaceIx := strings.Index(line, " ")
+		if spaceIx > 0 {
+			lines[i] = line[:spaceIx+1] + strings.TrimLeft(line[spaceIx:], " ")
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
 func (s *DockerSwarmSuite) TestStackRemoveUnknown(c *check.C) {
 	d := s.AddDaemon(c, true, true)
 
@@ -59,13 +70,13 @@ func (s *DockerSwarmSuite) TestStackDeployComposeFile(c *check.C) {
 
 	out, err = d.Cmd("stack", "ls")
 	c.Assert(err, checker.IsNil)
-	c.Assert(out, check.Equals, "NAME        SERVICES\n"+"testdeploy  2\n")
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n"+"testdeploy 2\n")
 
 	out, err = d.Cmd("stack", "rm", testStackName)
 	c.Assert(err, checker.IsNil)
 	out, err = d.Cmd("stack", "ls")
 	c.Assert(err, checker.IsNil)
-	c.Assert(out, check.Equals, "NAME  SERVICES\n")
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n")
 }
 
 func (s *DockerSwarmSuite) TestStackDeployWithSecretsTwice(c *check.C) {
@@ -180,7 +191,7 @@ func (s *DockerSwarmSuite) TestStackDeployWithDAB(c *check.C) {
 	stackArgs = []string{"stack", "ls"}
 	out, err = d.Cmd(stackArgs...)
 	c.Assert(err, checker.IsNil)
-	c.Assert(out, check.Equals, "NAME  SERVICES\n"+"test  2\n")
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n"+"test 2\n")
 	// rm
 	stackArgs = []string{"stack", "rm", testStackName}
 	out, err = d.Cmd(stackArgs...)
@@ -191,5 +202,5 @@ func (s *DockerSwarmSuite) TestStackDeployWithDAB(c *check.C) {
 	stackArgs = []string{"stack", "ls"}
 	out, err = d.Cmd(stackArgs...)
 	c.Assert(err, checker.IsNil)
-	c.Assert(out, check.Equals, "NAME  SERVICES\n")
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n")
 }