Browse code

Add `--filter enabled=true` for `docker plugin ls`

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>

Yong Tang authored on 2016/11/23 21:58:15
Showing 11 changed files
... ...
@@ -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