2023-07-20 11:25:36 -04:00
|
|
|
|
package manager
|
|
|
|
|
|
|
|
|
|
import (
|
2024-04-26 07:03:56 -04:00
|
|
|
|
"context"
|
2023-07-20 11:25:36 -04:00
|
|
|
|
"encoding/json"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/docker/cli/cli-plugins/hooks"
|
|
|
|
|
"github.com/docker/cli/cli/command"
|
2024-05-17 04:59:54 -04:00
|
|
|
|
"github.com/sirupsen/logrus"
|
2023-07-20 11:25:36 -04:00
|
|
|
|
"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 {
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
// 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`.
|
2024-04-22 12:12:53 -04:00
|
|
|
|
RootCmd string
|
|
|
|
|
Flags map[string]string
|
|
|
|
|
CommandError string
|
2023-07-20 11:25:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
// 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.
|
2024-04-26 07:03:56 -04:00
|
|
|
|
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
|
|
|
|
flags := getCommandFlags(subCommand)
|
|
|
|
|
|
2024-04-26 07:03:56 -04:00
|
|
|
|
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RunPluginHooks is the entrypoint for the hooks execution flow
|
|
|
|
|
// after a plugin command was just executed by the CLI.
|
2024-04-26 07:03:56 -04:00
|
|
|
|
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
commandName := strings.Join(args, " ")
|
|
|
|
|
flags := getNaiveFlags(args)
|
|
|
|
|
|
2024-04-26 07:03:56 -04:00
|
|
|
|
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-26 07:03:56 -04:00
|
|
|
|
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)
|
2023-07-20 11:25:36 -04:00
|
|
|
|
|
|
|
|
|
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-26 07:03:56 -04:00
|
|
|
|
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:
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-20 11:25:36 -04:00
|
|
|
|
pluginsCfg := dockerCli.ConfigFile().Plugins
|
|
|
|
|
if pluginsCfg == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nextSteps := make([]string, 0, len(pluginsCfg))
|
|
|
|
|
for pluginName, cfg := range pluginsCfg {
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
match, ok := pluginMatch(cfg, subCmdStr)
|
|
|
|
|
if !ok {
|
2023-07-20 11:25:36 -04:00
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-26 07:03:56 -04:00
|
|
|
|
hookReturn, err := p.RunHook(ctx, HookPluginData{
|
2024-04-22 12:12:53 -04:00
|
|
|
|
RootCmd: match,
|
|
|
|
|
Flags: flags,
|
|
|
|
|
CommandError: cmdErrorMessage,
|
|
|
|
|
})
|
2023-07-20 11:25:36 -04:00
|
|
|
|
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
|
|
|
|
|
}
|
2024-05-17 04:59:54 -04:00
|
|
|
|
|
|
|
|
|
var appended bool
|
|
|
|
|
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
|
|
|
|
|
if !appended {
|
|
|
|
|
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
|
|
|
|
|
}
|
2023-07-20 11:25:36 -04:00
|
|
|
|
}
|
|
|
|
|
return nextSteps
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-17 04:59:54 -04:00
|
|
|
|
// appendNextSteps appends the processed hook output to the nextSteps slice.
|
|
|
|
|
// If the processed hook output is empty, it is not appended.
|
|
|
|
|
// Empty lines are not stripped if there's at least one non-empty line.
|
|
|
|
|
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
|
|
|
|
empty := true
|
|
|
|
|
for _, l := range processed {
|
|
|
|
|
if strings.TrimSpace(l) != "" {
|
|
|
|
|
empty = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if empty {
|
|
|
|
|
return nextSteps, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return append(nextSteps, processed...), true
|
|
|
|
|
}
|
|
|
|
|
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
// 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
|
2023-07-20 11:25:36 -04:00
|
|
|
|
}
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
|
|
|
|
|
commands := strings.Split(configuredPluginHooks, ",")
|
2023-07-20 11:25:36 -04:00
|
|
|
|
for _, hookCmd := range commands {
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
if hookMatch(hookCmd, subCmd) {
|
|
|
|
|
return hookCmd, true
|
2023-07-20 11:25:36 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
hooks: include full configured command
Before, for plugin commands, only the plugin name (such as `buildx`)
would be both included as `RootCmd` when passed to the hook plugin,
which isn't enough information for a plugin to decide whether to execute
a hook or not since plugins implement multiple varied commands (`buildx
build`, `buildx prune`, etc.).
This commit changes the hook logic to account for this situation, so
that the the entire configured hook is passed, i.e., if a user has a
hook configured for `buildx imagetools inspect` and the command
`docker buildx imagetools inspect alpine` is called, then the plugin
hooks will be passed `buildx imagetools inspect`.
This logic works for aliased commands too, so whether `docker build ...`
or `docker buildx build` is executed (unless Buildx is disabled) the
hook will be invoked with `buildx build`.
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
hooks: include full match when invoking plugins
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-19 07:32:18 -04:00
|
|
|
|
|
|
|
|
|
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
|
2023-07-20 11:25:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|