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  }