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) Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
} }

View File

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

View File

@ -101,6 +101,45 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
return httputils.WriteJSON(w, http.StatusOK, privileges) return httputils.WriteJSON(w, http.StatusOK, privileges)
} }
func (pr *pluginRouter) 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 { 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 { if err := httputils.ParseForm(r); err != nil {
return errors.Wrap(err, "failed to parse form") 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) metaHeaders, authConfig := parseHeaders(r.Header)
ref, tag, err := parseRemoteRef(r.FormValue("remote")) ref, tag, err := parseRemoteRef(r.FormValue("remote"))
if err != nil { if err != nil {
return err return err
} }
name := r.FormValue("name") name, err := getName(ref, tag, r.FormValue("name"))
if name == "" { if err != nil {
if _, ok := ref.(reference.Canonical); ok { return err
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()
}
} }
w.Header().Set("Docker-Plugin-Name", name) w.Header().Set("Docker-Plugin-Name", name)
@ -166,6 +179,38 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
return nil 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 { 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 { if err := httputils.ParseForm(r); err != nil {
return err return err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
PluginSet(ctx context.Context, name string, args []string) error PluginSet(ctx context.Context, name string, args []string) error
PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)

View File

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

37
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 push](plugin_push.md)
* [plugin rm](plugin_rm.md) * [plugin rm](plugin_rm.md)
* [plugin set](plugin_set.md) * [plugin set](plugin_set.md)
* [plugin upgrade](plugin_upgrade.md)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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") out, _ = dockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc")
c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput) c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput)
} }
func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
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) return computePrivileges(config)
} }
// Upgrade upgrades a plugin
func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
p, err := pm.config.Store.GetV2Plugin(name)
if err != nil {
return errors.Wrap(err, "plugin must be installed before upgrading")
}
if p.IsEnabled() {
return fmt.Errorf("plugin must be disabled before upgrading")
}
pm.muGC.RLock()
defer pm.muGC.RUnlock()
// revalidate because Pull is public
nameref, err := reference.ParseNamed(name)
if err != nil {
return errors.Wrapf(err, "failed to parse %q", name)
}
name = reference.WithDefaultTag(nameref).String()
tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
defer os.RemoveAll(tmpRootFSDir)
dm := &downloadManager{
tmpDir: tmpRootFSDir,
blobStore: pm.blobStore,
}
pluginPullConfig := &distribution.ImagePullConfig{
Config: distribution.Config{
MetaHeaders: metaHeader,
AuthConfig: authConfig,
RegistryService: pm.config.RegistryService,
ImageEventLogger: pm.config.LogPluginEvent,
ImageStore: dm,
},
DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
Schema2Types: distribution.PluginTypes,
}
err = pm.pull(ctx, ref, pluginPullConfig, outStream)
if err != nil {
go pm.GC()
return err
}
if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
return err
}
p.PluginObj.PluginReference = ref.String()
return nil
}
// Pull pulls a plugin, check if the correct privileges are provided and install the plugin. // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) { func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
pm.muGC.RLock() pm.muGC.RLock()
@ -257,9 +311,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
return err return err
} }
if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil { p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
if err != nil {
return err return err
} }
p.PluginObj.PluginReference = ref.String()
return nil return nil
} }
@ -573,7 +629,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
if _, ok := ref.(reference.Canonical); ok { if _, ok := ref.(reference.Canonical); ok {
return errors.Errorf("canonical references are not permitted") return errors.Errorf("canonical references are not permitted")
} }
name := reference.WithDefaultTag(ref).String() taggedRef := reference.WithDefaultTag(ref)
name := taggedRef.String()
if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin() if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
return err return err
@ -655,6 +712,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
if err != nil { if err != nil {
return err return err
} }
p.PluginObj.PluginReference = taggedRef.String()
pm.config.LogPluginEvent(p.PluginObj.ID, name, "create") pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")

View File

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

View File

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