github.1git.de/docker/cli@v26.1.3+incompatible/cli-plugins/manager/hooks.go (about)

     1  package manager
     2  
     3  import (
     4  	"encoding/json"
     5  	"strings"
     6  
     7  	"github.com/docker/cli/cli-plugins/hooks"
     8  	"github.com/docker/cli/cli/command"
     9  	"github.com/spf13/cobra"
    10  	"github.com/spf13/pflag"
    11  )
    12  
    13  // HookPluginData is the type representing the information
    14  // that plugins declaring support for hooks get passed when
    15  // being invoked following a CLI command execution.
    16  type HookPluginData struct {
    17  	// RootCmd is a string representing the matching hook configuration
    18  	// which is currently being invoked. If a hook for `docker context` is
    19  	// configured and the user executes `docker context ls`, the plugin will
    20  	// be invoked with `context`.
    21  	RootCmd      string
    22  	Flags        map[string]string
    23  	CommandError string
    24  }
    25  
    26  // RunCLICommandHooks is the entrypoint into the hooks execution flow after
    27  // a main CLI command was executed. It calls the hook subcommand for all
    28  // present CLI plugins that declare support for hooks in their metadata and
    29  // parses/prints their responses.
    30  func RunCLICommandHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
    31  	commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
    32  	flags := getCommandFlags(subCommand)
    33  
    34  	runHooks(dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
    35  }
    36  
    37  // RunPluginHooks is the entrypoint for the hooks execution flow
    38  // after a plugin command was just executed by the CLI.
    39  func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
    40  	commandName := strings.Join(args, " ")
    41  	flags := getNaiveFlags(args)
    42  
    43  	runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "")
    44  }
    45  
    46  func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
    47  	nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
    48  
    49  	hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
    50  }
    51  
    52  func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
    53  	pluginsCfg := dockerCli.ConfigFile().Plugins
    54  	if pluginsCfg == nil {
    55  		return nil
    56  	}
    57  
    58  	nextSteps := make([]string, 0, len(pluginsCfg))
    59  	for pluginName, cfg := range pluginsCfg {
    60  		match, ok := pluginMatch(cfg, subCmdStr)
    61  		if !ok {
    62  			continue
    63  		}
    64  
    65  		p, err := GetPlugin(pluginName, dockerCli, rootCmd)
    66  		if err != nil {
    67  			continue
    68  		}
    69  
    70  		hookReturn, err := p.RunHook(HookPluginData{
    71  			RootCmd:      match,
    72  			Flags:        flags,
    73  			CommandError: cmdErrorMessage,
    74  		})
    75  		if err != nil {
    76  			// skip misbehaving plugins, but don't halt execution
    77  			continue
    78  		}
    79  
    80  		var hookMessageData hooks.HookMessage
    81  		err = json.Unmarshal(hookReturn, &hookMessageData)
    82  		if err != nil {
    83  			continue
    84  		}
    85  
    86  		// currently the only hook type
    87  		if hookMessageData.Type != hooks.NextSteps {
    88  			continue
    89  		}
    90  
    91  		processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
    92  		if err != nil {
    93  			continue
    94  		}
    95  		nextSteps = append(nextSteps, processedHook...)
    96  	}
    97  	return nextSteps
    98  }
    99  
   100  // pluginMatch takes a plugin configuration and a string representing the
   101  // command being executed (such as 'image ls' – the root 'docker' is omitted)
   102  // and, if the configuration includes a hook for the invoked command, returns
   103  // the configured hook string.
   104  func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
   105  	configuredPluginHooks, ok := pluginCfg["hooks"]
   106  	if !ok || configuredPluginHooks == "" {
   107  		return "", false
   108  	}
   109  
   110  	commands := strings.Split(configuredPluginHooks, ",")
   111  	for _, hookCmd := range commands {
   112  		if hookMatch(hookCmd, subCmd) {
   113  			return hookCmd, true
   114  		}
   115  	}
   116  
   117  	return "", false
   118  }
   119  
   120  func hookMatch(hookCmd, subCmd string) bool {
   121  	hookCmdTokens := strings.Split(hookCmd, " ")
   122  	subCmdTokens := strings.Split(subCmd, " ")
   123  
   124  	if len(hookCmdTokens) > len(subCmdTokens) {
   125  		return false
   126  	}
   127  
   128  	for i, v := range hookCmdTokens {
   129  		if v != subCmdTokens[i] {
   130  			return false
   131  		}
   132  	}
   133  
   134  	return true
   135  }
   136  
   137  func getCommandFlags(cmd *cobra.Command) map[string]string {
   138  	flags := make(map[string]string)
   139  	cmd.Flags().Visit(func(f *pflag.Flag) {
   140  		var fValue string
   141  		if f.Value.Type() == "bool" {
   142  			fValue = f.Value.String()
   143  		}
   144  		flags[f.Name] = fValue
   145  	})
   146  	return flags
   147  }
   148  
   149  // getNaiveFlags string-matches argv and parses them into a map.
   150  // This is used when calling hooks after a plugin command, since
   151  // in this case we can't rely on the cobra command tree to parse
   152  // flags in this case. In this case, no values are ever passed,
   153  // since we don't have enough information to process them.
   154  func getNaiveFlags(args []string) map[string]string {
   155  	flags := make(map[string]string)
   156  	for _, arg := range args {
   157  		if strings.HasPrefix(arg, "--") {
   158  			flags[arg[2:]] = ""
   159  			continue
   160  		}
   161  		if strings.HasPrefix(arg, "-") {
   162  			flags[arg[1:]] = ""
   163  		}
   164  	}
   165  	return flags
   166  }