diff --git a/api/server/router/plugin/backend.go b/api/server/router/plugin/backend.go index fee78195d6..dadc169a26 100644 --- a/api/server/router/plugin/backend.go +++ b/api/server/router/plugin/backend.go @@ -5,6 +5,7 @@ import ( "net/http" enginetypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/reference" "golang.org/x/net/context" ) @@ -13,7 +14,7 @@ import ( type Backend interface { Disable(name string, config *enginetypes.PluginDisableConfig) error Enable(name string, config *enginetypes.PluginEnableConfig) error - List() ([]enginetypes.Plugin, error) + List(filters.Args) ([]enginetypes.Plugin, error) Inspect(name string) (*enginetypes.Plugin, error) Remove(name string, config *enginetypes.PluginRmConfig) error Set(name string, args []string) error diff --git a/api/server/router/plugin/plugin_routes.go b/api/server/router/plugin/plugin_routes.go index 2d3eb8fea1..a0a1abb24a 100644 --- a/api/server/router/plugin/plugin_routes.go +++ b/api/server/router/plugin/plugin_routes.go @@ -10,6 +10,7 @@ import ( distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/reference" @@ -253,7 +254,15 @@ func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r } func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - l, err := pr.backend.List() + if err := httputils.ParseForm(r); err != nil { + return err + } + + pluginFilters, err := filters.FromParam(r.Form.Get("filters")) + if err != nil { + return err + } + l, err := pr.backend.List(pluginFilters) if err != nil { return err } diff --git a/cli/command/plugin/list.go b/cli/command/plugin/list.go index 51590224b0..a1b231f570 100644 --- a/cli/command/plugin/list.go +++ b/cli/command/plugin/list.go @@ -4,6 +4,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/opts" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -12,10 +13,11 @@ type listOptions struct { quiet bool noTrunc bool format string + filter opts.FilterOpt } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts listOptions + opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -32,12 +34,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { 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") + flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'enabled=true')") return cmd } func runList(dockerCli *command.DockerCli, opts listOptions) error { - plugins, err := dockerCli.Client().PluginList(context.Background()) + plugins, err := dockerCli.Client().PluginList(context.Background(), opts.filter.Value()) if err != nil { return err } diff --git a/client/interface.go b/client/interface.go index 771a3d9a06..d30ba5f705 100644 --- a/client/interface.go +++ b/client/interface.go @@ -108,7 +108,7 @@ type NodeAPIClient interface { // PluginAPIClient defines API client methods for the plugins type PluginAPIClient interface { - PluginList(ctx context.Context) (types.PluginsListResponse, error) + PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error diff --git a/client/plugin_list.go b/client/plugin_list.go index 88c480a3e1..3acde3b966 100644 --- a/client/plugin_list.go +++ b/client/plugin_list.go @@ -2,15 +2,26 @@ package client import ( "encoding/json" + "net/url" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // PluginList returns the installed plugins -func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) { +func (cli *Client) PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) { var plugins types.PluginsListResponse - resp, err := cli.get(ctx, "/plugins", nil, nil) + query := url.Values{} + + if filter.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, filter) + if err != nil { + return plugins, err + } + query.Set("filters", filterJSON) + } + resp, err := cli.get(ctx, "/plugins", query, nil) if err != nil { return plugins, err } diff --git a/client/plugin_list_test.go b/client/plugin_list_test.go index 173e4b87f5..6a0e9844fc 100644 --- a/client/plugin_list_test.go +++ b/client/plugin_list_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) @@ -18,7 +19,7 @@ func TestPluginListError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.PluginList(context.Background()) + _, err := client.PluginList(context.Background(), filters.NewArgs()) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -26,34 +27,69 @@ func TestPluginListError(t *testing.T) { func TestPluginList(t *testing.T) { expectedURL := "/plugins" - client := &Client{ - client: newMockClient(func(req *http.Request) (*http.Response, error) { - if !strings.HasPrefix(req.URL.Path, expectedURL) { - return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) - } - content, err := json.Marshal([]*types.Plugin{ - { - ID: "plugin_id1", - }, - { - ID: "plugin_id2", - }, - }) - if err != nil { - return nil, err - } - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader(content)), - }, nil - }), + + enabledFilters := filters.NewArgs() + enabledFilters.Add("enabled", "true") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.NewArgs(), + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": "", + }, + }, + { + filters: enabledFilters, + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": `{"enabled":{"true":true}}`, + }, + }, } - plugins, err := client.PluginList(context.Background()) - if err != nil { - t.Fatal(err) - } - if len(plugins) != 2 { - t.Fatalf("expected 2 plugins, got %v", plugins) + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]*types.Plugin{ + { + ID: "plugin_id1", + }, + { + ID: "plugin_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + plugins, err := client.PluginList(context.Background(), listCase.filters) + if err != nil { + t.Fatal(err) + } + if len(plugins) != 2 { + t.Fatalf("expected 2 plugins, got %v", plugins) + } } } diff --git a/daemon/cluster/executor/container/executor.go b/daemon/cluster/executor/container/executor.go index 24617b6847..80f614731e 100644 --- a/daemon/cluster/executor/container/executor.go +++ b/daemon/cluster/executor/container/executor.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" executorpkg "github.com/docker/docker/daemon/cluster/executor" clustertypes "github.com/docker/docker/daemon/cluster/provider" @@ -53,7 +54,7 @@ func (e *executor) Describe(ctx context.Context) (*api.NodeDescription, error) { addPlugins("Authorization", info.Plugins.Authorization) // add v2 plugins - v2Plugins, err := e.backend.PluginManager().List() + v2Plugins, err := e.backend.PluginManager().List(filters.NewArgs()) if err == nil { for _, plgn := range v2Plugins { for _, typ := range plgn.Config.Interface.Types { diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md index 898b650d86..b6cac6ffb7 100644 --- a/docs/reference/commandline/plugin_ls.md +++ b/docs/reference/commandline/plugin_ls.md @@ -24,6 +24,7 @@ Aliases: ls, list Options: + -f, --filter filter Provide filter values (e.g. 'enabled=true') --format string Pretty-print plugins using a Go template --help Print usage --no-trunc Don't truncate output @@ -32,6 +33,8 @@ Options: Lists all the plugins that are currently installed. You can install plugins using the [`docker plugin install`](plugin_install.md) command. +You can also filter using the `-f` or `--filter` flag. +Refer to the [filtering](#filtering) section for more information about available filter options. Example output: @@ -42,6 +45,20 @@ ID NAME TAG DESCRIP 69553ca1d123 tiborvass/sample-volume-plugin latest A test plugin for Docker true ``` +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more +than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* enabled (boolean - true or false, 0 or 1) + +### enabled + +The `enabled` filter matches on plugins enabled or disabled. + + ## Formatting The formatting options (`--format`) pretty-prints plugins output @@ -68,6 +85,7 @@ $ docker plugin ls --format "{{.ID}}: {{.Name}}" 4be01827a72e: tiborvass/no-remove ``` + ## Related information * [plugin create](plugin_create.md) diff --git a/integration-cli/docker_cli_daemon_plugins_test.go b/integration-cli/docker_cli_daemon_plugins_test.go index 8457382c58..764a207f35 100644 --- a/integration-cli/docker_cli_daemon_plugins_test.go +++ b/integration-cli/docker_cli_daemon_plugins_test.go @@ -285,3 +285,31 @@ func existsMountpointWithPrefix(mountpointPrefix string) (bool, error) { } return false, nil } + +func (s *DockerDaemonSuite) TestPluginListFilterEnabled(c *check.C) { + testRequires(c, Network) + + s.d.Start(c) + + out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pNameWithTag, "--disable") + c.Assert(err, check.IsNil, check.Commentf(out)) + + defer func() { + if out, err := s.d.Cmd("plugin", "remove", pNameWithTag); err != nil { + c.Fatalf("Could not remove plugin: %v %s", err, out) + } + }() + + out, err = s.d.Cmd("plugin", "ls", "--filter", "enabled=true") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Not(checker.Contains), pName) + + out, err = s.d.Cmd("plugin", "ls", "--filter", "enabled=false") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, pName) + c.Assert(out, checker.Contains, "false") + + out, err = s.d.Cmd("plugin", "ls") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, pName) +} diff --git a/plugin/backend_linux.go b/plugin/backend_linux.go index 91406d5a33..0b376198e2 100644 --- a/plugin/backend_linux.go +++ b/plugin/backend_linux.go @@ -18,6 +18,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/distribution/manifest/schema2" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/distribution" progressutils "github.com/docker/docker/distribution/utils" "github.com/docker/docker/distribution/xfer" @@ -33,6 +34,10 @@ import ( "golang.org/x/net/context" ) +var acceptedPluginFilterTags = map[string]bool{ + "enabled": true, +} + // Disable deactivates a plugin. This means resources (volumes, networks) cant use them. func (pm *Manager) Disable(refOrID string, config *types.PluginDisableConfig) error { p, err := pm.config.Store.GetV2Plugin(refOrID) @@ -259,10 +264,33 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m } // List displays the list of plugins and associated metadata. -func (pm *Manager) List() ([]types.Plugin, error) { +func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) { + if err := pluginFilters.Validate(acceptedPluginFilterTags); err != nil { + return nil, err + } + + enabledOnly := false + disabledOnly := false + if pluginFilters.Include("enabled") { + if pluginFilters.ExactMatch("enabled", "true") { + enabledOnly = true + } else if pluginFilters.ExactMatch("enabled", "false") { + disabledOnly = true + } else { + return nil, fmt.Errorf("Invalid filter 'enabled=%s'", pluginFilters.Get("enabled")) + } + } + plugins := pm.config.Store.GetAll() out := make([]types.Plugin, 0, len(plugins)) + for _, p := range plugins { + if enabledOnly && !p.PluginObj.Enabled { + continue + } + if disabledOnly && p.PluginObj.Enabled { + continue + } out = append(out, p.PluginObj) } return out, nil diff --git a/plugin/backend_unsupported.go b/plugin/backend_unsupported.go index becb361fe2..f9a4d5c6b6 100644 --- a/plugin/backend_unsupported.go +++ b/plugin/backend_unsupported.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/reference" "golang.org/x/net/context" ) @@ -40,7 +41,7 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m } // List displays the list of plugins and associated metadata. -func (pm *Manager) List() ([]types.Plugin, error) { +func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) { return nil, errNotSupported }