Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the format option to the docker stack ls command #31557

Merged
merged 1 commit into from Apr 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 67 additions & 0 deletions 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MarshalJSON is needed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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 changes: 64 additions & 0 deletions 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)
}
}
}
65 changes: 19 additions & 46 deletions cli/command/stack/list.go
@@ -1,27 +1,21 @@
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"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)

const (
listItemFmt = "%s\t%s\n"
)

type listOptions struct {
format string
}

func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
Expand All @@ -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
}

Expand All @@ -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]
Expand All @@ -106,15 +79,15 @@ func getStacks(
}
ztack, ok := m[name]
if !ok {
m[name] = &stack{
m[name] = &formatter.Stack{
Name: name,
Services: 1,
}
} else {
ztack.Services++
}
}
var stacks []*stack
var stacks []*formatter.Stack
for _, stack := range m {
stacks = append(stacks, stack)
}
Expand Down
27 changes: 26 additions & 1 deletion docs/reference/commandline/stack_ls.md
Expand Up @@ -24,7 +24,8 @@ Aliases:
ls, list

Options:
--help Print usage
--help Print usage
--format string Pretty-print stacks using a Go template
```

## Description
Expand All @@ -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)
Expand Down
19 changes: 15 additions & 4 deletions integration-cli/docker_cli_stack_test.go
Expand Up @@ -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)

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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...)
Expand All @@ -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")
}