diff --git a/components/cli/command/formatter/plugin.go b/components/cli/command/formatter/plugin.go new file mode 100644 index 0000000000..5f94714a6b --- /dev/null +++ b/components/cli/command/formatter/plugin.go @@ -0,0 +1,87 @@ +package formatter + +import ( + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" +) + +const ( + defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}" + + pluginIDHeader = "ID" + descriptionHeader = "DESCRIPTION" + enabledHeader = "ENABLED" +) + +// NewPluginFormat returns a Format for rendering using a plugin Context +func NewPluginFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultPluginTableFormat + case RawFormatKey: + if quiet { + return `plugin_id: {{.ID}}` + } + return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` + } + return Format(source) +} + +// PluginWrite writes the context +func PluginWrite(ctx Context, plugins []*types.Plugin) error { + render := func(format func(subContext subContext) error) error { + for _, plugin := range plugins { + pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} + if err := format(pluginCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(&pluginContext{}, render) +} + +type pluginContext struct { + HeaderContext + trunc bool + p types.Plugin +} + +func (c *pluginContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *pluginContext) ID() string { + c.AddHeader(pluginIDHeader) + if c.trunc { + return stringid.TruncateID(c.p.ID) + } + return c.p.ID +} + +func (c *pluginContext) Name() string { + c.AddHeader(nameHeader) + return c.p.Name +} + +func (c *pluginContext) Description() string { + c.AddHeader(descriptionHeader) + desc := strings.Replace(c.p.Config.Description, "\n", "", -1) + desc = strings.Replace(desc, "\r", "", -1) + if c.trunc { + desc = stringutils.Ellipsis(desc, 45) + } + + return desc +} + +func (c *pluginContext) Enabled() bool { + c.AddHeader(enabledHeader) + return c.p.Enabled +} diff --git a/components/cli/command/formatter/plugin_test.go b/components/cli/command/formatter/plugin_test.go new file mode 100644 index 0000000000..9ddbe11dff --- /dev/null +++ b/components/cli/command/formatter/plugin_test.go @@ -0,0 +1,188 @@ +package formatter + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestPluginContext(t *testing.T) { + pluginID := stringid.GenerateRandomID() + + var ctx pluginContext + cases := []struct { + pluginCtx pluginContext + expValue string + expHeader string + call func() string + }{ + {pluginContext{ + p: types.Plugin{ID: pluginID}, + trunc: false, + }, pluginID, pluginIDHeader, ctx.ID}, + {pluginContext{ + p: types.Plugin{ID: pluginID}, + trunc: true, + }, stringid.TruncateID(pluginID), pluginIDHeader, ctx.ID}, + {pluginContext{ + p: types.Plugin{Name: "plugin_name"}, + }, "plugin_name", nameHeader, ctx.Name}, + {pluginContext{ + p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, + }, "plugin_description", descriptionHeader, ctx.Description}, + } + + for _, c := range cases { + ctx = c.pluginCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.FullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +func TestPluginContextWrite(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 is not a command +`, + }, + // Table format + { + Context{Format: NewPluginFormat("table", false)}, + `ID NAME DESCRIPTION ENABLED +pluginID1 foobar_baz description 1 true +pluginID2 foobar_bar description 2 false +`, + }, + { + Context{Format: NewPluginFormat("table", true)}, + `pluginID1 +pluginID2 +`, + }, + { + Context{Format: NewPluginFormat("table {{.Name}}", false)}, + `NAME +foobar_baz +foobar_bar +`, + }, + { + Context{Format: NewPluginFormat("table {{.Name}}", true)}, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + Context{Format: NewPluginFormat("raw", false)}, + `plugin_id: pluginID1 +name: foobar_baz +description: description 1 +enabled: true + +plugin_id: pluginID2 +name: foobar_bar +description: description 2 +enabled: false + +`, + }, + { + Context{Format: NewPluginFormat("raw", true)}, + `plugin_id: pluginID1 +plugin_id: pluginID2 +`, + }, + // Custom Format + { + Context{Format: NewPluginFormat("{{.Name}}", false)}, + `foobar_baz +foobar_bar +`, + }, + } + + for _, testcase := range cases { + plugins := []*types.Plugin{ + {ID: "pluginID1", Name: "foobar_baz", Config: types.PluginConfig{Description: "description 1"}, Enabled: true}, + {ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false}, + } + out := bytes.NewBufferString("") + testcase.context.Output = out + err := PluginWrite(testcase.context, plugins) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} + +func TestPluginContextWriteJSON(t *testing.T) { + plugins := []*types.Plugin{ + {ID: "pluginID1", Name: "foobar_baz"}, + {ID: "pluginID2", Name: "foobar_bar"}, + } + expectedJSONs := []map[string]interface{}{ + {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz"}, + {"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar"}, + } + + out := bytes.NewBufferString("") + err := PluginWrite(Context{Format: "{{json .}}", Output: out}, plugins) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} + +func TestPluginContextWriteJSONField(t *testing.T) { + plugins := []*types.Plugin{ + {ID: "pluginID1", Name: "foobar_baz"}, + {ID: "pluginID2", Name: "foobar_bar"}, + } + out := bytes.NewBufferString("") + err := PluginWrite(Context{Format: "{{json .ID}}", Output: out}, plugins) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, plugins[i].ID) + } +} diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index 8fd16dae3f..51590224b0 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -1,20 +1,17 @@ package plugin import ( - "fmt" - "strings" - "text/tabwriter" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" "golang.org/x/net/context" ) type listOptions struct { + quiet bool noTrunc bool + format string } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -32,7 +29,9 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display plugin IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.StringVar(&opts.format, "format", "", "Pretty-print plugins using a Go template") return cmd } @@ -43,21 +42,19 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "ID \tNAME \tDESCRIPTION\tENABLED") - fmt.Fprintf(w, "\n") - - for _, p := range plugins { - id := p.ID - desc := strings.Replace(p.Config.Description, "\n", " ", -1) - desc = strings.Replace(desc, "\r", " ", -1) - if !opts.noTrunc { - id = stringid.TruncateID(p.ID) - desc = stringutils.Ellipsis(desc, 45) + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().PluginsFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().PluginsFormat + } else { + format = formatter.TableFormatKey } - - fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", id, p.Name, desc, p.Enabled) } - w.Flush() - return nil + + pluginsCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewPluginFormat(format, opts.quiet), + Trunc: !opts.noTrunc, + } + return formatter.PluginWrite(pluginsCtx, plugins) } diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index 39097133a4..e8fe96e847 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -27,6 +27,7 @@ type ConfigFile struct { PsFormat string `json:"psFormat,omitempty"` ImagesFormat string `json:"imagesFormat,omitempty"` NetworksFormat string `json:"networksFormat,omitempty"` + PluginsFormat string `json:"pluginsFormat,omitempty"` VolumesFormat string `json:"volumesFormat,omitempty"` StatsFormat string `json:"statsFormat,omitempty"` DetachKeys string `json:"detachKeys,omitempty"`