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>
This commit is contained in:
Yong Tang 2016-11-23 04:58:15 -08:00
parent 541150da53
commit a66e0dc349
11 changed files with 174 additions and 38 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
}
}
}

View file

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

View file

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

View file

@ -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)
}

View file

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

View file

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