diff --git a/api/server/router/plugin/backend.go b/api/server/router/plugin/backend.go index fee78195d6..ab006b2256 100644 --- a/api/server/router/plugin/backend.go +++ b/api/server/router/plugin/backend.go @@ -20,5 +20,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 } diff --git a/api/server/router/plugin/plugin.go b/api/server/router/plugin/plugin.go index 9aa82f338c..e4ea9e23bf 100644 --- a/api/server/router/plugin/plugin.go +++ b/api/server/router/plugin/plugin.go @@ -32,6 +32,7 @@ func (r *pluginRouter) initRoutes() { router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin), router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)), router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)), + router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/upgrade", r.upgradePlugin)), router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin), router.NewPostRoute("/plugins/create", r.createPlugin), } diff --git a/api/server/router/plugin/plugin_routes.go b/api/server/router/plugin/plugin_routes.go index 2d3eb8fea1..693fa95baf 100644 --- a/api/server/router/plugin/plugin_routes.go +++ b/api/server/router/plugin/plugin_routes.go @@ -100,6 +100,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") @@ -115,40 +154,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) @@ -165,6 +178,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 diff --git a/api/swagger.yaml b/api/swagger.yaml index dfa463cf12..7aeba0aa59 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1379,6 +1379,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" diff --git a/api/types/plugin.go b/api/types/plugin.go index 46f47be26f..6cc7a23b02 100644 --- a/api/types/plugin.go +++ b/api/types/plugin.go @@ -22,6 +22,9 @@ type Plugin struct { // Required: true Name string `json:"Name"` + // plugin remote reference used to push/pull the plugin + PluginReference string `json:"PluginReference,omitempty"` + // settings // Required: true Settings PluginSettings `json:"Settings"` diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go index 2173943f89..92c990a975 100644 --- a/cli/command/plugin/cmd.go +++ b/cli/command/plugin/cmd.go @@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { newSetCommand(dockerCli), newPushCommand(dockerCli), newCreateCommand(dockerCli), + newUpgradeCommand(dockerCli), ) return cmd } diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go index a64dc2525a..2c3170c54a 100644 --- a/cli/command/plugin/install.go +++ b/cli/command/plugin/install.go @@ -16,15 +16,22 @@ import ( "github.com/docker/docker/reference" "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.AddTrustedFlags(flags, true) } func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -34,7 +41,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:] } @@ -43,12 +50,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.AddTrustedFlags(flags, true) - + flags.StringVar(&options.localName, "alias", "", "Local name for plugin") return cmd } @@ -84,60 +88,48 @@ 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) { // Parse name using distribution reference package to support name // containing both tag and digest. 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 := distreference.ParseNamed(opts.name) + ref, err := distreference.ParseNamed(opts.remote) if err != nil { - return err + return types.PluginInstallOptions{}, err } - alias := "" - if opts.alias != "" { - aref, err := reference.ParseNamed(opts.alias) - if err != nil { - return err - } - aref = reference.WithDefaultTag(aref) - if _, ok := aref.(reference.NamedTagged); !ok { - return fmt.Errorf("invalid name: %s", opts.alias) - } - alias = aref.String() - } - ctx := context.Background() - index, err := getRepoIndexFromUnnormalizedRef(ref) if err != nil { - return err + return types.PluginInstallOptions{}, err } + repoInfoIndex, err := getRepoIndexFromUnnormalizedRef(ref) + if err != nil { + return types.PluginInstallOptions{}, err + } remote := ref.String() _, isCanonical := ref.(distreference.Canonical) if command.IsTrusted() && !isCanonical { - if alias == "" { - alias = ref.String() - } var nt reference.NamedTagged named, err := reference.ParseNamed(ref.Name()) if err != nil { - return err + return types.PluginInstallOptions{}, err } if tagged, ok := ref.(distreference.Tagged); ok { nt, err = reference.WithTag(named, tagged.Tag()) if err != nil { - return err + return types.PluginInstallOptions{}, err } } else { named = reference.WithDefaultTag(named) nt = named.(reference.NamedTagged) } + ctx := context.Background() trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) if err != nil { - return err + return types.PluginInstallOptions{}, err } remote = trusted.String() } @@ -146,23 +138,44 @@ 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, index, "plugin install") + registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfoIndex, 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.ParseNamed(opts.localName) + if err != nil { + return err + } + aref = reference.WithDefaultTag(aref) + if _, ok := aref.(reference.NamedTagged); !ok { + return fmt.Errorf("invalid name: %s", opts.localName) + } + localName = aref.String() + } + + 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`") @@ -173,7 +186,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 } diff --git a/cli/command/plugin/upgrade.go b/cli/command/plugin/upgrade.go new file mode 100644 index 0000000000..d212cd7e52 --- /dev/null +++ b/cli/command/plugin/upgrade.go @@ -0,0 +1,100 @@ +package plugin + +import ( + "bufio" + "context" + "fmt" + "strings" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/reference" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command { + var options pluginOptions + cmd := &cobra.Command{ + Use: "upgrade [OPTIONS] PLUGIN [REMOTE]", + Short: "Upgrade an existing plugin", + Args: cli.RequiresRangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + options.localName = args[0] + if len(args) == 2 { + options.remote = args[1] + } + return runUpgrade(dockerCli, options) + }, + } + + flags := cmd.Flags() + loadPullFlags(&options, flags) + flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image") + return cmd +} + +func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { + ctx := context.Background() + p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName) + if err != nil { + return fmt.Errorf("error reading plugin data: %v", err) + } + + if p.Enabled { + return fmt.Errorf("the plugin must be disabled before upgrading") + } + + opts.localName = p.Name + if opts.remote == "" { + opts.remote = p.PluginReference + } + remote, err := reference.ParseNamed(opts.remote) + if err != nil { + return errors.Wrap(err, "error parsing remote upgrade image reference") + } + remote = reference.WithDefaultTag(remote) + + old, err := reference.ParseNamed(p.PluginReference) + if err != nil { + return errors.Wrap(err, "error parsing current image reference") + } + old = reference.WithDefaultTag(old) + + fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote) + if !opts.skipRemoteCheck && remote.String() != old.String() { + _, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ") + if err != nil { + return errors.Wrap(err, "error writing to stdout") + } + + rdr := bufio.NewReader(dockerCli.In()) + line, _, err := rdr.ReadLine() + if err != nil { + return errors.Wrap(err, "error reading from stdin") + } + if strings.ToLower(string(line)) != "y" { + return errors.New("canceling upgrade request") + } + } + + options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade") + if err != nil { + return err + } + + responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options) + if err != nil { + if strings.Contains(err.Error(), "target is image") { + return errors.New(err.Error() + " - Use `docker image pull`") + } + return err + } + defer responseBody.Close() + if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result + return nil +} diff --git a/client/interface.go b/client/interface.go index 924b22bc04..05978039b7 100644 --- a/client/interface.go +++ b/client/interface.go @@ -112,6 +112,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) diff --git a/client/plugin_install.go b/client/plugin_install.go index b305780cfb..3217c4cf39 100644 --- a/client/plugin_install.go +++ b/client/plugin_install.go @@ -20,43 +20,15 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types } query.Set("remote", options.RemoteRef) - resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) - if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { - // todo: do inspect before to check existing name before checking privileges - newAuthHeader, privilegeErr := options.PrivilegeFunc() - if privilegeErr != nil { - ensureReaderClosed(resp) - return nil, privilegeErr - } - options.RegistryAuth = newAuthHeader - resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) - } + privileges, err := cli.checkPluginPermissions(ctx, query, options) if err != nil { - ensureReaderClosed(resp) return nil, err } - var privileges types.PluginPrivileges - if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { - ensureReaderClosed(resp) - return nil, err - } - ensureReaderClosed(resp) - - if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { - accept, err := options.AcceptPermissionsFunc(privileges) - if err != nil { - return nil, err - } - if !accept { - return nil, pluginPermissionDenied{options.RemoteRef} - } - } - // set name for plugin pull, if empty should default to remote reference query.Set("name", name) - resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) + resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) if err != nil { return nil, err } @@ -103,3 +75,39 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg headers := map[string][]string{"X-Registry-Auth": {registryAuth}} return cli.post(ctx, "/plugins/pull", query, privileges, headers) } + +func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) { + resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + // todo: do inspect before to check existing name before checking privileges + newAuthHeader, privilegeErr := options.PrivilegeFunc() + if privilegeErr != nil { + ensureReaderClosed(resp) + return nil, privilegeErr + } + options.RegistryAuth = newAuthHeader + resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) + } + if err != nil { + ensureReaderClosed(resp) + return nil, err + } + + var privileges types.PluginPrivileges + if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { + ensureReaderClosed(resp) + return nil, err + } + ensureReaderClosed(resp) + + if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { + accept, err := options.AcceptPermissionsFunc(privileges) + if err != nil { + return nil, err + } + if !accept { + return nil, pluginPermissionDenied{options.RemoteRef} + } + } + return privileges, nil +} diff --git a/client/plugin_upgrade.go b/client/plugin_upgrade.go new file mode 100644 index 0000000000..95a4356b97 --- /dev/null +++ b/client/plugin_upgrade.go @@ -0,0 +1,37 @@ +package client + +import ( + "fmt" + "io" + "net/url" + + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +// PluginUpgrade upgrades a plugin +func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) { + query := url.Values{} + if _, err := reference.ParseNamed(options.RemoteRef); err != nil { + return nil, errors.Wrap(err, "invalid remote reference") + } + query.Set("remote", options.RemoteRef) + + privileges, err := cli.checkPluginPermissions(ctx, query, options) + if err != nil { + return nil, err + } + + resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth) + if err != nil { + return nil, err + } + return resp.body, nil +} + +func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers) +} diff --git a/docs/reference/commandline/plugin_create.md b/docs/reference/commandline/plugin_create.md index f1593f05f4..9d4e99e56a 100644 --- a/docs/reference/commandline/plugin_create.md +++ b/docs/reference/commandline/plugin_create.md @@ -57,3 +57,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) diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md index e8d16c4253..451f1ace9c 100644 --- a/docs/reference/commandline/plugin_disable.md +++ b/docs/reference/commandline/plugin_disable.md @@ -63,3 +63,4 @@ ID NAME TAG DESCRIP * [plugin push](plugin_push.md) * [plugin rm](plugin_rm.md) * [plugin set](plugin_set.md) +* [plugin upgrade](plugin_upgrade.md) diff --git a/docs/reference/commandline/plugin_enable.md b/docs/reference/commandline/plugin_enable.md index 060592ef07..df8bee3af5 100644 --- a/docs/reference/commandline/plugin_enable.md +++ b/docs/reference/commandline/plugin_enable.md @@ -62,3 +62,4 @@ ID NAME TAG DESCRIP * [plugin push](plugin_push.md) * [plugin rm](plugin_rm.md) * [plugin set](plugin_set.md) +* [plugin upgrade](plugin_upgrade.md) diff --git a/docs/reference/commandline/plugin_inspect.md b/docs/reference/commandline/plugin_inspect.md index d40cd40e75..fdcc030c43 100644 --- a/docs/reference/commandline/plugin_inspect.md +++ b/docs/reference/commandline/plugin_inspect.md @@ -37,6 +37,7 @@ $ docker plugin inspect tiborvass/sample-volume-plugin:latest { "Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21", "Name": "tiborvass/sample-volume-plugin:latest", + "PluginReference": "tiborvas/sample-volume-plugin:latest", "Enabled": true, "Config": { "Mounts": [ @@ -160,3 +161,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/sample-volume-plugin:latest * [plugin push](plugin_push.md) * [plugin rm](plugin_rm.md) * [plugin set](plugin_set.md) +* [plugin upgrade](plugin_upgrade.md) diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md index 78dd23825f..0601193ce0 100644 --- a/docs/reference/commandline/plugin_install.md +++ b/docs/reference/commandline/plugin_install.md @@ -68,3 +68,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) diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md index e436213ecc..7a3426d95f 100644 --- a/docs/reference/commandline/plugin_ls.md +++ b/docs/reference/commandline/plugin_ls.md @@ -50,3 +50,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) diff --git a/docs/reference/commandline/plugin_push.md b/docs/reference/commandline/plugin_push.md index f869e43f92..e61d10994c 100644 --- a/docs/reference/commandline/plugin_push.md +++ b/docs/reference/commandline/plugin_push.md @@ -47,3 +47,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) diff --git a/docs/reference/commandline/plugin_rm.md b/docs/reference/commandline/plugin_rm.md index 31029324b6..323ce83f3c 100644 --- a/docs/reference/commandline/plugin_rm.md +++ b/docs/reference/commandline/plugin_rm.md @@ -53,3 +53,4 @@ tiborvass/sample-volume-plugin * [plugin ls](plugin_ls.md) * [plugin push](plugin_push.md) * [plugin set](plugin_set.md) +* [plugin upgrade](plugin_upgrade.md) diff --git a/docs/reference/commandline/plugin_upgrade.md b/docs/reference/commandline/plugin_upgrade.md new file mode 100644 index 0000000000..20efc577aa --- /dev/null +++ b/docs/reference/commandline/plugin_upgrade.md @@ -0,0 +1,84 @@ +--- +title: "plugin upgrade" +description: "the plugin upgrade command description and usage" +keywords: "plugin, upgrade" +--- + + + +# 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) diff --git a/integration-cli/docker_cli_plugins_test.go b/integration-cli/docker_cli_plugins_test.go index fd8f1a11fa..6376da78c1 100644 --- a/integration-cli/docker_cli_plugins_test.go +++ b/integration-cli/docker_cli_plugins_test.go @@ -359,3 +359,30 @@ func (s *DockerTrustSuite) TestPluginUntrustedInstall(c *check.C) { c.Assert(err, check.NotNil, check.Commentf(out)) c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out)) } + +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(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(dockerBasePath, "plugins", id, "rootfs", "v2")) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "plugin", "enable", plugin) +} diff --git a/plugin/backend_linux.go b/plugin/backend_linux.go index 3426eba08f..97046ee185 100644 --- a/plugin/backend_linux.go +++ b/plugin/backend_linux.go @@ -209,6 +209,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() @@ -251,9 +305,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 } @@ -536,7 +592,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 @@ -618,6 +675,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") diff --git a/plugin/backend_unsupported.go b/plugin/backend_unsupported.go index becb361fe2..66e6dab9e8 100644 --- a/plugin/backend_unsupported.go +++ b/plugin/backend_unsupported.go @@ -39,6 +39,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() ([]types.Plugin, error) { return nil, errNotSupported diff --git a/plugin/manager_linux.go b/plugin/manager_linux.go index 4e3c98de30..f13b43c415 100644 --- a/plugin/manager_linux.go +++ b/plugin/manager_linux.go @@ -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,