Browse code

Add docker plugin upgrade

This allows a plugin to be upgraded without requiring to
uninstall/reinstall a plugin.
Since plugin resources (e.g. volumes) are tied to a plugin ID, this is
important to ensure resources aren't lost.

The plugin must be disabled while upgrading (errors out if enabled).
This does not add any convenience flags for automatically
disabling/re-enabling the plugin during before/after upgrade.

Since an upgrade may change requested permissions, the user is required
to accept permissions just like `docker plugin install`.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 03c694973968f63743ed53cef83d0b7455695081)
Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2017/01/29 09:54:32
Showing 24 changed files
... ...
@@ -20,5 +20,6 @@ type Backend interface {
20 20
 	Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
21 21
 	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
22 22
 	Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
23
+	Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
23 24
 	CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
24 25
 }
... ...
@@ -32,6 +32,7 @@ func (r *pluginRouter) initRoutes() {
32 32
 		router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
33 33
 		router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
34 34
 		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)),
35
+		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/upgrade", r.upgradePlugin)),
35 36
 		router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
36 37
 		router.NewPostRoute("/plugins/create", r.createPlugin),
37 38
 	}
... ...
@@ -100,7 +100,7 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
100 100
 	return httputils.WriteJSON(w, http.StatusOK, privileges)
101 101
 }
102 102
 
103
-func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
103
+func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
104 104
 	if err := httputils.ParseForm(r); err != nil {
105 105
 		return errors.Wrap(err, "failed to parse form")
106 106
 	}
... ...
@@ -115,20 +115,77 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
115 115
 	}
116 116
 
117 117
 	metaHeaders, authConfig := parseHeaders(r.Header)
118
+	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
119
+	if err != nil {
120
+		return err
121
+	}
122
+
123
+	name, err := getName(ref, tag, vars["name"])
124
+	if err != nil {
125
+		return err
126
+	}
127
+	w.Header().Set("Docker-Plugin-Name", name)
118 128
 
129
+	w.Header().Set("Content-Type", "application/json")
130
+	output := ioutils.NewWriteFlusher(w)
131
+
132
+	if err := pr.backend.Upgrade(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
133
+		if !output.Flushed() {
134
+			return err
135
+		}
136
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
137
+	}
138
+
139
+	return nil
140
+}
141
+
142
+func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
143
+	if err := httputils.ParseForm(r); err != nil {
144
+		return errors.Wrap(err, "failed to parse form")
145
+	}
146
+
147
+	var privileges types.PluginPrivileges
148
+	dec := json.NewDecoder(r.Body)
149
+	if err := dec.Decode(&privileges); err != nil {
150
+		return errors.Wrap(err, "failed to parse privileges")
151
+	}
152
+	if dec.More() {
153
+		return errors.New("invalid privileges")
154
+	}
155
+
156
+	metaHeaders, authConfig := parseHeaders(r.Header)
119 157
 	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
120 158
 	if err != nil {
121 159
 		return err
122 160
 	}
123 161
 
124
-	name := r.FormValue("name")
162
+	name, err := getName(ref, tag, r.FormValue("name"))
163
+	if err != nil {
164
+		return err
165
+	}
166
+	w.Header().Set("Docker-Plugin-Name", name)
167
+
168
+	w.Header().Set("Content-Type", "application/json")
169
+	output := ioutils.NewWriteFlusher(w)
170
+
171
+	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
172
+		if !output.Flushed() {
173
+			return err
174
+		}
175
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
176
+	}
177
+
178
+	return nil
179
+}
180
+
181
+func getName(ref reference.Named, tag, name string) (string, error) {
125 182
 	if name == "" {
126 183
 		if _, ok := ref.(reference.Canonical); ok {
127 184
 			trimmed := reference.TrimNamed(ref)
128 185
 			if tag != "" {
129 186
 				nt, err := reference.WithTag(trimmed, tag)
130 187
 				if err != nil {
131
-					return err
188
+					return "", err
132 189
 				}
133 190
 				name = nt.String()
134 191
 			} else {
... ...
@@ -140,29 +197,17 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
140 140
 	} else {
141 141
 		localRef, err := reference.ParseNamed(name)
142 142
 		if err != nil {
143
-			return err
143
+			return "", err
144 144
 		}
145 145
 		if _, ok := localRef.(reference.Canonical); ok {
146
-			return errors.New("cannot use digest in plugin tag")
146
+			return "", errors.New("cannot use digest in plugin tag")
147 147
 		}
148 148
 		if distreference.IsNameOnly(localRef) {
149 149
 			// TODO: log change in name to out stream
150 150
 			name = reference.WithDefaultTag(localRef).String()
151 151
 		}
152 152
 	}
153
-	w.Header().Set("Docker-Plugin-Name", name)
154
-
155
-	w.Header().Set("Content-Type", "application/json")
156
-	output := ioutils.NewWriteFlusher(w)
157
-
158
-	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
159
-		if !output.Flushed() {
160
-			return err
161
-		}
162
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
163
-	}
164
-
165
-	return nil
153
+	return name, nil
166 154
 }
167 155
 
168 156
 func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
... ...
@@ -1379,6 +1379,10 @@ definitions:
1379 1379
             type: "array"
1380 1380
             items:
1381 1381
               $ref: "#/definitions/PluginDevice"
1382
+      PluginReference:
1383
+        description: "plugin remote reference used to push/pull the plugin"
1384
+        type: "string"
1385
+        x-nullable: false
1382 1386
       Config:
1383 1387
         description: "The config of a plugin."
1384 1388
         type: "object"
... ...
@@ -22,6 +22,9 @@ type Plugin struct {
22 22
 	// Required: true
23 23
 	Name string `json:"Name"`
24 24
 
25
+	// plugin remote reference used to push/pull the plugin
26
+	PluginReference string `json:"PluginReference,omitempty"`
27
+
25 28
 	// settings
26 29
 	// Required: true
27 30
 	Settings PluginSettings `json:"Settings"`
... ...
@@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
25 25
 		newSetCommand(dockerCli),
26 26
 		newPushCommand(dockerCli),
27 27
 		newCreateCommand(dockerCli),
28
+		newUpgradeCommand(dockerCli),
28 29
 	)
29 30
 	return cmd
30 31
 }
... ...
@@ -16,15 +16,22 @@ import (
16 16
 	"github.com/docker/docker/reference"
17 17
 	"github.com/docker/docker/registry"
18 18
 	"github.com/spf13/cobra"
19
+	"github.com/spf13/pflag"
19 20
 	"golang.org/x/net/context"
20 21
 )
21 22
 
22 23
 type pluginOptions struct {
23
-	name       string
24
-	alias      string
25
-	grantPerms bool
26
-	disable    bool
27
-	args       []string
24
+	remote          string
25
+	localName       string
26
+	grantPerms      bool
27
+	disable         bool
28
+	args            []string
29
+	skipRemoteCheck bool
30
+}
31
+
32
+func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) {
33
+	flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
34
+	command.AddTrustedFlags(flags, true)
28 35
 }
29 36
 
30 37
 func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
... ...
@@ -34,7 +41,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
34 34
 		Short: "Install a plugin",
35 35
 		Args:  cli.RequiresMinArgs(1),
36 36
 		RunE: func(cmd *cobra.Command, args []string) error {
37
-			options.name = args[0]
37
+			options.remote = args[0]
38 38
 			if len(args) > 1 {
39 39
 				options.args = args[1:]
40 40
 			}
... ...
@@ -43,12 +50,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
43 43
 	}
44 44
 
45 45
 	flags := cmd.Flags()
46
-	flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
46
+	loadPullFlags(&options, flags)
47 47
 	flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
48
-	flags.StringVar(&options.alias, "alias", "", "Local name for plugin")
49
-
50
-	command.AddTrustedFlags(flags, true)
51
-
48
+	flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
52 49
 	return cmd
53 50
 }
54 51
 
... ...
@@ -84,60 +88,48 @@ func newRegistryService() registry.Service {
84 84
 	}
85 85
 }
86 86
 
87
-func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
87
+func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
88 88
 	// Parse name using distribution reference package to support name
89 89
 	// containing both tag and digest. Names with both tag and digest
90 90
 	// will be treated by the daemon as a pull by digest with
91 91
 	// an alias for the tag (if no alias is provided).
92
-	ref, err := distreference.ParseNamed(opts.name)
92
+	ref, err := distreference.ParseNamed(opts.remote)
93 93
 	if err != nil {
94
-		return err
94
+		return types.PluginInstallOptions{}, err
95 95
 	}
96 96
 
97
-	alias := ""
98
-	if opts.alias != "" {
99
-		aref, err := reference.ParseNamed(opts.alias)
100
-		if err != nil {
101
-			return err
102
-		}
103
-		aref = reference.WithDefaultTag(aref)
104
-		if _, ok := aref.(reference.NamedTagged); !ok {
105
-			return fmt.Errorf("invalid name: %s", opts.alias)
106
-		}
107
-		alias = aref.String()
108
-	}
109
-	ctx := context.Background()
110
-
111 97
 	index, err := getRepoIndexFromUnnormalizedRef(ref)
112 98
 	if err != nil {
113
-		return err
99
+		return types.PluginInstallOptions{}, err
114 100
 	}
115 101
 
102
+	repoInfoIndex, err := getRepoIndexFromUnnormalizedRef(ref)
103
+	if err != nil {
104
+		return types.PluginInstallOptions{}, err
105
+	}
116 106
 	remote := ref.String()
117 107
 
118 108
 	_, isCanonical := ref.(distreference.Canonical)
119 109
 	if command.IsTrusted() && !isCanonical {
120
-		if alias == "" {
121
-			alias = ref.String()
122
-		}
123 110
 		var nt reference.NamedTagged
124 111
 		named, err := reference.ParseNamed(ref.Name())
125 112
 		if err != nil {
126
-			return err
113
+			return types.PluginInstallOptions{}, err
127 114
 		}
128 115
 		if tagged, ok := ref.(distreference.Tagged); ok {
129 116
 			nt, err = reference.WithTag(named, tagged.Tag())
130 117
 			if err != nil {
131
-				return err
118
+				return types.PluginInstallOptions{}, err
132 119
 			}
133 120
 		} else {
134 121
 			named = reference.WithDefaultTag(named)
135 122
 			nt = named.(reference.NamedTagged)
136 123
 		}
137 124
 
125
+		ctx := context.Background()
138 126
 		trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
139 127
 		if err != nil {
140
-			return err
128
+			return types.PluginInstallOptions{}, err
141 129
 		}
142 130
 		remote = trusted.String()
143 131
 	}
... ...
@@ -146,23 +138,44 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
146 146
 
147 147
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
148 148
 	if err != nil {
149
-		return err
149
+		return types.PluginInstallOptions{}, err
150 150
 	}
151 151
 
152
-	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, index, "plugin install")
152
+	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfoIndex, cmdName)
153 153
 
154 154
 	options := types.PluginInstallOptions{
155 155
 		RegistryAuth:          encodedAuth,
156 156
 		RemoteRef:             remote,
157 157
 		Disabled:              opts.disable,
158 158
 		AcceptAllPermissions:  opts.grantPerms,
159
-		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
159
+		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
160 160
 		// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
161 161
 		PrivilegeFunc: registryAuthFunc,
162 162
 		Args:          opts.args,
163 163
 	}
164
+	return options, nil
165
+}
164 166
 
165
-	responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options)
167
+func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
168
+	var localName string
169
+	if opts.localName != "" {
170
+		aref, err := reference.ParseNamed(opts.localName)
171
+		if err != nil {
172
+			return err
173
+		}
174
+		aref = reference.WithDefaultTag(aref)
175
+		if _, ok := aref.(reference.NamedTagged); !ok {
176
+			return fmt.Errorf("invalid name: %s", opts.localName)
177
+		}
178
+		localName = aref.String()
179
+	}
180
+
181
+	ctx := context.Background()
182
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install")
183
+	if err != nil {
184
+		return err
185
+	}
186
+	responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
166 187
 	if err != nil {
167 188
 		if strings.Contains(err.Error(), "target is image") {
168 189
 			return errors.New(err.Error() + " - Use `docker image pull`")
... ...
@@ -173,7 +186,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
173 173
 	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
174 174
 		return err
175 175
 	}
176
-	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result
176
+	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result
177 177
 	return nil
178 178
 }
179 179
 
180 180
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package plugin
1
+
2
+import (
3
+	"bufio"
4
+	"context"
5
+	"fmt"
6
+	"strings"
7
+
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/docker/cli/command"
10
+	"github.com/docker/docker/pkg/jsonmessage"
11
+	"github.com/docker/docker/reference"
12
+	"github.com/pkg/errors"
13
+	"github.com/spf13/cobra"
14
+)
15
+
16
+func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command {
17
+	var options pluginOptions
18
+	cmd := &cobra.Command{
19
+		Use:   "upgrade [OPTIONS] PLUGIN [REMOTE]",
20
+		Short: "Upgrade an existing plugin",
21
+		Args:  cli.RequiresRangeArgs(1, 2),
22
+		RunE: func(cmd *cobra.Command, args []string) error {
23
+			options.localName = args[0]
24
+			if len(args) == 2 {
25
+				options.remote = args[1]
26
+			}
27
+			return runUpgrade(dockerCli, options)
28
+		},
29
+	}
30
+
31
+	flags := cmd.Flags()
32
+	loadPullFlags(&options, flags)
33
+	flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image")
34
+	return cmd
35
+}
36
+
37
+func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error {
38
+	ctx := context.Background()
39
+	p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName)
40
+	if err != nil {
41
+		return fmt.Errorf("error reading plugin data: %v", err)
42
+	}
43
+
44
+	if p.Enabled {
45
+		return fmt.Errorf("the plugin must be disabled before upgrading")
46
+	}
47
+
48
+	opts.localName = p.Name
49
+	if opts.remote == "" {
50
+		opts.remote = p.PluginReference
51
+	}
52
+	remote, err := reference.ParseNamed(opts.remote)
53
+	if err != nil {
54
+		return errors.Wrap(err, "error parsing remote upgrade image reference")
55
+	}
56
+	remote = reference.WithDefaultTag(remote)
57
+
58
+	old, err := reference.ParseNamed(p.PluginReference)
59
+	if err != nil {
60
+		return errors.Wrap(err, "error parsing current image reference")
61
+	}
62
+	old = reference.WithDefaultTag(old)
63
+
64
+	fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote)
65
+	if !opts.skipRemoteCheck && remote.String() != old.String() {
66
+		_, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ")
67
+		if err != nil {
68
+			return errors.Wrap(err, "error writing to stdout")
69
+		}
70
+
71
+		rdr := bufio.NewReader(dockerCli.In())
72
+		line, _, err := rdr.ReadLine()
73
+		if err != nil {
74
+			return errors.Wrap(err, "error reading from stdin")
75
+		}
76
+		if strings.ToLower(string(line)) != "y" {
77
+			return errors.New("canceling upgrade request")
78
+		}
79
+	}
80
+
81
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade")
82
+	if err != nil {
83
+		return err
84
+	}
85
+
86
+	responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options)
87
+	if err != nil {
88
+		if strings.Contains(err.Error(), "target is image") {
89
+			return errors.New(err.Error() + " - Use `docker image pull`")
90
+		}
91
+		return err
92
+	}
93
+	defer responseBody.Close()
94
+	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
95
+		return err
96
+	}
97
+	fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result
98
+	return nil
99
+}
... ...
@@ -112,6 +112,7 @@ type PluginAPIClient interface {
112 112
 	PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
113 113
 	PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
114 114
 	PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
115
+	PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
115 116
 	PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
116 117
 	PluginSet(ctx context.Context, name string, args []string) error
117 118
 	PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)
... ...
@@ -20,43 +20,15 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types
20 20
 	}
21 21
 	query.Set("remote", options.RemoteRef)
22 22
 
23
-	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
24
-	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
25
-		// todo: do inspect before to check existing name before checking privileges
26
-		newAuthHeader, privilegeErr := options.PrivilegeFunc()
27
-		if privilegeErr != nil {
28
-			ensureReaderClosed(resp)
29
-			return nil, privilegeErr
30
-		}
31
-		options.RegistryAuth = newAuthHeader
32
-		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
33
-	}
23
+	privileges, err := cli.checkPluginPermissions(ctx, query, options)
34 24
 	if err != nil {
35
-		ensureReaderClosed(resp)
36
-		return nil, err
37
-	}
38
-
39
-	var privileges types.PluginPrivileges
40
-	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
41
-		ensureReaderClosed(resp)
42 25
 		return nil, err
43 26
 	}
44
-	ensureReaderClosed(resp)
45
-
46
-	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
47
-		accept, err := options.AcceptPermissionsFunc(privileges)
48
-		if err != nil {
49
-			return nil, err
50
-		}
51
-		if !accept {
52
-			return nil, pluginPermissionDenied{options.RemoteRef}
53
-		}
54
-	}
55 27
 
56 28
 	// set name for plugin pull, if empty should default to remote reference
57 29
 	query.Set("name", name)
58 30
 
59
-	resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
31
+	resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
60 32
 	if err != nil {
61 33
 		return nil, err
62 34
 	}
... ...
@@ -103,3 +75,39 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg
103 103
 	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
104 104
 	return cli.post(ctx, "/plugins/pull", query, privileges, headers)
105 105
 }
106
+
107
+func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) {
108
+	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
109
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
110
+		// todo: do inspect before to check existing name before checking privileges
111
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
112
+		if privilegeErr != nil {
113
+			ensureReaderClosed(resp)
114
+			return nil, privilegeErr
115
+		}
116
+		options.RegistryAuth = newAuthHeader
117
+		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
118
+	}
119
+	if err != nil {
120
+		ensureReaderClosed(resp)
121
+		return nil, err
122
+	}
123
+
124
+	var privileges types.PluginPrivileges
125
+	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
126
+		ensureReaderClosed(resp)
127
+		return nil, err
128
+	}
129
+	ensureReaderClosed(resp)
130
+
131
+	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
132
+		accept, err := options.AcceptPermissionsFunc(privileges)
133
+		if err != nil {
134
+			return nil, err
135
+		}
136
+		if !accept {
137
+			return nil, pluginPermissionDenied{options.RemoteRef}
138
+		}
139
+	}
140
+	return privileges, nil
141
+}
106 142
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+package client
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"net/url"
6
+
7
+	"github.com/docker/distribution/reference"
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/pkg/errors"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// PluginUpgrade upgrades a plugin
14
+func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
15
+	query := url.Values{}
16
+	if _, err := reference.ParseNamed(options.RemoteRef); err != nil {
17
+		return nil, errors.Wrap(err, "invalid remote reference")
18
+	}
19
+	query.Set("remote", options.RemoteRef)
20
+
21
+	privileges, err := cli.checkPluginPermissions(ctx, query, options)
22
+	if err != nil {
23
+		return nil, err
24
+	}
25
+
26
+	resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth)
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+	return resp.body, nil
31
+}
32
+
33
+func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) {
34
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
35
+	return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers)
36
+}
... ...
@@ -57,3 +57,4 @@ The plugin can subsequently be enabled for local use or pushed to the public reg
57 57
 * [plugin push](plugin_push.md)
58 58
 * [plugin rm](plugin_rm.md)
59 59
 * [plugin set](plugin_set.md)
60
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -63,3 +63,4 @@ ID                  NAME                             TAG                 DESCRIP
63 63
 * [plugin push](plugin_push.md)
64 64
 * [plugin rm](plugin_rm.md)
65 65
 * [plugin set](plugin_set.md)
66
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -62,3 +62,4 @@ ID                  NAME                             TAG                 DESCRIP
62 62
 * [plugin push](plugin_push.md)
63 63
 * [plugin rm](plugin_rm.md)
64 64
 * [plugin set](plugin_set.md)
65
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -37,6 +37,7 @@ $ docker plugin inspect tiborvass/sample-volume-plugin:latest
37 37
 {
38 38
   "Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21",
39 39
   "Name": "tiborvass/sample-volume-plugin:latest",
40
+  "PluginReference": "tiborvas/sample-volume-plugin:latest",
40 41
   "Enabled": true,
41 42
   "Config": {
42 43
     "Mounts": [
... ...
@@ -160,3 +161,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/sample-volume-plugin:latest
160 160
 * [plugin push](plugin_push.md)
161 161
 * [plugin rm](plugin_rm.md)
162 162
 * [plugin set](plugin_set.md)
163
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -68,3 +68,4 @@ ID                  NAME                  TAG                 DESCRIPTION
68 68
 * [plugin push](plugin_push.md)
69 69
 * [plugin rm](plugin_rm.md)
70 70
 * [plugin set](plugin_set.md)
71
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -50,3 +50,4 @@ ID                  NAME                             TAG                 DESCRIP
50 50
 * [plugin push](plugin_push.md)
51 51
 * [plugin rm](plugin_rm.md)
52 52
 * [plugin set](plugin_set.md)
53
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -47,3 +47,4 @@ $ docker plugin push user/plugin
47 47
 * [plugin ls](plugin_ls.md)
48 48
 * [plugin rm](plugin_rm.md)
49 49
 * [plugin set](plugin_set.md)
50
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -53,3 +53,4 @@ tiborvass/sample-volume-plugin
53 53
 * [plugin ls](plugin_ls.md)
54 54
 * [plugin push](plugin_push.md)
55 55
 * [plugin set](plugin_set.md)
56
+* [plugin upgrade](plugin_upgrade.md)
56 57
new file mode 100644
... ...
@@ -0,0 +1,84 @@
0
+---
1
+title: "plugin upgrade"
2
+description: "the plugin upgrade command description and usage"
3
+keywords: "plugin, upgrade"
4
+---
5
+
6
+<!-- This file is maintained within the docker/docker Github
7
+     repository at https://github.com/docker/docker/. Make all
8
+     pull requests against that repo. If you see this file in
9
+     another repository, consider it read-only there, as it will
10
+     periodically be overwritten by the definitive file. Pull
11
+     requests which include edits to this file in other repositories
12
+     will be rejected.
13
+-->
14
+
15
+# plugin upgrade
16
+
17
+```markdown
18
+Usage:  docker plugin upgrade [OPTIONS] PLUGIN [REMOTE]
19
+
20
+Upgrade a plugin
21
+
22
+Options:
23
+      --disable-content-trust   Skip image verification (default true)
24
+      --grant-all-permissions   Grant all permissions necessary to run the plugin
25
+      --help                    Print usage
26
+      --skip-remote-check       Do not check if specified remote plugin matches existing plugin image
27
+```
28
+
29
+Upgrades an existing plugin to the specified remote plugin image. If no remote
30
+is specified, Docker will re-pull the current image and use the updated version.
31
+All existing references to the plugin will continue to work.
32
+The plugin must be disabled before running the upgrade.
33
+
34
+The following example installs `vieus/sshfs` plugin, uses it to create and use
35
+a volume, then upgrades the plugin.
36
+
37
+```bash
38
+$ docker plugin install vieux/sshfs DEBUG=1
39
+
40
+Plugin "vieux/sshfs:next" is requesting the following privileges:
41
+ - network: [host]
42
+ - device: [/dev/fuse]
43
+ - capabilities: [CAP_SYS_ADMIN]
44
+Do you grant the above permissions? [y/N] y
45
+vieux/sshfs:next
46
+
47
+$ docker volume create -d vieux/sshfs:next -o sshcmd=root@1.2.3.4:/tmp/shared -o password=XXX sshvolume
48
+sshvolume
49
+$ docker run -it -v sshvolume:/data alpine sh -c "touch /data/hello"
50
+$ docker plugin disable -f vieux/sshfs:next
51
+viex/sshfs:next
52
+
53
+# Here docker volume ls doesn't show 'sshfsvolume', since the plugin is disabled
54
+$ docker volume ls
55
+DRIVER              VOLUME NAME
56
+
57
+$ docker plugin upgrade vieux/sshfs:next vieux/sshfs:next
58
+Plugin "vieux/sshfs:next" is requesting the following privileges:
59
+ - network: [host]
60
+ - device: [/dev/fuse]
61
+ - capabilities: [CAP_SYS_ADMIN]
62
+Do you grant the above permissions? [y/N] y
63
+Upgrade plugin vieux/sshfs:next to vieux/sshfs:next
64
+$ docker plugin enable vieux/sshfs:next
65
+viex/sshfs:next
66
+$ docker volume ls
67
+DRIVER              VOLUME NAME
68
+viuex/sshfs:next    sshvolume
69
+$ docker run -it -v sshvolume:/data alpine sh -c "ls /data"
70
+hello
71
+```
72
+
73
+## Related information
74
+
75
+* [plugin create](plugin_create.md)
76
+* [plugin disable](plugin_disable.md)
77
+* [plugin enable](plugin_enable.md)
78
+* [plugin inspect](plugin_inspect.md)
79
+* [plugin install](plugin_install.md)
80
+* [plugin ls](plugin_ls.md)
81
+* [plugin push](plugin_push.md)
82
+* [plugin rm](plugin_rm.md)
83
+* [plugin set](plugin_set.md)
... ...
@@ -359,3 +359,30 @@ func (s *DockerTrustSuite) TestPluginUntrustedInstall(c *check.C) {
359 359
 	c.Assert(err, check.NotNil, check.Commentf(out))
360 360
 	c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out))
361 361
 }
362
+
363
+func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
364
+	testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
365
+	plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
366
+	pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
367
+
368
+	dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
369
+	out, _, err := dockerCmdWithError("plugin", "upgrade", "--grant-all-permissions", plugin, pluginV2)
370
+	c.Assert(err, checker.NotNil, check.Commentf(out))
371
+	c.Assert(out, checker.Contains, "disabled before upgrading")
372
+
373
+	out, _ = dockerCmd(c, "plugin", "inspect", "--format={{.ID}}", plugin)
374
+	id := strings.TrimSpace(out)
375
+
376
+	// make sure "v2" does not exists
377
+	_, err = os.Stat(filepath.Join(dockerBasePath, "plugins", id, "rootfs", "v2"))
378
+	c.Assert(os.IsNotExist(err), checker.True, check.Commentf(out))
379
+
380
+	dockerCmd(c, "plugin", "disable", plugin)
381
+	dockerCmd(c, "plugin", "upgrade", "--grant-all-permissions", "--skip-remote-check", plugin, pluginV2)
382
+
383
+	// make sure "v2" file exists
384
+	_, err = os.Stat(filepath.Join(dockerBasePath, "plugins", id, "rootfs", "v2"))
385
+	c.Assert(err, checker.IsNil)
386
+
387
+	dockerCmd(c, "plugin", "enable", plugin)
388
+}
... ...
@@ -209,6 +209,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
209 209
 	return computePrivileges(config)
210 210
 }
211 211
 
212
+// Upgrade upgrades a plugin
213
+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) {
214
+	p, err := pm.config.Store.GetV2Plugin(name)
215
+	if err != nil {
216
+		return errors.Wrap(err, "plugin must be installed before upgrading")
217
+	}
218
+
219
+	if p.IsEnabled() {
220
+		return fmt.Errorf("plugin must be disabled before upgrading")
221
+	}
222
+
223
+	pm.muGC.RLock()
224
+	defer pm.muGC.RUnlock()
225
+
226
+	// revalidate because Pull is public
227
+	nameref, err := reference.ParseNamed(name)
228
+	if err != nil {
229
+		return errors.Wrapf(err, "failed to parse %q", name)
230
+	}
231
+	name = reference.WithDefaultTag(nameref).String()
232
+
233
+	tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
234
+	defer os.RemoveAll(tmpRootFSDir)
235
+
236
+	dm := &downloadManager{
237
+		tmpDir:    tmpRootFSDir,
238
+		blobStore: pm.blobStore,
239
+	}
240
+
241
+	pluginPullConfig := &distribution.ImagePullConfig{
242
+		Config: distribution.Config{
243
+			MetaHeaders:      metaHeader,
244
+			AuthConfig:       authConfig,
245
+			RegistryService:  pm.config.RegistryService,
246
+			ImageEventLogger: pm.config.LogPluginEvent,
247
+			ImageStore:       dm,
248
+		},
249
+		DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
250
+		Schema2Types:    distribution.PluginTypes,
251
+	}
252
+
253
+	err = pm.pull(ctx, ref, pluginPullConfig, outStream)
254
+	if err != nil {
255
+		go pm.GC()
256
+		return err
257
+	}
258
+
259
+	if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
260
+		return err
261
+	}
262
+	p.PluginObj.PluginReference = ref.String()
263
+	return nil
264
+}
265
+
212 266
 // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
213 267
 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) {
214 268
 	pm.muGC.RLock()
... ...
@@ -251,9 +305,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
251 251
 		return err
252 252
 	}
253 253
 
254
-	if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
254
+	p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
255
+	if err != nil {
255 256
 		return err
256 257
 	}
258
+	p.PluginObj.PluginReference = ref.String()
257 259
 
258 260
 	return nil
259 261
 }
... ...
@@ -536,7 +592,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
536 536
 	if _, ok := ref.(reference.Canonical); ok {
537 537
 		return errors.Errorf("canonical references are not permitted")
538 538
 	}
539
-	name := reference.WithDefaultTag(ref).String()
539
+	taggedRef := reference.WithDefaultTag(ref)
540
+	name := taggedRef.String()
540 541
 
541 542
 	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
542 543
 		return err
... ...
@@ -618,6 +675,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
618 618
 	if err != nil {
619 619
 		return err
620 620
 	}
621
+	p.PluginObj.PluginReference = taggedRef.String()
621 622
 
622 623
 	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
623 624
 
... ...
@@ -39,6 +39,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
39 39
 	return errNotSupported
40 40
 }
41 41
 
42
+// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
43
+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 {
44
+	return errNotSupported
45
+}
46
+
42 47
 // List displays the list of plugins and associated metadata.
43 48
 func (pm *Manager) List() ([]types.Plugin, error) {
44 49
 	return nil, errNotSupported
... ...
@@ -149,37 +149,91 @@ func (pm *Manager) Shutdown() {
149 149
 	}
150 150
 }
151 151
 
152
-// createPlugin creates a new plugin. take lock before calling.
153
-func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
154
-	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
155
-		return nil, err
152
+func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (err error) {
153
+	config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
154
+	if err != nil {
155
+		return err
156
+	}
157
+
158
+	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
159
+	orig := filepath.Join(pdir, "rootfs")
160
+	backup := orig + "-old"
161
+	if err := os.Rename(orig, backup); err != nil {
162
+		return err
156 163
 	}
157 164
 
165
+	defer func() {
166
+		if err != nil {
167
+			if rmErr := os.RemoveAll(orig); rmErr != nil && !os.IsNotExist(rmErr) {
168
+				logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up after failed upgrade")
169
+				return
170
+			}
171
+
172
+			if err := os.Rename(backup, orig); err != nil {
173
+				err = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
174
+			}
175
+			if rmErr := os.RemoveAll(tmpRootFSDir); rmErr != nil && !os.IsNotExist(rmErr) {
176
+				logrus.WithError(rmErr).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
177
+			}
178
+		} else {
179
+			if rmErr := os.RemoveAll(backup); rmErr != nil && !os.IsNotExist(rmErr) {
180
+				logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
181
+			}
182
+
183
+			p.Config = configDigest
184
+			p.Blobsums = blobsums
185
+		}
186
+	}()
187
+
188
+	if err := os.Rename(tmpRootFSDir, orig); err != nil {
189
+		return errors.Wrap(err, "error upgrading")
190
+	}
191
+
192
+	p.PluginObj.Config = config
193
+	err = pm.save(p)
194
+	return errors.Wrap(err, "error saving upgraded plugin config")
195
+}
196
+
197
+func (pm *Manager) setupNewPlugin(configDigest digest.Digest, blobsums []digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
158 198
 	configRC, err := pm.blobStore.Get(configDigest)
159 199
 	if err != nil {
160
-		return nil, err
200
+		return types.PluginConfig{}, err
161 201
 	}
162 202
 	defer configRC.Close()
163 203
 
164 204
 	var config types.PluginConfig
165 205
 	dec := json.NewDecoder(configRC)
166 206
 	if err := dec.Decode(&config); err != nil {
167
-		return nil, errors.Wrapf(err, "failed to parse config")
207
+		return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
168 208
 	}
169 209
 	if dec.More() {
170
-		return nil, errors.New("invalid config json")
210
+		return types.PluginConfig{}, errors.New("invalid config json")
171 211
 	}
172 212
 
173 213
 	requiredPrivileges, err := computePrivileges(config)
174 214
 	if err != nil {
175
-		return nil, err
215
+		return types.PluginConfig{}, err
176 216
 	}
177 217
 	if privileges != nil {
178 218
 		if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
179
-			return nil, err
219
+			return types.PluginConfig{}, err
180 220
 		}
181 221
 	}
182 222
 
223
+	return config, nil
224
+}
225
+
226
+// createPlugin creates a new plugin. take lock before calling.
227
+func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
228
+	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
229
+		return nil, err
230
+	}
231
+
232
+	config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
233
+	if err != nil {
234
+		return nil, err
235
+	}
236
+
183 237
 	p = &v2.Plugin{
184 238
 		PluginObj: types.Plugin{
185 239
 			Name:   name,