From d98ab3d3ab63706e1d731581cdbce286de20d60a Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 28 Jan 2017 16:54:32 -0800 Subject: [PATCH] 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 --- command/formatter/plugin.go | 5 ++ command/formatter/plugin_test.go | 4 +- command/plugin/cmd.go | 1 + command/plugin/install.go | 89 ++++++++++++++------------- command/plugin/upgrade.go | 100 +++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 command/plugin/upgrade.go diff --git a/command/formatter/plugin.go b/command/formatter/plugin.go index 5f94714a6b..00bdf3d0f4 100644 --- a/command/formatter/plugin.go +++ b/command/formatter/plugin.go @@ -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 +} diff --git a/command/formatter/plugin_test.go b/command/formatter/plugin_test.go index 9ddbe11dff..a6c8f7e6c1 100644 --- a/command/formatter/plugin_test.go +++ b/command/formatter/plugin_test.go @@ -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("") diff --git a/command/plugin/cmd.go b/command/plugin/cmd.go index 2173943f89..92c990a975 100644 --- a/command/plugin/cmd.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index ebfe1f1eec..631917a07c 100644 --- a/command/plugin/install.go +++ b/command/plugin/install.go @@ -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 } diff --git a/command/plugin/upgrade.go b/command/plugin/upgrade.go new file mode 100644 index 0000000000..d212cd7e52 --- /dev/null +++ b/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 +}