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