mirror of https://github.com/docker/cli.git
Integrate CLI plugins into `docker help` output.
To do this we add a stub `cobra.Command` for each installed plugin (only when invoking `help`, not for normal running). This requires a function to list all available plugins so that is added here. Signed-off-by: Ian Campbell <ijc@docker.com>
This commit is contained in:
parent
5db336798c
commit
f912b55bd1
|
@ -0,0 +1,57 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CommandAnnotationPlugin is added to every stub command added by
|
||||||
|
// AddPluginCommandStubs with the value "true" and so can be
|
||||||
|
// used to distinguish plugin stubs from regular commands.
|
||||||
|
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
||||||
|
|
||||||
|
// CommandAnnotationPluginVendor is added to every stub command
|
||||||
|
// added by AddPluginCommandStubs and contains the vendor of
|
||||||
|
// that plugin.
|
||||||
|
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
||||||
|
|
||||||
|
// CommandAnnotationPluginInvalid is added to any stub command
|
||||||
|
// added by AddPluginCommandStubs for an invalid command (that
|
||||||
|
// is, one which failed it's candidate test) and contains the
|
||||||
|
// reason for the failure.
|
||||||
|
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddPluginCommandStubs adds a stub cobra.Commands for each plugin
|
||||||
|
// (optionally including invalid ones). The command stubs will have
|
||||||
|
// several annotations added, see `CommandAnnotationPlugin*`.
|
||||||
|
func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command, includeInvalid bool) error {
|
||||||
|
plugins, err := ListPlugins(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range plugins {
|
||||||
|
if !includeInvalid && p.Err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vendor := p.Vendor
|
||||||
|
if vendor == "" {
|
||||||
|
vendor = "unknown"
|
||||||
|
}
|
||||||
|
annotations := map[string]string{
|
||||||
|
CommandAnnotationPlugin: "true",
|
||||||
|
CommandAnnotationPluginVendor: vendor,
|
||||||
|
}
|
||||||
|
if p.Err != nil {
|
||||||
|
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||||
|
}
|
||||||
|
cmd.AddCommand(&cobra.Command{
|
||||||
|
Use: p.Name,
|
||||||
|
Short: p.ShortDescription,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {},
|
||||||
|
Annotations: annotations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
|
@ -41,6 +43,81 @@ func getPluginDirs(dockerCli command.Cli) []string {
|
||||||
return pluginDirs
|
return pluginDirs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||||
|
dentries, err := ioutil.ReadDir(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, dentry := range dentries {
|
||||||
|
switch dentry.Mode() & os.ModeType {
|
||||||
|
case 0, os.ModeSymlink:
|
||||||
|
// Regular file or symlink, keep going
|
||||||
|
default:
|
||||||
|
// Something else, ignore.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := dentry.Name()
|
||||||
|
if !strings.HasPrefix(name, NamePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = strings.TrimPrefix(name, NamePrefix)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
exe := ".exe"
|
||||||
|
if !strings.HasSuffix(name, exe) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = strings.TrimSuffix(name, exe)
|
||||||
|
}
|
||||||
|
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
||||||
|
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
||||||
|
result := make(map[string][]string)
|
||||||
|
for _, d := range dirs {
|
||||||
|
// Silently ignore any directories which we cannot
|
||||||
|
// Stat (e.g. due to permissions or anything else) or
|
||||||
|
// which is not a directory.
|
||||||
|
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
||||||
|
// Silently ignore paths which don't exist.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err // Or return partial result?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPlugins produces a list of the plugins available on the system
|
||||||
|
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||||
|
candidates, err := listPluginCandidates(getPluginDirs(dockerCli))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugins []Plugin
|
||||||
|
for _, paths := range candidates {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c := &candidate{paths[0]}
|
||||||
|
p, err := newPlugin(c, rootcmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.ShadowedPaths = paths[1:]
|
||||||
|
plugins = append(plugins, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
|
||||||
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||||
|
|
|
@ -7,8 +7,79 @@ import (
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
|
"gotest.tools/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestListPluginCandidates(t *testing.T) {
|
||||||
|
// Populate a selection of directories with various shadowed and bogus/obscure plugin candidates.
|
||||||
|
// For the purposes of this test no contents is required and permissions are irrelevant.
|
||||||
|
dir := fs.NewDir(t, t.Name(),
|
||||||
|
fs.WithDir(
|
||||||
|
"plugins1",
|
||||||
|
fs.WithFile("docker-plugin1", ""), // This appears in each directory
|
||||||
|
fs.WithFile("not-a-plugin", ""), // Should be ignored
|
||||||
|
fs.WithFile("docker-symlinked1", ""), // This and ...
|
||||||
|
fs.WithSymlink("docker-symlinked2", "docker-symlinked1"), // ... this should both appear
|
||||||
|
fs.WithDir("ignored1"), // A directory should be ignored
|
||||||
|
),
|
||||||
|
fs.WithDir(
|
||||||
|
"plugins2",
|
||||||
|
fs.WithFile("docker-plugin1", ""),
|
||||||
|
fs.WithFile("also-not-a-plugin", ""),
|
||||||
|
fs.WithFile("docker-hardlink1", ""), // This and ...
|
||||||
|
fs.WithHardlink("docker-hardlink2", "docker-hardlink1"), // ... this should both appear
|
||||||
|
fs.WithDir("ignored2"),
|
||||||
|
),
|
||||||
|
fs.WithDir(
|
||||||
|
"plugins3-target", // Will be referenced as a symlink from below
|
||||||
|
fs.WithFile("docker-plugin1", ""),
|
||||||
|
fs.WithDir("ignored3"),
|
||||||
|
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||||
|
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||||
|
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||||
|
),
|
||||||
|
fs.WithSymlink("plugins3", "plugins3-target"),
|
||||||
|
fs.WithFile("/plugins4", ""),
|
||||||
|
fs.WithSymlink("plugins5", "plugins5-nonexistent-target"),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
|
||||||
|
var dirs []string
|
||||||
|
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
|
||||||
|
dirs = append(dirs, dir.Join(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates, err := listPluginCandidates(dirs)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
exp := map[string][]string{
|
||||||
|
"plugin1": {
|
||||||
|
dir.Join("plugins1", "docker-plugin1"),
|
||||||
|
dir.Join("plugins2", "docker-plugin1"),
|
||||||
|
dir.Join("plugins3", "docker-plugin1"),
|
||||||
|
},
|
||||||
|
"symlinked1": {
|
||||||
|
dir.Join("plugins1", "docker-symlinked1"),
|
||||||
|
},
|
||||||
|
"symlinked2": {
|
||||||
|
dir.Join("plugins1", "docker-symlinked2"),
|
||||||
|
},
|
||||||
|
"hardlink1": {
|
||||||
|
dir.Join("plugins2", "docker-hardlink1"),
|
||||||
|
},
|
||||||
|
"hardlink2": {
|
||||||
|
dir.Join("plugins2", "docker-hardlink2"),
|
||||||
|
},
|
||||||
|
"brokensymlink": {
|
||||||
|
dir.Join("plugins3", "docker-brokensymlink"),
|
||||||
|
},
|
||||||
|
"symlinked": {
|
||||||
|
dir.Join("plugins3", "docker-symlinked"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.DeepEqual(t, candidates, exp)
|
||||||
|
}
|
||||||
|
|
||||||
func TestErrPluginNotFound(t *testing.T) {
|
func TestErrPluginNotFound(t *testing.T) {
|
||||||
var err error = errPluginNotFound("test")
|
var err error = errPluginNotFound("test")
|
||||||
err.(errPluginNotFound).NotFound()
|
err.(errPluginNotFound).NotFound()
|
||||||
|
|
|
@ -69,6 +69,12 @@ func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
|
||||||
|
|
||||||
if rootcmd != nil {
|
if rootcmd != nil {
|
||||||
for _, cmd := range rootcmd.Commands() {
|
for _, cmd := range rootcmd.Commands() {
|
||||||
|
// Ignore conflicts with commands which are
|
||||||
|
// just plugin stubs (i.e. from a previous
|
||||||
|
// call to AddPluginCommandStubs).
|
||||||
|
if p := cmd.Annotations[CommandAnnotationPlugin]; p == "true" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if cmd.Name() == p.Name {
|
if cmd.Name() == p.Name {
|
||||||
p.Err = errors.Errorf("plugin %q duplicates builtin command", p.Name)
|
p.Err = errors.Errorf("plugin %q duplicates builtin command", p.Name)
|
||||||
return p, nil
|
return p, nil
|
||||||
|
|
34
cli/cobra.go
34
cli/cobra.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
cliconfig "github.com/docker/cli/cli/config"
|
cliconfig "github.com/docker/cli/cli/config"
|
||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
|
@ -14,7 +15,7 @@ import (
|
||||||
|
|
||||||
// setupCommonRootCommand contains the setup common to
|
// setupCommonRootCommand contains the setup common to
|
||||||
// SetupRootCommand and SetupPluginRootCommand.
|
// SetupRootCommand and SetupPluginRootCommand.
|
||||||
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||||
opts := cliflags.NewClientOptions()
|
opts := cliflags.NewClientOptions()
|
||||||
flags := rootCmd.Flags()
|
flags := rootCmd.Flags()
|
||||||
|
|
||||||
|
@ -26,19 +27,21 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *p
|
||||||
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
|
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
|
||||||
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
|
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
|
||||||
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
|
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
|
||||||
|
cobra.AddTemplateFunc("commandVendor", commandVendor)
|
||||||
|
cobra.AddTemplateFunc("isFirstLevelCommand", isFirstLevelCommand) // is it an immediate sub-command of the root
|
||||||
|
|
||||||
rootCmd.SetUsageTemplate(usageTemplate)
|
rootCmd.SetUsageTemplate(usageTemplate)
|
||||||
rootCmd.SetHelpTemplate(helpTemplate)
|
rootCmd.SetHelpTemplate(helpTemplate)
|
||||||
rootCmd.SetFlagErrorFunc(FlagErrorFunc)
|
rootCmd.SetFlagErrorFunc(FlagErrorFunc)
|
||||||
rootCmd.SetHelpCommand(helpCommand)
|
rootCmd.SetHelpCommand(helpCommand)
|
||||||
|
|
||||||
return opts, flags
|
return opts, flags, helpCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupRootCommand sets default usage, help, and error handling for the
|
// SetupRootCommand sets default usage, help, and error handling for the
|
||||||
// root command.
|
// root command.
|
||||||
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||||
opts, flags := setupCommonRootCommand(rootCmd)
|
opts, flags, helpCmd := setupCommonRootCommand(rootCmd)
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
||||||
|
|
||||||
|
@ -46,12 +49,12 @@ func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.F
|
||||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
||||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||||
|
|
||||||
return opts, flags
|
return opts, flags, helpCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
|
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
|
||||||
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
||||||
opts, flags := setupCommonRootCommand(rootCmd)
|
opts, flags, _ := setupCommonRootCommand(rootCmd)
|
||||||
|
|
||||||
rootCmd.PersistentFlags().BoolP("help", "", false, "Print usage")
|
rootCmd.PersistentFlags().BoolP("help", "", false, "Print usage")
|
||||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||||
|
@ -138,6 +141,21 @@ func wrappedFlagUsages(cmd *cobra.Command) string {
|
||||||
return cmd.Flags().FlagUsagesWrapped(width - 1)
|
return cmd.Flags().FlagUsagesWrapped(width - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isFirstLevelCommand(cmd *cobra.Command) bool {
|
||||||
|
return cmd.Parent() == cmd.Root()
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandVendor(cmd *cobra.Command) string {
|
||||||
|
width := 13
|
||||||
|
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok {
|
||||||
|
if len(v) > width-2 {
|
||||||
|
v = v[:width-3] + "…"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%-*s", width, "("+v+")")
|
||||||
|
}
|
||||||
|
return strings.Repeat(" ", width)
|
||||||
|
}
|
||||||
|
|
||||||
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||||
cmds := []*cobra.Command{}
|
cmds := []*cobra.Command{}
|
||||||
for _, sub := range cmd.Commands() {
|
for _, sub := range cmd.Commands() {
|
||||||
|
@ -178,7 +196,7 @@ Options:
|
||||||
Management Commands:
|
Management Commands:
|
||||||
|
|
||||||
{{- range managementSubCommands . }}
|
{{- range managementSubCommands . }}
|
||||||
{{rpad .Name .NamePadding }} {{.Short}}
|
{{rpad .Name .NamePadding }} {{ if isFirstLevelCommand .}}{{commandVendor .}} {{ end}}{{.Short}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
@ -187,7 +205,7 @@ Management Commands:
|
||||||
Commands:
|
Commands:
|
||||||
|
|
||||||
{{- range operationSubCommands . }}
|
{{- range operationSubCommands . }}
|
||||||
{{rpad .Name .NamePadding }} {{.Short}}
|
{{rpad .Name .NamePadding }} {{ if isFirstLevelCommand .}}{{commandVendor .}} {{ end}}{{.Short}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package cli
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
)
|
)
|
||||||
|
@ -28,3 +29,29 @@ func TestVisitAll(t *testing.T) {
|
||||||
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||||
assert.DeepEqual(t, expected, visited)
|
assert.DeepEqual(t, expected, visited)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandVendor(t *testing.T) {
|
||||||
|
// Non plugin.
|
||||||
|
assert.Equal(t, commandVendor(&cobra.Command{Use: "test"}), " ")
|
||||||
|
|
||||||
|
// Plugins with various lengths of vendor.
|
||||||
|
for _, tc := range []struct {
|
||||||
|
vendor string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{vendor: "vendor", expected: "(vendor) "},
|
||||||
|
{vendor: "vendor12345", expected: "(vendor12345)"},
|
||||||
|
{vendor: "vendor123456", expected: "(vendor1234…)"},
|
||||||
|
{vendor: "vendor1234567", expected: "(vendor1234…)"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.vendor, func(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "test",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, commandVendor(cmd), tc.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,8 +21,9 @@ import (
|
||||||
|
|
||||||
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
var (
|
var (
|
||||||
opts *cliflags.ClientOptions
|
opts *cliflags.ClientOptions
|
||||||
flags *pflag.FlagSet
|
flags *pflag.FlagSet
|
||||||
|
helpCmd *cobra.Command
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
@ -57,11 +58,12 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
Version: fmt.Sprintf("%s, build %s", version.Version, version.GitCommit),
|
Version: fmt.Sprintf("%s, build %s", version.Version, version.GitCommit),
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
}
|
}
|
||||||
opts, flags = cli.SetupRootCommand(cmd)
|
opts, flags, helpCmd = cli.SetupRootCommand(cmd)
|
||||||
flags.BoolP("version", "v", false, "Print version information and quit")
|
flags.BoolP("version", "v", false, "Print version information and quit")
|
||||||
|
|
||||||
setFlagErrorFunc(dockerCli, cmd, flags, opts)
|
setFlagErrorFunc(dockerCli, cmd, flags, opts)
|
||||||
|
|
||||||
|
setupHelpCommand(dockerCli, cmd, helpCmd, flags, opts)
|
||||||
setHelpFunc(dockerCli, cmd, flags, opts)
|
setHelpFunc(dockerCli, cmd, flags, opts)
|
||||||
|
|
||||||
cmd.SetOutput(dockerCli.Out())
|
cmd.SetOutput(dockerCli.Out())
|
||||||
|
@ -90,6 +92,34 @@ func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *p
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupHelpCommand(dockerCli *command.DockerCli, rootCmd, helpCmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
||||||
|
origRun := helpCmd.Run
|
||||||
|
origRunE := helpCmd.RunE
|
||||||
|
|
||||||
|
helpCmd.Run = nil
|
||||||
|
helpCmd.RunE = func(c *cobra.Command, args []string) error {
|
||||||
|
// No Persistent* hooks are called for help, so we must initialize here.
|
||||||
|
if err := initializeDockerCli(dockerCli, flags, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a stub entry for every plugin so they are
|
||||||
|
// included in the help output. If we have no args
|
||||||
|
// then this is being used for `docker help` and we
|
||||||
|
// want to include broken plugins, otherwise this is
|
||||||
|
// `help «foo»` and we do not.
|
||||||
|
if err := pluginmanager.AddPluginCommandStubs(dockerCli, rootCmd, len(args) == 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if origRunE != nil {
|
||||||
|
return origRunE(c, args)
|
||||||
|
}
|
||||||
|
origRun(c, args)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
||||||
defaultHelpFunc := cmd.HelpFunc()
|
defaultHelpFunc := cmd.HelpFunc()
|
||||||
cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
|
cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package cliplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
"gotest.tools/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGlobalHelp ensures correct behaviour when running `docker help`
|
||||||
|
func TestGlobalHelp(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("help"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(res.Stdout()))
|
||||||
|
|
||||||
|
// Instead of baking in the full current output of `docker
|
||||||
|
// help`, which can be expected to change regularly, bake in
|
||||||
|
// some checkpoints. Key things we are looking for:
|
||||||
|
//
|
||||||
|
// - The top-level description
|
||||||
|
// - Each of the main headings
|
||||||
|
// - Some builtin commands under the main headings
|
||||||
|
// - The `helloworld` plugin in the appropriate place
|
||||||
|
//
|
||||||
|
// Regexps are needed because the width depends on `unix.TIOCGWINSZ` or similar.
|
||||||
|
for _, expected := range []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`^A self-sufficient runtime for containers$`),
|
||||||
|
regexp.MustCompile(`^Management Commands:$`),
|
||||||
|
regexp.MustCompile(`^ container\s+Manage containers$`),
|
||||||
|
regexp.MustCompile(`^Commands:$`),
|
||||||
|
regexp.MustCompile(`^ create\s+Create a new container$`),
|
||||||
|
regexp.MustCompile(`^ helloworld\s+\(Docker Inc\.\)\s+A basic Hello World plugin for tests$`),
|
||||||
|
regexp.MustCompile(`^ ps\s+List containers$`),
|
||||||
|
} {
|
||||||
|
var found bool
|
||||||
|
for scanner.Scan() {
|
||||||
|
if expected.MatchString(scanner.Text()) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Assert(t, found, "Did not find match for %q in `docker help` output", expected)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue