chore(cli): plugins always have an argocd prefix (#24768)

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
This commit is contained in:
Blake Pettersson
2025-09-29 05:26:26 -10:00
committed by GitHub
parent cbea4e7ef3
commit 116707bed1
4 changed files with 57 additions and 88 deletions

View File

@@ -15,18 +15,17 @@ import (
log "github.com/sirupsen/logrus"
)
// DefaultPluginHandler implements the PluginHandler interface
const prefix = "argocd"
type DefaultPluginHandler struct {
ValidPrefixes []string
lookPath func(file string) (string, error)
run func(cmd *exec.Cmd) error
lookPath func(file string) (string, error)
run func(cmd *exec.Cmd) error
}
// NewDefaultPluginHandler instantiates the DefaultPluginHandler
func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler {
// NewDefaultPluginHandler instantiates a DefaultPluginHandler
func NewDefaultPluginHandler() *DefaultPluginHandler {
return &DefaultPluginHandler{
ValidPrefixes: validPrefixes,
lookPath: exec.LookPath,
lookPath: exec.LookPath,
run: func(cmd *exec.Cmd) error {
return cmd.Run()
},
@@ -34,8 +33,8 @@ func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler {
}
// HandleCommandExecutionError processes the error returned from executing the command.
// It handles both standard Argo CD commands and plugin commands. We don't require to return
// error but we are doing it to cover various test scenarios.
// It handles both standard Argo CD commands and plugin commands. We don't require returning
// an error, but we are doing it to cover various test scenarios.
func (h *DefaultPluginHandler) HandleCommandExecutionError(err error, isArgocdCLI bool, args []string) error {
// the log level needs to be setup manually here since the initConfig()
// set by the cobra.OnInitialize() was never executed because cmd.Execute()
@@ -87,27 +86,24 @@ func (h *DefaultPluginHandler) handlePluginCommand(cmdArgs []string) (string, er
// lookForPlugin looks for a plugin in the PATH that starts with argocd prefix
func (h *DefaultPluginHandler) lookForPlugin(filename string) (string, bool) {
for _, prefix := range h.ValidPrefixes {
pluginName := fmt.Sprintf("%s-%s", prefix, filename)
path, err := h.lookPath(pluginName)
if err != nil {
// error if a plugin is found in a relative path
if errors.Is(err, exec.ErrDot) {
log.Errorf("Plugin '%s' found in relative path: %v", pluginName, err)
} else {
log.Warnf("error looking for plugin '%s': %v", pluginName, err)
}
continue
pluginName := fmt.Sprintf("%s-%s", prefix, filename)
path, err := h.lookPath(pluginName)
if err != nil {
// error if a plugin is found in a relative path
if errors.Is(err, exec.ErrDot) {
log.Errorf("Plugin '%s' found in relative path: %v", pluginName, err)
} else {
log.Warnf("error looking for plugin '%s': %v", pluginName, err)
}
if path == "" {
return "", false
}
return path, true
return "", false
}
return "", false
if path == "" {
return "", false
}
return path, true
}
// executePlugin implements PluginHandler and executes a plugin found
@@ -147,17 +143,11 @@ func (h *DefaultPluginHandler) command(name string, arg ...string) *exec.Cmd {
// ListAvailablePlugins returns a list of plugin names that are available in the user's PATH
// for tab completion. It searches for executables matching the ValidPrefixes pattern.
func (h *DefaultPluginHandler) ListAvailablePlugins() []string {
// Split PATH into individual directories
pathDirs := filepath.SplitList(os.Getenv("PATH"))
if len(pathDirs) == 0 {
return []string{}
}
// Track seen plugin names to avoid duplicates
seenPlugins := make(map[string]bool)
// Search through each directory in PATH
for _, dir := range pathDirs {
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
// Skip empty directories
if dir == "" {
continue
@@ -178,35 +168,27 @@ func (h *DefaultPluginHandler) ListAvailablePlugins() []string {
name := entry.Name()
// Check if the file matches any of our valid prefixes
for _, prefix := range h.ValidPrefixes {
pluginPrefix := prefix + "-"
if strings.HasPrefix(name, pluginPrefix) {
// Extract the plugin command name (everything after the prefix)
pluginName := strings.TrimPrefix(name, pluginPrefix)
// Check if the file is a valid argocd plugin
pluginPrefix := prefix + "-"
if strings.HasPrefix(name, pluginPrefix) {
// Extract the plugin command name (everything after the prefix)
pluginName := strings.TrimPrefix(name, pluginPrefix)
// Skip empty plugin names or names with path separators
if pluginName == "" || strings.Contains(pluginName, "/") || strings.Contains(pluginName, "\\") {
continue
}
// Skip empty plugin names or names with path separators
if pluginName == "" || strings.Contains(pluginName, "/") || strings.Contains(pluginName, "\\") {
continue
}
// Check if the file is executable
if info, err := entry.Info(); err == nil {
// On Unix-like systems, check executable bit
if info.Mode()&0o111 != 0 {
seenPlugins[pluginName] = true
}
// Check if the file is executable
if info, err := entry.Info(); err == nil {
// On Unix-like systems, check executable bit
if info.Mode()&0o111 != 0 {
seenPlugins[pluginName] = true
}
}
}
}
}
// Convert map keys to sorted slice and return
if len(seenPlugins) == 0 {
return []string{}
}
plugins := slices.Collect(maps.Keys(seenPlugins))
slices.Sort(plugins)
return plugins
return slices.Sorted(maps.Keys(seenPlugins))
}

View File

@@ -28,7 +28,7 @@ func setupPluginPath(t *testing.T) {
func TestNormalCommandWithPlugin(t *testing.T) {
setupPluginPath(t)
_ = NewDefaultPluginHandler([]string{"argocd"})
_ = NewDefaultPluginHandler()
args := []string{"argocd", "version", "--short", "--client"}
buf := new(bytes.Buffer)
cmd := NewVersionCmd(&argocdclient.ClientOptions{}, nil)
@@ -47,7 +47,7 @@ func TestNormalCommandWithPlugin(t *testing.T) {
func TestPluginExecution(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
cmd := NewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
@@ -101,7 +101,7 @@ func TestPluginExecution(t *testing.T) {
func TestNormalCommandError(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
args := []string{"argocd", "version", "--non-existent-flag"}
cmd := NewVersionCmd(&argocdclient.ClientOptions{}, nil)
cmd.SetArgs(args[1:])
@@ -118,7 +118,7 @@ func TestNormalCommandError(t *testing.T) {
// TestUnknownCommandNoPlugin tests the scenario when the command is neither a normal ArgoCD command
// nor exists as a plugin
func TestUnknownCommandNoPlugin(t *testing.T) {
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
cmd := NewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
@@ -137,7 +137,7 @@ func TestUnknownCommandNoPlugin(t *testing.T) {
func TestPluginNoExecutePermission(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
cmd := NewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
@@ -156,7 +156,7 @@ func TestPluginNoExecutePermission(t *testing.T) {
func TestPluginExecutionError(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
cmd := NewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
@@ -187,7 +187,7 @@ func TestPluginInRelativePathIgnored(t *testing.T) {
t.Setenv("PATH", os.Getenv("PATH")+string(os.PathListSeparator)+relativePath)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
cmd := NewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
@@ -206,7 +206,7 @@ func TestPluginInRelativePathIgnored(t *testing.T) {
func TestPluginFlagParsing(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
tests := []struct {
name string
@@ -255,7 +255,7 @@ func TestPluginFlagParsing(t *testing.T) {
func TestPluginStatusCode(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
tests := []struct {
name string
@@ -320,25 +320,14 @@ func TestListAvailablePlugins(t *testing.T) {
expected []string
}{
{
name: "Standard argocd prefix finds plugins",
validPrefix: []string{"argocd"},
expected: []string{"demo_plugin", "error", "foo", "status-code-plugin", "test-plugin", "version"},
},
{
name: "Multiple prefixes",
validPrefix: []string{"argocd", "kubectl"},
expected: []string{"demo_plugin", "error", "foo", "status-code-plugin", "test-plugin", "version"},
},
{
name: "Non-existent prefix finds no plugins",
validPrefix: []string{"nonexistent"},
expected: []string{},
name: "Standard argocd prefix finds plugins",
expected: []string{"demo_plugin", "error", "foo", "status-code-plugin", "test-plugin", "version"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pluginHandler := NewDefaultPluginHandler(tt.validPrefix)
pluginHandler := NewDefaultPluginHandler()
plugins := pluginHandler.ListAvailablePlugins()
assert.Equal(t, tt.expected, plugins)
@@ -351,7 +340,7 @@ func TestListAvailablePluginsEmptyPath(t *testing.T) {
// Set empty PATH
t.Setenv("PATH", "")
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
plugins := pluginHandler.ListAvailablePlugins()
assert.Empty(t, plugins, "Should return empty list when PATH is empty")
@@ -361,7 +350,7 @@ func TestListAvailablePluginsEmptyPath(t *testing.T) {
func TestListAvailablePluginsNonExecutableFiles(t *testing.T) {
setupPluginPath(t)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
plugins := pluginHandler.ListAvailablePlugins()
// Should not include 'no-permission' since it's not executable
@@ -388,7 +377,7 @@ func TestListAvailablePluginsDeduplication(t *testing.T) {
testPath := dir1 + string(os.PathListSeparator) + dir2
t.Setenv("PATH", testPath)
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
pluginHandler := NewDefaultPluginHandler()
plugins := pluginHandler.ListAvailablePlugins()
assert.Equal(t, []string{"duplicate"}, plugins)

View File

@@ -46,8 +46,7 @@ func NewCommand() *cobra.Command {
SilenceUsage: true,
ValidArgsFunction: func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
// Return available plugin commands for tab completion
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
plugins := pluginHandler.ListAvailablePlugins()
plugins := NewDefaultPluginHandler().ListAvailablePlugins()
return plugins, cobra.ShellCompDirectiveNoFileComp
},
}

View File

@@ -83,12 +83,11 @@ func main() {
}
err := command.Execute()
// if the err is non-nil, try to look for various scenarios
// if an error is present, try to look for various scenarios
// such as if the error is from the execution of a normal argocd command,
// unknown command error or any other.
if err != nil {
pluginHandler := cli.NewDefaultPluginHandler([]string{"argocd"})
pluginErr := pluginHandler.HandleCommandExecutionError(err, isArgocdCLI, os.Args)
pluginErr := cli.NewDefaultPluginHandler().HandleCommandExecutionError(err, isArgocdCLI, os.Args)
if pluginErr != nil {
var exitErr *exec.ExitError
if errors.As(pluginErr, &exitErr) {