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  }