Browse code

Add format to docker stack ls

Signed-off-by: Boaz Shuster <ripcurld.github@gmail.com>

Boaz Shuster authored on 2017/03/06 03:02:03
Showing 5 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,67 @@
0
+package formatter
1
+
2
+import (
3
+	"strconv"
4
+)
5
+
6
+const (
7
+	defaultStackTableFormat = "table {{.Name}}\t{{.Services}}"
8
+
9
+	stackServicesHeader = "SERVICES"
10
+)
11
+
12
+// Stack contains deployed stack information.
13
+type Stack struct {
14
+	// Name is the name of the stack
15
+	Name string
16
+	// Services is the number of the services
17
+	Services int
18
+}
19
+
20
+// NewStackFormat returns a format for use with a stack Context
21
+func NewStackFormat(source string) Format {
22
+	switch source {
23
+	case TableFormatKey:
24
+		return defaultStackTableFormat
25
+	}
26
+	return Format(source)
27
+}
28
+
29
+// StackWrite writes formatted stacks using the Context
30
+func StackWrite(ctx Context, stacks []*Stack) error {
31
+	render := func(format func(subContext subContext) error) error {
32
+		for _, stack := range stacks {
33
+			if err := format(&stackContext{s: stack}); err != nil {
34
+				return err
35
+			}
36
+		}
37
+		return nil
38
+	}
39
+	return ctx.Write(newStackContext(), render)
40
+}
41
+
42
+type stackContext struct {
43
+	HeaderContext
44
+	s *Stack
45
+}
46
+
47
+func newStackContext() *stackContext {
48
+	stackCtx := stackContext{}
49
+	stackCtx.header = map[string]string{
50
+		"Name":     nameHeader,
51
+		"Services": stackServicesHeader,
52
+	}
53
+	return &stackCtx
54
+}
55
+
56
+func (s *stackContext) MarshalJSON() ([]byte, error) {
57
+	return marshalJSON(s)
58
+}
59
+
60
+func (s *stackContext) Name() string {
61
+	return s.s.Name
62
+}
63
+
64
+func (s *stackContext) Services() string {
65
+	return strconv.Itoa(s.s.Services)
66
+}
0 67
new file mode 100644
... ...
@@ -0,0 +1,64 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"github.com/stretchr/testify/assert"
7
+)
8
+
9
+func TestStackContextWrite(t *testing.T) {
10
+	cases := []struct {
11
+		context  Context
12
+		expected string
13
+	}{
14
+		// Errors
15
+		{
16
+			Context{Format: "{{InvalidFunction}}"},
17
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
18
+`,
19
+		},
20
+		{
21
+			Context{Format: "{{nil}}"},
22
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
23
+`,
24
+		},
25
+		// Table format
26
+		{
27
+			Context{Format: NewStackFormat("table")},
28
+			`NAME                SERVICES
29
+baz                 2
30
+bar                 1
31
+`,
32
+		},
33
+		{
34
+			Context{Format: NewStackFormat("table {{.Name}}")},
35
+			`NAME
36
+baz
37
+bar
38
+`,
39
+		},
40
+		// Custom Format
41
+		{
42
+			Context{Format: NewStackFormat("{{.Name}}")},
43
+			`baz
44
+bar
45
+`,
46
+		},
47
+	}
48
+
49
+	stacks := []*Stack{
50
+		{Name: "baz", Services: 2},
51
+		{Name: "bar", Services: 1},
52
+	}
53
+	for _, testcase := range cases {
54
+		out := bytes.NewBufferString("")
55
+		testcase.context.Output = out
56
+		err := StackWrite(testcase.context, stacks)
57
+		if err != nil {
58
+			assert.Error(t, err, testcase.expected)
59
+		} else {
60
+			assert.Equal(t, out.String(), testcase.expected)
61
+		}
62
+	}
63
+}
... ...
@@ -1,15 +1,12 @@
1 1
 package stack
2 2
 
3 3
 import (
4
-	"fmt"
5
-	"io"
6 4
 	"sort"
7
-	"strconv"
8
-	"text/tabwriter"
9 5
 
10 6
 	"github.com/docker/docker/api/types"
11 7
 	"github.com/docker/docker/cli"
12 8
 	"github.com/docker/docker/cli/command"
9
+	"github.com/docker/docker/cli/command/formatter"
13 10
 	"github.com/docker/docker/cli/compose/convert"
14 11
 	"github.com/docker/docker/client"
15 12
 	"github.com/pkg/errors"
... ...
@@ -17,11 +14,8 @@ import (
17 17
 	"golang.org/x/net/context"
18 18
 )
19 19
 
20
-const (
21
-	listItemFmt = "%s\t%s\n"
22
-)
23
-
24 20
 type listOptions struct {
21
+	format string
25 22
 }
26 23
 
27 24
 func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
... ...
@@ -37,6 +31,8 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
37 37
 		},
38 38
 	}
39 39
 
40
+	flags := cmd.Flags()
41
+	flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template")
40 42
 	return cmd
41 43
 }
42 44
 
... ...
@@ -48,55 +44,32 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
48 48
 	if err != nil {
49 49
 		return err
50 50
 	}
51
-
52
-	out := dockerCli.Out()
53
-	printTable(out, stacks)
54
-	return nil
51
+	format := opts.format
52
+	if len(format) == 0 {
53
+		format = formatter.TableFormatKey
54
+	}
55
+	stackCtx := formatter.Context{
56
+		Output: dockerCli.Out(),
57
+		Format: formatter.NewStackFormat(format),
58
+	}
59
+	sort.Sort(byName(stacks))
60
+	return formatter.StackWrite(stackCtx, stacks)
55 61
 }
56 62
 
57
-type byName []*stack
63
+type byName []*formatter.Stack
58 64
 
59 65
 func (n byName) Len() int           { return len(n) }
60 66
 func (n byName) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }
61 67
 func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
62 68
 
63
-func printTable(out io.Writer, stacks []*stack) {
64
-	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
65
-
66
-	// Ignore flushing errors
67
-	defer writer.Flush()
68
-
69
-	sort.Sort(byName(stacks))
70
-
71
-	fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES")
72
-	for _, stack := range stacks {
73
-		fmt.Fprintf(
74
-			writer,
75
-			listItemFmt,
76
-			stack.Name,
77
-			strconv.Itoa(stack.Services),
78
-		)
79
-	}
80
-}
81
-
82
-type stack struct {
83
-	// Name is the name of the stack
84
-	Name string
85
-	// Services is the number of the services
86
-	Services int
87
-}
88
-
89
-func getStacks(
90
-	ctx context.Context,
91
-	apiclient client.APIClient,
92
-) ([]*stack, error) {
69
+func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) {
93 70
 	services, err := apiclient.ServiceList(
94 71
 		ctx,
95 72
 		types.ServiceListOptions{Filters: getAllStacksFilter()})
96 73
 	if err != nil {
97 74
 		return nil, err
98 75
 	}
99
-	m := make(map[string]*stack, 0)
76
+	m := make(map[string]*formatter.Stack, 0)
100 77
 	for _, service := range services {
101 78
 		labels := service.Spec.Labels
102 79
 		name, ok := labels[convert.LabelNamespace]
... ...
@@ -106,7 +79,7 @@ func getStacks(
106 106
 		}
107 107
 		ztack, ok := m[name]
108 108
 		if !ok {
109
-			m[name] = &stack{
109
+			m[name] = &formatter.Stack{
110 110
 				Name:     name,
111 111
 				Services: 1,
112 112
 			}
... ...
@@ -114,7 +87,7 @@ func getStacks(
114 114
 			ztack.Services++
115 115
 		}
116 116
 	}
117
-	var stacks []*stack
117
+	var stacks []*formatter.Stack
118 118
 	for _, stack := range m {
119 119
 		stacks = append(stacks, stack)
120 120
 	}
... ...
@@ -24,7 +24,8 @@ Aliases:
24 24
   ls, list
25 25
 
26 26
 Options:
27
-      --help   Print usage
27
+      --help            Print usage
28
+      --format string   Pretty-print stacks using a Go template
28 29
 ```
29 30
 
30 31
 ## Description
... ...
@@ -43,6 +44,30 @@ vossibility-stack  6
43 43
 myapp              2
44 44
 ```
45 45
 
46
+### Formatting
47
+
48
+The formatting option (`--format`) pretty-prints stacks using a Go template.
49
+
50
+Valid placeholders for the Go template are listed below:
51
+
52
+| Placeholder | Description        |
53
+| ----------- | ------------------ |
54
+| `.Name`     | Stack name         |
55
+| `.Services` | Number of services |
56
+
57
+When using the `--format` option, the `stack ls` command either outputs
58
+the data exactly as the template declares or, when using the
59
+`table` directive, includes column headers as well.
60
+
61
+The following example uses a template without headers and outputs the
62
+`Name` and `Services` entries separated by a colon for all stacks:
63
+
64
+```bash
65
+$ docker stack ls --format "{{.Name}}: {{.Services}}"
66
+web-server: 1
67
+web-cache: 4
68
+```
69
+
46 70
 ## Related commands
47 71
 
48 72
 * [stack deploy](stack_deploy.md)
... ...
@@ -15,6 +15,17 @@ import (
15 15
 	"github.com/go-check/check"
16 16
 )
17 17
 
18
+var cleanSpaces = func(s string) string {
19
+	lines := strings.Split(s, "\n")
20
+	for i, line := range lines {
21
+		spaceIx := strings.Index(line, " ")
22
+		if spaceIx > 0 {
23
+			lines[i] = line[:spaceIx+1] + strings.TrimLeft(line[spaceIx:], " ")
24
+		}
25
+	}
26
+	return strings.Join(lines, "\n")
27
+}
28
+
18 29
 func (s *DockerSwarmSuite) TestStackRemoveUnknown(c *check.C) {
19 30
 	d := s.AddDaemon(c, true, true)
20 31
 
... ...
@@ -59,13 +70,13 @@ func (s *DockerSwarmSuite) TestStackDeployComposeFile(c *check.C) {
59 59
 
60 60
 	out, err = d.Cmd("stack", "ls")
61 61
 	c.Assert(err, checker.IsNil)
62
-	c.Assert(out, check.Equals, "NAME        SERVICES\n"+"testdeploy  2\n")
62
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n"+"testdeploy 2\n")
63 63
 
64 64
 	out, err = d.Cmd("stack", "rm", testStackName)
65 65
 	c.Assert(err, checker.IsNil)
66 66
 	out, err = d.Cmd("stack", "ls")
67 67
 	c.Assert(err, checker.IsNil)
68
-	c.Assert(out, check.Equals, "NAME  SERVICES\n")
68
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n")
69 69
 }
70 70
 
71 71
 func (s *DockerSwarmSuite) TestStackDeployWithSecretsTwice(c *check.C) {
... ...
@@ -180,7 +191,7 @@ func (s *DockerSwarmSuite) TestStackDeployWithDAB(c *check.C) {
180 180
 	stackArgs = []string{"stack", "ls"}
181 181
 	out, err = d.Cmd(stackArgs...)
182 182
 	c.Assert(err, checker.IsNil)
183
-	c.Assert(out, check.Equals, "NAME  SERVICES\n"+"test  2\n")
183
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n"+"test 2\n")
184 184
 	// rm
185 185
 	stackArgs = []string{"stack", "rm", testStackName}
186 186
 	out, err = d.Cmd(stackArgs...)
... ...
@@ -191,5 +202,5 @@ func (s *DockerSwarmSuite) TestStackDeployWithDAB(c *check.C) {
191 191
 	stackArgs = []string{"stack", "ls"}
192 192
 	out, err = d.Cmd(stackArgs...)
193 193
 	c.Assert(err, checker.IsNil)
194
-	c.Assert(out, check.Equals, "NAME  SERVICES\n")
194
+	c.Assert(cleanSpaces(out), check.Equals, "NAME SERVICES\n")
195 195
 }