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 }