From 5ce08ddfcae5ded73ef0c93c9daba7c916c21ae5 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 23 Jun 2016 05:00:21 +0000 Subject: [PATCH] add `docker stack ls` Signed-off-by: Akihiro Suda --- cli/command/stack/cmd_experimental.go | 1 + cli/command/stack/list.go | 119 +++++++++++++++++++ cli/command/stack/services.go | 4 - docs/reference/commandline/stack_config.md | 1 + docs/reference/commandline/stack_deploy.md | 1 + docs/reference/commandline/stack_ls.md | 37 ++++++ docs/reference/commandline/stack_rm.md | 1 + docs/reference/commandline/stack_services.md | 1 + docs/reference/commandline/stack_tasks.md | 1 + experimental/docker-stacks-and-bundles.md | 1 + integration-cli/docker_cli_stack_test.go | 54 +++++++++ 11 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 cli/command/stack/list.go create mode 100644 docs/reference/commandline/stack_ls.md diff --git a/cli/command/stack/cmd_experimental.go b/cli/command/stack/cmd_experimental.go index d459e0a9a1..b32d925330 100644 --- a/cli/command/stack/cmd_experimental.go +++ b/cli/command/stack/cmd_experimental.go @@ -23,6 +23,7 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { cmd.AddCommand( newConfigCommand(dockerCli), newDeployCommand(dockerCli), + newListCommand(dockerCli), newRemoveCommand(dockerCli), newServicesCommand(dockerCli), newPsCommand(dockerCli), diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go new file mode 100644 index 0000000000..9fe626d96d --- /dev/null +++ b/cli/command/stack/list.go @@ -0,0 +1,119 @@ +// +build experimental + +package stack + +import ( + "fmt" + "io" + "strconv" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/client" + "github.com/spf13/cobra" +) + +const ( + listItemFmt = "%s\t%s\n" +) + +type listOptions struct { +} + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{} + + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List stacks", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + return cmd +} + +func runList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + stacks, err := getStacks(ctx, client) + if err != nil { + return err + } + + out := dockerCli.Out() + printTable(out, stacks) + return nil +} + +func printTable(out io.Writer, stacks []*stack) { + writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) + + // Ignore flushing errors + defer writer.Flush() + + 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) { + + filter := filters.NewArgs() + filter.Add("label", labelNamespace) + + services, err := apiclient.ServiceList( + ctx, + types.ServiceListOptions{Filter: filter}) + if err != nil { + return nil, err + } + m := make(map[string]*stack, 0) + for _, service := range services { + labels := service.Spec.Labels + name, ok := labels[labelNamespace] + if !ok { + return nil, fmt.Errorf("cannot get label %s for service %s", + labelNamespace, service.ID) + } + ztack, ok := m[name] + if !ok { + m[name] = &stack{ + Name: name, + Services: 1, + } + } else { + ztack.Services++ + } + } + var stacks []*stack + for _, stack := range m { + stacks = append(stacks, stack) + } + return stacks, nil +} diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go index 22906378d6..60f52c30c7 100644 --- a/cli/command/stack/services.go +++ b/cli/command/stack/services.go @@ -16,10 +16,6 @@ import ( "github.com/spf13/cobra" ) -const ( - listItemFmt = "%s\t%s\t%s\t%s\t%s\n" -) - type servicesOptions struct { quiet bool filter opts.FilterOpt diff --git a/docs/reference/commandline/stack_config.md b/docs/reference/commandline/stack_config.md index 4c6cfa1089..9a39d27116 100644 --- a/docs/reference/commandline/stack_config.md +++ b/docs/reference/commandline/stack_config.md @@ -29,3 +29,4 @@ Displays the configuration of a stack. * [stack rm](stack_rm.md) * [stack services](stack_services.md) * [stack tasks](stack_tasks.md) +* [stack ls](stack_ls.md) diff --git a/docs/reference/commandline/stack_deploy.md b/docs/reference/commandline/stack_deploy.md index a1a9366c7f..bcafb7f686 100644 --- a/docs/reference/commandline/stack_deploy.md +++ b/docs/reference/commandline/stack_deploy.md @@ -58,3 +58,4 @@ axqh55ipl40h vossibility-stack_vossibility-collector 1 icecrime/vossibility-co * [stack rm](stack_rm.md) * [stack services](stack_services.md) * [stack tasks](stack_tasks.md) +* [stack ls](stack_ls.md) diff --git a/docs/reference/commandline/stack_ls.md b/docs/reference/commandline/stack_ls.md new file mode 100644 index 0000000000..11abc34727 --- /dev/null +++ b/docs/reference/commandline/stack_ls.md @@ -0,0 +1,37 @@ + + +# stack ls (experimental) + +```markdown +Usage: docker stack ls + +List stacks +``` + +Lists the stacks. + +For example, the following command shows all stacks and some additional information: + +```bash +$ docker stack ls + +ID SERVICES +vossibility-stack 6 +myapp 2 +``` + +## Related information + +* [stack config](stack_config.md) +* [stack deploy](stack_deploy.md) +* [stack rm](stack_rm.md) +* [stack tasks](stack_tasks.md) diff --git a/docs/reference/commandline/stack_rm.md b/docs/reference/commandline/stack_rm.md index 8e3f2259a7..5d4e4d57be 100644 --- a/docs/reference/commandline/stack_rm.md +++ b/docs/reference/commandline/stack_rm.md @@ -32,3 +32,4 @@ a manager node. * [stack deploy](stack_deploy.md) * [stack services](stack_services.md) * [stack tasks](stack_tasks.md) +* [stack ls](stack_ls.md) diff --git a/docs/reference/commandline/stack_services.md b/docs/reference/commandline/stack_services.md index dedb22f8c8..8f28410bf3 100644 --- a/docs/reference/commandline/stack_services.md +++ b/docs/reference/commandline/stack_services.md @@ -63,3 +63,4 @@ The currently supported filters are: * [stack deploy](stack_deploy.md) * [stack rm](stack_rm.md) * [stack tasks](stack_tasks.md) +* [stack ls](stack_ls.md) diff --git a/docs/reference/commandline/stack_tasks.md b/docs/reference/commandline/stack_tasks.md index b8bc91aa9d..24b00e69cc 100644 --- a/docs/reference/commandline/stack_tasks.md +++ b/docs/reference/commandline/stack_tasks.md @@ -45,3 +45,4 @@ The currently supported filters are: * [stack deploy](stack_deploy.md) * [stack rm](stack_rm.md) * [stack services](stack_services.md) +* [stack ls](stack_ls.md) diff --git a/experimental/docker-stacks-and-bundles.md b/experimental/docker-stacks-and-bundles.md index 3bae4f4c4b..8902a19ea1 100644 --- a/experimental/docker-stacks-and-bundles.md +++ b/experimental/docker-stacks-and-bundles.md @@ -93,6 +93,7 @@ Options: Commands: config Print the stack configuration deploy Create and update a stack + ls List stacks rm Remove the stack services List the services in the stack tasks List the tasks in the stack diff --git a/integration-cli/docker_cli_stack_test.go b/integration-cli/docker_cli_stack_test.go index 1fcc4cfbd6..fd65f39a43 100644 --- a/integration-cli/docker_cli_stack_test.go +++ b/integration-cli/docker_cli_stack_test.go @@ -3,6 +3,9 @@ package main import ( + "io/ioutil" + "os" + "github.com/docker/docker/pkg/integration/checker" "github.com/go-check/check" ) @@ -36,3 +39,54 @@ func (s *DockerSwarmSuite) TestStackServices(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(out, check.Equals, "Nothing found in stack: UNKNOWN_STACK\n") } + +// testDAB is the DAB JSON used for testing. +// TODO: Use template/text and substitute "Image" with the result of +// `docker inspect --format '{{index .RepoDigests 0}}' busybox:latest` +const testDAB = `{ + "Version": "0.1", + "Services": { + "srv1": { + "Image": "busybox@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0", + "Command": ["top"] + }, + "srv2": { + "Image": "busybox@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0", + "Command": ["tail"], + "Args": ["-f", "/dev/null"] + } + } +}` + +func (s *DockerSwarmSuite) TestStackWithDAB(c *check.C) { + // setup + testStackName := "test" + testDABFileName := testStackName + ".dab" + defer os.RemoveAll(testDABFileName) + err := ioutil.WriteFile(testDABFileName, []byte(testDAB), 0444) + c.Assert(err, checker.IsNil) + d := s.AddDaemon(c, true, true) + // deploy + stackArgs := []string{"stack", "deploy", testStackName} + out, err := d.Cmd(stackArgs...) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "Loading bundle from test.dab\n") + c.Assert(out, checker.Contains, "Creating service test_srv1\n") + c.Assert(out, checker.Contains, "Creating service test_srv2\n") + // ls + 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") + // rm + stackArgs = []string{"stack", "rm", testStackName} + out, err = d.Cmd(stackArgs...) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "Removing service test_srv1\n") + c.Assert(out, checker.Contains, "Removing service test_srv2\n") + // ls (empty) + stackArgs = []string{"stack", "ls"} + out, err = d.Cmd(stackArgs...) + c.Assert(err, checker.IsNil) + c.Assert(out, check.Equals, "NAME SERVICES\n") +}