Browse code

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 <yong.tang.github@outlook.com>

Yong Tang authored on 2016/11/24 13:04:44
Showing 5 changed files
... ...
@@ -1,12 +1,9 @@
1 1
 package plugin
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/docker/docker/cli"
7 5
 	"github.com/docker/docker/cli/command"
8 6
 	"github.com/docker/docker/cli/command/inspect"
9
-	"github.com/docker/docker/reference"
10 7
 	"github.com/spf13/cobra"
11 8
 	"golang.org/x/net/context"
12 9
 )
... ...
@@ -20,7 +17,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
20 20
 	var opts inspectOptions
21 21
 
22 22
 	cmd := &cobra.Command{
23
-		Use:   "inspect [OPTIONS] PLUGIN [PLUGIN...]",
23
+		Use:   "inspect [OPTIONS] PLUGIN|ID [PLUGIN|ID...]",
24 24
 		Short: "Display detailed information on one or more plugins",
25 25
 		Args:  cli.RequiresMinArgs(1),
26 26
 		RunE: func(cmd *cobra.Command, args []string) error {
... ...
@@ -37,20 +34,8 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
37 37
 func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
38 38
 	client := dockerCli.Client()
39 39
 	ctx := context.Background()
40
-	getRef := func(name string) (interface{}, []byte, error) {
41
-		named, err := reference.ParseNamed(name) // FIXME: validate
42
-		if err != nil {
43
-			return nil, nil, err
44
-		}
45
-		if reference.IsNameOnly(named) {
46
-			named = reference.WithDefaultTag(named)
47
-		}
48
-		ref, ok := named.(reference.NamedTagged)
49
-		if !ok {
50
-			return nil, nil, fmt.Errorf("invalid name: %s", named.String())
51
-		}
52
-
53
-		return client.PluginInspectWithRaw(ctx, ref.String())
40
+	getRef := func(ref string) (interface{}, []byte, error) {
41
+		return client.PluginInspectWithRaw(ctx, ref)
54 42
 	}
55 43
 
56 44
 	return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef)
... ...
@@ -16,13 +16,13 @@ keywords: "plugin, inspect"
16 16
 # plugin inspect
17 17
 
18 18
 ```markdown
19
-Usage:  docker plugin inspect [OPTIONS] PLUGIN [PLUGIN...]
19
+Usage:	docker plugin inspect [OPTIONS] PLUGIN|ID [PLUGIN|ID...]
20 20
 
21 21
 Display detailed information on one or more plugins
22 22
 
23 23
 Options:
24
-      -f, --format string   Format the output using the given Go template
25
-          --help            Print usage
24
+  -f, --format string   Format the output using the given Go template
25
+      --help            Print usage
26 26
 ```
27 27
 
28 28
 Returns information about a plugin. By default, this command renders all results
... ...
@@ -201,3 +201,52 @@ func (s *DockerSuite) TestPluginCreate(c *check.C) {
201 201
 	// The output will consists of one HEADER line and one line of foo/bar-driver
202 202
 	c.Assert(len(strings.Split(strings.TrimSpace(out), "\n")), checker.Equals, 2)
203 203
 }
204
+
205
+func (s *DockerSuite) TestPluginInspect(c *check.C) {
206
+	testRequires(c, DaemonIsLinux, Network)
207
+	_, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pNameWithTag)
208
+	c.Assert(err, checker.IsNil)
209
+
210
+	out, _, err := dockerCmdWithError("plugin", "ls")
211
+	c.Assert(err, checker.IsNil)
212
+	c.Assert(out, checker.Contains, pName)
213
+	c.Assert(out, checker.Contains, pTag)
214
+	c.Assert(out, checker.Contains, "true")
215
+
216
+	// Find the ID first
217
+	out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pNameWithTag)
218
+	c.Assert(err, checker.IsNil)
219
+	id := strings.TrimSpace(out)
220
+	c.Assert(id, checker.Not(checker.Equals), "")
221
+
222
+	// Long form
223
+	out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id)
224
+	c.Assert(err, checker.IsNil)
225
+	c.Assert(strings.TrimSpace(out), checker.Equals, id)
226
+
227
+	// Short form
228
+	out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id[:5])
229
+	c.Assert(err, checker.IsNil)
230
+	c.Assert(strings.TrimSpace(out), checker.Equals, id)
231
+
232
+	// Name with tag form
233
+	out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pNameWithTag)
234
+	c.Assert(err, checker.IsNil)
235
+	c.Assert(strings.TrimSpace(out), checker.Equals, id)
236
+
237
+	// Name without tag form
238
+	out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pName)
239
+	c.Assert(err, checker.IsNil)
240
+	c.Assert(strings.TrimSpace(out), checker.Equals, id)
241
+
242
+	_, _, err = dockerCmdWithError("plugin", "disable", pNameWithTag)
243
+	c.Assert(err, checker.IsNil)
244
+
245
+	out, _, err = dockerCmdWithError("plugin", "remove", pNameWithTag)
246
+	c.Assert(err, checker.IsNil)
247
+	c.Assert(out, checker.Contains, pNameWithTag)
248
+
249
+	// After remove nothing should be found
250
+	_, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id[:5])
251
+	c.Assert(err, checker.NotNil)
252
+}
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	"net/http"
12 12
 	"os"
13 13
 	"path/filepath"
14
+	"regexp"
14 15
 
15 16
 	"github.com/Sirupsen/logrus"
16 17
 	"github.com/docker/docker/api/types"
... ...
@@ -23,6 +24,11 @@ import (
23 23
 	"golang.org/x/net/context"
24 24
 )
25 25
 
26
+var (
27
+	validFullID    = regexp.MustCompile(`^([a-f0-9]{64})$`)
28
+	validPartialID = regexp.MustCompile(`^([a-f0-9]{1,64})$`)
29
+)
30
+
26 31
 // Disable deactivates a plugin, which implies that they cannot be used by containers.
27 32
 func (pm *Manager) Disable(name string) error {
28 33
 	p, err := pm.pluginStore.GetByName(name)
... ...
@@ -53,12 +59,32 @@ func (pm *Manager) Enable(name string, config *types.PluginEnableConfig) error {
53 53
 }
54 54
 
55 55
 // Inspect examines a plugin config
56
-func (pm *Manager) Inspect(name string) (tp types.Plugin, err error) {
57
-	p, err := pm.pluginStore.GetByName(name)
58
-	if err != nil {
56
+func (pm *Manager) Inspect(refOrID string) (tp types.Plugin, err error) {
57
+	// Match on full ID
58
+	if validFullID.MatchString(refOrID) {
59
+		p, err := pm.pluginStore.GetByID(refOrID)
60
+		if err == nil {
61
+			return p.PluginObj, nil
62
+		}
63
+	}
64
+
65
+	// Match on full name
66
+	if pluginName, err := getPluginName(refOrID); err == nil {
67
+		if p, err := pm.pluginStore.GetByName(pluginName); err == nil {
68
+			return p.PluginObj, nil
69
+		}
70
+	}
71
+
72
+	// Match on partial ID
73
+	if validPartialID.MatchString(refOrID) {
74
+		p, err := pm.pluginStore.Search(refOrID)
75
+		if err == nil {
76
+			return p.PluginObj, nil
77
+		}
59 78
 		return tp, err
60 79
 	}
61
-	return p.PluginObj, nil
80
+
81
+	return tp, fmt.Errorf("no plugin name or ID associated with %q", refOrID)
62 82
 }
63 83
 
64 84
 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
244 244
 
245 245
 	return nil
246 246
 }
247
+
248
+func getPluginName(name string) (string, error) {
249
+	named, err := reference.ParseNamed(name) // FIXME: validate
250
+	if err != nil {
251
+		return "", err
252
+	}
253
+	if reference.IsNameOnly(named) {
254
+		named = reference.WithDefaultTag(named)
255
+	}
256
+	ref, ok := named.(reference.NamedTagged)
257
+	if !ok {
258
+		return "", fmt.Errorf("invalid name: %s", named.String())
259
+	}
260
+	return ref.String(), nil
261
+}
... ...
@@ -30,6 +30,13 @@ type ErrNotFound string
30 30
 
31 31
 func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
32 32
 
33
+// ErrAmbiguous indicates that a plugin was not found locally.
34
+type ErrAmbiguous string
35
+
36
+func (name ErrAmbiguous) Error() string {
37
+	return fmt.Sprintf("multiple plugins found for %q", string(name))
38
+}
39
+
33 40
 // GetByName retreives a plugin by name.
34 41
 func (ps *Store) GetByName(name string) (*v2.Plugin, error) {
35 42
 	ps.RLock()
... ...
@@ -253,3 +260,25 @@ func (ps *Store) CallHandler(p *v2.Plugin) {
253 253
 		}
254 254
 	}
255 255
 }
256
+
257
+// Search retreives a plugin by ID Prefix
258
+// If no plugin is found, then ErrNotFound is returned
259
+// If multiple plugins are found, then ErrAmbiguous is returned
260
+func (ps *Store) Search(partialID string) (*v2.Plugin, error) {
261
+	ps.RLock()
262
+	defer ps.RUnlock()
263
+
264
+	var found *v2.Plugin
265
+	for id, p := range ps.plugins {
266
+		if strings.HasPrefix(id, partialID) {
267
+			if found != nil {
268
+				return nil, ErrAmbiguous(partialID)
269
+			}
270
+			found = p
271
+		}
272
+	}
273
+	if found == nil {
274
+		return nil, ErrNotFound(partialID)
275
+	}
276
+	return found, nil
277
+}