Browse Source

Merge pull request #29414 from cpuguy83/plugin_upgrade

Add docker plugin upgrade
Anusha Ragunathan 8 năm trước cách đây
mục cha
commit
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)
 	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
 	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
 	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
 	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.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
 		router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
 		router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
 		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)),
 		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/{name:.*}/set", r.setPlugin),
 		router.NewPostRoute("/plugins/create", r.createPlugin),
 		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)
 	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 {
 	if err := httputils.ParseForm(r); err != nil {
 		return errors.Wrap(err, "failed to parse form")
 		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)
 	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"))
 	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
 	if err != nil {
 	if err != nil {
 		return err
 		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 name == "" {
 		if _, ok := ref.(reference.Canonical); ok {
 		if _, ok := ref.(reference.Canonical); ok {
 			trimmed := reference.TrimNamed(ref)
 			trimmed := reference.TrimNamed(ref)
 			if tag != "" {
 			if tag != "" {
 				nt, err := reference.WithTag(trimmed, tag)
 				nt, err := reference.WithTag(trimmed, tag)
 				if err != nil {
 				if err != nil {
-					return err
+					return "", err
 				}
 				}
 				name = nt.String()
 				name = nt.String()
 			} else {
 			} else {
@@ -141,29 +198,17 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
 	} else {
 	} else {
 		localRef, err := reference.ParseNamed(name)
 		localRef, err := reference.ParseNamed(name)
 		if err != nil {
 		if err != nil {
-			return err
+			return "", err
 		}
 		}
 		if _, ok := localRef.(reference.Canonical); ok {
 		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) {
 		if distreference.IsNameOnly(localRef) {
 			// TODO: log change in name to out stream
 			// TODO: log change in name to out stream
 			name = reference.WithDefaultTag(localRef).String()
 			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 {
 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"
             type: "array"
             items:
             items:
               $ref: "#/definitions/PluginDevice"
               $ref: "#/definitions/PluginDevice"
+      PluginReference:
+        description: "plugin remote reference used to push/pull the plugin"
+        type: "string"
+        x-nullable: false
       Config:
       Config:
         description: "The config of a plugin."
         description: "The config of a plugin."
         type: "object"
         type: "object"

+ 3 - 0
api/types/plugin.go

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

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

@@ -85,3 +85,8 @@ func (c *pluginContext) Enabled() bool {
 	c.AddHeader(enabledHeader)
 	c.AddHeader(enabledHeader)
 	return c.p.Enabled
 	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"},
 		{ID: "pluginID2", Name: "foobar_bar"},
 	}
 	}
 	expectedJSONs := []map[string]interface{}{
 	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("")
 	out := bytes.NewBufferString("")

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

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

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

@@ -15,15 +15,22 @@ import (
 	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/registry"
 	"github.com/docker/docker/registry"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
 )
 )
 
 
 type pluginOptions struct {
 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 {
 func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
@@ -33,7 +40,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
 		Short: "Install a plugin",
 		Short: "Install a plugin",
 		Args:  cli.RequiresMinArgs(1),
 		Args:  cli.RequiresMinArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 		RunE: func(cmd *cobra.Command, args []string) error {
-			options.name = args[0]
+			options.remote = args[0]
 			if len(args) > 1 {
 			if len(args) > 1 {
 				options.args = args[1:]
 				options.args = args[1:]
 			}
 			}
@@ -42,12 +49,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
 	}
 	}
 
 
 	flags := cmd.Flags()
 	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.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
 	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
 	// 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 {
 	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)
 	repoInfo, err := registry.ParseRepositoryInfo(ref)
 	if err != nil {
 	if err != nil {
-		return err
+		return types.PluginInstallOptions{}, err
 	}
 	}
 
 
 	remote := ref.String()
 	remote := ref.String()
 
 
 	_, isCanonical := ref.(reference.Canonical)
 	_, isCanonical := ref.(reference.Canonical)
 	if command.IsTrusted() && !isCanonical {
 	if command.IsTrusted() && !isCanonical {
-		if alias == "" {
-			alias = reference.FamiliarString(ref)
-		}
-
 		nt, ok := ref.(reference.NamedTagged)
 		nt, ok := ref.(reference.NamedTagged)
 		if !ok {
 		if !ok {
 			nt = reference.EnsureTagged(ref)
 			nt = reference.EnsureTagged(ref)
 		}
 		}
 
 
+		ctx := context.Background()
 		trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
 		trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
 		if err != nil {
 		if err != nil {
-			return err
+			return types.PluginInstallOptions{}, err
 		}
 		}
 		remote = reference.FamiliarString(trusted)
 		remote = reference.FamiliarString(trusted)
 	}
 	}
@@ -134,23 +122,42 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
 
 
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
 	if err != nil {
 	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{
 	options := types.PluginInstallOptions{
 		RegistryAuth:          encodedAuth,
 		RegistryAuth:          encodedAuth,
 		RemoteRef:             remote,
 		RemoteRef:             remote,
 		Disabled:              opts.disable,
 		Disabled:              opts.disable,
 		AcceptAllPermissions:  opts.grantPerms,
 		AcceptAllPermissions:  opts.grantPerms,
-		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
+		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
 		// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
 		// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
 		PrivilegeFunc: registryAuthFunc,
 		PrivilegeFunc: registryAuthFunc,
 		Args:          opts.args,
 		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 err != nil {
 		if strings.Contains(err.Error(), "target is image") {
 		if strings.Contains(err.Error(), "target is image") {
 			return errors.New(err.Error() + " - Use `docker image pull`")
 			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 {
 	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
 		return err
 		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
 	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
 	PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
 	PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
 	PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
 	PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, 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)
 	PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
 	PluginSet(ctx context.Context, name string, args []string) error
 	PluginSet(ctx context.Context, name string, args []string) error
 	PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, 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)
 	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 {
 	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
 		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
 	// set name for plugin pull, if empty should default to remote reference
 	query.Set("name", name)
 	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 {
 	if err != nil {
 		return nil, err
 		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}}
 	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
 	return cli.post(ctx, "/plugins/pull", query, privileges, headers)
 	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*
 - **`propagatedMount`** *string*
 
 
    path to be mounted as rshared, so that mounts under that path are visible to docker. This is useful for volume plugins.
    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*
 - **`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 push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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 push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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 push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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",
   "Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21",
   "Name": "tiborvass/sample-volume-plugin:latest",
   "Name": "tiborvass/sample-volume-plugin:latest",
+  "PluginReference": "tiborvas/sample-volume-plugin:latest",
   "Enabled": true,
   "Enabled": true,
   "Config": {
   "Config": {
     "Mounts": [
     "Mounts": [
@@ -160,3 +161,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/sample-volume-plugin:latest
 * [plugin push](plugin_push.md)
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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 push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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
 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
 When using the `--format` option, the `plugin ls` command will either
 output the data exactly as the template declares or, when using the
 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 push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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 ls](plugin_ls.md)
 * [plugin rm](plugin_rm.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.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 ls](plugin_ls.md)
 * [plugin push](plugin_push.md)
 * [plugin push](plugin_push.md)
 * [plugin set](plugin_set.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")
 	out, _ = dockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc")
 	c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput)
 	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"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"sort"
 	"strings"
 	"strings"
 
 
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
@@ -25,6 +26,7 @@ import (
 	"github.com/docker/docker/image"
 	"github.com/docker/docker/image"
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/pkg/chrootarchive"
 	"github.com/docker/docker/pkg/chrootarchive"
+	"github.com/docker/docker/pkg/mount"
 	"github.com/docker/docker/pkg/pools"
 	"github.com/docker/docker/pkg/pools"
 	"github.com/docker/docker/pkg/progress"
 	"github.com/docker/docker/pkg/progress"
 	"github.com/docker/docker/plugin/v2"
 	"github.com/docker/docker/plugin/v2"
@@ -215,6 +217,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
 	return computePrivileges(config)
 	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.
 // 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) {
 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()
 	pm.muGC.RLock()
@@ -257,9 +313,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
 		return err
 		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
 		return err
 	}
 	}
+	p.PluginObj.PluginReference = ref.String()
 
 
 	return nil
 	return nil
 }
 }
@@ -541,6 +599,9 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
 	id := p.GetID()
 	id := p.GetID()
 	pm.config.Store.Remove(p)
 	pm.config.Store.Remove(p)
 	pluginDir := filepath.Join(pm.config.Root, id)
 	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 {
 	if err := os.RemoveAll(pluginDir); err != nil {
 		logrus.Warnf("unable to remove %q from plugin remove: %v", pluginDir, err)
 		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
 	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
 // Set sets plugin args
 func (pm *Manager) Set(name string, args []string) error {
 func (pm *Manager) Set(name string, args []string) error {
 	p, err := pm.config.Store.GetV2Plugin(name)
 	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 {
 	if _, ok := ref.(reference.Canonical); ok {
 		return errors.Errorf("canonical references are not permitted")
 		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()
 	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
 		return err
 		return err
@@ -655,6 +754,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	p.PluginObj.PluginReference = taggedRef.String()
 
 
 	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
 	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
 	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.
 // List displays the list of plugins and associated metadata.
 func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
 func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
 	return nil, errNotSupported
 	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 {
 			if err := mount.Unmount(p.PropagatedMount); err != nil {
 				logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
 				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 {
 		if restart {
@@ -193,6 +197,27 @@ func (pm *Manager) reload() error { // todo: restore
 			for _, typ := range p.PluginObj.Config.Interface.Types {
 			for _, typ := range p.PluginObj.Config.Interface.Types {
 				if (typ.Capability == "volumedriver" || typ.Capability == "graphdriver") && typ.Prefix == "docker" && strings.HasPrefix(typ.Version, "1.") {
 				if (typ.Capability == "volumedriver" || typ.Capability == "graphdriver") && typ.Prefix == "docker" && strings.HasPrefix(typ.Version, "1.") {
 					if p.PluginObj.Config.PropagatedMount != "" {
 					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
 						// TODO: sanitize PropagatedMount and prevent breakout
 						p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
 						p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
 						if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {
 						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.cMap[p] = c
 	pm.mu.Unlock()
 	pm.mu.Unlock()
 
 
+	var propRoot string
 	if p.PropagatedMount != "" {
 	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 {
 			if err := mount.Unmount(p.PropagatedMount); err != nil {
 				logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
 				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)
 		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)
 	configRC, err := pm.blobStore.Get(configDigest)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return types.PluginConfig{}, err
 	}
 	}
 	defer configRC.Close()
 	defer configRC.Close()
 
 
 	var config types.PluginConfig
 	var config types.PluginConfig
 	dec := json.NewDecoder(configRC)
 	dec := json.NewDecoder(configRC)
 	if err := dec.Decode(&config); err != nil {
 	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() {
 	if dec.More() {
-		return nil, errors.New("invalid config json")
+		return types.PluginConfig{}, errors.New("invalid config json")
 	}
 	}
 
 
 	requiredPrivileges, err := computePrivileges(config)
 	requiredPrivileges, err := computePrivileges(config)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return types.PluginConfig{}, err
 	}
 	}
 	if privileges != nil {
 	if privileges != nil {
 		if err := validatePrivileges(requiredPrivileges, *privileges); err != 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{
 	p = &v2.Plugin{
 		PluginObj: types.Plugin{
 		PluginObj: types.Plugin{
 			Name:   name,
 			Name:   name,