Merge pull request #5030 from laurazard/hooks-plugin-name

hooks: include plugin name in hook data
This commit is contained in:
Paweł Gronowski 2024-04-22 17:22:08 +02:00 committed by GitHub
commit d8fc76ea56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 138 additions and 32 deletions

View File

@ -14,31 +14,41 @@ import (
// that plugins declaring support for hooks get passed when // that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution. // being invoked following a CLI command execution.
type HookPluginData struct { 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 RootCmd string
Flags map[string]string Flags map[string]string
} }
// RunPluginHooks calls the hook subcommand for all present // RunCLICommandHooks is the entrypoint into the hooks execution flow after
// CLI plugins that declare support for hooks in their metadata // a main CLI command was executed. It calls the hook subcommand for all
// and parses/prints their responses. // present CLI plugins that declare support for hooks in their metadata and
func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, plugin string, args []string) error { // parses/prints their responses.
subCmdName := subCommand.Name() func RunCLICommandHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command) {
if plugin != "" { commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
subCmdName = plugin flags := getCommandFlags(subCommand)
runHooks(dockerCli, rootCmd, subCommand, commandName, flags)
} }
var flags map[string]string
if plugin == "" { // RunPluginHooks is the entrypoint for the hooks execution flow
flags = getCommandFlags(subCommand) // after a plugin command was just executed by the CLI.
} else { func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
flags = getNaiveFlags(args) commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(dockerCli, rootCmd, subCommand, commandName, flags)
} }
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, subCmdName, flags)
func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string) {
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps) hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
return nil
} }
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, hookCmdName string, flags map[string]string) []string { func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string) []string {
pluginsCfg := dockerCli.ConfigFile().Plugins pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil { if pluginsCfg == nil {
return nil return nil
@ -46,7 +56,8 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command
nextSteps := make([]string, 0, len(pluginsCfg)) nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg { for pluginName, cfg := range pluginsCfg {
if !registersHook(cfg, hookCmdName) { match, ok := pluginMatch(cfg, subCmdStr)
if !ok {
continue continue
} }
@ -55,7 +66,7 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command
continue continue
} }
hookReturn, err := p.RunHook(hookCmdName, flags) hookReturn, err := p.RunHook(match, flags)
if err != nil { if err != nil {
// skip misbehaving plugins, but don't halt execution // skip misbehaving plugins, but don't halt execution
continue continue
@ -81,20 +92,43 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command
return nextSteps return nextSteps
} }
func registersHook(pluginCfg map[string]string, subCmdName string) bool { // pluginMatch takes a plugin configuration and a string representing the
hookCmdStr, ok := pluginCfg["hooks"] // command being executed (such as 'image ls' the root 'docker' is omitted)
if !ok { // and, if the configuration includes a hook for the invoked command, returns
return false // 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(hookCmdStr, ",")
commands := strings.Split(configuredPluginHooks, ",")
for _, hookCmd := range commands { for _, hookCmd := range commands {
if hookCmd == subCmdName { if hookMatch(hookCmd, subCmd) {
return true 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 return false
} }
for i, v := range hookCmdTokens {
if v != subCmdTokens[i] {
return false
}
}
return true
}
func getCommandFlags(cmd *cobra.Command) map[string]string { func getCommandFlags(cmd *cobra.Command) map[string]string {
flags := make(map[string]string) flags := make(map[string]string)
cmd.Flags().Visit(func(f *pflag.Flag) { cmd.Flags().Visit(func(f *pflag.Flag) {

View File

@ -36,3 +36,75 @@ func TestGetNaiveFlags(t *testing.T) {
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags) assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
} }
} }
func TestPluginMatch(t *testing.T) {
testCases := []struct {
commandString string
pluginConfig map[string]string
expectedMatch string
expectedOk bool
}{
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "build",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "context ls",
},
expectedMatch: "context ls",
expectedOk: true,
},
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image ls,image",
},
expectedMatch: "image ls",
expectedOk: true,
},
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image i",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
}
for _, tc := range testCases {
match, ok := pluginMatch(tc.pluginConfig, tc.commandString)
assert.Equal(t, ok, tc.expectedOk)
assert.Equal(t, match, tc.expectedMatch)
}
}

View File

@ -336,7 +336,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
err := tryPluginRun(dockerCli, cmd, args[0], envs) err := tryPluginRun(dockerCli, cmd, args[0], envs)
if err == nil { if err == nil {
if dockerCli.HooksEnabled() && dockerCli.Out().IsTerminal() && ccmd != nil { if dockerCli.HooksEnabled() && dockerCli.Out().IsTerminal() && ccmd != nil {
_ = pluginmanager.RunPluginHooks(dockerCli, cmd, ccmd, args[0], args) pluginmanager.RunPluginHooks(dockerCli, cmd, ccmd, args)
} }
return nil return nil
} }
@ -354,10 +354,10 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
cmd.SetArgs(args) cmd.SetArgs(args)
err = cmd.Execute() err = cmd.Execute()
// If the command is being executed in an interactive terminal, // If the command is being executed in an interactive terminal
// run the plugin hooks (but don't throw an error if something misbehaves) // and hook are enabled, run the plugin hooks.
if dockerCli.HooksEnabled() && dockerCli.Out().IsTerminal() && subCommand != nil { if dockerCli.HooksEnabled() && dockerCli.Out().IsTerminal() && subCommand != nil {
_ = pluginmanager.RunPluginHooks(dockerCli, cmd, subCommand, "", args) pluginmanager.RunCLICommandHooks(dockerCli, cmd, subCommand)
} }
return err return err