github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/plugin.go (about)

     1  package commands
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/argoproj/argo-cd/v3/util/cli"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  )
    15  
    16  // DefaultPluginHandler implements the PluginHandler interface
    17  type DefaultPluginHandler struct {
    18  	ValidPrefixes []string
    19  	lookPath      func(file string) (string, error)
    20  	run           func(cmd *exec.Cmd) error
    21  }
    22  
    23  // NewDefaultPluginHandler instantiates the DefaultPluginHandler
    24  func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler {
    25  	return &DefaultPluginHandler{
    26  		ValidPrefixes: validPrefixes,
    27  		lookPath:      exec.LookPath,
    28  		run: func(cmd *exec.Cmd) error {
    29  			return cmd.Run()
    30  		},
    31  	}
    32  }
    33  
    34  // HandleCommandExecutionError processes the error returned from executing the command.
    35  // It handles both standard Argo CD commands and plugin commands. We don't require to return
    36  // error but we are doing it to cover various test scenarios.
    37  func (h *DefaultPluginHandler) HandleCommandExecutionError(err error, isArgocdCLI bool, args []string) error {
    38  	// the log level needs to be setup manually here since the initConfig()
    39  	// set by the cobra.OnInitialize() was never executed because cmd.Execute()
    40  	// gave us a non-nil error.
    41  	initConfig()
    42  	cli.SetLogFormat("text")
    43  	// If it's an unknown command error, attempt to handle it as a plugin.
    44  	// Unfortunately, cobra doesn't handle this error, so we need to assume
    45  	// that error consists of substring "unknown command".
    46  	// https://github.com/spf13/cobra/pull/2167
    47  	if isArgocdCLI && strings.Contains(err.Error(), "unknown command") {
    48  		pluginPath, pluginErr := h.handlePluginCommand(args[1:])
    49  		// IMP: If a plugin doesn't exist, the returned path will be empty along with nil error
    50  		// This means the command is neither a normal Argo CD Command nor a plugin.
    51  		if pluginErr != nil {
    52  			// If plugin handling fails, report the plugin error and exit
    53  			fmt.Printf("Error: %v\n", pluginErr)
    54  			return pluginErr
    55  		} else if pluginPath == "" {
    56  			fmt.Printf("Error: %v\nRun 'argocd --help' for usage.\n", err)
    57  			return err
    58  		}
    59  	} else {
    60  		// If it's any other error (not an unknown command), report it directly and exit
    61  		fmt.Printf("Error: %v\n", err)
    62  		return err
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  // handlePluginCommand is  responsible for finding and executing a plugin when a command isn't recognized as a built-in command
    69  func (h *DefaultPluginHandler) handlePluginCommand(cmdArgs []string) (string, error) {
    70  	foundPluginPath := ""
    71  	path, found := h.lookForPlugin(cmdArgs[0])
    72  	if !found {
    73  		return foundPluginPath, nil
    74  	}
    75  
    76  	foundPluginPath = path
    77  
    78  	// Execute the plugin that is found
    79  	if err := h.executePlugin(foundPluginPath, cmdArgs[1:], os.Environ()); err != nil {
    80  		return foundPluginPath, err
    81  	}
    82  
    83  	return foundPluginPath, nil
    84  }
    85  
    86  // lookForPlugin looks for a plugin in the PATH that starts with argocd prefix
    87  func (h *DefaultPluginHandler) lookForPlugin(filename string) (string, bool) {
    88  	for _, prefix := range h.ValidPrefixes {
    89  		pluginName := fmt.Sprintf("%s-%s", prefix, filename)
    90  		path, err := h.lookPath(pluginName)
    91  		if err != nil {
    92  			//  error if a plugin is found in a relative path
    93  			if errors.Is(err, exec.ErrDot) {
    94  				log.Errorf("Plugin '%s' found in relative path: %v", pluginName, err)
    95  			} else {
    96  				log.Warnf("error looking for plugin '%s': %v", pluginName, err)
    97  			}
    98  			continue
    99  		}
   100  
   101  		if path == "" {
   102  			return "", false
   103  		}
   104  
   105  		return path, true
   106  	}
   107  
   108  	return "", false
   109  }
   110  
   111  // executePlugin implements PluginHandler and executes a plugin found
   112  func (h *DefaultPluginHandler) executePlugin(executablePath string, cmdArgs, environment []string) error {
   113  	cmd := h.command(executablePath, cmdArgs...)
   114  	cmd.Stdout = os.Stdout
   115  	cmd.Stderr = os.Stderr
   116  	cmd.Stdin = os.Stdin
   117  	cmd.Env = environment
   118  
   119  	err := h.run(cmd)
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  // command creates a new command for all OSs
   128  func (h *DefaultPluginHandler) command(name string, arg ...string) *exec.Cmd {
   129  	cmd := &exec.Cmd{
   130  		Path: name,
   131  		Args: append([]string{name}, arg...),
   132  	}
   133  	if filepath.Base(name) == name {
   134  		lp, err := h.lookPath(name)
   135  		if lp != "" && err != nil {
   136  			// Update cmd.Path even if err is non-nil.
   137  			// If err is ErrDot (especially on Windows), lp may include a resolved
   138  			// extension (like .exe or .bat) that should be preserved.
   139  			cmd.Path = lp
   140  		}
   141  	}
   142  	return cmd
   143  }