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 |
|