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