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