Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
| ... | ... |
@@ -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 |
+} |
| 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) |
| ... | ... |
@@ -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 |
+} |