From 62567078ffe3960ed3286a0ae0332fd02ceda5e2 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 15 May 2017 15:23:20 -0700 Subject: [PATCH] Add --pretty option to "secret inspect" and "config inspect" This adds a pretty template for both inspect subcommands. For configs, it's particularly useful because it's a way to expose the config payload in the CLI in a non-base64-encoded way. Signed-off-by: Aaron Lehmann --- cli/command/config/inspect.go | 29 ++++++- cli/command/config/inspect_test.go | 37 +++++++++ .../config-inspect-pretty.simple.golden | 8 ++ cli/command/formatter/config.go | 79 ++++++++++++++++++- cli/command/formatter/node.go | 5 +- cli/command/formatter/node_test.go | 1 - cli/command/formatter/secret.go | 70 +++++++++++++++- cli/command/secret/inspect.go | 28 ++++++- cli/command/secret/inspect_test.go | 36 +++++++++ .../secret-inspect-pretty.simple.golden | 6 ++ cli/internal/test/builders/config.go | 7 ++ 11 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 cli/command/config/testdata/config-inspect-pretty.simple.golden create mode 100644 cli/command/secret/testdata/secret-inspect-pretty.simple.golden diff --git a/cli/command/config/inspect.go b/cli/command/config/inspect.go index fdf3bc5e7e..d7a3422a71 100644 --- a/cli/command/config/inspect.go +++ b/cli/command/config/inspect.go @@ -1,9 +1,12 @@ package config import ( + "fmt" + "strings" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/inspect" + "github.com/docker/cli/cli/command/formatter" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -11,6 +14,7 @@ import ( type inspectOptions struct { names []string format string + pretty bool } func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command { @@ -26,6 +30,7 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command { } cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") + cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") return cmd } @@ -33,9 +38,29 @@ func runConfigInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() + if opts.pretty { + opts.format = "pretty" + } + getRef := func(id string) (interface{}, []byte, error) { return client.ConfigInspectWithRaw(ctx, id) } + f := opts.format + + // check if the user is trying to apply a template to the pretty format, which + // is not supported + if strings.HasPrefix(f, "pretty") && f != "pretty" { + return fmt.Errorf("Cannot supply extra formatting options to the pretty template") + } + + configCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewConfigFormat(f, false), + } + + if err := formatter.ConfigInspectWrite(configCtx, opts.names, getRef); err != nil { + return cli.StatusError{StatusCode: 1, Status: err.Error()} + } + return nil - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef) } diff --git a/cli/command/config/inspect_test.go b/cli/command/config/inspect_test.go index 13ef40549c..69d36cb7ad 100644 --- a/cli/command/config/inspect_test.go +++ b/cli/command/config/inspect_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "testing" + "time" "github.com/docker/cli/cli/internal/test" "github.com/docker/docker/api/types/swarm" @@ -148,3 +149,39 @@ func TestConfigInspectWithFormat(t *testing.T) { testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) } } + +func TestConfigInspectPretty(t *testing.T) { + testCases := []struct { + name string + configInspectFunc func(string) (swarm.Config, []byte, error) + }{ + { + name: "simple", + configInspectFunc: func(id string) (swarm.Config, []byte, error) { + return *Config( + ConfigLabels(map[string]string{ + "lbl1": "value1", + }), + ConfigID("configID"), + ConfigName("configName"), + ConfigCreatedAt(time.Time{}), + ConfigUpdatedAt(time.Time{}), + ConfigData([]byte("payload here")), + ), []byte{}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newConfigInspectCommand( + test.NewFakeCli(&fakeClient{ + configInspectFunc: tc.configInspectFunc, + }, buf)) + cmd.SetArgs([]string{"configID"}) + cmd.Flags().Set("pretty", "true") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name)) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/config/testdata/config-inspect-pretty.simple.golden b/cli/command/config/testdata/config-inspect-pretty.simple.golden new file mode 100644 index 0000000000..d5eefec543 --- /dev/null +++ b/cli/command/config/testdata/config-inspect-pretty.simple.golden @@ -0,0 +1,8 @@ +ID: configID +Name: configName +Labels: + - lbl1=value1 +Created at: 0001-01-01 00:00:00+0000 utc +Updated at: 0001-01-01 00:00:00+0000 utc +Data: +payload here diff --git a/cli/command/formatter/config.go b/cli/command/formatter/config.go index 69b30e9010..72203f35fb 100644 --- a/cli/command/formatter/config.go +++ b/cli/command/formatter/config.go @@ -5,20 +5,35 @@ import ( "strings" "time" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/inspect" "github.com/docker/docker/api/types/swarm" units "github.com/docker/go-units" ) const ( - defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" - configIDHeader = "ID" - configCreatedHeader = "CREATED" - configUpdatedHeader = "UPDATED" + defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" + configIDHeader = "ID" + configCreatedHeader = "CREATED" + configUpdatedHeader = "UPDATED" + configInspectPrettyTemplate Format = `ID: {{.ID}} +Name: {{.Name}} +{{- if .Labels }} +Labels: +{{- range $k, $v := .Labels }} + - {{ $k }}{{if $v }}={{ $v }}{{ end }} +{{- end }}{{ end }} +Created at: {{.CreatedAt}} +Updated at: {{.UpdatedAt}} +Data: +{{.Data}}` ) // NewConfigFormat returns a Format for rendering using a config Context func NewConfigFormat(source string, quiet bool) Format { switch source { + case PrettyFormatKey: + return configInspectPrettyTemplate case TableFormatKey: if quiet { return defaultQuietFormat @@ -98,3 +113,59 @@ func (c *configContext) Label(name string) string { } return c.c.Spec.Annotations.Labels[name] } + +// ConfigInspectWrite renders the context for a list of configs +func ConfigInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { + if ctx.Format != configInspectPrettyTemplate { + return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) + } + render := func(format func(subContext subContext) error) error { + for _, ref := range refs { + configI, _, err := getRef(ref) + if err != nil { + return err + } + config, ok := configI.(swarm.Config) + if !ok { + return fmt.Errorf("got wrong object to inspect :%v", ok) + } + if err := format(&configInspectContext{Config: config}); err != nil { + return err + } + } + return nil + } + return ctx.Write(&configInspectContext{}, render) +} + +type configInspectContext struct { + swarm.Config + subContext +} + +func (ctx *configInspectContext) ID() string { + return ctx.Config.ID +} + +func (ctx *configInspectContext) Name() string { + return ctx.Config.Spec.Name +} + +func (ctx *configInspectContext) Labels() map[string]string { + return ctx.Config.Spec.Labels +} + +func (ctx *configInspectContext) CreatedAt() string { + return command.PrettyPrint(ctx.Config.CreatedAt) +} + +func (ctx *configInspectContext) UpdatedAt() string { + return command.PrettyPrint(ctx.Config.UpdatedAt) +} + +func (ctx *configInspectContext) Data() string { + if ctx.Config.Spec.Data == nil { + return "" + } + return string(ctx.Config.Spec.Data) +} diff --git a/cli/command/formatter/node.go b/cli/command/formatter/node.go index 9b5953ca60..09bcd3f9f7 100644 --- a/cli/command/formatter/node.go +++ b/cli/command/formatter/node.go @@ -69,8 +69,7 @@ TLS Info: {{.TLSInfoTrustRoot}} Issuer Subject: {{.TLSInfoCertIssuerSubject}} Issuer Public Key: {{.TLSInfoCertIssuerPublicKey}} -{{- end}} -` +{{- end}}` nodeIDHeader = "ID" selfHeader = "" hostnameHeader = "HOSTNAME" @@ -177,7 +176,7 @@ func (c *nodeContext) TLSStatus() string { return "Needs Rotation" } -// NodeInspectWrite renders the context for a list of services +// NodeInspectWrite renders the context for a list of nodes func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { if ctx.Format != nodeInspectPrettyTemplate { return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) diff --git a/cli/command/formatter/node_test.go b/cli/command/formatter/node_test.go index 11784dea0e..9ad25d10d5 100644 --- a/cli/command/formatter/node_test.go +++ b/cli/command/formatter/node_test.go @@ -341,7 +341,6 @@ data Issuer Subject: c3ViamVjdA== Issuer Public Key: cHViS2V5 - ` assert.Equal(t, expected, out.String()) } diff --git a/cli/command/formatter/secret.go b/cli/command/formatter/secret.go index b3e53e8f78..04bf944ecd 100644 --- a/cli/command/formatter/secret.go +++ b/cli/command/formatter/secret.go @@ -5,20 +5,33 @@ import ( "strings" "time" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/inspect" "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" - secretCreatedHeader = "CREATED" - secretUpdatedHeader = "UPDATED" + defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" + secretIDHeader = "ID" + secretCreatedHeader = "CREATED" + secretUpdatedHeader = "UPDATED" + secretInspectPrettyTemplate Format = `ID: {{.ID}} +Name: {{.Name}} +{{- if .Labels }} +Labels: +{{- range $k, $v := .Labels }} + - {{ $k }}{{if $v }}={{ $v }}{{ end }} +{{- end }}{{ end }} +Created at: {{.CreatedAt}} +Updated at: {{.UpdatedAt}}` ) // NewSecretFormat returns a Format for rendering using a secret Context func NewSecretFormat(source string, quiet bool) Format { switch source { + case PrettyFormatKey: + return secretInspectPrettyTemplate case TableFormatKey: if quiet { return defaultQuietFormat @@ -98,3 +111,52 @@ func (c *secretContext) Label(name string) string { } return c.s.Spec.Annotations.Labels[name] } + +// SecretInspectWrite renders the context for a list of secrets +func SecretInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { + if ctx.Format != secretInspectPrettyTemplate { + return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) + } + render := func(format func(subContext subContext) error) error { + for _, ref := range refs { + secretI, _, err := getRef(ref) + if err != nil { + return err + } + secret, ok := secretI.(swarm.Secret) + if !ok { + return fmt.Errorf("got wrong object to inspect :%v", ok) + } + if err := format(&secretInspectContext{Secret: secret}); err != nil { + return err + } + } + return nil + } + return ctx.Write(&secretInspectContext{}, render) +} + +type secretInspectContext struct { + swarm.Secret + subContext +} + +func (ctx *secretInspectContext) ID() string { + return ctx.Secret.ID +} + +func (ctx *secretInspectContext) Name() string { + return ctx.Secret.Spec.Name +} + +func (ctx *secretInspectContext) Labels() map[string]string { + return ctx.Secret.Spec.Labels +} + +func (ctx *secretInspectContext) CreatedAt() string { + return command.PrettyPrint(ctx.Secret.CreatedAt) +} + +func (ctx *secretInspectContext) UpdatedAt() string { + return command.PrettyPrint(ctx.Secret.UpdatedAt) +} diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go index 744da735c6..51410487a5 100644 --- a/cli/command/secret/inspect.go +++ b/cli/command/secret/inspect.go @@ -1,9 +1,12 @@ package secret import ( + "fmt" + "strings" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/inspect" + "github.com/docker/cli/cli/command/formatter" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -11,6 +14,7 @@ import ( type inspectOptions struct { names []string format string + pretty bool } func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { @@ -26,6 +30,7 @@ func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { } cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") + cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") return cmd } @@ -33,9 +38,28 @@ func runSecretInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() + if opts.pretty { + opts.format = "pretty" + } + getRef := func(id string) (interface{}, []byte, error) { return client.SecretInspectWithRaw(ctx, id) } + f := opts.format - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef) + // check if the user is trying to apply a template to the pretty format, which + // is not supported + if strings.HasPrefix(f, "pretty") && f != "pretty" { + return fmt.Errorf("Cannot supply extra formatting options to the pretty template") + } + + secretCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewSecretFormat(f, false), + } + + if err := formatter.SecretInspectWrite(secretCtx, opts.names, getRef); err != nil { + return cli.StatusError{StatusCode: 1, Status: err.Error()} + } + return nil } diff --git a/cli/command/secret/inspect_test.go b/cli/command/secret/inspect_test.go index 29ac5bb8d5..07a3d7a313 100644 --- a/cli/command/secret/inspect_test.go +++ b/cli/command/secret/inspect_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "testing" + "time" "github.com/docker/cli/cli/internal/test" "github.com/docker/docker/api/types/swarm" @@ -148,3 +149,38 @@ func TestSecretInspectWithFormat(t *testing.T) { testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) } } + +func TestSecretInspectPretty(t *testing.T) { + testCases := []struct { + name string + secretInspectFunc func(string) (swarm.Secret, []byte, error) + }{ + { + name: "simple", + secretInspectFunc: func(id string) (swarm.Secret, []byte, error) { + return *Secret( + SecretLabels(map[string]string{ + "lbl1": "value1", + }), + SecretID("secretID"), + SecretName("secretName"), + SecretCreatedAt(time.Time{}), + SecretUpdatedAt(time.Time{}), + ), []byte{}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretInspectCommand( + test.NewFakeCli(&fakeClient{ + secretInspectFunc: tc.secretInspectFunc, + }, buf)) + cmd.SetArgs([]string{"secretID"}) + cmd.Flags().Set("pretty", "true") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-pretty.%s.golden", tc.name)) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/secret/testdata/secret-inspect-pretty.simple.golden b/cli/command/secret/testdata/secret-inspect-pretty.simple.golden new file mode 100644 index 0000000000..cc14091e8b --- /dev/null +++ b/cli/command/secret/testdata/secret-inspect-pretty.simple.golden @@ -0,0 +1,6 @@ +ID: secretID +Name: secretName +Labels: + - lbl1=value1 +Created at: 0001-01-01 00:00:00+0000 utc +Updated at: 0001-01-01 00:00:00+0000 utc diff --git a/cli/internal/test/builders/config.go b/cli/internal/test/builders/config.go index dee6e90a82..be48c30397 100644 --- a/cli/internal/test/builders/config.go +++ b/cli/internal/test/builders/config.go @@ -59,3 +59,10 @@ func ConfigUpdatedAt(t time.Time) func(*swarm.Config) { config.UpdatedAt = t } } + +// ConfigData sets the config payload. +func ConfigData(data []byte) func(*swarm.Config) { + return func(config *swarm.Config) { + config.Spec.Data = data + } +}