File: hooks.go

package info (click to toggle)
docker.io 26.1.5%2Bdfsg1-9
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 68,576 kB
  • sloc: sh: 5,748; makefile: 912; ansic: 664; asm: 228; python: 162
file content (191 lines) | stat: -rw-r--r-- 5,576 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package manager

import (
	"encoding/json"
	"strings"

	"github.com/docker/cli/cli-plugins/hooks"
	"github.com/docker/cli/cli/command"
	"github.com/sirupsen/logrus"
	"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(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
	commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
	flags := getCommandFlags(subCommand)

	runHooks(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(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
	commandName := strings.Join(args, " ")
	flags := getNaiveFlags(args)

	runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "")
}

func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
	nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)

	hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
}

func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
	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(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
		}

		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))
		}
	}
	return nextSteps
}

// 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
}

// 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
}