diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go b/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go index 2f1ba8d054a..781ada501b6 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go @@ -17,11 +17,17 @@ limitations under the License. package completion import ( + "bytes" "fmt" "io" + "io/ioutil" + "path/filepath" + "strings" "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/cmd/plugin" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" @@ -155,6 +161,7 @@ func RunCompletion(out io.Writer, boilerPlate string, cmd *cobra.Command, args [ if !found { return cmdutil.UsageErrorf(cmd, "Unsupported shell type %q.", args[0]) } + addPluginCommands(cmd.Root()) return run(out, boilerPlate, cmd.Parent()) } @@ -206,3 +213,47 @@ func runCompletionPwsh(out io.Writer, boilerPlate string, kubectl *cobra.Command return kubectl.GenPowerShellCompletionWithDesc(out) } + +// addPluginCommand adds plugin commands under the given command so that +// completion includes them +func addPluginCommands(kubectl *cobra.Command) { + streams := genericclioptions.IOStreams{ + In: &bytes.Buffer{}, + Out: ioutil.Discard, + ErrOut: ioutil.Discard, + } + + o := &plugin.PluginListOptions{IOStreams: streams} + o.Complete(kubectl) + plugins, _ := o.ListPlugins() + + for _, plugin := range plugins { + plugin = filepath.Base(plugin) + args := []string{} + + // Plugins are named "kubectl-" or with more - such as + // "kubectl--..." + for _, arg := range strings.Split(plugin, "-")[1:] { + // Underscores (_) in plugin's filename are replaced with dashes(-) + // e.g. foo_bar -> foo-bar + args = append(args, strings.Replace(arg, "_", "-", -1)) + } + + // In order to avoid that the same plugin command is added, + // find the lowest command given args from the root command + parentCmd, remainingArgs, _ := kubectl.Find(args) + if parentCmd == nil { + parentCmd = kubectl + } + + for _, remainingArg := range remainingArgs { + cmd := &cobra.Command{ + Use: remainingArg, + // A Run is required for it to be a valid command + Run: func(cmd *cobra.Command, args []string) {}, + } + parentCmd.AddCommand(cmd) + parentCmd = cmd + } + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go index d620a09a0d7..fa0acd46c76 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go @@ -115,58 +115,27 @@ func (o *PluginListOptions) Complete(cmd *cobra.Command) error { } func (o *PluginListOptions) Run() error { - pluginsFound := false - isFirstFile := true - pluginErrors := []error{} - pluginWarnings := 0 + plugins, pluginErrors := o.ListPlugins() - for _, dir := range uniquePathsList(o.PluginPaths) { - if len(strings.TrimSpace(dir)) == 0 { - continue - } - - files, err := ioutil.ReadDir(dir) - if err != nil { - if _, ok := err.(*os.PathError); ok { - fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err) - continue - } - - pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) - continue - } - - for _, f := range files { - if f.IsDir() { - continue - } - if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { - continue - } - - if isFirstFile { - fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n") - pluginsFound = true - isFirstFile = false - } - - pluginPath := f.Name() - if !o.NameOnly { - pluginPath = filepath.Join(dir, pluginPath) - } - - fmt.Fprintf(o.Out, "%s\n", pluginPath) - if errs := o.Verifier.Verify(filepath.Join(dir, f.Name())); len(errs) != 0 { - for _, err := range errs { - fmt.Fprintf(o.ErrOut, " - %s\n", err) - pluginWarnings++ - } - } - } + if len(plugins) > 0 { + fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n") + } else { + pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH")) } - if !pluginsFound { - pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH")) + pluginWarnings := 0 + for _, pluginPath := range plugins { + if o.NameOnly { + fmt.Fprintf(o.Out, "%s\n", filepath.Base(pluginPath)) + } else { + fmt.Fprintf(o.Out, "%s\n", pluginPath) + } + if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 { + for _, err := range errs { + fmt.Fprintf(o.ErrOut, " - %s\n", err) + pluginWarnings++ + } + } } if pluginWarnings > 0 { @@ -187,6 +156,42 @@ func (o *PluginListOptions) Run() error { return nil } +// ListPlugins returns list of plugin paths. +func (o *PluginListOptions) ListPlugins() ([]string, []error) { + plugins := []string{} + errors := []error{} + + for _, dir := range uniquePathsList(o.PluginPaths) { + if len(strings.TrimSpace(dir)) == 0 { + continue + } + + files, err := ioutil.ReadDir(dir) + if err != nil { + if _, ok := err.(*os.PathError); ok { + fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err) + continue + } + + errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) + continue + } + + for _, f := range files { + if f.IsDir() { + continue + } + if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { + continue + } + + plugins = append(plugins, filepath.Join(dir, f.Name())) + } + } + + return plugins, errors +} + // pathVerifier receives a path and determines if it is valid or not type PathVerifier interface { // Verify determines if a given path is valid diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go index 013fe7f184e..639bc6f8dac 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go @@ -20,6 +20,8 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" + "reflect" "strings" "testing" @@ -178,6 +180,34 @@ func TestPluginPathsAreValid(t *testing.T) { } } +func TestListPlugins(t *testing.T) { + pluginPath, _ := filepath.Abs("./testdata") + expectPlugins := []string{ + filepath.Join(pluginPath, "kubectl-foo"), + filepath.Join(pluginPath, "kubectl-version"), + } + + verifier := newFakePluginPathVerifier() + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() + pluginPaths := []string{pluginPath} + + o := &PluginListOptions{ + Verifier: verifier, + IOStreams: ioStreams, + + PluginPaths: pluginPaths, + } + + plugins, errs := o.ListPlugins() + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + + if !reflect.DeepEqual(expectPlugins, plugins) { + t.Fatalf("saw unexpected plugins. Expecting %v, got %v", expectPlugins, plugins) + } +} + type duplicatePathError struct { path string }