-
Notifications
You must be signed in to change notification settings - Fork 2.1k
cli-plugins: separate hook types from manager and refactor #6859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+416
−165
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
5343bdc
cli-plugins/manager: Plugin.RunHook: improve error message
thaJeztah 0501cf8
cli-plugins/manager: simplify ctx-cancel check
thaJeztah dd91ed3
cli-plugins/manager: refactor for easier debugging
thaJeztah 6018092
cli-plugins/manager: move HookPluginData to hooks.Request
thaJeztah 607ebfc
cli-plugins/hooks: rename HookMessage to Response
thaJeztah 0431e4d
cli-plugins/hooks: rename HookType to ResponseType
thaJeztah e26f94d
cli-plugins/hooks: add JSON labels, omitzero
thaJeztah dce201d
cli-plugins/hooks: move template utils separate from render code
thaJeztah aadfe62
cli-plugins/hooks: update tests
thaJeztah cd05360
cli-plugins/hooks: slight tweaks in templates
thaJeztah 4142d40
cli-plugins/hooks: detect if templating is needed
thaJeztah 4a1b2ef
cli-plugins/hooks: update godoc
thaJeztah 9243240
cli-plugins/hooks: add commandInfo type for templating
thaJeztah dd1f7f5
cli-plugins/hooks: simplify templating formats
thaJeztah 4bf4d56
cli-plugins/hooks: PrintNextSteps: slight cleanup
thaJeztah File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: | ||
| //go:build go1.24 | ||
|
|
||
| // Package hooks defines the contract between the Docker CLI and CLI plugin hook | ||
| // implementations. | ||
| // | ||
| // # Audience | ||
| // | ||
| // This package is intended to be imported by CLI plugin implementations that | ||
| // implement a "hooks" subcommand, and by the Docker CLI when invoking those | ||
| // hooks. | ||
| // | ||
| // # Contract and wire format | ||
| // | ||
| // Hook inputs (see [Request]) are serialized as JSON and passed to the plugin hook | ||
| // subcommand (currently as a command-line argument). Hook outputs are emitted by | ||
| // the plugin as JSON (see [Response]). | ||
| // | ||
| // # Stability | ||
| // | ||
| // The types that represent the hook contract ([Request], [Response] and related | ||
| // constants) are considered part of Docker CLI's public Go API. | ||
| // Fields and values may be extended in a backwards-compatible way (for example, | ||
| // adding new fields), but existing fields and their meaning should remain stable. | ||
| // Plugins should ignore unknown fields and unknown hook types to remain | ||
| // forwards-compatible. | ||
| package hooks | ||
|
|
||
| // ResponseType is the type of response from the plugin. | ||
| type ResponseType int | ||
|
|
||
| const ( | ||
| NextSteps ResponseType = 0 | ||
| ) | ||
|
|
||
| // Request is the type representing the information | ||
| // that plugins declaring support for hooks get passed when | ||
| // being invoked following a CLI command execution. | ||
| type Request 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 | ||
| // is invoked with "context". | ||
| RootCmd string `json:"RootCmd,omitzero"` | ||
|
|
||
| // Flags contains flags that were set on the command for which the | ||
| // hook was invoked. It uses flag names as key, with leading hyphens | ||
| // removed ("--flag" and "-flag" are included as "flag" and "f"). | ||
| // | ||
| // Flag values are not included and are set to an empty string, | ||
| // except for boolean flags known to the CLI itself, for which | ||
| // the value is either "true", or "false". | ||
| // | ||
| // Plugins can use this information to adjust their [Response] | ||
| // based on whether the command triggering the hook was invoked | ||
| // with. | ||
| Flags map[string]string `json:"Flags,omitzero"` | ||
|
|
||
| // CommandError is a string containing the error output (if any) | ||
| // of the command for which the hook was invoked. | ||
| CommandError string `json:"CommandError,omitzero"` | ||
| } | ||
|
|
||
| // Response represents a plugin hook response. Plugins | ||
| // declaring support for CLI hooks need to print a JSON | ||
| // representation of this type when their hook subcommand | ||
| // is invoked. | ||
| type Response struct { | ||
| Type ResponseType `json:"Type"` | ||
| Template string `json:"Template,omitzero"` | ||
| } | ||
|
|
||
| // HookType is the type of response from the plugin. | ||
| // | ||
| // Deprecated: use [ResponseType] instead. | ||
| // | ||
| //go:fix inline | ||
| type HookType = ResponseType | ||
|
|
||
| // HookMessage represents a plugin hook response. | ||
| // | ||
| // Deprecated: use [Response] instead. | ||
| // | ||
| //go:fix inline | ||
| type HookMessage = Response | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package hooks | ||
|
|
||
| import ( | ||
| "fmt" | ||
| ) | ||
|
|
||
| const ( | ||
| hookTemplateCommandName = `{{command}}` | ||
| hookTemplateFlagValue = `{{flagValue %q}}` | ||
| hookTemplateArg = `{{argValue %d}}` | ||
| ) | ||
|
|
||
| // TemplateReplaceSubcommandName returns a hook template string | ||
| // that will be replaced by the CLI subcommand being executed | ||
| // | ||
| // Example: | ||
| // | ||
| // Response{ | ||
| // Type: NextSteps, | ||
| // Template: "you ran the subcommand: " + TemplateReplaceSubcommandName(), | ||
| // } | ||
| // | ||
| // When being executed after the command: | ||
| // | ||
| // docker run --name "my-container" alpine | ||
| // | ||
| // It results in the message: | ||
| // | ||
| // you ran the subcommand: run | ||
| func TemplateReplaceSubcommandName() string { | ||
| return hookTemplateCommandName | ||
| } | ||
|
|
||
| // TemplateReplaceFlagValue returns a hook template string that will be | ||
| // replaced with the flags value when printed by the CLI. | ||
| // | ||
| // Example: | ||
| // | ||
| // Response{ | ||
| // Type: NextSteps, | ||
| // Template: "you ran a container named: " + TemplateReplaceFlagValue("name"), | ||
| // } | ||
| // | ||
| // when executed after the command: | ||
| // | ||
| // docker run --name "my-container" alpine | ||
| // | ||
| // it results in the message: | ||
| // | ||
| // you ran a container named: my-container | ||
| func TemplateReplaceFlagValue(flag string) string { | ||
| return fmt.Sprintf(hookTemplateFlagValue, flag) | ||
| } | ||
|
|
||
| // TemplateReplaceArg takes an index i and returns a hook | ||
| // template string that the CLI will replace the template with | ||
| // the ith argument after processing the passed flags. | ||
| // | ||
| // Example: | ||
| // | ||
| // Response{ | ||
| // Type: NextSteps, | ||
| // Template: "run this image with `docker run " + TemplateReplaceArg(0) + "`", | ||
| // } | ||
| // | ||
| // when being executed after the command: | ||
| // | ||
| // docker pull alpine | ||
| // | ||
| // It results in the message: | ||
| // | ||
| // Run this image with `docker run alpine` | ||
| func TemplateReplaceArg(i int) string { | ||
| return fmt.Sprintf(hookTemplateArg, i) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package hooks_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/docker/cli/cli-plugins/hooks" | ||
| ) | ||
|
|
||
| func TestTemplateHelpers(t *testing.T) { | ||
| tests := []struct { | ||
| doc string | ||
| got func() string | ||
| want string | ||
| }{ | ||
| { | ||
| doc: "subcommand name", | ||
| got: hooks.TemplateReplaceSubcommandName, | ||
| want: `{{command}}`, | ||
| }, | ||
| { | ||
| doc: "flag value", | ||
| got: func() string { | ||
| return hooks.TemplateReplaceFlagValue("name") | ||
| }, | ||
| want: `{{flagValue "name"}}`, | ||
| }, | ||
| { | ||
| doc: "arg", | ||
| got: func() string { | ||
| return hooks.TemplateReplaceArg(0) | ||
| }, | ||
| want: `{{argValue 0}}`, | ||
| }, | ||
| { | ||
| doc: "arg", | ||
| got: func() string { | ||
| return hooks.TemplateReplaceArg(3) | ||
| }, | ||
| want: `{{argValue 3}}`, | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range tests { | ||
| t.Run(tc.doc, func(t *testing.T) { | ||
| if got := tc.got(); got != tc.want { | ||
| t.Fatalf("expected %q, got %q", tc.want, got) | ||
| } | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,23 @@ | ||
| package hooks | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| import "io" | ||
|
|
||
| "github.com/morikuni/aec" | ||
| const ( | ||
| whatsNext = "\n\033[1mWhat's next:\033[0m\n" | ||
| indent = " " | ||
| ) | ||
|
|
||
| // PrintNextSteps renders list of [NextSteps] messages and writes them | ||
| // to out. It is a no-op if messages is empty. | ||
| func PrintNextSteps(out io.Writer, messages []string) { | ||
| if len(messages) == 0 { | ||
| return | ||
| } | ||
| _, _ = fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:")) | ||
| for _, n := range messages { | ||
| _, _ = fmt.Fprintln(out, " ", n) | ||
|
|
||
| _, _ = io.WriteString(out, whatsNext) | ||
| for _, msg := range messages { | ||
| _, _ = io.WriteString(out, indent) | ||
| _, _ = io.WriteString(out, msg) | ||
| _, _ = io.WriteString(out, "\n") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,45 @@ | ||
| package hooks | ||
| package hooks_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/morikuni/aec" | ||
| "github.com/docker/cli/cli-plugins/hooks" | ||
| "gotest.tools/v3/assert" | ||
| ) | ||
|
|
||
| func TestPrintHookMessages(t *testing.T) { | ||
| testCases := []struct { | ||
| const header = "\n\x1b[1mWhat's next:\x1b[0m\n" | ||
|
|
||
| tests := []struct { | ||
| doc string | ||
| messages []string | ||
| expectedOutput string | ||
| }{ | ||
| { | ||
| messages: []string{}, | ||
| doc: "no messages", | ||
| messages: nil, | ||
| expectedOutput: "", | ||
| }, | ||
| { | ||
| doc: "single message", | ||
| messages: []string{"Bork!"}, | ||
| expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" + | ||
| expectedOutput: header + | ||
| " Bork!\n", | ||
| }, | ||
| { | ||
| doc: "multiple messages", | ||
| messages: []string{"Foo", "bar"}, | ||
| expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" + | ||
| expectedOutput: header + | ||
| " Foo\n" + | ||
| " bar\n", | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
| w := bytes.Buffer{} | ||
| PrintNextSteps(&w, tc.messages) | ||
| assert.Equal(t, w.String(), tc.expectedOutput) | ||
| for _, tc := range tests { | ||
| t.Run(tc.doc, func(t *testing.T) { | ||
| var w strings.Builder | ||
| hooks.PrintNextSteps(&w, tc.messages) | ||
| assert.Equal(t, w.String(), tc.expectedOutput) | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.