This fix adds `--filter enabled=true` to `docker plugin ls`,
as was specified in 28624.
The related API and docs has been updated.
An integration test has been added.
This fix fixes 28624.
Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"net/http" |
| 6 | 6 |
|
| 7 | 7 |
enginetypes "github.com/docker/docker/api/types" |
| 8 |
+ "github.com/docker/docker/api/types/filters" |
|
| 8 | 9 |
"github.com/docker/docker/reference" |
| 9 | 10 |
"golang.org/x/net/context" |
| 10 | 11 |
) |
| ... | ... |
@@ -13,7 +14,7 @@ import ( |
| 13 | 13 |
type Backend interface {
|
| 14 | 14 |
Disable(name string, config *enginetypes.PluginDisableConfig) error |
| 15 | 15 |
Enable(name string, config *enginetypes.PluginEnableConfig) error |
| 16 |
- List() ([]enginetypes.Plugin, error) |
|
| 16 |
+ List(filters.Args) ([]enginetypes.Plugin, error) |
|
| 17 | 17 |
Inspect(name string) (*enginetypes.Plugin, error) |
| 18 | 18 |
Remove(name string, config *enginetypes.PluginRmConfig) error |
| 19 | 19 |
Set(name string, args []string) error |
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
distreference "github.com/docker/distribution/reference" |
| 11 | 11 |
"github.com/docker/docker/api/server/httputils" |
| 12 | 12 |
"github.com/docker/docker/api/types" |
| 13 |
+ "github.com/docker/docker/api/types/filters" |
|
| 13 | 14 |
"github.com/docker/docker/pkg/ioutils" |
| 14 | 15 |
"github.com/docker/docker/pkg/streamformatter" |
| 15 | 16 |
"github.com/docker/docker/reference" |
| ... | ... |
@@ -253,7 +254,15 @@ func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r |
| 253 | 253 |
} |
| 254 | 254 |
|
| 255 | 255 |
func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 256 |
- l, err := pr.backend.List() |
|
| 256 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 257 |
+ return err |
|
| 258 |
+ } |
|
| 259 |
+ |
|
| 260 |
+ pluginFilters, err := filters.FromParam(r.Form.Get("filters"))
|
|
| 261 |
+ if err != nil {
|
|
| 262 |
+ return err |
|
| 263 |
+ } |
|
| 264 |
+ l, err := pr.backend.List(pluginFilters) |
|
| 257 | 265 |
if err != nil {
|
| 258 | 266 |
return err |
| 259 | 267 |
} |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"github.com/docker/docker/cli" |
| 5 | 5 |
"github.com/docker/docker/cli/command" |
| 6 | 6 |
"github.com/docker/docker/cli/command/formatter" |
| 7 |
+ "github.com/docker/docker/opts" |
|
| 7 | 8 |
"github.com/spf13/cobra" |
| 8 | 9 |
"golang.org/x/net/context" |
| 9 | 10 |
) |
| ... | ... |
@@ -12,10 +13,11 @@ type listOptions struct {
|
| 12 | 12 |
quiet bool |
| 13 | 13 |
noTrunc bool |
| 14 | 14 |
format string |
| 15 |
+ filter opts.FilterOpt |
|
| 15 | 16 |
} |
| 16 | 17 |
|
| 17 | 18 |
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 18 |
- var opts listOptions |
|
| 19 |
+ opts := listOptions{filter: opts.NewFilterOpt()}
|
|
| 19 | 20 |
|
| 20 | 21 |
cmd := &cobra.Command{
|
| 21 | 22 |
Use: "ls [OPTIONS]", |
| ... | ... |
@@ -32,12 +34,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 32 | 32 |
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display plugin IDs") |
| 33 | 33 |
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") |
| 34 | 34 |
flags.StringVar(&opts.format, "format", "", "Pretty-print plugins using a Go template") |
| 35 |
+ flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'enabled=true')") |
|
| 35 | 36 |
|
| 36 | 37 |
return cmd |
| 37 | 38 |
} |
| 38 | 39 |
|
| 39 | 40 |
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 40 |
- plugins, err := dockerCli.Client().PluginList(context.Background()) |
|
| 41 |
+ plugins, err := dockerCli.Client().PluginList(context.Background(), opts.filter.Value()) |
|
| 41 | 42 |
if err != nil {
|
| 42 | 43 |
return err |
| 43 | 44 |
} |
| ... | ... |
@@ -108,7 +108,7 @@ type NodeAPIClient interface {
|
| 108 | 108 |
|
| 109 | 109 |
// PluginAPIClient defines API client methods for the plugins |
| 110 | 110 |
type PluginAPIClient interface {
|
| 111 |
- PluginList(ctx context.Context) (types.PluginsListResponse, error) |
|
| 111 |
+ PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) |
|
| 112 | 112 |
PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error |
| 113 | 113 |
PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error |
| 114 | 114 |
PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error |
| ... | ... |
@@ -2,15 +2,26 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "net/url" |
|
| 5 | 6 |
|
| 6 | 7 |
"github.com/docker/docker/api/types" |
| 8 |
+ "github.com/docker/docker/api/types/filters" |
|
| 7 | 9 |
"golang.org/x/net/context" |
| 8 | 10 |
) |
| 9 | 11 |
|
| 10 | 12 |
// PluginList returns the installed plugins |
| 11 |
-func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) {
|
|
| 13 |
+func (cli *Client) PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) {
|
|
| 12 | 14 |
var plugins types.PluginsListResponse |
| 13 |
- resp, err := cli.get(ctx, "/plugins", nil, nil) |
|
| 15 |
+ query := url.Values{}
|
|
| 16 |
+ |
|
| 17 |
+ if filter.Len() > 0 {
|
|
| 18 |
+ filterJSON, err := filters.ToParamWithVersion(cli.version, filter) |
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return plugins, err |
|
| 21 |
+ } |
|
| 22 |
+ query.Set("filters", filterJSON)
|
|
| 23 |
+ } |
|
| 24 |
+ resp, err := cli.get(ctx, "/plugins", query, nil) |
|
| 14 | 25 |
if err != nil {
|
| 15 | 26 |
return plugins, err |
| 16 | 27 |
} |
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"testing" |
| 11 | 11 |
|
| 12 | 12 |
"github.com/docker/docker/api/types" |
| 13 |
+ "github.com/docker/docker/api/types/filters" |
|
| 13 | 14 |
"golang.org/x/net/context" |
| 14 | 15 |
) |
| 15 | 16 |
|
| ... | ... |
@@ -18,7 +19,7 @@ func TestPluginListError(t *testing.T) {
|
| 18 | 18 |
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
| 19 | 19 |
} |
| 20 | 20 |
|
| 21 |
- _, err := client.PluginList(context.Background()) |
|
| 21 |
+ _, err := client.PluginList(context.Background(), filters.NewArgs()) |
|
| 22 | 22 |
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
| 23 | 23 |
t.Fatalf("expected a Server Error, got %v", err)
|
| 24 | 24 |
} |
| ... | ... |
@@ -26,34 +27,69 @@ func TestPluginListError(t *testing.T) {
|
| 26 | 26 |
|
| 27 | 27 |
func TestPluginList(t *testing.T) {
|
| 28 | 28 |
expectedURL := "/plugins" |
| 29 |
- client := &Client{
|
|
| 30 |
- client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 31 |
- if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 32 |
- return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 33 |
- } |
|
| 34 |
- content, err := json.Marshal([]*types.Plugin{
|
|
| 35 |
- {
|
|
| 36 |
- ID: "plugin_id1", |
|
| 37 |
- }, |
|
| 38 |
- {
|
|
| 39 |
- ID: "plugin_id2", |
|
| 40 |
- }, |
|
| 41 |
- }) |
|
| 42 |
- if err != nil {
|
|
| 43 |
- return nil, err |
|
| 44 |
- } |
|
| 45 |
- return &http.Response{
|
|
| 46 |
- StatusCode: http.StatusOK, |
|
| 47 |
- Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 48 |
- }, nil |
|
| 49 |
- }), |
|
| 50 |
- } |
|
| 51 | 29 |
|
| 52 |
- plugins, err := client.PluginList(context.Background()) |
|
| 53 |
- if err != nil {
|
|
| 54 |
- t.Fatal(err) |
|
| 30 |
+ enabledFilters := filters.NewArgs() |
|
| 31 |
+ enabledFilters.Add("enabled", "true")
|
|
| 32 |
+ |
|
| 33 |
+ listCases := []struct {
|
|
| 34 |
+ filters filters.Args |
|
| 35 |
+ expectedQueryParams map[string]string |
|
| 36 |
+ }{
|
|
| 37 |
+ {
|
|
| 38 |
+ filters: filters.NewArgs(), |
|
| 39 |
+ expectedQueryParams: map[string]string{
|
|
| 40 |
+ "all": "", |
|
| 41 |
+ "filter": "", |
|
| 42 |
+ "filters": "", |
|
| 43 |
+ }, |
|
| 44 |
+ }, |
|
| 45 |
+ {
|
|
| 46 |
+ filters: enabledFilters, |
|
| 47 |
+ expectedQueryParams: map[string]string{
|
|
| 48 |
+ "all": "", |
|
| 49 |
+ "filter": "", |
|
| 50 |
+ "filters": `{"enabled":{"true":true}}`,
|
|
| 51 |
+ }, |
|
| 52 |
+ }, |
|
| 55 | 53 |
} |
| 56 |
- if len(plugins) != 2 {
|
|
| 57 |
- t.Fatalf("expected 2 plugins, got %v", plugins)
|
|
| 54 |
+ |
|
| 55 |
+ for _, listCase := range listCases {
|
|
| 56 |
+ client := &Client{
|
|
| 57 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 58 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 59 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 60 |
+ } |
|
| 61 |
+ query := req.URL.Query() |
|
| 62 |
+ for key, expected := range listCase.expectedQueryParams {
|
|
| 63 |
+ actual := query.Get(key) |
|
| 64 |
+ if actual != expected {
|
|
| 65 |
+ return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
|
|
| 66 |
+ } |
|
| 67 |
+ } |
|
| 68 |
+ content, err := json.Marshal([]*types.Plugin{
|
|
| 69 |
+ {
|
|
| 70 |
+ ID: "plugin_id1", |
|
| 71 |
+ }, |
|
| 72 |
+ {
|
|
| 73 |
+ ID: "plugin_id2", |
|
| 74 |
+ }, |
|
| 75 |
+ }) |
|
| 76 |
+ if err != nil {
|
|
| 77 |
+ return nil, err |
|
| 78 |
+ } |
|
| 79 |
+ return &http.Response{
|
|
| 80 |
+ StatusCode: http.StatusOK, |
|
| 81 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 82 |
+ }, nil |
|
| 83 |
+ }), |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ plugins, err := client.PluginList(context.Background(), listCase.filters) |
|
| 87 |
+ if err != nil {
|
|
| 88 |
+ t.Fatal(err) |
|
| 89 |
+ } |
|
| 90 |
+ if len(plugins) != 2 {
|
|
| 91 |
+ t.Fatalf("expected 2 plugins, got %v", plugins)
|
|
| 92 |
+ } |
|
| 58 | 93 |
} |
| 59 | 94 |
} |
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"strings" |
| 6 | 6 |
|
| 7 | 7 |
"github.com/docker/docker/api/types" |
| 8 |
+ "github.com/docker/docker/api/types/filters" |
|
| 8 | 9 |
"github.com/docker/docker/api/types/network" |
| 9 | 10 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 10 | 11 |
clustertypes "github.com/docker/docker/daemon/cluster/provider" |
| ... | ... |
@@ -53,7 +54,7 @@ func (e *executor) Describe(ctx context.Context) (*api.NodeDescription, error) {
|
| 53 | 53 |
addPlugins("Authorization", info.Plugins.Authorization)
|
| 54 | 54 |
|
| 55 | 55 |
// add v2 plugins |
| 56 |
- v2Plugins, err := e.backend.PluginManager().List() |
|
| 56 |
+ v2Plugins, err := e.backend.PluginManager().List(filters.NewArgs()) |
|
| 57 | 57 |
if err == nil {
|
| 58 | 58 |
for _, plgn := range v2Plugins {
|
| 59 | 59 |
for _, typ := range plgn.Config.Interface.Types {
|
| ... | ... |
@@ -24,6 +24,7 @@ Aliases: |
| 24 | 24 |
ls, list |
| 25 | 25 |
|
| 26 | 26 |
Options: |
| 27 |
+ -f, --filter filter Provide filter values (e.g. 'enabled=true') |
|
| 27 | 28 |
--format string Pretty-print plugins using a Go template |
| 28 | 29 |
--help Print usage |
| 29 | 30 |
--no-trunc Don't truncate output |
| ... | ... |
@@ -32,6 +33,8 @@ Options: |
| 32 | 32 |
|
| 33 | 33 |
Lists all the plugins that are currently installed. You can install plugins |
| 34 | 34 |
using the [`docker plugin install`](plugin_install.md) command. |
| 35 |
+You can also filter using the `-f` or `--filter` flag. |
|
| 36 |
+Refer to the [filtering](#filtering) section for more information about available filter options. |
|
| 35 | 37 |
|
| 36 | 38 |
Example output: |
| 37 | 39 |
|
| ... | ... |
@@ -42,6 +45,20 @@ ID NAME TAG DESCRIP |
| 42 | 42 |
69553ca1d123 tiborvass/sample-volume-plugin latest A test plugin for Docker true |
| 43 | 43 |
``` |
| 44 | 44 |
|
| 45 |
+## Filtering |
|
| 46 |
+ |
|
| 47 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more |
|
| 48 |
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
|
| 49 |
+ |
|
| 50 |
+The currently supported filters are: |
|
| 51 |
+ |
|
| 52 |
+* enabled (boolean - true or false, 0 or 1) |
|
| 53 |
+ |
|
| 54 |
+### enabled |
|
| 55 |
+ |
|
| 56 |
+The `enabled` filter matches on plugins enabled or disabled. |
|
| 57 |
+ |
|
| 58 |
+ |
|
| 45 | 59 |
## Formatting |
| 46 | 60 |
|
| 47 | 61 |
The formatting options (`--format`) pretty-prints plugins output |
| ... | ... |
@@ -68,6 +85,7 @@ $ docker plugin ls --format "{{.ID}}: {{.Name}}"
|
| 68 | 68 |
4be01827a72e: tiborvass/no-remove |
| 69 | 69 |
``` |
| 70 | 70 |
|
| 71 |
+ |
|
| 71 | 72 |
## Related information |
| 72 | 73 |
|
| 73 | 74 |
* [plugin create](plugin_create.md) |
| ... | ... |
@@ -285,3 +285,31 @@ func existsMountpointWithPrefix(mountpointPrefix string) (bool, error) {
|
| 285 | 285 |
} |
| 286 | 286 |
return false, nil |
| 287 | 287 |
} |
| 288 |
+ |
|
| 289 |
+func (s *DockerDaemonSuite) TestPluginListFilterEnabled(c *check.C) {
|
|
| 290 |
+ testRequires(c, Network) |
|
| 291 |
+ |
|
| 292 |
+ s.d.Start(c) |
|
| 293 |
+ |
|
| 294 |
+ out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pNameWithTag, "--disable")
|
|
| 295 |
+ c.Assert(err, check.IsNil, check.Commentf(out)) |
|
| 296 |
+ |
|
| 297 |
+ defer func() {
|
|
| 298 |
+ if out, err := s.d.Cmd("plugin", "remove", pNameWithTag); err != nil {
|
|
| 299 |
+ c.Fatalf("Could not remove plugin: %v %s", err, out)
|
|
| 300 |
+ } |
|
| 301 |
+ }() |
|
| 302 |
+ |
|
| 303 |
+ out, err = s.d.Cmd("plugin", "ls", "--filter", "enabled=true")
|
|
| 304 |
+ c.Assert(err, checker.IsNil) |
|
| 305 |
+ c.Assert(out, checker.Not(checker.Contains), pName) |
|
| 306 |
+ |
|
| 307 |
+ out, err = s.d.Cmd("plugin", "ls", "--filter", "enabled=false")
|
|
| 308 |
+ c.Assert(err, checker.IsNil) |
|
| 309 |
+ c.Assert(out, checker.Contains, pName) |
|
| 310 |
+ c.Assert(out, checker.Contains, "false") |
|
| 311 |
+ |
|
| 312 |
+ out, err = s.d.Cmd("plugin", "ls")
|
|
| 313 |
+ c.Assert(err, checker.IsNil) |
|
| 314 |
+ c.Assert(out, checker.Contains, pName) |
|
| 315 |
+} |
| ... | ... |
@@ -18,6 +18,7 @@ import ( |
| 18 | 18 |
"github.com/Sirupsen/logrus" |
| 19 | 19 |
"github.com/docker/distribution/manifest/schema2" |
| 20 | 20 |
"github.com/docker/docker/api/types" |
| 21 |
+ "github.com/docker/docker/api/types/filters" |
|
| 21 | 22 |
"github.com/docker/docker/distribution" |
| 22 | 23 |
progressutils "github.com/docker/docker/distribution/utils" |
| 23 | 24 |
"github.com/docker/docker/distribution/xfer" |
| ... | ... |
@@ -33,6 +34,10 @@ import ( |
| 33 | 33 |
"golang.org/x/net/context" |
| 34 | 34 |
) |
| 35 | 35 |
|
| 36 |
+var acceptedPluginFilterTags = map[string]bool{
|
|
| 37 |
+ "enabled": true, |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 36 | 40 |
// Disable deactivates a plugin. This means resources (volumes, networks) cant use them. |
| 37 | 41 |
func (pm *Manager) Disable(refOrID string, config *types.PluginDisableConfig) error {
|
| 38 | 42 |
p, err := pm.config.Store.GetV2Plugin(refOrID) |
| ... | ... |
@@ -259,10 +264,33 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m |
| 259 | 259 |
} |
| 260 | 260 |
|
| 261 | 261 |
// List displays the list of plugins and associated metadata. |
| 262 |
-func (pm *Manager) List() ([]types.Plugin, error) {
|
|
| 262 |
+func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
|
|
| 263 |
+ if err := pluginFilters.Validate(acceptedPluginFilterTags); err != nil {
|
|
| 264 |
+ return nil, err |
|
| 265 |
+ } |
|
| 266 |
+ |
|
| 267 |
+ enabledOnly := false |
|
| 268 |
+ disabledOnly := false |
|
| 269 |
+ if pluginFilters.Include("enabled") {
|
|
| 270 |
+ if pluginFilters.ExactMatch("enabled", "true") {
|
|
| 271 |
+ enabledOnly = true |
|
| 272 |
+ } else if pluginFilters.ExactMatch("enabled", "false") {
|
|
| 273 |
+ disabledOnly = true |
|
| 274 |
+ } else {
|
|
| 275 |
+ return nil, fmt.Errorf("Invalid filter 'enabled=%s'", pluginFilters.Get("enabled"))
|
|
| 276 |
+ } |
|
| 277 |
+ } |
|
| 278 |
+ |
|
| 263 | 279 |
plugins := pm.config.Store.GetAll() |
| 264 | 280 |
out := make([]types.Plugin, 0, len(plugins)) |
| 281 |
+ |
|
| 265 | 282 |
for _, p := range plugins {
|
| 283 |
+ if enabledOnly && !p.PluginObj.Enabled {
|
|
| 284 |
+ continue |
|
| 285 |
+ } |
|
| 286 |
+ if disabledOnly && p.PluginObj.Enabled {
|
|
| 287 |
+ continue |
|
| 288 |
+ } |
|
| 266 | 289 |
out = append(out, p.PluginObj) |
| 267 | 290 |
} |
| 268 | 291 |
return out, nil |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"net/http" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/docker/docker/api/types" |
| 11 |
+ "github.com/docker/docker/api/types/filters" |
|
| 11 | 12 |
"github.com/docker/docker/reference" |
| 12 | 13 |
"golang.org/x/net/context" |
| 13 | 14 |
) |
| ... | ... |
@@ -40,7 +41,7 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m |
| 40 | 40 |
} |
| 41 | 41 |
|
| 42 | 42 |
// List displays the list of plugins and associated metadata. |
| 43 |
-func (pm *Manager) List() ([]types.Plugin, error) {
|
|
| 43 |
+func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
|
|
| 44 | 44 |
return nil, errNotSupported |
| 45 | 45 |
} |
| 46 | 46 |
|