diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go index d15b710c48b..fd0d3613259 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go @@ -279,8 +279,15 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command { Run: runHelp, // Hook before and after Run initialize and write profiles to disk, // respectively. - PersistentPreRunE: func(*cobra.Command, []string) error { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { rest.SetDefaultWarningHandler(warningHandler) + + if cmd.Name() == cobra.ShellCompRequestCmd { + // This is the __complete or __completeNoDesc command which + // indicates shell completion has been requested. + plugin.SetupPluginCompletion(cmd, args) + } + return initProfiling() }, PersistentPostRunE: func(*cobra.Command, []string) error { 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 781ada501b6..2f1ba8d054a 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go @@ -17,17 +17,11 @@ 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" @@ -161,7 +155,6 @@ 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()) } @@ -213,47 +206,3 @@ 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_completion.go b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go new file mode 100644 index 00000000000..357dc7aa6fa --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go @@ -0,0 +1,231 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// SetupPluginCompletion adds a Cobra command to the command tree for each +// plugin. This is only done when performing shell completion that relate +// to plugins. +func SetupPluginCompletion(cmd *cobra.Command, args []string) { + if len(args) > 0 { + if strings.HasPrefix(args[0], "-") { + // Plugins are not supported if the first argument is a flag, + // so no need to add them in that case. + return + } + + if len(args) == 1 { + // We are completing a subcommand at the first level so + // we should include all plugins names. + addPluginCommands(cmd) + return + } + + // We have more than one argument. + // Check if we know the first level subcommand. + // If we don't it could be a plugin and we'll need to add + // the plugin commands for completion to work. + found := false + for _, subCmd := range cmd.Root().Commands() { + if args[0] == subCmd.Name() { + found = true + break + } + } + + if !found { + // We don't know the subcommand for which completion + // is being called: it could be a plugin. + // + // When using a plugin, the kubectl global flags are not supported. + // Therefore, when doing completion, we need to remove these flags + // to avoid them being included in the completion choices. + // This must be done *before* adding the plugin commands so that + // when creating those plugin commands, the flags don't exist. + cmd.Root().ResetFlags() + cobra.CompDebugln("Cleared global flags for plugin completion", true) + + addPluginCommands(cmd) + } + } +} + +// addPluginCommand adds a Cobra command to the command tree +// for each plugin so that the completion logic knows about the plugins +func addPluginCommands(cmd *cobra.Command) { + kubectl := cmd.Root() + streams := genericclioptions.IOStreams{ + In: &bytes.Buffer{}, + Out: ioutil.Discard, + ErrOut: ioutil.Discard, + } + + o := &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 more than once, + // 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, + // Add a description that will be shown with completion choices. + // Make each one different by including the plugin name to avoid + // all plugins being grouped in a single line during completion for zsh. + Short: fmt.Sprintf("The command %s is a plugin installed by the user", remainingArg), + DisableFlagParsing: true, + // Allow plugins to provide their own completion choices + ValidArgsFunction: pluginCompletion, + // A Run is required for it to be a valid command + Run: func(cmd *cobra.Command, args []string) {}, + } + parentCmd.AddCommand(cmd) + parentCmd = cmd + } + } +} + +// pluginCompletion deals with shell completion beyond the plugin name, it allows to complete +// plugin arguments and flags. +// It will look on $PATH for a specific executable file that will provide completions +// for the plugin in question. +// +// When called, this completion executable should print the completion choices to stdout. +// The arguments passed to the executable file will be the arguments for the plugin currently +// on the command-line. For example, if a user types: +// +// kubectl myplugin arg1 arg2 a +// +// the completion executable will be called with arguments: "arg1" "arg2" "a". +// And if a user types: +// +// kubectl myplugin arg1 arg2 +// +// the completion executable will be called with arguments: "arg1" "arg2" "". Notice the empty +// last argument which indicates that a new word should be completed but that the user has not +// typed anything for it yet. +// +// Kubectl's plugin completion logic supports Cobra's ShellCompDirective system. This means a plugin +// can optionally print : as its very last line to provide +// directives to the shell on how to perform completion. If this directive is not present, the +// cobra.ShellCompDirectiveDefault will be used. Please see Cobra's documentation for more details: +// https://github.com/spf13/cobra/blob/master/shell_completions.md#dynamic-completion-of-nouns +// +// The completion executable should be named kubectl_complete-. For example, for a plugin +// named kubectl-get_all, the completion file should be named kubectl_complete-get_all. The completion +// executable must have executable permissions set on it and must be on $PATH. +func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Recreate the plugin name from the commandPath + pluginName := strings.Replace(strings.Replace(cmd.CommandPath(), "-", "_", -1), " ", "-", -1) + + path, found := lookupCompletionExec(pluginName) + if !found { + cobra.CompDebugln(fmt.Sprintf("Plugin %s does not provide a matching completion executable", pluginName), true) + return nil, cobra.ShellCompDirectiveDefault + } + + args = append(args, toComplete) + cobra.CompDebugln(fmt.Sprintf("About to call: %s %s", path, strings.Join(args, " ")), true) + return getPluginCompletions(path, args, os.Environ()) +} + +// lookupCompletionExec will look for the existence of an executable +// that can provide completion for the given plugin name. +// The first filepath to match is returned, or a boolean false if +// such an executable is not found. +func lookupCompletionExec(pluginName string) (string, bool) { + // Convert the plugin name into the plugin completion name by inserting "_complete" before the first -. + // For example, convert kubectl-get_all to kubectl_complete-get_all + pluginCompExec := strings.Replace(pluginName, "-", "_complete-", 1) + cobra.CompDebugln(fmt.Sprintf("About to look for: %s", pluginCompExec), true) + path, err := exec.LookPath(pluginCompExec) + if err != nil || len(path) == 0 { + return "", false + } + return path, true +} + +// getPluginCompletions receives an executable's filepath, a slice +// of arguments, and a slice of environment variables +// to relay to the executable. +// The executable is responsible for printing the completions of the +// plugin for the current set of arguments. +func getPluginCompletions(executablePath string, cmdArgs, environment []string) ([]string, cobra.ShellCompDirective) { + buf := new(bytes.Buffer) + + prog := exec.Command(executablePath, cmdArgs...) + prog.Stdin = os.Stdin + prog.Stdout = buf + prog.Stderr = os.Stderr + prog.Env = environment + + var comps []string + directive := cobra.ShellCompDirectiveDefault + if err := prog.Run(); err == nil { + for _, comp := range strings.Split(buf.String(), "\n") { + // Remove any empty lines + if len(comp) > 0 { + comps = append(comps, comp) + } + } + + // Check if the last line of output is of the form :, which + // indicates a Cobra ShellCompDirective. We do this for plugins + // that use Cobra or the ones that wish to use this directive to + // communicate a special behavior for the shell. + if len(comps) > 0 { + lastLine := comps[len(comps)-1] + if len(lastLine) > 1 && lastLine[0] == ':' { + if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { + directive = cobra.ShellCompDirective(strInt) + comps = comps[:len(comps)-1] + } + } + } + } + return comps, directive +}