/* Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "bytes" "context" "fmt" "io" "log" "os" "path/filepath" "slices" "strconv" "strings" "helm.sh/helm/v4/internal/plugin/schema" "github.com/spf13/cobra" "sigs.k8s.io/yaml" "helm.sh/helm/v4/internal/plugin" ) // TODO: move pluginDynamicCompletionExecutable pkg/plugin/runtime_subprocess.go // any references to executables should be for [plugin.SubprocessPluginRuntime] only // this should also be for backwards compatibility in [plugin.Legacy] only // // TODO: for v1 make this configurable with a new CompletionCommand field for // [plugin.RuntimeConfigSubprocess] const ( pluginStaticCompletionFile = "completion.yaml" pluginDynamicCompletionExecutable = "plugin.complete" ) // loadCLIPlugins loads CLI plugins into the command list. // // This follows a different pattern than the other commands because it has // to inspect its environment and then add commands to the base command // as it finds them. func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { // If HELM_NO_PLUGINS is set to 1, do not load plugins. if os.Getenv("HELM_NO_PLUGINS") == "1" { return } dirs := filepath.SplitList(settings.PluginsDirectory) descriptor := plugin.Descriptor{ Type: "cli/v1", } found, err := plugin.FindPlugins(dirs, descriptor) if err != nil { fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err) return } // Now we create commands for all of these. for _, plug := range found { var use, short, long string var ignoreFlags bool if cliConfig, ok := plug.Metadata().Config.(*schema.ConfigCLIV1); ok { use = cliConfig.Usage short = cliConfig.ShortHelp long = cliConfig.LongHelp ignoreFlags = cliConfig.IgnoreFlags } // Set defaults if use == "" { use = plug.Metadata().Name } if short == "" { short = fmt.Sprintf("the %q plugin", plug.Metadata().Name) } // long has no default, empty is ok c := &cobra.Command{ Use: use, Short: short, Long: long, RunE: func(cmd *cobra.Command, args []string) error { u, err := processParent(cmd, args) if err != nil { return err } // For CLI plugin types runtime, set extra args and settings extraArgs := []string{} if !ignoreFlags { extraArgs = u } // Prepare environment env := os.Environ() for k, v := range settings.EnvVars() { env = append(env, fmt.Sprintf("%s=%s", k, v)) } // Invoke plugin input := &plugin.Input{ Message: schema.InputMessageCLIV1{ ExtraArgs: extraArgs, Settings: settings, }, Env: env, Stdin: os.Stdin, Stdout: out, Stderr: os.Stderr, } _, err = plug.Invoke(context.Background(), input) if execErr, ok := err.(*plugin.InvokeExecError); ok { return CommandError{ error: execErr.Err, ExitCode: execErr.ExitCode, } } return err }, // This passes all the flags to the subcommand. DisableFlagParsing: true, } // TODO: Make sure a command with this name does not already exist. baseCmd.AddCommand(c) // For completion, we try to load more details about the plugins so as to allow for command and // flag completion of the plugin itself. // We only do this when necessary (for the "completion" and "__complete" commands) to avoid the // risk of a rogue plugin affecting Helm's normal behavior. subCmd, _, err := baseCmd.Find(os.Args[1:]) if (err == nil && ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || /* for the tests */ subCmd == baseCmd.Root() { loadCompletionForPlugin(c, plug) } } } func processParent(cmd *cobra.Command, args []string) ([]string, error) { k, u := manuallyProcessArgs(args) if err := cmd.Parent().ParseFlags(k); err != nil { return nil, err } return u, nil } // manuallyProcessArgs processes an arg array, removing special args. // // Returns two sets of args: known and unknown (in that order) func manuallyProcessArgs(args []string) ([]string, []string) { known := []string{} unknown := []string{} 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", "--kube-insecure-skip-tls-verify", "--kube-tls-server-name"} knownArg := func(a string) bool { for _, pre := range kvargs { if strings.HasPrefix(a, pre+"=") { return true } } return false } isKnown := func(v string) string { if slices.Contains(kvargs, v) { return v } return "" } for i := 0; i < len(args); i++ { switch a := args[i]; a { case "--debug": known = append(known, a) case isKnown(a): known = append(known, a) i++ if i < len(args) { known = append(known, args[i]) } default: if knownArg(a) { known = append(known, a) continue } unknown = append(unknown, a) } } return known, unknown } // pluginCommand represents the optional completion.yaml file of a plugin type pluginCommand struct { Name string `json:"name"` ValidArgs []string `json:"validArgs"` Flags []string `json:"flags"` Commands []pluginCommand `json:"commands"` } // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin // and add the dynamic completion hook to call the optional plugin.complete func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) { // Parse the yaml file providing the plugin's sub-commands and flags cmds, err := loadFile(strings.Join( []string{plug.Dir(), pluginStaticCompletionFile}, string(filepath.Separator))) if err != nil { // The file could be missing or invalid. No static completion for this plugin. if settings.Debug { log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) } // Continue to setup dynamic completion. cmds = &pluginCommand{} } // Preserve the Usage string specified for the plugin cmds.Name = pluginCmd.Use addPluginCommands(plug, pluginCmd, cmds) } // addPluginCommands is a recursive method that adds each different level // of sub-commands and flags for the plugins that have provided such information func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { if cmds == nil { return } if len(cmds.Name) == 0 { // Missing name for a command if settings.Debug { log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath())) } return } baseCmd.Use = cmds.Name baseCmd.ValidArgs = cmds.ValidArgs // Setup the same dynamic completion for each plugin sub-command. // This is because if dynamic completion is triggered, there is a single executable // to call (plugin.complete), so every sub-commands calls it in the same fashion. if cmds.Commands == nil { // Only setup dynamic completion if there are no sub-commands. This avoids // calling plugin.complete at every completion, which greatly simplifies // development of plugin.complete for plugin developers. baseCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return pluginDynamicComp(plug, cmd, args, toComplete) } } // Create fake flags. if len(cmds.Flags) > 0 { // The flags can be created with any type, since we only need them for completion. // pflag does not allow to create short flags without a corresponding long form // so we look for all short flags and match them to any long flag. This will allow // plugins to provide short flags without a long form. // If there are more short-flags than long ones, we'll create an extra long flag with // the same single letter as the short form. shorts := []string{} longs := []string{} for _, flag := range cmds.Flags { if len(flag) == 1 { shorts = append(shorts, flag) } else { longs = append(longs, flag) } } f := baseCmd.Flags() if len(longs) >= len(shorts) { for i := range longs { if i < len(shorts) { f.BoolP(longs[i], shorts[i], false, "") } else { f.Bool(longs[i], false, "") } } } else { for i := range shorts { if i < len(longs) { f.BoolP(longs[i], shorts[i], false, "") } else { // Create a long flag with the same name as the short flag. // Not a perfect solution, but it's better than ignoring the extra short flags. f.BoolP(shorts[i], shorts[i], false, "") } } } } // Recursively add any sub-commands for _, cmd := range cmds.Commands { // Create a fake command so that completion can be done for the sub-commands of the plugin subCmd := &cobra.Command{ // This prevents Cobra from removing the flags. We want to keep the flags to pass them // to the dynamic completion script of the plugin. DisableFlagParsing: true, // A Run is required for it to be a valid command without subcommands Run: func(_ *cobra.Command, _ []string) {}, } baseCmd.AddCommand(subCmd) addPluginCommands(plug, subCmd, &cmd) } } // loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object func loadFile(path string) (*pluginCommand, error) { cmds := new(pluginCommand) b, err := os.ReadFile(path) if err != nil { return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path) } err = yaml.Unmarshal(b, cmds) return cmds, err } // pluginDynamicComp call the plugin.complete script of the plugin (if available) // to obtain the dynamic completion choices. It must pass all the flags and sub-commands // specified in the command-line to the plugin.complete executable (except helm's global flags) func pluginDynamicComp(plug plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { subprocessPlug, ok := plug.(*plugin.SubprocessPluginRuntime) if !ok { // Completion only supported for subprocess plugins (TODO: fix this) cobra.CompDebugln(fmt.Sprintf("Unsupported plugin runtime: %q", plug.Metadata().Runtime), settings.Debug) return nil, cobra.ShellCompDirectiveDefault } var ignoreFlags bool if cliConfig, ok := subprocessPlug.Metadata().Config.(*schema.ConfigCLIV1); ok { ignoreFlags = cliConfig.IgnoreFlags } u, err := processParent(cmd, args) if err != nil { return nil, cobra.ShellCompDirectiveError } // We will call the dynamic completion script of the plugin main := strings.Join([]string{plug.Dir(), pluginDynamicCompletionExecutable}, string(filepath.Separator)) // We must include all sub-commands passed on the command-line. // To do that, we pass-in the entire CommandPath, except the first two elements // which are 'helm' and 'pluginName'. argv := strings.Split(cmd.CommandPath(), " ")[2:] if !ignoreFlags { argv = append(argv, u...) argv = append(argv, toComplete) } cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug) buf := new(bytes.Buffer) // Prepare environment env := os.Environ() for k, v := range settings.EnvVars() { env = append(env, fmt.Sprintf("%s=%s", k, v)) } // For subprocess runtime, use InvokeWithEnv for dynamic completion if err := subprocessPlug.InvokeWithEnv(main, argv, env, nil, buf, buf); err != nil { // The dynamic completion file is optional for a plugin, so this error is ok. cobra.CompDebugln(fmt.Sprintf("Unable to call %s: %v", main, err.Error()), settings.Debug) return nil, cobra.ShellCompDirectiveDefault } var completions []string for comp := range strings.SplitSeq(buf.String(), "\n") { // Remove any empty lines if len(comp) > 0 { completions = append(completions, comp) } } // Check if the last line of output is of the form :, which // indicates the BashCompletionDirective. directive := cobra.ShellCompDirectiveDefault if len(completions) > 0 { lastLine := completions[len(completions)-1] if len(lastLine) > 1 && lastLine[0] == ':' { if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { directive = cobra.ShellCompDirective(strInt) completions = completions[:len(completions)-1] } } } return completions, directive }