...

Source file src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go

Documentation: k8s.io/kubectl/pkg/cmd/plugin

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package plugin
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"github.com/spf13/cobra"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions"
    31  	"k8s.io/kubectl/pkg/util/i18n"
    32  	"k8s.io/kubectl/pkg/util/templates"
    33  )
    34  
    35  func GetPluginCommandGroup(kubectl *cobra.Command) templates.CommandGroup {
    36  	// Find root level
    37  	return templates.CommandGroup{
    38  		Message:  i18n.T("Subcommands provided by plugins:"),
    39  		Commands: registerPluginCommands(kubectl, false),
    40  	}
    41  }
    42  
    43  // SetupPluginCompletion adds a Cobra command to the command tree for each
    44  // plugin.  This is only done when performing shell completion that relate
    45  // to plugins.
    46  func SetupPluginCompletion(cmd *cobra.Command, args []string) {
    47  	kubectl := cmd.Root()
    48  	if len(args) > 0 {
    49  		if strings.HasPrefix(args[0], "-") {
    50  			// Plugins are not supported if the first argument is a flag,
    51  			// so no need to add them in that case.
    52  			return
    53  		}
    54  
    55  		if len(args) == 1 {
    56  			// We are completing a subcommand at the first level so
    57  			// we should include all plugins names.
    58  			registerPluginCommands(kubectl, true)
    59  			return
    60  		}
    61  
    62  		// We have more than one argument.
    63  		// Check if we know the first level subcommand.
    64  		// If we don't it could be a plugin and we'll need to add
    65  		// the plugin commands for completion to work.
    66  		found := false
    67  		for _, subCmd := range kubectl.Commands() {
    68  			if args[0] == subCmd.Name() {
    69  				found = true
    70  				break
    71  			}
    72  		}
    73  
    74  		if !found {
    75  			// We don't know the subcommand for which completion
    76  			// is being called: it could be a plugin.
    77  			//
    78  			// When using a plugin, the kubectl global flags are not supported.
    79  			// Therefore, when doing completion, we need to remove these flags
    80  			// to avoid them being included in the completion choices.
    81  			// This must be done *before* adding the plugin commands so that
    82  			// when creating those plugin commands, the flags don't exist.
    83  			kubectl.ResetFlags()
    84  			cobra.CompDebugln("Cleared global flags for plugin completion", true)
    85  
    86  			registerPluginCommands(kubectl, true)
    87  		}
    88  	}
    89  }
    90  
    91  // registerPluginCommand allows adding Cobra command to the command tree or extracting them for usage in
    92  // e.g. the help function or for registering the completion function
    93  func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Command) {
    94  	userDefinedCommands := []*cobra.Command{}
    95  
    96  	streams := genericclioptions.IOStreams{
    97  		In:     &bytes.Buffer{},
    98  		Out:    io.Discard,
    99  		ErrOut: io.Discard,
   100  	}
   101  
   102  	o := &PluginListOptions{IOStreams: streams}
   103  	o.Complete(kubectl)
   104  	plugins, _ := o.ListPlugins()
   105  
   106  	for _, plugin := range plugins {
   107  		plugin = filepath.Base(plugin)
   108  		args := []string{}
   109  
   110  		// Plugins are named "kubectl-<name>" or with more - such as
   111  		// "kubectl-<name>-<subcmd1>..."
   112  		rawPluginArgs := strings.Split(plugin, "-")[1:]
   113  		pluginArgs := rawPluginArgs[:1]
   114  		if list {
   115  			pluginArgs = rawPluginArgs
   116  		}
   117  
   118  		// Iterate through all segments, for kubectl-my_plugin-sub_cmd, we will end up with
   119  		// two iterations: one for my_plugin and one for sub_cmd.
   120  		for _, arg := range pluginArgs {
   121  			// Underscores (_) in plugin's filename are replaced with dashes(-)
   122  			// e.g. foo_bar -> foo-bar
   123  			args = append(args, strings.ReplaceAll(arg, "_", "-"))
   124  		}
   125  
   126  		// In order to avoid that the same plugin command is added more than once,
   127  		// find the lowest command given args from the root command
   128  		parentCmd, remainingArgs, _ := kubectl.Find(args)
   129  		if parentCmd == nil {
   130  			parentCmd = kubectl
   131  		}
   132  
   133  		for _, remainingArg := range remainingArgs {
   134  			cmd := &cobra.Command{
   135  				Use: remainingArg,
   136  				// Add a description that will be shown with completion choices.
   137  				// Make each one different by including the plugin name to avoid
   138  				// all plugins being grouped in a single line during completion for zsh.
   139  				Short:              fmt.Sprintf(i18n.T("The command %s is a plugin installed by the user"), remainingArg),
   140  				DisableFlagParsing: true,
   141  				// Allow plugins to provide their own completion choices
   142  				ValidArgsFunction: pluginCompletion,
   143  				// A Run is required for it to be a valid command
   144  				Run: func(cmd *cobra.Command, args []string) {},
   145  			}
   146  			// Add the plugin command to the list of user defined commands
   147  			userDefinedCommands = append(userDefinedCommands, cmd)
   148  
   149  			if list {
   150  				parentCmd.AddCommand(cmd)
   151  				parentCmd = cmd
   152  			}
   153  		}
   154  	}
   155  
   156  	return userDefinedCommands
   157  }
   158  
   159  // pluginCompletion deals with shell completion beyond the plugin name, it allows to complete
   160  // plugin arguments and flags.
   161  // It will look on $PATH for a specific executable file that will provide completions
   162  // for the plugin in question.
   163  //
   164  // When called, this completion executable should print the completion choices to stdout.
   165  // The arguments passed to the executable file will be the arguments for the plugin currently
   166  // on the command-line.  For example, if a user types:
   167  //
   168  //	kubectl myplugin arg1 arg2 a<TAB>
   169  //
   170  // the completion executable will be called with arguments: "arg1" "arg2" "a".
   171  // And if a user types:
   172  //
   173  //	kubectl myplugin arg1 arg2 <TAB>
   174  //
   175  // the completion executable will be called with arguments: "arg1" "arg2" "".  Notice the empty
   176  // last argument which indicates that a new word should be completed but that the user has not
   177  // typed anything for it yet.
   178  //
   179  // Kubectl's plugin completion logic supports Cobra's ShellCompDirective system.  This means a plugin
   180  // can optionally print :<value of a shell completion directive> as its very last line to provide
   181  // directives to the shell on how to perform completion.  If this directive is not present, the
   182  // cobra.ShellCompDirectiveDefault will be used. Please see Cobra's documentation for more details:
   183  // https://github.com/spf13/cobra/blob/master/shell_completions.md#dynamic-completion-of-nouns
   184  //
   185  // The completion executable should be named kubectl_complete-<plugin>.  For example, for a plugin
   186  // named kubectl-get_all, the completion file should be named kubectl_complete-get_all.  The completion
   187  // executable must have executable permissions set on it and must be on $PATH.
   188  func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   189  	// Recreate the plugin name from the commandPath
   190  	pluginName := strings.ReplaceAll(strings.ReplaceAll(cmd.CommandPath(), "-", "_"), " ", "-")
   191  
   192  	path, found := lookupCompletionExec(pluginName)
   193  	if !found {
   194  		cobra.CompDebugln(fmt.Sprintf("Plugin %s does not provide a matching completion executable", pluginName), true)
   195  		return nil, cobra.ShellCompDirectiveDefault
   196  	}
   197  
   198  	args = append(args, toComplete)
   199  	cobra.CompDebugln(fmt.Sprintf("About to call: %s %s", path, strings.Join(args, " ")), true)
   200  	return getPluginCompletions(path, args, os.Environ())
   201  }
   202  
   203  // lookupCompletionExec will look for the existence of an executable
   204  // that can provide completion for the given plugin name.
   205  // The first filepath to match is returned, or a boolean false if
   206  // such an executable is not found.
   207  func lookupCompletionExec(pluginName string) (string, bool) {
   208  	// Convert the plugin name into the plugin completion name by inserting "_complete" before the first -.
   209  	// For example, convert kubectl-get_all to kubectl_complete-get_all
   210  	pluginCompExec := strings.Replace(pluginName, "-", "_complete-", 1)
   211  	cobra.CompDebugln(fmt.Sprintf("About to look for: %s", pluginCompExec), true)
   212  	path, err := exec.LookPath(pluginCompExec)
   213  	if err != nil || len(path) == 0 {
   214  		return "", false
   215  	}
   216  	return path, true
   217  }
   218  
   219  // getPluginCompletions receives an executable's filepath, a slice
   220  // of arguments, and a slice of environment variables
   221  // to relay to the executable.
   222  // The executable is responsible for printing the completions of the
   223  // plugin for the current set of arguments.
   224  func getPluginCompletions(executablePath string, cmdArgs, environment []string) ([]string, cobra.ShellCompDirective) {
   225  	buf := new(bytes.Buffer)
   226  
   227  	prog := exec.Command(executablePath, cmdArgs...)
   228  	prog.Stdin = os.Stdin
   229  	prog.Stdout = buf
   230  	prog.Stderr = os.Stderr
   231  	prog.Env = environment
   232  
   233  	var comps []string
   234  	directive := cobra.ShellCompDirectiveDefault
   235  	if err := prog.Run(); err == nil {
   236  		for _, comp := range strings.Split(buf.String(), "\n") {
   237  			// Remove any empty lines
   238  			if len(comp) > 0 {
   239  				comps = append(comps, comp)
   240  			}
   241  		}
   242  
   243  		// Check if the last line of output is of the form :<integer>, which
   244  		// indicates a Cobra ShellCompDirective.  We do this for plugins
   245  		// that use Cobra or the ones that wish to use this directive to
   246  		// communicate a special behavior for the shell.
   247  		if len(comps) > 0 {
   248  			lastLine := comps[len(comps)-1]
   249  			if len(lastLine) > 1 && lastLine[0] == ':' {
   250  				if strInt, err := strconv.Atoi(lastLine[1:]); err == nil {
   251  					directive = cobra.ShellCompDirective(strInt)
   252  					comps = comps[:len(comps)-1]
   253  				}
   254  			}
   255  		}
   256  	}
   257  	return comps, directive
   258  }
   259  

View as plain text