From 0ce6e070f7d1e4b1b478d52b4464f8480dbfd9b7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 20:04:44 -0800 Subject: [PATCH] Allow `docker plugin inspect` to search based on ID or name This fix tries to address the issue raised in discussion of PR 28735 where it was not possible to manage plugin based on plugin ID. Previously it was not possible to invoke `docker plugin inspect` with a plugin ID (or ID prefix). This fix updates the implementation of `docker plugin inspect` so that it is possbile to search based on a plugin name, or a plugin ID. A short format of plugin ID (prefix) is also possible, as long as there is no ambiguity. Previously the check of `docker plugin inspect` was mostly done on the client side. This could potentially cause inconsistency between API and CMD. This fix move all the checks to daemon side so that API and CMD will be consistent. An integration test has been added to cover the changes. Signed-off-by: Yong Tang --- cli/command/plugin/inspect.go | 21 ++------- docs/reference/commandline/plugin_inspect.md | 6 +-- integration-cli/docker_cli_plugins_test.go | 49 ++++++++++++++++++++ plugin/backend_linux.go | 49 ++++++++++++++++++-- plugin/store/store.go | 29 ++++++++++++ 5 files changed, 129 insertions(+), 25 deletions(-) diff --git a/cli/command/plugin/inspect.go b/cli/command/plugin/inspect.go index 13c7fa72d8..46ec7b229b 100644 --- a/cli/command/plugin/inspect.go +++ b/cli/command/plugin/inspect.go @@ -1,12 +1,9 @@ package plugin import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" - "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -20,7 +17,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ - Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]", + Use: "inspect [OPTIONS] PLUGIN|ID [PLUGIN|ID...]", Short: "Display detailed information on one or more plugins", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -37,20 +34,8 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() - getRef := func(name string) (interface{}, []byte, error) { - named, err := reference.ParseNamed(name) // FIXME: validate - if err != nil { - return nil, nil, err - } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) - } - ref, ok := named.(reference.NamedTagged) - if !ok { - return nil, nil, fmt.Errorf("invalid name: %s", named.String()) - } - - return client.PluginInspectWithRaw(ctx, ref.String()) + getRef := func(ref string) (interface{}, []byte, error) { + return client.PluginInspectWithRaw(ctx, ref) } return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) diff --git a/docs/reference/commandline/plugin_inspect.md b/docs/reference/commandline/plugin_inspect.md index 80b8e8c881..9e09e0c587 100644 --- a/docs/reference/commandline/plugin_inspect.md +++ b/docs/reference/commandline/plugin_inspect.md @@ -16,13 +16,13 @@ keywords: "plugin, inspect" # plugin inspect ```markdown -Usage: docker plugin inspect [OPTIONS] PLUGIN [PLUGIN...] +Usage: docker plugin inspect [OPTIONS] PLUGIN|ID [PLUGIN|ID...] Display detailed information on one or more plugins Options: - -f, --format string Format the output using the given Go template - --help Print usage + -f, --format string Format the output using the given Go template + --help Print usage ``` Returns information about a plugin. By default, this command renders all results diff --git a/integration-cli/docker_cli_plugins_test.go b/integration-cli/docker_cli_plugins_test.go index f6d2f28b54..730580e568 100644 --- a/integration-cli/docker_cli_plugins_test.go +++ b/integration-cli/docker_cli_plugins_test.go @@ -201,3 +201,52 @@ func (s *DockerSuite) TestPluginCreate(c *check.C) { // The output will consists of one HEADER line and one line of foo/bar-driver c.Assert(len(strings.Split(strings.TrimSpace(out), "\n")), checker.Equals, 2) } + +func (s *DockerSuite) TestPluginInspect(c *check.C) { + testRequires(c, DaemonIsLinux, Network) + _, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pNameWithTag) + c.Assert(err, checker.IsNil) + + out, _, err := dockerCmdWithError("plugin", "ls") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, pName) + c.Assert(out, checker.Contains, pTag) + c.Assert(out, checker.Contains, "true") + + // Find the ID first + out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pNameWithTag) + c.Assert(err, checker.IsNil) + id := strings.TrimSpace(out) + c.Assert(id, checker.Not(checker.Equals), "") + + // Long form + out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, id) + + // Short form + out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id[:5]) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, id) + + // Name with tag form + out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pNameWithTag) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, id) + + // Name without tag form + out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pName) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, id) + + _, _, err = dockerCmdWithError("plugin", "disable", pNameWithTag) + c.Assert(err, checker.IsNil) + + out, _, err = dockerCmdWithError("plugin", "remove", pNameWithTag) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, pNameWithTag) + + // After remove nothing should be found + _, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id[:5]) + c.Assert(err, checker.NotNil) +} diff --git a/plugin/backend_linux.go b/plugin/backend_linux.go index d40dd83e06..8c1a152d9b 100644 --- a/plugin/backend_linux.go +++ b/plugin/backend_linux.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" @@ -23,6 +24,11 @@ import ( "golang.org/x/net/context" ) +var ( + validFullID = regexp.MustCompile(`^([a-f0-9]{64})$`) + validPartialID = regexp.MustCompile(`^([a-f0-9]{1,64})$`) +) + // Disable deactivates a plugin, which implies that they cannot be used by containers. func (pm *Manager) Disable(name string) error { p, err := pm.pluginStore.GetByName(name) @@ -53,12 +59,32 @@ func (pm *Manager) Enable(name string, config *types.PluginEnableConfig) error { } // Inspect examines a plugin config -func (pm *Manager) Inspect(name string) (tp types.Plugin, err error) { - p, err := pm.pluginStore.GetByName(name) - if err != nil { +func (pm *Manager) Inspect(refOrID string) (tp types.Plugin, err error) { + // Match on full ID + if validFullID.MatchString(refOrID) { + p, err := pm.pluginStore.GetByID(refOrID) + if err == nil { + return p.PluginObj, nil + } + } + + // Match on full name + if pluginName, err := getPluginName(refOrID); err == nil { + if p, err := pm.pluginStore.GetByName(pluginName); err == nil { + return p.PluginObj, nil + } + } + + // Match on partial ID + if validPartialID.MatchString(refOrID) { + p, err := pm.pluginStore.Search(refOrID) + if err == nil { + return p.PluginObj, nil + } return tp, err } - return p.PluginObj, nil + + return tp, fmt.Errorf("no plugin name or ID associated with %q", refOrID) } func (pm *Manager) pull(ref reference.Named, metaHeader http.Header, authConfig *types.AuthConfig, pluginID string) (types.PluginPrivileges, error) { @@ -244,3 +270,18 @@ func (pm *Manager) createFromContext(ctx context.Context, pluginID, pluginDir st return nil } + +func getPluginName(name string) (string, error) { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return "", err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return "", fmt.Errorf("invalid name: %s", named.String()) + } + return ref.String(), nil +} diff --git a/plugin/store/store.go b/plugin/store/store.go index fe834dc4d7..0517f8f2c7 100644 --- a/plugin/store/store.go +++ b/plugin/store/store.go @@ -30,6 +30,13 @@ type ErrNotFound string func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) } +// ErrAmbiguous indicates that a plugin was not found locally. +type ErrAmbiguous string + +func (name ErrAmbiguous) Error() string { + return fmt.Sprintf("multiple plugins found for %q", string(name)) +} + // GetByName retreives a plugin by name. func (ps *Store) GetByName(name string) (*v2.Plugin, error) { ps.RLock() @@ -253,3 +260,25 @@ func (ps *Store) CallHandler(p *v2.Plugin) { } } } + +// Search retreives a plugin by ID Prefix +// If no plugin is found, then ErrNotFound is returned +// If multiple plugins are found, then ErrAmbiguous is returned +func (ps *Store) Search(partialID string) (*v2.Plugin, error) { + ps.RLock() + defer ps.RUnlock() + + var found *v2.Plugin + for id, p := range ps.plugins { + if strings.HasPrefix(id, partialID) { + if found != nil { + return nil, ErrAmbiguous(partialID) + } + found = p + } + } + if found == nil { + return nil, ErrNotFound(partialID) + } + return found, nil +}