github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/cmd/helm/load_plugins.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package main 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "log" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "strconv" 28 "strings" 29 "syscall" 30 31 "github.com/pkg/errors" 32 "github.com/spf13/cobra" 33 "sigs.k8s.io/yaml" 34 35 "github.com/stefanmcshane/helm/pkg/plugin" 36 ) 37 38 const ( 39 pluginStaticCompletionFile = "completion.yaml" 40 pluginDynamicCompletionExecutable = "plugin.complete" 41 ) 42 43 type pluginError struct { 44 error 45 code int 46 } 47 48 // loadPlugins loads plugins into the command list. 49 // 50 // This follows a different pattern than the other commands because it has 51 // to inspect its environment and then add commands to the base command 52 // as it finds them. 53 func loadPlugins(baseCmd *cobra.Command, out io.Writer) { 54 55 // If HELM_NO_PLUGINS is set to 1, do not load plugins. 56 if os.Getenv("HELM_NO_PLUGINS") == "1" { 57 return 58 } 59 60 found, err := plugin.FindPlugins(settings.PluginsDirectory) 61 if err != nil { 62 fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err) 63 return 64 } 65 66 // Now we create commands for all of these. 67 for _, plug := range found { 68 plug := plug 69 md := plug.Metadata 70 if md.Usage == "" { 71 md.Usage = fmt.Sprintf("the %q plugin", md.Name) 72 } 73 74 c := &cobra.Command{ 75 Use: md.Name, 76 Short: md.Usage, 77 Long: md.Description, 78 RunE: func(cmd *cobra.Command, args []string) error { 79 u, err := processParent(cmd, args) 80 if err != nil { 81 return err 82 } 83 84 // Call setupEnv before PrepareCommand because 85 // PrepareCommand uses os.ExpandEnv and expects the 86 // setupEnv vars. 87 plugin.SetupPluginEnv(settings, md.Name, plug.Dir) 88 main, argv, prepCmdErr := plug.PrepareCommand(u) 89 if prepCmdErr != nil { 90 os.Stderr.WriteString(prepCmdErr.Error()) 91 return errors.Errorf("plugin %q exited with error", md.Name) 92 } 93 94 return callPluginExecutable(md.Name, main, argv, out) 95 }, 96 // This passes all the flags to the subcommand. 97 DisableFlagParsing: true, 98 } 99 100 // TODO: Make sure a command with this name does not already exist. 101 baseCmd.AddCommand(c) 102 103 // For completion, we try to load more details about the plugins so as to allow for command and 104 // flag completion of the plugin itself. 105 // We only do this when necessary (for the "completion" and "__complete" commands) to avoid the 106 // risk of a rogue plugin affecting Helm's normal behavior. 107 subCmd, _, err := baseCmd.Find(os.Args[1:]) 108 if (err == nil && 109 ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || 110 /* for the tests */ subCmd == baseCmd.Root() { 111 loadCompletionForPlugin(c, plug) 112 } 113 } 114 } 115 116 func processParent(cmd *cobra.Command, args []string) ([]string, error) { 117 k, u := manuallyProcessArgs(args) 118 if err := cmd.Parent().ParseFlags(k); err != nil { 119 return nil, err 120 } 121 return u, nil 122 } 123 124 // This function is used to setup the environment for the plugin and then 125 // call the executable specified by the parameter 'main' 126 func callPluginExecutable(pluginName string, main string, argv []string, out io.Writer) error { 127 env := os.Environ() 128 for k, v := range settings.EnvVars() { 129 env = append(env, fmt.Sprintf("%s=%s", k, v)) 130 } 131 132 prog := exec.Command(main, argv...) 133 prog.Env = env 134 prog.Stdin = os.Stdin 135 prog.Stdout = out 136 prog.Stderr = os.Stderr 137 if err := prog.Run(); err != nil { 138 if eerr, ok := err.(*exec.ExitError); ok { 139 os.Stderr.Write(eerr.Stderr) 140 status := eerr.Sys().(syscall.WaitStatus) 141 return pluginError{ 142 error: errors.Errorf("plugin %q exited with error", pluginName), 143 code: status.ExitStatus(), 144 } 145 } 146 return err 147 } 148 return nil 149 } 150 151 // manuallyProcessArgs processes an arg array, removing special args. 152 // 153 // Returns two sets of args: known and unknown (in that order) 154 func manuallyProcessArgs(args []string) ([]string, []string) { 155 known := []string{} 156 unknown := []string{} 157 kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config", "--insecure-skip-tls-verify", "--tls-server-name"} 158 knownArg := func(a string) bool { 159 for _, pre := range kvargs { 160 if strings.HasPrefix(a, pre+"=") { 161 return true 162 } 163 } 164 return false 165 } 166 167 isKnown := func(v string) string { 168 for _, i := range kvargs { 169 if i == v { 170 return v 171 } 172 } 173 return "" 174 } 175 176 for i := 0; i < len(args); i++ { 177 switch a := args[i]; a { 178 case "--debug": 179 known = append(known, a) 180 case isKnown(a): 181 known = append(known, a) 182 i++ 183 if i < len(args) { 184 known = append(known, args[i]) 185 } 186 default: 187 if knownArg(a) { 188 known = append(known, a) 189 continue 190 } 191 unknown = append(unknown, a) 192 } 193 } 194 return known, unknown 195 } 196 197 // pluginCommand represents the optional completion.yaml file of a plugin 198 type pluginCommand struct { 199 Name string `json:"name"` 200 ValidArgs []string `json:"validArgs"` 201 Flags []string `json:"flags"` 202 Commands []pluginCommand `json:"commands"` 203 } 204 205 // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin 206 // and add the dynamic completion hook to call the optional plugin.complete 207 func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) { 208 // Parse the yaml file providing the plugin's sub-commands and flags 209 cmds, err := loadFile(strings.Join( 210 []string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) 211 212 if err != nil { 213 // The file could be missing or invalid. No static completion for this plugin. 214 if settings.Debug { 215 log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) 216 } 217 // Continue to setup dynamic completion. 218 cmds = &pluginCommand{} 219 } 220 221 // Preserve the Usage string specified for the plugin 222 cmds.Name = pluginCmd.Use 223 224 addPluginCommands(plugin, pluginCmd, cmds) 225 } 226 227 // addPluginCommands is a recursive method that adds each different level 228 // of sub-commands and flags for the plugins that have provided such information 229 func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { 230 if cmds == nil { 231 return 232 } 233 234 if len(cmds.Name) == 0 { 235 // Missing name for a command 236 if settings.Debug { 237 log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath())) 238 } 239 return 240 } 241 242 baseCmd.Use = cmds.Name 243 baseCmd.ValidArgs = cmds.ValidArgs 244 // Setup the same dynamic completion for each plugin sub-command. 245 // This is because if dynamic completion is triggered, there is a single executable 246 // to call (plugin.complete), so every sub-commands calls it in the same fashion. 247 if cmds.Commands == nil { 248 // Only setup dynamic completion if there are no sub-commands. This avoids 249 // calling plugin.complete at every completion, which greatly simplifies 250 // development of plugin.complete for plugin developers. 251 baseCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 252 return pluginDynamicComp(plugin, cmd, args, toComplete) 253 } 254 } 255 256 // Create fake flags. 257 if len(cmds.Flags) > 0 { 258 // The flags can be created with any type, since we only need them for completion. 259 // pflag does not allow to create short flags without a corresponding long form 260 // so we look for all short flags and match them to any long flag. This will allow 261 // plugins to provide short flags without a long form. 262 // If there are more short-flags than long ones, we'll create an extra long flag with 263 // the same single letter as the short form. 264 shorts := []string{} 265 longs := []string{} 266 for _, flag := range cmds.Flags { 267 if len(flag) == 1 { 268 shorts = append(shorts, flag) 269 } else { 270 longs = append(longs, flag) 271 } 272 } 273 274 f := baseCmd.Flags() 275 if len(longs) >= len(shorts) { 276 for i := range longs { 277 if i < len(shorts) { 278 f.BoolP(longs[i], shorts[i], false, "") 279 } else { 280 f.Bool(longs[i], false, "") 281 } 282 } 283 } else { 284 for i := range shorts { 285 if i < len(longs) { 286 f.BoolP(longs[i], shorts[i], false, "") 287 } else { 288 // Create a long flag with the same name as the short flag. 289 // Not a perfect solution, but its better than ignoring the extra short flags. 290 f.BoolP(shorts[i], shorts[i], false, "") 291 } 292 } 293 } 294 } 295 296 // Recursively add any sub-commands 297 for _, cmd := range cmds.Commands { 298 // Create a fake command so that completion can be done for the sub-commands of the plugin 299 subCmd := &cobra.Command{ 300 // This prevents Cobra from removing the flags. We want to keep the flags to pass them 301 // to the dynamic completion script of the plugin. 302 DisableFlagParsing: true, 303 // A Run is required for it to be a valid command without subcommands 304 Run: func(cmd *cobra.Command, args []string) {}, 305 } 306 baseCmd.AddCommand(subCmd) 307 addPluginCommands(plugin, subCmd, &cmd) 308 } 309 } 310 311 // loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object 312 func loadFile(path string) (*pluginCommand, error) { 313 cmds := new(pluginCommand) 314 b, err := ioutil.ReadFile(path) 315 if err != nil { 316 return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path) 317 } 318 319 err = yaml.Unmarshal(b, cmds) 320 return cmds, err 321 } 322 323 // pluginDynamicComp call the plugin.complete script of the plugin (if available) 324 // to obtain the dynamic completion choices. It must pass all the flags and sub-commands 325 // specified in the command-line to the plugin.complete executable (except helm's global flags) 326 func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 327 md := plug.Metadata 328 329 u, err := processParent(cmd, args) 330 if err != nil { 331 return nil, cobra.ShellCompDirectiveError 332 } 333 334 // We will call the dynamic completion script of the plugin 335 main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator)) 336 337 // We must include all sub-commands passed on the command-line. 338 // To do that, we pass-in the entire CommandPath, except the first two elements 339 // which are 'helm' and 'pluginName'. 340 argv := strings.Split(cmd.CommandPath(), " ")[2:] 341 if !md.IgnoreFlags { 342 argv = append(argv, u...) 343 argv = append(argv, toComplete) 344 } 345 plugin.SetupPluginEnv(settings, md.Name, plug.Dir) 346 347 cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug) 348 buf := new(bytes.Buffer) 349 if err := callPluginExecutable(md.Name, main, argv, buf); err != nil { 350 // The dynamic completion file is optional for a plugin, so this error is ok. 351 cobra.CompDebugln(fmt.Sprintf("Unable to call %s: %v", main, err.Error()), settings.Debug) 352 return nil, cobra.ShellCompDirectiveDefault 353 } 354 355 var completions []string 356 for _, comp := range strings.Split(buf.String(), "\n") { 357 // Remove any empty lines 358 if len(comp) > 0 { 359 completions = append(completions, comp) 360 } 361 } 362 363 // Check if the last line of output is of the form :<integer>, which 364 // indicates the BashCompletionDirective. 365 directive := cobra.ShellCompDirectiveDefault 366 if len(completions) > 0 { 367 lastLine := completions[len(completions)-1] 368 if len(lastLine) > 1 && lastLine[0] == ':' { 369 if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { 370 directive = cobra.ShellCompDirective(strInt) 371 completions = completions[:len(completions)-1] 372 } 373 } 374 } 375 376 return completions, directive 377 }