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>
191 lines
5.6 KiB
Go
191 lines
5.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/docker/api/types"
|
|
registrytypes "github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/cli"
|
|
"github.com/docker/docker/cli/command"
|
|
"github.com/docker/docker/cli/command/image"
|
|
"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 {
|
|
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 {
|
|
var options pluginOptions
|
|
cmd := &cobra.Command{
|
|
Use: "install [OPTIONS] PLUGIN [KEY=VALUE...]",
|
|
Short: "Install a plugin",
|
|
Args: cli.RequiresMinArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
options.remote = args[0]
|
|
if len(args) > 1 {
|
|
options.args = args[1:]
|
|
}
|
|
return runInstall(dockerCli, options)
|
|
},
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
loadPullFlags(&options, flags)
|
|
flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
|
|
flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
|
|
return cmd
|
|
}
|
|
|
|
func getRepoIndexFromUnnormalizedRef(ref reference.Named) (*registrytypes.IndexInfo, error) {
|
|
named, err := reference.ParseNormalizedNamed(ref.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoInfo, err := registry.ParseRepositoryInfo(named)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return repoInfo.Index, nil
|
|
}
|
|
|
|
type pluginRegistryService struct {
|
|
registry.Service
|
|
}
|
|
|
|
func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) {
|
|
repoInfo, err = s.Service.ResolveRepository(name)
|
|
if repoInfo != nil {
|
|
repoInfo.Class = "plugin"
|
|
}
|
|
return
|
|
}
|
|
|
|
func newRegistryService() registry.Service {
|
|
return pluginRegistryService{
|
|
Service: registry.NewService(registry.ServiceOptions{V2Only: true}),
|
|
}
|
|
}
|
|
|
|
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 a local name for the tag
|
|
// (if no local name is provided).
|
|
ref, err := reference.ParseNormalizedNamed(opts.remote)
|
|
if err != nil {
|
|
return types.PluginInstallOptions{}, err
|
|
}
|
|
|
|
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
|
if err != nil {
|
|
return types.PluginInstallOptions{}, err
|
|
}
|
|
|
|
remote := ref.String()
|
|
|
|
_, isCanonical := ref.(reference.Canonical)
|
|
if command.IsTrusted() && !isCanonical {
|
|
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 types.PluginInstallOptions{}, err
|
|
}
|
|
remote = reference.FamiliarString(trusted)
|
|
}
|
|
|
|
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
|
|
|
|
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
|
|
if err != nil {
|
|
return types.PluginInstallOptions{}, err
|
|
}
|
|
registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName)
|
|
|
|
options := types.PluginInstallOptions{
|
|
RegistryAuth: encodedAuth,
|
|
RemoteRef: remote,
|
|
Disabled: opts.disable,
|
|
AcceptAllPermissions: opts.grantPerms,
|
|
AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
|
|
// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
|
|
PrivilegeFunc: registryAuthFunc,
|
|
Args: opts.args,
|
|
}
|
|
return options, nil
|
|
}
|
|
|
|
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`")
|
|
}
|
|
return err
|
|
}
|
|
defer responseBody.Close()
|
|
if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result
|
|
return nil
|
|
}
|
|
|
|
func acceptPrivileges(dockerCli *command.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) {
|
|
return func(privileges types.PluginPrivileges) (bool, error) {
|
|
fmt.Fprintf(dockerCli.Out(), "Plugin %q is requesting the following privileges:\n", name)
|
|
for _, privilege := range privileges {
|
|
fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value)
|
|
}
|
|
|
|
fmt.Fprint(dockerCli.Out(), "Do you grant the above permissions? [y/N] ")
|
|
reader := bufio.NewReader(dockerCli.In())
|
|
line, _, err := reader.ReadLine()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return strings.ToLower(string(line)) == "y", nil
|
|
}
|
|
}
|