浏览代码

Merge pull request #29414 from cpuguy83/plugin_upgrade

Add docker plugin upgrade
Anusha Ragunathan 8 年之前
父节点
当前提交
fa49c076d4

+ 1 - 0
api/server/router/plugin/backend.go

@@ -21,5 +21,6 @@ type Backend interface {
 	Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
 	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
 	Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
+	Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
 	CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
 }

+ 1 - 0
api/server/router/plugin/plugin.go

@@ -32,6 +32,7 @@ func (r *pluginRouter) initRoutes() {
 		router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
 		router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
 		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)),
+		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/upgrade", r.upgradePlugin)),
 		router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
 		router.NewPostRoute("/plugins/create", r.createPlugin),
 	}

+ 63 - 18
api/server/router/plugin/plugin_routes.go

@@ -101,7 +101,7 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
 	return httputils.WriteJSON(w, http.StatusOK, privileges)
 }
 
-func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if err := httputils.ParseForm(r); err != nil {
 		return errors.Wrap(err, "failed to parse form")
 	}
@@ -116,20 +116,77 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
 	}
 
 	metaHeaders, authConfig := parseHeaders(r.Header)
+	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
+	if err != nil {
+		return err
+	}
+
+	name, err := getName(ref, tag, vars["name"])
+	if err != nil {
+		return err
+	}
+	w.Header().Set("Docker-Plugin-Name", name)
 
+	w.Header().Set("Content-Type", "application/json")
+	output := ioutils.NewWriteFlusher(w)
+
+	if err := pr.backend.Upgrade(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
+		if !output.Flushed() {
+			return err
+		}
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
+	}
+
+	return nil
+}
+
+func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return errors.Wrap(err, "failed to parse form")
+	}
+
+	var privileges types.PluginPrivileges
+	dec := json.NewDecoder(r.Body)
+	if err := dec.Decode(&privileges); err != nil {
+		return errors.Wrap(err, "failed to parse privileges")
+	}
+	if dec.More() {
+		return errors.New("invalid privileges")
+	}
+
+	metaHeaders, authConfig := parseHeaders(r.Header)
 	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
 	if err != nil {
 		return err
 	}
 
-	name := r.FormValue("name")
+	name, err := getName(ref, tag, r.FormValue("name"))
+	if err != nil {
+		return err
+	}
+	w.Header().Set("Docker-Plugin-Name", name)
+
+	w.Header().Set("Content-Type", "application/json")
+	output := ioutils.NewWriteFlusher(w)
+
+	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
+		if !output.Flushed() {
+			return err
+		}
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
+	}
+
+	return nil
+}
+
+func getName(ref reference.Named, tag, name string) (string, error) {
 	if name == "" {
 		if _, ok := ref.(reference.Canonical); ok {
 			trimmed := reference.TrimNamed(ref)
 			if tag != "" {
 				nt, err := reference.WithTag(trimmed, tag)
 				if err != nil {
-					return err
+					return "", err
 				}
 				name = nt.String()
 			} else {
@@ -141,29 +198,17 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
 	} else {
 		localRef, err := reference.ParseNamed(name)
 		if err != nil {
-			return err
+			return "", err
 		}
 		if _, ok := localRef.(reference.Canonical); ok {
-			return errors.New("cannot use digest in plugin tag")
+			return "", errors.New("cannot use digest in plugin tag")
 		}
 		if distreference.IsNameOnly(localRef) {
 			// TODO: log change in name to out stream
 			name = reference.WithDefaultTag(localRef).String()
 		}
 	}
-	w.Header().Set("Docker-Plugin-Name", name)
-
-	w.Header().Set("Content-Type", "application/json")
-	output := ioutils.NewWriteFlusher(w)
-
-	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
-		if !output.Flushed() {
-			return err
-		}
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
-	}
-
-	return nil
+	return name, nil
 }
 
 func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

+ 4 - 0
api/swagger.yaml

@@ -1412,6 +1412,10 @@ definitions:
             type: "array"
             items:
               $ref: "#/definitions/PluginDevice"
+      PluginReference:
+        description: "plugin remote reference used to push/pull the plugin"
+        type: "string"
+        x-nullable: false
       Config:
         description: "The config of a plugin."
         type: "object"

+ 3 - 0
api/types/plugin.go

@@ -22,6 +22,9 @@ type Plugin struct {
 	// Required: true
 	Name string `json:"Name"`
 
+	// plugin remote reference used to push/pull the plugin
+	PluginReference string `json:"PluginReference,omitempty"`
+
 	// settings
 	// Required: true
 	Settings PluginSettings `json:"Settings"`

+ 5 - 0
cli/command/formatter/plugin.go

@@ -85,3 +85,8 @@ func (c *pluginContext) Enabled() bool {
 	c.AddHeader(enabledHeader)
 	return c.p.Enabled
 }
+
+func (c *pluginContext) PluginReference() string {
+	c.AddHeader(imageHeader)
+	return c.p.PluginReference
+}

+ 2 - 2
cli/command/formatter/plugin_test.go

@@ -150,8 +150,8 @@ func TestPluginContextWriteJSON(t *testing.T) {
 		{ID: "pluginID2", Name: "foobar_bar"},
 	}
 	expectedJSONs := []map[string]interface{}{
-		{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz"},
-		{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar"},
+		{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""},
+		{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""},
 	}
 
 	out := bytes.NewBufferString("")

+ 1 - 0
cli/command/plugin/cmd.go

@@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
 		newSetCommand(dockerCli),
 		newPushCommand(dockerCli),
 		newCreateCommand(dockerCli),
+		newUpgradeCommand(dockerCli),
 	)
 	return cmd
 }

+ 48 - 41
cli/command/plugin/install.go

@@ -15,15 +15,22 @@ import (
 	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/registry"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 	"golang.org/x/net/context"
 )
 
 type pluginOptions struct {
-	name       string
-	alias      string
-	grantPerms bool
-	disable    bool
-	args       []string
+	remote          string
+	localName       string
+	grantPerms      bool
+	disable         bool
+	args            []string
+	skipRemoteCheck bool
+}
+
+func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) {
+	flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
+	command.AddTrustVerificationFlags(flags)
 }
 
 func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
@@ -33,7 +40,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
 		Short: "Install a plugin",
 		Args:  cli.RequiresMinArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			options.name = args[0]
+			options.remote = args[0]
 			if len(args) > 1 {
 				options.args = args[1:]
 			}
@@ -42,12 +49,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
 	}
 
 	flags := cmd.Flags()
-	flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
+	loadPullFlags(&options, flags)
 	flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
-	flags.StringVar(&options.alias, "alias", "", "Local name for plugin")
-
-	command.AddTrustVerificationFlags(flags)
-
+	flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
 	return cmd
 }
 
@@ -83,49 +87,33 @@ func newRegistryService() registry.Service {
 	}
 }
 
-func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
+func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
 	// Names with both tag and digest will be treated by the daemon
-	// as a pull by digest with an alias for the tag
-	// (if no alias is provided).
-	ref, err := reference.ParseNormalizedNamed(opts.name)
+	// as a pull by digest with a local name for the tag
+	// (if no local name is provided).
+	ref, err := reference.ParseNormalizedNamed(opts.remote)
 	if err != nil {
-		return err
-	}
-
-	alias := ""
-	if opts.alias != "" {
-		aref, err := reference.ParseNormalizedNamed(opts.alias)
-		if err != nil {
-			return err
-		}
-		if _, ok := aref.(reference.Canonical); ok {
-			return fmt.Errorf("invalid name: %s", opts.alias)
-		}
-		alias = reference.FamiliarString(reference.EnsureTagged(aref))
+		return types.PluginInstallOptions{}, err
 	}
-	ctx := context.Background()
 
 	repoInfo, err := registry.ParseRepositoryInfo(ref)
 	if err != nil {
-		return err
+		return types.PluginInstallOptions{}, err
 	}
 
 	remote := ref.String()
 
 	_, isCanonical := ref.(reference.Canonical)
 	if command.IsTrusted() && !isCanonical {
-		if alias == "" {
-			alias = reference.FamiliarString(ref)
-		}
-
 		nt, ok := ref.(reference.NamedTagged)
 		if !ok {
 			nt = reference.EnsureTagged(ref)
 		}
 
+		ctx := context.Background()
 		trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
 		if err != nil {
-			return err
+			return types.PluginInstallOptions{}, err
 		}
 		remote = reference.FamiliarString(trusted)
 	}
@@ -134,23 +122,42 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
 
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
 	if err != nil {
-		return err
+		return types.PluginInstallOptions{}, err
 	}
-
-	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install")
+	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName)
 
 	options := types.PluginInstallOptions{
 		RegistryAuth:          encodedAuth,
 		RemoteRef:             remote,
 		Disabled:              opts.disable,
 		AcceptAllPermissions:  opts.grantPerms,
-		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
+		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
 		// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
 		PrivilegeFunc: registryAuthFunc,
 		Args:          opts.args,
 	}
+	return options, nil
+}
 
-	responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options)
+func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
+	var localName string
+	if opts.localName != "" {
+		aref, err := reference.ParseNormalizedNamed(opts.localName)
+		if err != nil {
+			return err
+		}
+		if _, ok := aref.(reference.Canonical); ok {
+			return fmt.Errorf("invalid name: %s", opts.localName)
+		}
+		localName = reference.FamiliarString(reference.EnsureTagged(aref))
+	}
+
+	ctx := context.Background()
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install")
+	if err != nil {
+		return err
+	}
+	responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
 	if err != nil {
 		if strings.Contains(err.Error(), "target is image") {
 			return errors.New(err.Error() + " - Use `docker image pull`")
@@ -161,7 +168,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
 	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
 		return err
 	}
-	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result
+	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result
 	return nil
 }
 

+ 100 - 0
cli/command/plugin/upgrade.go

@@ -0,0 +1,100 @@
+package plugin
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/pkg/jsonmessage"
+	"github.com/docker/docker/reference"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command {
+	var options pluginOptions
+	cmd := &cobra.Command{
+		Use:   "upgrade [OPTIONS] PLUGIN [REMOTE]",
+		Short: "Upgrade an existing plugin",
+		Args:  cli.RequiresRangeArgs(1, 2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			options.localName = args[0]
+			if len(args) == 2 {
+				options.remote = args[1]
+			}
+			return runUpgrade(dockerCli, options)
+		},
+	}
+
+	flags := cmd.Flags()
+	loadPullFlags(&options, flags)
+	flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image")
+	return cmd
+}
+
+func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error {
+	ctx := context.Background()
+	p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName)
+	if err != nil {
+		return fmt.Errorf("error reading plugin data: %v", err)
+	}
+
+	if p.Enabled {
+		return fmt.Errorf("the plugin must be disabled before upgrading")
+	}
+
+	opts.localName = p.Name
+	if opts.remote == "" {
+		opts.remote = p.PluginReference
+	}
+	remote, err := reference.ParseNamed(opts.remote)
+	if err != nil {
+		return errors.Wrap(err, "error parsing remote upgrade image reference")
+	}
+	remote = reference.WithDefaultTag(remote)
+
+	old, err := reference.ParseNamed(p.PluginReference)
+	if err != nil {
+		return errors.Wrap(err, "error parsing current image reference")
+	}
+	old = reference.WithDefaultTag(old)
+
+	fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote)
+	if !opts.skipRemoteCheck && remote.String() != old.String() {
+		_, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ")
+		if err != nil {
+			return errors.Wrap(err, "error writing to stdout")
+		}
+
+		rdr := bufio.NewReader(dockerCli.In())
+		line, _, err := rdr.ReadLine()
+		if err != nil {
+			return errors.Wrap(err, "error reading from stdin")
+		}
+		if strings.ToLower(string(line)) != "y" {
+			return errors.New("canceling upgrade request")
+		}
+	}
+
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade")
+	if err != nil {
+		return err
+	}
+
+	responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options)
+	if err != nil {
+		if strings.Contains(err.Error(), "target is image") {
+			return errors.New(err.Error() + " - Use `docker image pull`")
+		}
+		return err
+	}
+	defer responseBody.Close()
+	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
+		return err
+	}
+	fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result
+	return nil
+}

+ 1 - 0
client/interface.go

@@ -113,6 +113,7 @@ type PluginAPIClient interface {
 	PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
 	PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
 	PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
+	PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
 	PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
 	PluginSet(ctx context.Context, name string, args []string) error
 	PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)

+ 38 - 30
client/plugin_install.go

@@ -20,43 +20,15 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types
 	}
 	query.Set("remote", options.RemoteRef)
 
-	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
-	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
-		// todo: do inspect before to check existing name before checking privileges
-		newAuthHeader, privilegeErr := options.PrivilegeFunc()
-		if privilegeErr != nil {
-			ensureReaderClosed(resp)
-			return nil, privilegeErr
-		}
-		options.RegistryAuth = newAuthHeader
-		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
-	}
+	privileges, err := cli.checkPluginPermissions(ctx, query, options)
 	if err != nil {
-		ensureReaderClosed(resp)
-		return nil, err
-	}
-
-	var privileges types.PluginPrivileges
-	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
-		ensureReaderClosed(resp)
 		return nil, err
 	}
-	ensureReaderClosed(resp)
-
-	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
-		accept, err := options.AcceptPermissionsFunc(privileges)
-		if err != nil {
-			return nil, err
-		}
-		if !accept {
-			return nil, pluginPermissionDenied{options.RemoteRef}
-		}
-	}
 
 	// set name for plugin pull, if empty should default to remote reference
 	query.Set("name", name)
 
-	resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
+	resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
 	if err != nil {
 		return nil, err
 	}
@@ -103,3 +75,39 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg
 	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
 	return cli.post(ctx, "/plugins/pull", query, privileges, headers)
 }
+
+func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) {
+	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
+		// todo: do inspect before to check existing name before checking privileges
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
+		if privilegeErr != nil {
+			ensureReaderClosed(resp)
+			return nil, privilegeErr
+		}
+		options.RegistryAuth = newAuthHeader
+		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
+	}
+	if err != nil {
+		ensureReaderClosed(resp)
+		return nil, err
+	}
+
+	var privileges types.PluginPrivileges
+	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
+		ensureReaderClosed(resp)
+		return nil, err
+	}
+	ensureReaderClosed(resp)
+
+	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
+		accept, err := options.AcceptPermissionsFunc(privileges)
+		if err != nil {
+			return nil, err
+		}
+		if !accept {
+			return nil, pluginPermissionDenied{options.RemoteRef}
+		}
+	}
+	return privileges, nil
+}

+ 37 - 0
client/plugin_upgrade.go

@@ -0,0 +1,37 @@
+package client
+
+import (
+	"fmt"
+	"io"
+	"net/url"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+	"github.com/pkg/errors"
+	"golang.org/x/net/context"
+)
+
+// PluginUpgrade upgrades a plugin
+func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
+	query := url.Values{}
+	if _, err := reference.ParseNamed(options.RemoteRef); err != nil {
+		return nil, errors.Wrap(err, "invalid remote reference")
+	}
+	query.Set("remote", options.RemoteRef)
+
+	privileges, err := cli.checkPluginPermissions(ctx, query, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth)
+	if err != nil {
+		return nil, err
+	}
+	return resp.body, nil
+}
+
+func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) {
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
+	return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers)
+}

+ 2 - 0
docs/extend/config.md

@@ -118,6 +118,8 @@ Config provides the base accessible fields for working with V0 plugin format
 - **`propagatedMount`** *string*
 
    path to be mounted as rshared, so that mounts under that path are visible to docker. This is useful for volume plugins.
+   This path will be bind-mounted outisde of the plugin rootfs so it's contents
+   are preserved on upgrade.
 
 - **`env`** *PluginEnv array*
 

+ 1 - 0
docs/reference/commandline/plugin_create.md

@@ -58,3 +58,4 @@ The plugin can subsequently be enabled for local use or pushed to the public reg
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 1 - 0
docs/reference/commandline/plugin_disable.md

@@ -63,3 +63,4 @@ ID                  NAME                             TAG                 DESCRIP
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 1 - 0
docs/reference/commandline/plugin_enable.md

@@ -62,3 +62,4 @@ ID                  NAME                             TAG                 DESCRIP
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 2 - 0
docs/reference/commandline/plugin_inspect.md

@@ -37,6 +37,7 @@ $ docker plugin inspect tiborvass/sample-volume-plugin:latest
 {
   "Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21",
   "Name": "tiborvass/sample-volume-plugin:latest",
+  "PluginReference": "tiborvas/sample-volume-plugin:latest",
   "Enabled": true,
   "Config": {
     "Mounts": [
@@ -160,3 +161,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/sample-volume-plugin:latest
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 1 - 0
docs/reference/commandline/plugin_install.md

@@ -69,3 +69,4 @@ ID                  NAME                  TAG                 DESCRIPTION
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 6 - 4
docs/reference/commandline/plugin_ls.md

@@ -83,10 +83,11 @@ Valid placeholders for the Go template are listed below:
 
 Placeholder    | Description
 ---------------|------------------------------------------------------------------------------------------
-`.ID`          | Plugin ID
-`.Name`        | Plugin name
-`.Description` | Plugin description
-`.Enabled`     | Whether plugin is enabled or not
+`.ID`              | Plugin ID
+`.Name`            | Plugin name
+`.Description`     | Plugin description
+`.Enabled`         | Whether plugin is enabled or not
+`.PluginReference` | The reference used to push/pull from a registry
 
 When using the `--format` option, the `plugin ls` command will either
 output the data exactly as the template declares or, when using the
@@ -111,3 +112,4 @@ $ docker plugin ls --format "{{.ID}}: {{.Name}}"
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 1 - 0
docs/reference/commandline/plugin_push.md

@@ -48,3 +48,4 @@ $ docker plugin push user/plugin
 * [plugin ls](plugin_ls.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 1 - 0
docs/reference/commandline/plugin_rm.md

@@ -53,3 +53,4 @@ tiborvass/sample-volume-plugin
 * [plugin ls](plugin_ls.md)
 * [plugin push](plugin_push.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)

+ 84 - 0
docs/reference/commandline/plugin_upgrade.md

@@ -0,0 +1,84 @@
+---
+title: "plugin upgrade"
+description: "the plugin upgrade command description and usage"
+keywords: "plugin, upgrade"
+---
+
+<!-- This file is maintained within the docker/docker Github
+     repository at https://github.com/docker/docker/. Make all
+     pull requests against that repo. If you see this file in
+     another repository, consider it read-only there, as it will
+     periodically be overwritten by the definitive file. Pull
+     requests which include edits to this file in other repositories
+     will be rejected.
+-->
+
+# plugin upgrade
+
+```markdown
+Usage:  docker plugin upgrade [OPTIONS] PLUGIN [REMOTE]
+
+Upgrade a plugin
+
+Options:
+      --disable-content-trust   Skip image verification (default true)
+      --grant-all-permissions   Grant all permissions necessary to run the plugin
+      --help                    Print usage
+      --skip-remote-check       Do not check if specified remote plugin matches existing plugin image
+```
+
+Upgrades an existing plugin to the specified remote plugin image. If no remote
+is specified, Docker will re-pull the current image and use the updated version.
+All existing references to the plugin will continue to work.
+The plugin must be disabled before running the upgrade.
+
+The following example installs `vieus/sshfs` plugin, uses it to create and use
+a volume, then upgrades the plugin.
+
+```bash
+$ docker plugin install vieux/sshfs DEBUG=1
+
+Plugin "vieux/sshfs:next" is requesting the following privileges:
+ - network: [host]
+ - device: [/dev/fuse]
+ - capabilities: [CAP_SYS_ADMIN]
+Do you grant the above permissions? [y/N] y
+vieux/sshfs:next
+
+$ docker volume create -d vieux/sshfs:next -o sshcmd=root@1.2.3.4:/tmp/shared -o password=XXX sshvolume
+sshvolume
+$ docker run -it -v sshvolume:/data alpine sh -c "touch /data/hello"
+$ docker plugin disable -f vieux/sshfs:next
+viex/sshfs:next
+
+# Here docker volume ls doesn't show 'sshfsvolume', since the plugin is disabled
+$ docker volume ls
+DRIVER              VOLUME NAME
+
+$ docker plugin upgrade vieux/sshfs:next vieux/sshfs:next
+Plugin "vieux/sshfs:next" is requesting the following privileges:
+ - network: [host]
+ - device: [/dev/fuse]
+ - capabilities: [CAP_SYS_ADMIN]
+Do you grant the above permissions? [y/N] y
+Upgrade plugin vieux/sshfs:next to vieux/sshfs:next
+$ docker plugin enable vieux/sshfs:next
+viex/sshfs:next
+$ docker volume ls
+DRIVER              VOLUME NAME
+viuex/sshfs:next    sshvolume
+$ docker run -it -v sshvolume:/data alpine sh -c "ls /data"
+hello
+```
+
+## Related information
+
+* [plugin create](plugin_create.md)
+* [plugin disable](plugin_disable.md)
+* [plugin enable](plugin_enable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)
+* [plugin ls](plugin_ls.md)
+* [plugin push](plugin_push.md)
+* [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)

+ 32 - 0
integration-cli/docker_cli_plugins_test.go

@@ -427,3 +427,35 @@ enabled: true`, id, pNameWithTag)
 	out, _ = dockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc")
 	c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput)
 }
+
+func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
+	testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
+	plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
+	pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
+
+	dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
+	dockerCmd(c, "volume", "create", "--driver", plugin, "bananas")
+	dockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "touch /apple/core")
+
+	out, _, err := dockerCmdWithError("plugin", "upgrade", "--grant-all-permissions", plugin, pluginV2)
+	c.Assert(err, checker.NotNil, check.Commentf(out))
+	c.Assert(out, checker.Contains, "disabled before upgrading")
+
+	out, _ = dockerCmd(c, "plugin", "inspect", "--format={{.ID}}", plugin)
+	id := strings.TrimSpace(out)
+
+	// make sure "v2" does not exists
+	_, err = os.Stat(filepath.Join(testEnv.DockerBasePath(), "plugins", id, "rootfs", "v2"))
+	c.Assert(os.IsNotExist(err), checker.True, check.Commentf(out))
+
+	dockerCmd(c, "plugin", "disable", "-f", plugin)
+	dockerCmd(c, "plugin", "upgrade", "--grant-all-permissions", "--skip-remote-check", plugin, pluginV2)
+
+	// make sure "v2" file exists
+	_, err = os.Stat(filepath.Join(testEnv.DockerBasePath(), "plugins", id, "rootfs", "v2"))
+	c.Assert(err, checker.IsNil)
+
+	dockerCmd(c, "plugin", "enable", plugin)
+	dockerCmd(c, "volume", "inspect", "bananas")
+	dockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "ls -lh /apple/core")
+}

+ 102 - 2
plugin/backend_linux.go

@@ -13,6 +13,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"github.com/Sirupsen/logrus"
@@ -25,6 +26,7 @@ import (
 	"github.com/docker/docker/image"
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/pkg/chrootarchive"
+	"github.com/docker/docker/pkg/mount"
 	"github.com/docker/docker/pkg/pools"
 	"github.com/docker/docker/pkg/progress"
 	"github.com/docker/docker/plugin/v2"
@@ -215,6 +217,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
 	return computePrivileges(config)
 }
 
+// Upgrade upgrades a plugin
+func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
+	p, err := pm.config.Store.GetV2Plugin(name)
+	if err != nil {
+		return errors.Wrap(err, "plugin must be installed before upgrading")
+	}
+
+	if p.IsEnabled() {
+		return fmt.Errorf("plugin must be disabled before upgrading")
+	}
+
+	pm.muGC.RLock()
+	defer pm.muGC.RUnlock()
+
+	// revalidate because Pull is public
+	nameref, err := reference.ParseNamed(name)
+	if err != nil {
+		return errors.Wrapf(err, "failed to parse %q", name)
+	}
+	name = reference.WithDefaultTag(nameref).String()
+
+	tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
+	defer os.RemoveAll(tmpRootFSDir)
+
+	dm := &downloadManager{
+		tmpDir:    tmpRootFSDir,
+		blobStore: pm.blobStore,
+	}
+
+	pluginPullConfig := &distribution.ImagePullConfig{
+		Config: distribution.Config{
+			MetaHeaders:      metaHeader,
+			AuthConfig:       authConfig,
+			RegistryService:  pm.config.RegistryService,
+			ImageEventLogger: pm.config.LogPluginEvent,
+			ImageStore:       dm,
+		},
+		DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
+		Schema2Types:    distribution.PluginTypes,
+	}
+
+	err = pm.pull(ctx, ref, pluginPullConfig, outStream)
+	if err != nil {
+		go pm.GC()
+		return err
+	}
+
+	if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
+		return err
+	}
+	p.PluginObj.PluginReference = ref.String()
+	return nil
+}
+
 // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
 func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
 	pm.muGC.RLock()
@@ -257,9 +313,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
 		return err
 	}
 
-	if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
+	p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
+	if err != nil {
 		return err
 	}
+	p.PluginObj.PluginReference = ref.String()
 
 	return nil
 }
@@ -541,6 +599,9 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
 	id := p.GetID()
 	pm.config.Store.Remove(p)
 	pluginDir := filepath.Join(pm.config.Root, id)
+	if err := recursiveUnmount(pm.config.Root); err != nil {
+		logrus.WithField("dir", pm.config.Root).WithField("id", id).Warn(err)
+	}
 	if err := os.RemoveAll(pluginDir); err != nil {
 		logrus.Warnf("unable to remove %q from plugin remove: %v", pluginDir, err)
 	}
@@ -548,6 +609,43 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
 	return nil
 }
 
+func getMounts(root string) ([]string, error) {
+	infos, err := mount.GetMounts()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to read mount table while performing recursive unmount")
+	}
+
+	var mounts []string
+	for _, m := range infos {
+		if strings.HasPrefix(m.Mountpoint, root) {
+			mounts = append(mounts, m.Mountpoint)
+		}
+	}
+
+	return mounts, nil
+}
+
+func recursiveUnmount(root string) error {
+	mounts, err := getMounts(root)
+	if err != nil {
+		return err
+	}
+
+	// sort in reverse-lexicographic order so the root mount will always be last
+	sort.Sort(sort.Reverse(sort.StringSlice(mounts)))
+
+	for i, m := range mounts {
+		if err := mount.Unmount(m); err != nil {
+			if i == len(mounts)-1 {
+				return errors.Wrapf(err, "error performing recursive unmount on %s", root)
+			}
+			logrus.WithError(err).WithField("mountpoint", m).Warn("could not unmount")
+		}
+	}
+
+	return nil
+}
+
 // Set sets plugin args
 func (pm *Manager) Set(name string, args []string) error {
 	p, err := pm.config.Store.GetV2Plugin(name)
@@ -573,7 +671,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
 	if _, ok := ref.(reference.Canonical); ok {
 		return errors.Errorf("canonical references are not permitted")
 	}
-	name := reference.WithDefaultTag(ref).String()
+	taggedRef := reference.WithDefaultTag(ref)
+	name := taggedRef.String()
 
 	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
 		return err
@@ -655,6 +754,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
 	if err != nil {
 		return err
 	}
+	p.PluginObj.PluginReference = taggedRef.String()
 
 	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
 

+ 5 - 0
plugin/backend_unsupported.go

@@ -40,6 +40,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
 	return errNotSupported
 }
 
+// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
+func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
+	return errNotSupported
+}
+
 // List displays the list of plugins and associated metadata.
 func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
 	return nil, errNotSupported

+ 25 - 0
plugin/manager.go

@@ -145,6 +145,10 @@ func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
 			if err := mount.Unmount(p.PropagatedMount); err != nil {
 				logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
 			}
+			propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
+			if err := mount.Unmount(propRoot); err != nil {
+				logrus.Warn("Could not unmount %s: %v", propRoot, err)
+			}
 		}
 
 		if restart {
@@ -193,6 +197,27 @@ func (pm *Manager) reload() error { // todo: restore
 			for _, typ := range p.PluginObj.Config.Interface.Types {
 				if (typ.Capability == "volumedriver" || typ.Capability == "graphdriver") && typ.Prefix == "docker" && strings.HasPrefix(typ.Version, "1.") {
 					if p.PluginObj.Config.PropagatedMount != "" {
+						propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
+
+						// check if we need to migrate an older propagated mount from before
+						// these mounts were stored outside the plugin rootfs
+						if _, err := os.Stat(propRoot); os.IsNotExist(err) {
+							if _, err := os.Stat(p.PropagatedMount); err == nil {
+								// make sure nothing is mounted here
+								// don't care about errors
+								mount.Unmount(p.PropagatedMount)
+								if err := os.Rename(p.PropagatedMount, propRoot); err != nil {
+									logrus.WithError(err).WithField("dir", propRoot).Error("error migrating propagated mount storage")
+								}
+								if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {
+									logrus.WithError(err).WithField("dir", p.PropagatedMount).Error("error migrating propagated mount storage")
+								}
+							}
+						}
+
+						if err := os.MkdirAll(propRoot, 0755); err != nil {
+							logrus.Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
+						}
 						// TODO: sanitize PropagatedMount and prevent breakout
 						p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
 						if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {

+ 79 - 11
plugin/manager_linux.go

@@ -40,9 +40,20 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
 	pm.cMap[p] = c
 	pm.mu.Unlock()
 
+	var propRoot string
 	if p.PropagatedMount != "" {
-		if err := mount.MakeRShared(p.PropagatedMount); err != nil {
-			return errors.WithStack(err)
+		propRoot = filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
+
+		if err := os.MkdirAll(propRoot, 0755); err != nil {
+			logrus.Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
+		}
+
+		if err := mount.MakeRShared(propRoot); err != nil {
+			return errors.Wrap(err, "error setting up propagated mount dir")
+		}
+
+		if err := mount.Mount(propRoot, p.PropagatedMount, "none", "rbind"); err != nil {
+			return errors.Wrap(err, "error creating mount for propagated mount")
 		}
 	}
 
@@ -55,6 +66,9 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
 			if err := mount.Unmount(p.PropagatedMount); err != nil {
 				logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
 			}
+			if err := mount.Unmount(propRoot); err != nil {
+				logrus.Warnf("Could not unmount %s: %v", propRoot, err)
+			}
 		}
 		return errors.WithStack(err)
 	}
@@ -149,37 +163,91 @@ func (pm *Manager) Shutdown() {
 	}
 }
 
-// createPlugin creates a new plugin. take lock before calling.
-func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
-	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
-		return nil, err
+func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (err error) {
+	config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
+	if err != nil {
+		return err
 	}
 
+	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
+	orig := filepath.Join(pdir, "rootfs")
+	backup := orig + "-old"
+	if err := os.Rename(orig, backup); err != nil {
+		return err
+	}
+
+	defer func() {
+		if err != nil {
+			if rmErr := os.RemoveAll(orig); rmErr != nil && !os.IsNotExist(rmErr) {
+				logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up after failed upgrade")
+				return
+			}
+
+			if err := os.Rename(backup, orig); err != nil {
+				err = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
+			}
+			if rmErr := os.RemoveAll(tmpRootFSDir); rmErr != nil && !os.IsNotExist(rmErr) {
+				logrus.WithError(rmErr).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
+			}
+		} else {
+			if rmErr := os.RemoveAll(backup); rmErr != nil && !os.IsNotExist(rmErr) {
+				logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
+			}
+
+			p.Config = configDigest
+			p.Blobsums = blobsums
+		}
+	}()
+
+	if err := os.Rename(tmpRootFSDir, orig); err != nil {
+		return errors.Wrap(err, "error upgrading")
+	}
+
+	p.PluginObj.Config = config
+	err = pm.save(p)
+	return errors.Wrap(err, "error saving upgraded plugin config")
+}
+
+func (pm *Manager) setupNewPlugin(configDigest digest.Digest, blobsums []digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
 	configRC, err := pm.blobStore.Get(configDigest)
 	if err != nil {
-		return nil, err
+		return types.PluginConfig{}, err
 	}
 	defer configRC.Close()
 
 	var config types.PluginConfig
 	dec := json.NewDecoder(configRC)
 	if err := dec.Decode(&config); err != nil {
-		return nil, errors.Wrapf(err, "failed to parse config")
+		return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
 	}
 	if dec.More() {
-		return nil, errors.New("invalid config json")
+		return types.PluginConfig{}, errors.New("invalid config json")
 	}
 
 	requiredPrivileges, err := computePrivileges(config)
 	if err != nil {
-		return nil, err
+		return types.PluginConfig{}, err
 	}
 	if privileges != nil {
 		if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
-			return nil, err
+			return types.PluginConfig{}, err
 		}
 	}
 
+	return config, nil
+}
+
+// createPlugin creates a new plugin. take lock before calling.
+func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
+	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
+		return nil, err
+	}
+
+	config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
+	if err != nil {
+		return nil, err
+	}
+
 	p = &v2.Plugin{
 		PluginObj: types.Plugin{
 			Name:   name,