From 1bac314da583ece87b0788e712e95af85dbd6933 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Sun, 5 Mar 2017 13:11:04 +0200 Subject: [PATCH] Add format to secret ls Signed-off-by: Boaz Shuster --- command/formatter/secret.go | 101 +++++++++++++++++++++++++++++++ command/formatter/secret_test.go | 63 +++++++++++++++++++ command/secret/ls.go | 40 +++++------- config/configfile/file.go | 1 + 4 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 command/formatter/secret.go create mode 100644 command/formatter/secret_test.go diff --git a/command/formatter/secret.go b/command/formatter/secret.go new file mode 100644 index 0000000000..7ec6f9a62e --- /dev/null +++ b/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/command/formatter/secret_test.go b/command/formatter/secret_test.go new file mode 100644 index 0000000000..722b650565 --- /dev/null +++ b/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/command/secret/ls.go b/command/secret/ls.go index faeab314b7..211ebceb5f 100644 --- a/command/secret/ls.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index d83434676e..e97fbe47ba 100644 --- a/config/configfile/file.go +++ b/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