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 9f3d30afe3
commit d98ab3d3ab
5 changed files with 156 additions and 43 deletions

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

100
command/plugin/upgrade.go Normal file
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
}