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>
This commit is contained in:
Brian Goff 2017-01-28 16:54:32 -08:00
parent edd977db97
commit 03c6949739
26 changed files with 568 additions and 117 deletions

View file

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

View file

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

View file

@ -101,6 +101,45 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
return httputils.WriteJSON(w, http.StatusOK, privileges)
}
func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return errors.Wrap(err, "failed to parse form")
}
var privileges types.PluginPrivileges
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&privileges); err != nil {
return errors.Wrap(err, "failed to parse privileges")
}
if dec.More() {
return errors.New("invalid privileges")
}
metaHeaders, authConfig := parseHeaders(r.Header)
ref, tag, err := parseRemoteRef(r.FormValue("remote"))
if err != nil {
return err
}
name, 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")
@ -116,40 +155,14 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
}
metaHeaders, authConfig := parseHeaders(r.Header)
ref, tag, err := parseRemoteRef(r.FormValue("remote"))
if err != nil {
return err
}
name := r.FormValue("name")
if name == "" {
if _, ok := ref.(reference.Canonical); ok {
trimmed := reference.TrimNamed(ref)
if tag != "" {
nt, err := reference.WithTag(trimmed, tag)
if err != nil {
return err
}
name = nt.String()
} else {
name = reference.WithDefaultTag(trimmed).String()
}
} else {
name = ref.String()
}
} else {
localRef, err := reference.ParseNamed(name)
if err != nil {
return err
}
if _, ok := localRef.(reference.Canonical); ok {
return errors.New("cannot use digest in plugin tag")
}
if distreference.IsNameOnly(localRef) {
// TODO: log change in name to out stream
name = reference.WithDefaultTag(localRef).String()
}
name, err := getName(ref, tag, r.FormValue("name"))
if err != nil {
return err
}
w.Header().Set("Docker-Plugin-Name", name)
@ -166,6 +179,38 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
return nil
}
func getName(ref reference.Named, tag, name string) (string, error) {
if name == "" {
if _, ok := ref.(reference.Canonical); ok {
trimmed := reference.TrimNamed(ref)
if tag != "" {
nt, err := reference.WithTag(trimmed, tag)
if err != nil {
return "", err
}
name = nt.String()
} else {
name = reference.WithDefaultTag(trimmed).String()
}
} else {
name = ref.String()
}
} else {
localRef, err := reference.ParseNamed(name)
if err != nil {
return "", err
}
if _, ok := localRef.(reference.Canonical); ok {
return "", errors.New("cannot use digest in plugin tag")
}
if distreference.IsNameOnly(localRef) {
// TODO: log change in name to out stream
name = reference.WithDefaultTag(localRef).String()
}
}
return name, nil
}
func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}

View file

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

View file

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

37
client/plugin_upgrade.go Normal file
View file

@ -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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -427,3 +427,30 @@ enabled: true`, id, pNameWithTag)
out, _ = dockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc")
c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput)
}
func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
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", 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)
}

View file

@ -215,6 +215,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
return computePrivileges(config)
}
// Upgrade upgrades a plugin
func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
p, err := pm.config.Store.GetV2Plugin(name)
if err != nil {
return errors.Wrap(err, "plugin must be installed before upgrading")
}
if p.IsEnabled() {
return fmt.Errorf("plugin must be disabled before upgrading")
}
pm.muGC.RLock()
defer pm.muGC.RUnlock()
// revalidate because Pull is public
nameref, err := reference.ParseNamed(name)
if err != nil {
return errors.Wrapf(err, "failed to parse %q", name)
}
name = reference.WithDefaultTag(nameref).String()
tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
defer os.RemoveAll(tmpRootFSDir)
dm := &downloadManager{
tmpDir: tmpRootFSDir,
blobStore: pm.blobStore,
}
pluginPullConfig := &distribution.ImagePullConfig{
Config: distribution.Config{
MetaHeaders: metaHeader,
AuthConfig: authConfig,
RegistryService: pm.config.RegistryService,
ImageEventLogger: pm.config.LogPluginEvent,
ImageStore: dm,
},
DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
Schema2Types: distribution.PluginTypes,
}
err = pm.pull(ctx, ref, pluginPullConfig, outStream)
if err != nil {
go pm.GC()
return err
}
if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
return err
}
p.PluginObj.PluginReference = ref.String()
return nil
}
// Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
pm.muGC.RLock()
@ -257,9 +311,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
return err
}
if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
if err != nil {
return err
}
p.PluginObj.PluginReference = ref.String()
return nil
}
@ -573,7 +629,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
if _, ok := ref.(reference.Canonical); ok {
return errors.Errorf("canonical references are not permitted")
}
name := reference.WithDefaultTag(ref).String()
taggedRef := reference.WithDefaultTag(ref)
name := taggedRef.String()
if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
return err
@ -655,6 +712,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
if err != nil {
return err
}
p.PluginObj.PluginReference = taggedRef.String()
pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")

View file

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

View file

@ -149,37 +149,91 @@ func (pm *Manager) Shutdown() {
}
}
// createPlugin creates a new plugin. take lock before calling.
func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
return nil, err
func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (err error) {
config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
if err != nil {
return err
}
pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
orig := filepath.Join(pdir, "rootfs")
backup := orig + "-old"
if err := os.Rename(orig, backup); err != nil {
return err
}
defer func() {
if err != nil {
if rmErr := os.RemoveAll(orig); rmErr != nil && !os.IsNotExist(rmErr) {
logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up after failed upgrade")
return
}
if err := os.Rename(backup, orig); err != nil {
err = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
}
if rmErr := os.RemoveAll(tmpRootFSDir); rmErr != nil && !os.IsNotExist(rmErr) {
logrus.WithError(rmErr).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
}
} else {
if rmErr := os.RemoveAll(backup); rmErr != nil && !os.IsNotExist(rmErr) {
logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
}
p.Config = configDigest
p.Blobsums = blobsums
}
}()
if err := os.Rename(tmpRootFSDir, orig); err != nil {
return errors.Wrap(err, "error upgrading")
}
p.PluginObj.Config = config
err = pm.save(p)
return errors.Wrap(err, "error saving upgraded plugin config")
}
func (pm *Manager) setupNewPlugin(configDigest digest.Digest, blobsums []digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
configRC, err := pm.blobStore.Get(configDigest)
if err != nil {
return nil, err
return types.PluginConfig{}, err
}
defer configRC.Close()
var config types.PluginConfig
dec := json.NewDecoder(configRC)
if err := dec.Decode(&config); err != nil {
return nil, errors.Wrapf(err, "failed to parse config")
return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
}
if dec.More() {
return nil, errors.New("invalid config json")
return types.PluginConfig{}, errors.New("invalid config json")
}
requiredPrivileges, err := computePrivileges(config)
if err != nil {
return nil, err
return types.PluginConfig{}, err
}
if privileges != nil {
if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
return nil, err
return types.PluginConfig{}, err
}
}
return config, nil
}
// createPlugin creates a new plugin. take lock before calling.
func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
return nil, err
}
config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
if err != nil {
return nil, err
}
p = &v2.Plugin{
PluginObj: types.Plugin{
Name: name,