Skip to content

Commit bebdb71

Browse files
author
Ian Campbell
committed
Allow plugins to make use of the cobra PersistentPreRun hooks.
Previously a plugin which used these hooks would overwrite the top-level plugin command's use of this hook, resulting in the dockerCli object not being fully initialised. Provide a function which plugins can use to chain to the required behaviour. This required some fairly ugly arrangements to preserve state (which was previously in-scope in `newPluginCOmmand`) to be used by the new function. Signed-off-by: Ian Campbell <[email protected]>
1 parent 9963126 commit bebdb71

File tree

3 files changed

+74
-17
lines changed

3 files changed

+74
-17
lines changed

cli-plugins/examples/helloworld/main.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

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

67
cliplugins "github.com/docker/cli/cli-plugins"
@@ -18,16 +19,33 @@ func main() {
1819
fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
1920
},
2021
}
22+
apiversion := &cobra.Command{
23+
Use: "apiversion",
24+
Short: "Print the API version of the server",
25+
RunE: func(_ *cobra.Command, _ []string) error {
26+
cli := dockerCli.Client()
27+
ping, err := cli.Ping(context.Background())
28+
if err != nil {
29+
return err
30+
}
31+
fmt.Println(ping.APIVersion)
32+
return nil
33+
},
34+
}
2135

2236
cmd := &cobra.Command{
2337
Use: "helloworld",
2438
Short: "A basic Hello World plugin for tests",
39+
// This is redundant but included to exercise
40+
// the path where a plugin overrides this
41+
// hook.
42+
PersistentPreRunE: plugin.PersistentPreRunE,
2543
Run: func(cmd *cobra.Command, args []string) {
2644
fmt.Fprintln(dockerCli.Out(), "Hello World!")
2745
},
2846
}
2947

30-
cmd.AddCommand(goodbye)
48+
cmd.AddCommand(goodbye, apiversion)
3149
return cmd
3250
},
3351
cliplugins.Metadata{

cli-plugins/plugin/plugin.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7+
"sync"
78

89
"github.com/docker/cli/cli"
910
cliplugins "github.com/docker/cli/cli-plugins"
@@ -44,29 +45,53 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta cliplugins.Metadata) {
4445
}
4546
}
4647

47-
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta cliplugins.Metadata) *cobra.Command {
48-
var (
49-
opts *cliflags.ClientOptions
50-
flags *pflag.FlagSet
51-
)
48+
// options encapsulates the ClientOptions and FlagSet constructed by
49+
// `newPluginCommand` such that they can be finalized by our
50+
// `PersistentPreRunE`. This is necessary because otherwise a plugin's
51+
// own use of that hook will shadow anything we add to the top-level
52+
// command meaning the CLI is never Initialized.
53+
var options struct {
54+
init, prerun sync.Once
55+
opts *cliflags.ClientOptions
56+
flags *pflag.FlagSet
57+
dockerCli *command.DockerCli
58+
}
59+
60+
// PersistentPreRunE must be called by any plugin command (or
61+
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
62+
// which do not make use of `PersistentPreRun*` do not need to call
63+
// this (although it remains safe to do so). Plugins are recommended
64+
// to use `PersistenPreRunE` to enable the error to be
65+
// returned. Should not be called outside of a commands
66+
// PersistentPreRunE hook and must not be run unless Run has been
67+
// called.
68+
func PersistentPreRunE(cmd *cobra.Command, args []string) error {
69+
var err error
70+
options.prerun.Do(func() {
71+
if options.opts == nil || options.flags == nil || options.dockerCli == nil {
72+
panic("PersistentPreRunE called without Run successfully called first")
73+
}
74+
// flags must be the original top-level command flags, not cmd.Flags()
75+
options.opts.Common.SetDefaultOptions(options.flags)
76+
err = options.dockerCli.Initialize(options.opts)
77+
})
78+
return err
79+
}
5280

81+
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta cliplugins.Metadata) *cobra.Command {
5382
name := plugin.Use
5483
fullname := cliplugins.NamePrefix + name
5584

5685
cmd := &cobra.Command{
57-
Use: "docker" + " [OPTIONS] " + name + " [ARG...]",
58-
Short: fullname + " is a Docker CLI plugin",
59-
SilenceUsage: true,
60-
SilenceErrors: true,
61-
TraverseChildren: true,
62-
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
63-
// flags must be the top-level command flags, not cmd.Flags()
64-
opts.Common.SetDefaultOptions(flags)
65-
return dockerCli.Initialize(opts)
66-
},
86+
Use: "docker" + " [OPTIONS] " + name + " [ARG...]",
87+
Short: fullname + " is a Docker CLI plugin",
88+
SilenceUsage: true,
89+
SilenceErrors: true,
90+
TraverseChildren: true,
91+
PersistentPreRunE: PersistentPreRunE,
6792
DisableFlagsInUseLine: true,
6893
}
69-
opts, flags = cli.SetupPluginRootCommand(cmd)
94+
opts, flags := cli.SetupPluginRootCommand(cmd)
7095

7196
cmd.SetOutput(dockerCli.Out())
7297

@@ -77,6 +102,11 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
77102

78103
cli.DisableFlagsInUseLine(cmd)
79104

105+
options.init.Do(func() {
106+
options.opts = opts
107+
options.flags = flags
108+
options.dockerCli = dockerCli
109+
})
80110
return cmd
81111
}
82112

e2e/cli-plugins/run_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,12 @@ func TestGoodHelp(t *testing.T) {
104104
golden.Assert(t, res.Stdout(), "docker-help-helloworld.golden")
105105
assert.Assert(t, is.Equal(res.Stderr(), ""))
106106
}
107+
108+
// TestCliInitialized tests the code paths which ensure that the Cli
109+
// object is initialized even if the plugin uses PersistentRunE
110+
func TestCliInitialized(t *testing.T) {
111+
res := icmd.RunCmd(icmd.Command("docker", "helloworld", "apiversion"))
112+
res.Assert(t, icmd.Success)
113+
assert.Assert(t, res.Stdout() != "")
114+
assert.Assert(t, is.Equal(res.Stderr(), ""))
115+
}

0 commit comments

Comments
 (0)