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  }