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>

Brian Goff authored on 2017/01/29 09:54:32
Showing 26 changed files
... ...
@@ -21,5 +21,6 @@ type Backend interface {
21 21
 	Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
22 22
 	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
23 23
 	Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
24
+	Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
24 25
 	CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
25 26
 }
... ...
@@ -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
 	}
... ...
@@ -101,7 +101,7 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
101 101
 	return httputils.WriteJSON(w, http.StatusOK, privileges)
102 102
 }
103 103
 
104
-func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
104
+func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
105 105
 	if err := httputils.ParseForm(r); err != nil {
106 106
 		return errors.Wrap(err, "failed to parse form")
107 107
 	}
... ...
@@ -116,20 +116,77 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
116 116
 	}
117 117
 
118 118
 	metaHeaders, authConfig := parseHeaders(r.Header)
119
+	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
120
+	if err != nil {
121
+		return err
122
+	}
123
+
124
+	name, err := getName(ref, tag, vars["name"])
125
+	if err != nil {
126
+		return err
127
+	}
128
+	w.Header().Set("Docker-Plugin-Name", name)
119 129
 
130
+	w.Header().Set("Content-Type", "application/json")
131
+	output := ioutils.NewWriteFlusher(w)
132
+
133
+	if err := pr.backend.Upgrade(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
134
+		if !output.Flushed() {
135
+			return err
136
+		}
137
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
138
+	}
139
+
140
+	return nil
141
+}
142
+
143
+func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
144
+	if err := httputils.ParseForm(r); err != nil {
145
+		return errors.Wrap(err, "failed to parse form")
146
+	}
147
+
148
+	var privileges types.PluginPrivileges
149
+	dec := json.NewDecoder(r.Body)
150
+	if err := dec.Decode(&privileges); err != nil {
151
+		return errors.Wrap(err, "failed to parse privileges")
152
+	}
153
+	if dec.More() {
154
+		return errors.New("invalid privileges")
155
+	}
156
+
157
+	metaHeaders, authConfig := parseHeaders(r.Header)
120 158
 	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
121 159
 	if err != nil {
122 160
 		return err
123 161
 	}
124 162
 
125
-	name := r.FormValue("name")
163
+	name, err := getName(ref, tag, r.FormValue("name"))
164
+	if err != nil {
165
+		return err
166
+	}
167
+	w.Header().Set("Docker-Plugin-Name", name)
168
+
169
+	w.Header().Set("Content-Type", "application/json")
170
+	output := ioutils.NewWriteFlusher(w)
171
+
172
+	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
173
+		if !output.Flushed() {
174
+			return err
175
+		}
176
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
177
+	}
178
+
179
+	return nil
180
+}
181
+
182
+func getName(ref reference.Named, tag, name string) (string, error) {
126 183
 	if name == "" {
127 184
 		if _, ok := ref.(reference.Canonical); ok {
128 185
 			trimmed := reference.TrimNamed(ref)
129 186
 			if tag != "" {
130 187
 				nt, err := reference.WithTag(trimmed, tag)
131 188
 				if err != nil {
132
-					return err
189
+					return "", err
133 190
 				}
134 191
 				name = nt.String()
135 192
 			} else {
... ...
@@ -141,29 +198,17 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
141 141
 	} else {
142 142
 		localRef, err := reference.ParseNamed(name)
143 143
 		if err != nil {
144
-			return err
144
+			return "", err
145 145
 		}
146 146
 		if _, ok := localRef.(reference.Canonical); ok {
147
-			return errors.New("cannot use digest in plugin tag")
147
+			return "", errors.New("cannot use digest in plugin tag")
148 148
 		}
149 149
 		if distreference.IsNameOnly(localRef) {
150 150
 			// TODO: log change in name to out stream
151 151
 			name = reference.WithDefaultTag(localRef).String()
152 152
 		}
153 153
 	}
154
-	w.Header().Set("Docker-Plugin-Name", name)
155
-
156
-	w.Header().Set("Content-Type", "application/json")
157
-	output := ioutils.NewWriteFlusher(w)
158
-
159
-	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
160
-		if !output.Flushed() {
161
-			return err
162
-		}
163
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
164
-	}
165
-
166
-	return nil
154
+	return name, nil
167 155
 }
168 156
 
169 157
 func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
... ...
@@ -1412,6 +1412,10 @@ definitions:
1412 1412
             type: "array"
1413 1413
             items:
1414 1414
               $ref: "#/definitions/PluginDevice"
1415
+      PluginReference:
1416
+        description: "plugin remote reference used to push/pull the plugin"
1417
+        type: "string"
1418
+        x-nullable: false
1415 1419
       Config:
1416 1420
         description: "The config of a plugin."
1417 1421
         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"`
... ...
@@ -85,3 +85,8 @@ func (c *pluginContext) Enabled() bool {
85 85
 	c.AddHeader(enabledHeader)
86 86
 	return c.p.Enabled
87 87
 }
88
+
89
+func (c *pluginContext) PluginReference() string {
90
+	c.AddHeader(imageHeader)
91
+	return c.p.PluginReference
92
+}
... ...
@@ -150,8 +150,8 @@ func TestPluginContextWriteJSON(t *testing.T) {
150 150
 		{ID: "pluginID2", Name: "foobar_bar"},
151 151
 	}
152 152
 	expectedJSONs := []map[string]interface{}{
153
-		{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz"},
154
-		{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar"},
153
+		{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""},
154
+		{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""},
155 155
 	}
156 156
 
157 157
 	out := bytes.NewBufferString("")
... ...
@@ -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
 }
... ...
@@ -15,15 +15,22 @@ import (
15 15
 	"github.com/docker/docker/pkg/jsonmessage"
16 16
 	"github.com/docker/docker/registry"
17 17
 	"github.com/spf13/cobra"
18
+	"github.com/spf13/pflag"
18 19
 	"golang.org/x/net/context"
19 20
 )
20 21
 
21 22
 type pluginOptions struct {
22
-	name       string
23
-	alias      string
24
-	grantPerms bool
25
-	disable    bool
26
-	args       []string
23
+	remote          string
24
+	localName       string
25
+	grantPerms      bool
26
+	disable         bool
27
+	args            []string
28
+	skipRemoteCheck bool
29
+}
30
+
31
+func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) {
32
+	flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
33
+	command.AddTrustVerificationFlags(flags)
27 34
 }
28 35
 
29 36
 func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
... ...
@@ -33,7 +40,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
33 33
 		Short: "Install a plugin",
34 34
 		Args:  cli.RequiresMinArgs(1),
35 35
 		RunE: func(cmd *cobra.Command, args []string) error {
36
-			options.name = args[0]
36
+			options.remote = args[0]
37 37
 			if len(args) > 1 {
38 38
 				options.args = args[1:]
39 39
 			}
... ...
@@ -42,12 +49,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
42 42
 	}
43 43
 
44 44
 	flags := cmd.Flags()
45
-	flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
45
+	loadPullFlags(&options, flags)
46 46
 	flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
47
-	flags.StringVar(&options.alias, "alias", "", "Local name for plugin")
48
-
49
-	command.AddTrustVerificationFlags(flags)
50
-
47
+	flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
51 48
 	return cmd
52 49
 }
53 50
 
... ...
@@ -83,49 +87,33 @@ func newRegistryService() registry.Service {
83 83
 	}
84 84
 }
85 85
 
86
-func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
86
+func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
87 87
 	// Names with both tag and digest will be treated by the daemon
88
-	// as a pull by digest with an alias for the tag
89
-	// (if no alias is provided).
90
-	ref, err := reference.ParseNormalizedNamed(opts.name)
88
+	// as a pull by digest with a local name for the tag
89
+	// (if no local name is provided).
90
+	ref, err := reference.ParseNormalizedNamed(opts.remote)
91 91
 	if err != nil {
92
-		return err
93
-	}
94
-
95
-	alias := ""
96
-	if opts.alias != "" {
97
-		aref, err := reference.ParseNormalizedNamed(opts.alias)
98
-		if err != nil {
99
-			return err
100
-		}
101
-		if _, ok := aref.(reference.Canonical); ok {
102
-			return fmt.Errorf("invalid name: %s", opts.alias)
103
-		}
104
-		alias = reference.FamiliarString(reference.EnsureTagged(aref))
92
+		return types.PluginInstallOptions{}, err
105 93
 	}
106
-	ctx := context.Background()
107 94
 
108 95
 	repoInfo, err := registry.ParseRepositoryInfo(ref)
109 96
 	if err != nil {
110
-		return err
97
+		return types.PluginInstallOptions{}, err
111 98
 	}
112 99
 
113 100
 	remote := ref.String()
114 101
 
115 102
 	_, isCanonical := ref.(reference.Canonical)
116 103
 	if command.IsTrusted() && !isCanonical {
117
-		if alias == "" {
118
-			alias = reference.FamiliarString(ref)
119
-		}
120
-
121 104
 		nt, ok := ref.(reference.NamedTagged)
122 105
 		if !ok {
123 106
 			nt = reference.EnsureTagged(ref)
124 107
 		}
125 108
 
109
+		ctx := context.Background()
126 110
 		trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
127 111
 		if err != nil {
128
-			return err
112
+			return types.PluginInstallOptions{}, err
129 113
 		}
130 114
 		remote = reference.FamiliarString(trusted)
131 115
 	}
... ...
@@ -134,23 +122,42 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
134 134
 
135 135
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
136 136
 	if err != nil {
137
-		return err
137
+		return types.PluginInstallOptions{}, err
138 138
 	}
139
-
140
-	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install")
139
+	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName)
141 140
 
142 141
 	options := types.PluginInstallOptions{
143 142
 		RegistryAuth:          encodedAuth,
144 143
 		RemoteRef:             remote,
145 144
 		Disabled:              opts.disable,
146 145
 		AcceptAllPermissions:  opts.grantPerms,
147
-		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
146
+		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
148 147
 		// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
149 148
 		PrivilegeFunc: registryAuthFunc,
150 149
 		Args:          opts.args,
151 150
 	}
151
+	return options, nil
152
+}
152 153
 
153
-	responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options)
154
+func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
155
+	var localName string
156
+	if opts.localName != "" {
157
+		aref, err := reference.ParseNormalizedNamed(opts.localName)
158
+		if err != nil {
159
+			return err
160
+		}
161
+		if _, ok := aref.(reference.Canonical); ok {
162
+			return fmt.Errorf("invalid name: %s", opts.localName)
163
+		}
164
+		localName = reference.FamiliarString(reference.EnsureTagged(aref))
165
+	}
166
+
167
+	ctx := context.Background()
168
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install")
169
+	if err != nil {
170
+		return err
171
+	}
172
+	responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
154 173
 	if err != nil {
155 174
 		if strings.Contains(err.Error(), "target is image") {
156 175
 			return errors.New(err.Error() + " - Use `docker image pull`")
... ...
@@ -161,7 +168,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
161 161
 	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
162 162
 		return err
163 163
 	}
164
-	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result
164
+	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result
165 165
 	return nil
166 166
 }
167 167
 
168 168
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
+}
... ...
@@ -113,6 +113,7 @@ type PluginAPIClient interface {
113 113
 	PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
114 114
 	PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
115 115
 	PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
116
+	PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
116 117
 	PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
117 118
 	PluginSet(ctx context.Context, name string, args []string) error
118 119
 	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
+}
... ...
@@ -58,3 +58,4 @@ The plugin can subsequently be enabled for local use or pushed to the public reg
58 58
 * [plugin push](plugin_push.md)
59 59
 * [plugin rm](plugin_rm.md)
60 60
 * [plugin set](plugin_set.md)
61
+* [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)
... ...
@@ -69,3 +69,4 @@ ID                  NAME                  TAG                 DESCRIPTION
69 69
 * [plugin push](plugin_push.md)
70 70
 * [plugin rm](plugin_rm.md)
71 71
 * [plugin set](plugin_set.md)
72
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -83,10 +83,11 @@ Valid placeholders for the Go template are listed below:
83 83
 
84 84
 Placeholder    | Description
85 85
 ---------------|------------------------------------------------------------------------------------------
86
-`.ID`          | Plugin ID
87
-`.Name`        | Plugin name
88
-`.Description` | Plugin description
89
-`.Enabled`     | Whether plugin is enabled or not
86
+`.ID`              | Plugin ID
87
+`.Name`            | Plugin name
88
+`.Description`     | Plugin description
89
+`.Enabled`         | Whether plugin is enabled or not
90
+`.PluginReference` | The reference used to push/pull from a registry
90 91
 
91 92
 When using the `--format` option, the `plugin ls` command will either
92 93
 output the data exactly as the template declares or, when using the
... ...
@@ -111,3 +112,4 @@ $ docker plugin ls --format "{{.ID}}: {{.Name}}"
111 111
 * [plugin push](plugin_push.md)
112 112
 * [plugin rm](plugin_rm.md)
113 113
 * [plugin set](plugin_set.md)
114
+* [plugin upgrade](plugin_upgrade.md)
... ...
@@ -48,3 +48,4 @@ $ docker plugin push user/plugin
48 48
 * [plugin ls](plugin_ls.md)
49 49
 * [plugin rm](plugin_rm.md)
50 50
 * [plugin set](plugin_set.md)
51
+* [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)
... ...
@@ -427,3 +427,30 @@ enabled: true`, id, pNameWithTag)
427 427
 	out, _ = dockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc")
428 428
 	c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput)
429 429
 }
430
+
431
+func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
432
+	testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
433
+	plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
434
+	pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
435
+
436
+	dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
437
+	out, _, err := dockerCmdWithError("plugin", "upgrade", "--grant-all-permissions", plugin, pluginV2)
438
+	c.Assert(err, checker.NotNil, check.Commentf(out))
439
+	c.Assert(out, checker.Contains, "disabled before upgrading")
440
+
441
+	out, _ = dockerCmd(c, "plugin", "inspect", "--format={{.ID}}", plugin)
442
+	id := strings.TrimSpace(out)
443
+
444
+	// make sure "v2" does not exists
445
+	_, err = os.Stat(filepath.Join(testEnv.DockerBasePath(), "plugins", id, "rootfs", "v2"))
446
+	c.Assert(os.IsNotExist(err), checker.True, check.Commentf(out))
447
+
448
+	dockerCmd(c, "plugin", "disable", plugin)
449
+	dockerCmd(c, "plugin", "upgrade", "--grant-all-permissions", "--skip-remote-check", plugin, pluginV2)
450
+
451
+	// make sure "v2" file exists
452
+	_, err = os.Stat(filepath.Join(testEnv.DockerBasePath(), "plugins", id, "rootfs", "v2"))
453
+	c.Assert(err, checker.IsNil)
454
+
455
+	dockerCmd(c, "plugin", "enable", plugin)
456
+}
... ...
@@ -215,6 +215,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
215 215
 	return computePrivileges(config)
216 216
 }
217 217
 
218
+// Upgrade upgrades a plugin
219
+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) {
220
+	p, err := pm.config.Store.GetV2Plugin(name)
221
+	if err != nil {
222
+		return errors.Wrap(err, "plugin must be installed before upgrading")
223
+	}
224
+
225
+	if p.IsEnabled() {
226
+		return fmt.Errorf("plugin must be disabled before upgrading")
227
+	}
228
+
229
+	pm.muGC.RLock()
230
+	defer pm.muGC.RUnlock()
231
+
232
+	// revalidate because Pull is public
233
+	nameref, err := reference.ParseNamed(name)
234
+	if err != nil {
235
+		return errors.Wrapf(err, "failed to parse %q", name)
236
+	}
237
+	name = reference.WithDefaultTag(nameref).String()
238
+
239
+	tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
240
+	defer os.RemoveAll(tmpRootFSDir)
241
+
242
+	dm := &downloadManager{
243
+		tmpDir:    tmpRootFSDir,
244
+		blobStore: pm.blobStore,
245
+	}
246
+
247
+	pluginPullConfig := &distribution.ImagePullConfig{
248
+		Config: distribution.Config{
249
+			MetaHeaders:      metaHeader,
250
+			AuthConfig:       authConfig,
251
+			RegistryService:  pm.config.RegistryService,
252
+			ImageEventLogger: pm.config.LogPluginEvent,
253
+			ImageStore:       dm,
254
+		},
255
+		DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
256
+		Schema2Types:    distribution.PluginTypes,
257
+	}
258
+
259
+	err = pm.pull(ctx, ref, pluginPullConfig, outStream)
260
+	if err != nil {
261
+		go pm.GC()
262
+		return err
263
+	}
264
+
265
+	if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
266
+		return err
267
+	}
268
+	p.PluginObj.PluginReference = ref.String()
269
+	return nil
270
+}
271
+
218 272
 // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
219 273
 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) {
220 274
 	pm.muGC.RLock()
... ...
@@ -257,9 +311,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
257 257
 		return err
258 258
 	}
259 259
 
260
-	if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
260
+	p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
261
+	if err != nil {
261 262
 		return err
262 263
 	}
264
+	p.PluginObj.PluginReference = ref.String()
263 265
 
264 266
 	return nil
265 267
 }
... ...
@@ -573,7 +629,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
573 573
 	if _, ok := ref.(reference.Canonical); ok {
574 574
 		return errors.Errorf("canonical references are not permitted")
575 575
 	}
576
-	name := reference.WithDefaultTag(ref).String()
576
+	taggedRef := reference.WithDefaultTag(ref)
577
+	name := taggedRef.String()
577 578
 
578 579
 	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
579 580
 		return err
... ...
@@ -655,6 +712,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
655 655
 	if err != nil {
656 656
 		return err
657 657
 	}
658
+	p.PluginObj.PluginReference = taggedRef.String()
658 659
 
659 660
 	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
660 661
 
... ...
@@ -40,6 +40,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
40 40
 	return errNotSupported
41 41
 }
42 42
 
43
+// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
44
+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 {
45
+	return errNotSupported
46
+}
47
+
43 48
 // List displays the list of plugins and associated metadata.
44 49
 func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
45 50
 	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,