diff --git a/cli/command/formatter/secret.go b/cli/command/formatter/secret.go new file mode 100644 index 0000000000..7ec6f9a62e --- /dev/null +++ b/cli/command/formatter/secret.go @@ -0,0 +1,101 @@ +package formatter + +import ( + "fmt" + "strings" + "time" + + "github.com/docker/docker/api/types/swarm" + units "github.com/docker/go-units" +) + +const ( + defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" + secretIDHeader = "ID" + secretNameHeader = "NAME" + secretCreatedHeader = "CREATED" + secretUpdatedHeader = "UPDATED" +) + +// NewSecretFormat returns a Format for rendering using a network Context +func NewSecretFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultSecretTableFormat + } + return Format(source) +} + +// SecretWrite writes the context +func SecretWrite(ctx Context, secrets []swarm.Secret) error { + render := func(format func(subContext subContext) error) error { + for _, secret := range secrets { + secretCtx := &secretContext{s: secret} + if err := format(secretCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(newSecretContext(), render) +} + +func newSecretContext() *secretContext { + sCtx := &secretContext{} + + sCtx.header = map[string]string{ + "ID": secretIDHeader, + "Name": nameHeader, + "CreatedAt": secretCreatedHeader, + "UpdatedAt": secretUpdatedHeader, + "Labels": labelsHeader, + } + return sCtx +} + +type secretContext struct { + HeaderContext + s swarm.Secret +} + +func (c *secretContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *secretContext) ID() string { + return c.s.ID +} + +func (c *secretContext) Name() string { + return c.s.Spec.Annotations.Name +} + +func (c *secretContext) CreatedAt() string { + return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago" +} + +func (c *secretContext) UpdatedAt() string { + return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago" +} + +func (c *secretContext) Labels() string { + mapLabels := c.s.Spec.Annotations.Labels + if mapLabels == nil { + return "" + } + var joinLabels []string + for k, v := range mapLabels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *secretContext) Label(name string) string { + if c.s.Spec.Annotations.Labels == nil { + return "" + } + return c.s.Spec.Annotations.Labels[name] +} diff --git a/cli/command/formatter/secret_test.go b/cli/command/formatter/secret_test.go new file mode 100644 index 0000000000..722b650565 --- /dev/null +++ b/cli/command/formatter/secret_test.go @@ -0,0 +1,63 @@ +package formatter + +import ( + "bytes" + "testing" + "time" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSecretContextFormatWrite(t *testing.T) { + // Check default output format (verbose and non-verbose mode) for table headers + 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 is not a command +`, + }, + // Table format + {Context{Format: NewSecretFormat("table", false)}, + `ID NAME CREATED UPDATED +1 passwords Less than a second ago Less than a second ago +2 id_rsa Less than a second ago Less than a second ago +`}, + {Context{Format: NewSecretFormat("table {{.Name}}", true)}, + `NAME +passwords +id_rsa +`}, + {Context{Format: NewSecretFormat("{{.ID}}-{{.Name}}", false)}, + `1-passwords +2-id_rsa +`}, + } + + secrets := []swarm.Secret{ + {ID: "1", + Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "passwords"}}}, + {ID: "2", + Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}}, + } + for _, testcase := range cases { + out := bytes.NewBufferString("") + testcase.context.Output = out + if err := SecretWrite(testcase.context, secrets); err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go index faeab314b7..211ebceb5f 100644 --- a/cli/command/secret/ls.go +++ b/cli/command/secret/ls.go @@ -1,20 +1,17 @@ package secret import ( - "fmt" - "text/tabwriter" - "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/go-units" + "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" "golang.org/x/net/context" ) type listOptions struct { - quiet bool + quiet bool + format string } func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -32,6 +29,7 @@ func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVarP(&opts.format, "format", "", "", "Pretty-print secrets using a Go template") return cmd } @@ -44,25 +42,17 @@ func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { if err != nil { return err } - - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - if opts.quiet { - for _, s := range secrets { - fmt.Fprintf(w, "%s\n", s.ID) - } - } else { - fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED") - fmt.Fprintf(w, "\n") - - for _, s := range secrets { - created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago" - updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago" - - fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated) + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().SecretFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().SecretFormat + } else { + format = formatter.TableFormatKey } } - - w.Flush() - - return nil + secretCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewSecretFormat(format, opts.quiet), + } + return formatter.SecretWrite(secretCtx, secrets) } diff --git a/cli/config/configfile/file.go b/cli/config/configfile/file.go index d83434676e..e97fbe47ba 100644 --- a/cli/config/configfile/file.go +++ b/cli/config/configfile/file.go @@ -37,6 +37,7 @@ type ConfigFile struct { ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` ServicesFormat string `json:"servicesFormat,omitempty"` TasksFormat string `json:"tasksFormat,omitempty"` + SecretFormat string `json:"secretFormat,omitempty"` } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the diff --git a/docs/reference/commandline/cli.md b/docs/reference/commandline/cli.md index 666a353718..c290e61d5c 100644 --- a/docs/reference/commandline/cli.md +++ b/docs/reference/commandline/cli.md @@ -160,6 +160,14 @@ property is not set, the client falls back to the default table format. For a list of supported formatting directives, see [**Formatting** section in the `docker stats` documentation](stats.md) +The property `secretFormat` specifies the default format for `docker +secret ls` output. When the `--format` flag is not provided with the +`docker secret ls` command, Docker's client uses this property. If this +property is not set, the client falls back to the default table +format. For a list of supported formatting directives, see +[**Formatting** section in the `docker secret ls` documentation](secret_ls.md) + + The property `credsStore` specifies an external binary to serve as the default credential store. When this property is set, `docker login` will attempt to store credentials in the binary specified by `docker-credential-` which @@ -204,6 +212,7 @@ Following is a sample `config.json` file: "pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}", "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}", + "secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}", "serviceInspectFormat": "pretty", "detachKeys": "ctrl-e,e", "credsStore": "secretservice", diff --git a/docs/reference/commandline/secret_ls.md b/docs/reference/commandline/secret_ls.md index 72b9e4696d..271075449d 100644 --- a/docs/reference/commandline/secret_ls.md +++ b/docs/reference/commandline/secret_ls.md @@ -25,6 +25,7 @@ Aliases: Options: -q, --quiet Only display IDs + -format string Pretty-print secrets using a Go template ``` ## Description @@ -40,6 +41,49 @@ ID NAME CREATED mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC ``` +### Format the output + +The formatting option (`--format`) pretty prints secrets output +using a Go template. + +Valid placeholders for the Go template are listed below: + +| Placeholder | Description | +| ------------ | ------------------------------------------------------------------------------------ | +| `.ID` | Secret ID | +| `.Name` | Secret name | +| `.CreatedAt` | Time when the secret was created | +| `.UpdatedAt` | Time when the secret was updated | +| `.Labels` | All labels assigned to the secret | +| `.Label` | Value of a specific label for this secret. For example `{{.Label "secret.ssh.key"}}` | + +When using the `--format` option, the `secret ls` command will either +output the data exactly as the template declares or, when using the +`table` directive, will include column headers as well. + +The following example uses a template without headers and outputs the +`ID` and `Name` entries separated by a colon for all images: + +```bash +$ docker secret ls --format "{{.ID}}: {{.Name}}" + +77af4d6b9913: secret-1 +b6fa739cedf5: secret-2 +78a85c484f71: secret-3 +``` + +To list all secrets with their name and created date in a table format you +can use: + +```bash +$ docker secret ls --format "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}" + +ID NAME CREATED +77af4d6b9913 secret-1 5 minutes ago +b6fa739cedf5 secret-2 3 hours ago +78a85c484f71 secret-3 10 days ago +``` + ## Related commands * [secret create](secret_create.md)