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