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