Browse code

add `docker stack ls`

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>

Akihiro Suda authored on 2016/06/23 14:00:21
Showing 11 changed files
... ...
@@ -23,6 +23,7 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command {
23 23
 	cmd.AddCommand(
24 24
 		newConfigCommand(dockerCli),
25 25
 		newDeployCommand(dockerCli),
26
+		newListCommand(dockerCli),
26 27
 		newRemoveCommand(dockerCli),
27 28
 		newServicesCommand(dockerCli),
28 29
 		newPsCommand(dockerCli),
29 30
new file mode 100644
... ...
@@ -0,0 +1,119 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"fmt"
6
+	"io"
7
+	"strconv"
8
+	"text/tabwriter"
9
+
10
+	"golang.org/x/net/context"
11
+
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/api/types/filters"
14
+	"github.com/docker/docker/cli"
15
+	"github.com/docker/docker/cli/command"
16
+	"github.com/docker/docker/client"
17
+	"github.com/spf13/cobra"
18
+)
19
+
20
+const (
21
+	listItemFmt = "%s\t%s\n"
22
+)
23
+
24
+type listOptions struct {
25
+}
26
+
27
+func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
28
+	opts := listOptions{}
29
+
30
+	cmd := &cobra.Command{
31
+		Use:     "ls",
32
+		Aliases: []string{"list"},
33
+		Short:   "List stacks",
34
+		Args:    cli.NoArgs,
35
+		RunE: func(cmd *cobra.Command, args []string) error {
36
+			return runList(dockerCli, opts)
37
+		},
38
+	}
39
+
40
+	return cmd
41
+}
42
+
43
+func runList(dockerCli *command.DockerCli, opts listOptions) error {
44
+	client := dockerCli.Client()
45
+	ctx := context.Background()
46
+
47
+	stacks, err := getStacks(ctx, client)
48
+	if err != nil {
49
+		return err
50
+	}
51
+
52
+	out := dockerCli.Out()
53
+	printTable(out, stacks)
54
+	return nil
55
+}
56
+
57
+func printTable(out io.Writer, stacks []*stack) {
58
+	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
59
+
60
+	// Ignore flushing errors
61
+	defer writer.Flush()
62
+
63
+	fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES")
64
+	for _, stack := range stacks {
65
+		fmt.Fprintf(
66
+			writer,
67
+			listItemFmt,
68
+			stack.Name,
69
+			strconv.Itoa(stack.Services),
70
+		)
71
+	}
72
+}
73
+
74
+type stack struct {
75
+	// Name is the name of the stack
76
+	Name string
77
+	// Services is the number of the services
78
+	Services int
79
+}
80
+
81
+func getStacks(
82
+	ctx context.Context,
83
+	apiclient client.APIClient,
84
+) ([]*stack, error) {
85
+
86
+	filter := filters.NewArgs()
87
+	filter.Add("label", labelNamespace)
88
+
89
+	services, err := apiclient.ServiceList(
90
+		ctx,
91
+		types.ServiceListOptions{Filter: filter})
92
+	if err != nil {
93
+		return nil, err
94
+	}
95
+	m := make(map[string]*stack, 0)
96
+	for _, service := range services {
97
+		labels := service.Spec.Labels
98
+		name, ok := labels[labelNamespace]
99
+		if !ok {
100
+			return nil, fmt.Errorf("cannot get label %s for service %s",
101
+				labelNamespace, service.ID)
102
+		}
103
+		ztack, ok := m[name]
104
+		if !ok {
105
+			m[name] = &stack{
106
+				Name:     name,
107
+				Services: 1,
108
+			}
109
+		} else {
110
+			ztack.Services++
111
+		}
112
+	}
113
+	var stacks []*stack
114
+	for _, stack := range m {
115
+		stacks = append(stacks, stack)
116
+	}
117
+	return stacks, nil
118
+}
... ...
@@ -16,10 +16,6 @@ import (
16 16
 	"github.com/spf13/cobra"
17 17
 )
18 18
 
19
-const (
20
-	listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
21
-)
22
-
23 19
 type servicesOptions struct {
24 20
 	quiet     bool
25 21
 	filter    opts.FilterOpt
... ...
@@ -29,3 +29,4 @@ Displays the configuration of a stack.
29 29
 * [stack rm](stack_rm.md)
30 30
 * [stack services](stack_services.md)
31 31
 * [stack tasks](stack_tasks.md)
32
+* [stack ls](stack_ls.md)
... ...
@@ -58,3 +58,4 @@ axqh55ipl40h  vossibility-stack_vossibility-collector  1 icecrime/vossibility-co
58 58
 * [stack rm](stack_rm.md)
59 59
 * [stack services](stack_services.md)
60 60
 * [stack tasks](stack_tasks.md)
61
+* [stack ls](stack_ls.md)
61 62
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+<!--[metadata]>
1
+title = "stack ls"
2
+description = "The stack ls command description and usage"
3
+keywords = ["stack, ls"]
4
+advisory = "experimental"
5
+[menu.main]
6
+parent = "smn_cli"
7
+<![end-metadata]-->
8
+
9
+# stack ls (experimental)
10
+
11
+```markdown
12
+Usage:	docker stack ls
13
+
14
+List stacks
15
+```
16
+
17
+Lists the stacks.
18
+
19
+For example, the following command shows all stacks and some additional information:
20
+
21
+```bash
22
+$ docker stack ls
23
+
24
+ID                 SERVICES
25
+vossibility-stack  6
26
+myapp              2
27
+```
28
+
29
+## Related information
30
+
31
+* [stack config](stack_config.md)
32
+* [stack deploy](stack_deploy.md)
33
+* [stack rm](stack_rm.md)
34
+* [stack tasks](stack_tasks.md)
... ...
@@ -32,3 +32,4 @@ a manager node.
32 32
 * [stack deploy](stack_deploy.md)
33 33
 * [stack services](stack_services.md)
34 34
 * [stack tasks](stack_tasks.md)
35
+* [stack ls](stack_ls.md)
... ...
@@ -63,3 +63,4 @@ The currently supported filters are:
63 63
 * [stack deploy](stack_deploy.md)
64 64
 * [stack rm](stack_rm.md)
65 65
 * [stack tasks](stack_tasks.md)
66
+* [stack ls](stack_ls.md)
... ...
@@ -45,3 +45,4 @@ The currently supported filters are:
45 45
 * [stack deploy](stack_deploy.md)
46 46
 * [stack rm](stack_rm.md)
47 47
 * [stack services](stack_services.md)
48
+* [stack ls](stack_ls.md)
... ...
@@ -93,6 +93,7 @@ Options:
93 93
 Commands:
94 94
   config      Print the stack configuration
95 95
   deploy      Create and update a stack
96
+  ls          List stacks
96 97
   rm          Remove the stack
97 98
   services    List the services in the stack
98 99
   tasks       List the tasks in the stack
... ...
@@ -3,6 +3,9 @@
3 3
 package main
4 4
 
5 5
 import (
6
+	"io/ioutil"
7
+	"os"
8
+
6 9
 	"github.com/docker/docker/pkg/integration/checker"
7 10
 	"github.com/go-check/check"
8 11
 )
... ...
@@ -36,3 +39,54 @@ func (s *DockerSwarmSuite) TestStackServices(c *check.C) {
36 36
 	c.Assert(err, checker.IsNil)
37 37
 	c.Assert(out, check.Equals, "Nothing found in stack: UNKNOWN_STACK\n")
38 38
 }
39
+
40
+// testDAB is the DAB JSON used for testing.
41
+// TODO: Use template/text and substitute "Image" with the result of
42
+// `docker inspect --format '{{index .RepoDigests 0}}' busybox:latest`
43
+const testDAB = `{
44
+    "Version": "0.1",
45
+    "Services": {
46
+	"srv1": {
47
+	    "Image": "busybox@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0",
48
+	    "Command": ["top"]
49
+	},
50
+	"srv2": {
51
+	    "Image": "busybox@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0",
52
+	    "Command": ["tail"],
53
+	    "Args": ["-f", "/dev/null"]
54
+	}
55
+    }
56
+}`
57
+
58
+func (s *DockerSwarmSuite) TestStackWithDAB(c *check.C) {
59
+	// setup
60
+	testStackName := "test"
61
+	testDABFileName := testStackName + ".dab"
62
+	defer os.RemoveAll(testDABFileName)
63
+	err := ioutil.WriteFile(testDABFileName, []byte(testDAB), 0444)
64
+	c.Assert(err, checker.IsNil)
65
+	d := s.AddDaemon(c, true, true)
66
+	// deploy
67
+	stackArgs := []string{"stack", "deploy", testStackName}
68
+	out, err := d.Cmd(stackArgs...)
69
+	c.Assert(err, checker.IsNil)
70
+	c.Assert(out, checker.Contains, "Loading bundle from test.dab\n")
71
+	c.Assert(out, checker.Contains, "Creating service test_srv1\n")
72
+	c.Assert(out, checker.Contains, "Creating service test_srv2\n")
73
+	// ls
74
+	stackArgs = []string{"stack", "ls"}
75
+	out, err = d.Cmd(stackArgs...)
76
+	c.Assert(err, checker.IsNil)
77
+	c.Assert(out, check.Equals, "NAME  SERVICES\n"+"test  2\n")
78
+	// rm
79
+	stackArgs = []string{"stack", "rm", testStackName}
80
+	out, err = d.Cmd(stackArgs...)
81
+	c.Assert(err, checker.IsNil)
82
+	c.Assert(out, checker.Contains, "Removing service test_srv1\n")
83
+	c.Assert(out, checker.Contains, "Removing service test_srv2\n")
84
+	// ls (empty)
85
+	stackArgs = []string{"stack", "ls"}
86
+	out, err = d.Cmd(stackArgs...)
87
+	c.Assert(err, checker.IsNil)
88
+	c.Assert(out, check.Equals, "NAME  SERVICES\n")
89
+}