mirror of https://github.com/docker/cli.git
175 lines
5.0 KiB
Go
175 lines
5.0 KiB
Go
package manager
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
|
||
"github.com/docker/cli/cli-plugins/hooks"
|
||
"github.com/docker/cli/cli/command"
|
||
"github.com/spf13/cobra"
|
||
"github.com/spf13/pflag"
|
||
)
|
||
|
||
// HookPluginData is the type representing the information
|
||
// that plugins declaring support for hooks get passed when
|
||
// being invoked following a CLI command execution.
|
||
type HookPluginData struct {
|
||
// RootCmd is a string representing the matching hook configuration
|
||
// which is currently being invoked. If a hook for `docker context` is
|
||
// configured and the user executes `docker context ls`, the plugin will
|
||
// be invoked with `context`.
|
||
RootCmd string
|
||
Flags map[string]string
|
||
CommandError string
|
||
}
|
||
|
||
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
|
||
// a main CLI command was executed. It calls the hook subcommand for all
|
||
// present CLI plugins that declare support for hooks in their metadata and
|
||
// parses/prints their responses.
|
||
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
||
flags := getCommandFlags(subCommand)
|
||
|
||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||
}
|
||
|
||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
||
// after a plugin command was just executed by the CLI.
|
||
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||
commandName := strings.Join(args, " ")
|
||
flags := getNaiveFlags(args)
|
||
|
||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
|
||
}
|
||
|
||
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||
|
||
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
||
}
|
||
|
||
func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||
// check if the context was cancelled before invoking hooks
|
||
select {
|
||
case <-ctx.Done():
|
||
return nil
|
||
default:
|
||
}
|
||
|
||
pluginsCfg := dockerCli.ConfigFile().Plugins
|
||
if pluginsCfg == nil {
|
||
return nil
|
||
}
|
||
|
||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||
for pluginName, cfg := range pluginsCfg {
|
||
match, ok := pluginMatch(cfg, subCmdStr)
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
hookReturn, err := p.RunHook(ctx, HookPluginData{
|
||
RootCmd: match,
|
||
Flags: flags,
|
||
CommandError: cmdErrorMessage,
|
||
})
|
||
if err != nil {
|
||
// skip misbehaving plugins, but don't halt execution
|
||
continue
|
||
}
|
||
|
||
var hookMessageData hooks.HookMessage
|
||
err = json.Unmarshal(hookReturn, &hookMessageData)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
// currently the only hook type
|
||
if hookMessageData.Type != hooks.NextSteps {
|
||
continue
|
||
}
|
||
|
||
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
nextSteps = append(nextSteps, processedHook...)
|
||
}
|
||
return nextSteps
|
||
}
|
||
|
||
// pluginMatch takes a plugin configuration and a string representing the
|
||
// command being executed (such as 'image ls' – the root 'docker' is omitted)
|
||
// and, if the configuration includes a hook for the invoked command, returns
|
||
// the configured hook string.
|
||
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
|
||
configuredPluginHooks, ok := pluginCfg["hooks"]
|
||
if !ok || configuredPluginHooks == "" {
|
||
return "", false
|
||
}
|
||
|
||
commands := strings.Split(configuredPluginHooks, ",")
|
||
for _, hookCmd := range commands {
|
||
if hookMatch(hookCmd, subCmd) {
|
||
return hookCmd, true
|
||
}
|
||
}
|
||
|
||
return "", false
|
||
}
|
||
|
||
func hookMatch(hookCmd, subCmd string) bool {
|
||
hookCmdTokens := strings.Split(hookCmd, " ")
|
||
subCmdTokens := strings.Split(subCmd, " ")
|
||
|
||
if len(hookCmdTokens) > len(subCmdTokens) {
|
||
return false
|
||
}
|
||
|
||
for i, v := range hookCmdTokens {
|
||
if v != subCmdTokens[i] {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
func getCommandFlags(cmd *cobra.Command) map[string]string {
|
||
flags := make(map[string]string)
|
||
cmd.Flags().Visit(func(f *pflag.Flag) {
|
||
var fValue string
|
||
if f.Value.Type() == "bool" {
|
||
fValue = f.Value.String()
|
||
}
|
||
flags[f.Name] = fValue
|
||
})
|
||
return flags
|
||
}
|
||
|
||
// getNaiveFlags string-matches argv and parses them into a map.
|
||
// This is used when calling hooks after a plugin command, since
|
||
// in this case we can't rely on the cobra command tree to parse
|
||
// flags in this case. In this case, no values are ever passed,
|
||
// since we don't have enough information to process them.
|
||
func getNaiveFlags(args []string) map[string]string {
|
||
flags := make(map[string]string)
|
||
for _, arg := range args {
|
||
if strings.HasPrefix(arg, "--") {
|
||
flags[arg[2:]] = ""
|
||
continue
|
||
}
|
||
if strings.HasPrefix(arg, "-") {
|
||
flags[arg[1:]] = ""
|
||
}
|
||
}
|
||
return flags
|
||
}
|