Skip to content

Commit 1c23ad4

Browse files
committed
Fix: run plugin hooks on command failure, not just success
Signed-off-by: Derek Misler <derek.misler@docker.com>
1 parent e30ce84 commit 1c23ad4

File tree

3 files changed

+88
-5
lines changed

3 files changed

+88
-5
lines changed

cli-plugins/manager/hooks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd,
4242

4343
// RunPluginHooks is the entrypoint for the hooks execution flow
4444
// after a plugin command was just executed by the CLI.
45-
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) {
45+
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string, cmdErrorMessage string) {
4646
commandName := strings.Join(args, " ")
4747
flags := getNaiveFlags(args)
4848

49-
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, "")
49+
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
5050
}
5151

5252
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {

cli-plugins/manager/hooks_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
package manager
22

33
import (
4+
"context"
45
"testing"
56

7+
"github.com/docker/cli/cli/config/configfile"
8+
"github.com/spf13/cobra"
69
"gotest.tools/v3/assert"
710
is "gotest.tools/v3/assert/cmp"
811
)
912

13+
type fakeConfigProvider struct {
14+
cfg *configfile.ConfigFile
15+
}
16+
17+
func (f *fakeConfigProvider) ConfigFile() *configfile.ConfigFile {
18+
return f.cfg
19+
}
20+
1021
func TestGetNaiveFlags(t *testing.T) {
1122
testCases := []struct {
1223
args []string
@@ -141,3 +152,71 @@ func TestAppendNextSteps(t *testing.T) {
141152
})
142153
}
143154
}
155+
156+
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
157+
cfg := configfile.New("")
158+
cfg.Plugins = map[string]map[string]string{
159+
"test-plugin": {"hooks": "build"},
160+
}
161+
provider := &fakeConfigProvider{cfg: cfg}
162+
root := &cobra.Command{Use: "docker"}
163+
sub := &cobra.Command{Use: "build"}
164+
root.AddCommand(sub)
165+
166+
// Should not panic with empty error message (success case)
167+
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
168+
169+
// Should not panic with non-empty error message (failure case)
170+
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
171+
}
172+
173+
func TestInvokeAndCollectHooksForwardsErrorMessage(t *testing.T) {
174+
cfg := configfile.New("")
175+
cfg.Plugins = map[string]map[string]string{
176+
"nonexistent": {"hooks": "build"},
177+
}
178+
root := &cobra.Command{Use: "docker"}
179+
sub := &cobra.Command{Use: "build"}
180+
root.AddCommand(sub)
181+
182+
// Plugin binary doesn't exist — invokeAndCollectHooks skips it
183+
// gracefully and returns empty. Verifies the error message path
184+
// doesn't cause issues when forwarded through the call chain.
185+
result := invokeAndCollectHooks(
186+
context.Background(), cfg, root, sub,
187+
"build", map[string]string{}, "exit status 1",
188+
)
189+
assert.Check(t, is.Len(result, 0))
190+
}
191+
192+
func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
193+
cfg := configfile.New("")
194+
root := &cobra.Command{Use: "docker"}
195+
sub := &cobra.Command{Use: "build"}
196+
root.AddCommand(sub)
197+
198+
result := invokeAndCollectHooks(
199+
context.Background(), cfg, root, sub,
200+
"build", map[string]string{}, "some error",
201+
)
202+
assert.Check(t, is.Len(result, 0))
203+
}
204+
205+
func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
206+
cfg := configfile.New("")
207+
cfg.Plugins = map[string]map[string]string{
208+
"test-plugin": {"hooks": "build"},
209+
}
210+
root := &cobra.Command{Use: "docker"}
211+
sub := &cobra.Command{Use: "build"}
212+
root.AddCommand(sub)
213+
214+
ctx, cancel := context.WithCancel(context.Background())
215+
cancel() // cancel immediately
216+
217+
result := invokeAndCollectHooks(
218+
ctx, cfg, root, sub,
219+
"build", map[string]string{}, "exit status 1",
220+
)
221+
assert.Check(t, is.Nil(result))
222+
}

cmd/docker/docker.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,14 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
480480
subCommand = ccmd
481481
if err != nil || pluginmanager.IsPluginCommand(ccmd) {
482482
err := tryPluginRun(ctx, dockerCli, cmd, args[0], envs)
483-
if err == nil {
484-
if ccmd != nil && dockerCli.Out().IsTerminal() && dockerCli.HooksEnabled() {
485-
pluginmanager.RunPluginHooks(ctx, dockerCli, cmd, ccmd, args)
483+
if ccmd != nil && dockerCli.Out().IsTerminal() && dockerCli.HooksEnabled() {
484+
var errMessage string
485+
if err != nil {
486+
errMessage = err.Error()
486487
}
488+
pluginmanager.RunPluginHooks(ctx, dockerCli, cmd, ccmd, args, errMessage)
489+
}
490+
if err == nil {
487491
return nil
488492
}
489493
if !errdefs.IsNotFound(err) {

0 commit comments

Comments
 (0)