mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
feat(cli): Add Plugin Support to the Argo CD CLI (#20074)
Signed-off-by: nitishfy <justnitish06@gmail.com> Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
This commit is contained in:
parent
9a738b2880
commit
6cf29619ae
16 changed files with 1025 additions and 9 deletions
143
cmd/argocd/commands/plugin.go
Normal file
143
cmd/argocd/commands/plugin.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/cli"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DefaultPluginHandler implements the PluginHandler interface
|
||||
type DefaultPluginHandler struct {
|
||||
ValidPrefixes []string
|
||||
lookPath func(file string) (string, error)
|
||||
run func(cmd *exec.Cmd) error
|
||||
}
|
||||
|
||||
// NewDefaultPluginHandler instantiates the DefaultPluginHandler
|
||||
func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler {
|
||||
return &DefaultPluginHandler{
|
||||
ValidPrefixes: validPrefixes,
|
||||
lookPath: exec.LookPath,
|
||||
run: func(cmd *exec.Cmd) error {
|
||||
return cmd.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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()
|
||||
// gave us a non-nil error.
|
||||
initConfig()
|
||||
cli.SetLogFormat("text")
|
||||
// If it's an unknown command error, attempt to handle it as a plugin.
|
||||
// Unfortunately, cobra doesn't handle this error, so we need to assume
|
||||
// that error consists of substring "unknown command".
|
||||
// https://github.com/spf13/cobra/pull/2167
|
||||
if isArgocdCLI && strings.Contains(err.Error(), "unknown command") {
|
||||
pluginPath, pluginErr := h.handlePluginCommand(args[1:])
|
||||
// IMP: If a plugin doesn't exist, the returned path will be empty along with nil error
|
||||
// This means the command is neither a normal Argo CD Command nor a plugin.
|
||||
if pluginErr != nil {
|
||||
// If plugin handling fails, report the plugin error and exit
|
||||
fmt.Printf("Error: %v\n", pluginErr)
|
||||
return pluginErr
|
||||
} else if pluginPath == "" {
|
||||
fmt.Printf("Error: %v\nRun 'argocd --help' for usage.\n", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// If it's any other error (not an unknown command), report it directly and exit
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePluginCommand is responsible for finding and executing a plugin when a command isn't recognized as a built-in command
|
||||
func (h *DefaultPluginHandler) handlePluginCommand(cmdArgs []string) (string, error) {
|
||||
foundPluginPath := ""
|
||||
path, found := h.lookForPlugin(cmdArgs[0])
|
||||
if !found {
|
||||
return foundPluginPath, nil
|
||||
}
|
||||
|
||||
foundPluginPath = path
|
||||
|
||||
// Execute the plugin that is found
|
||||
if err := h.executePlugin(foundPluginPath, cmdArgs[1:], os.Environ()); err != nil {
|
||||
return foundPluginPath, err
|
||||
}
|
||||
|
||||
return foundPluginPath, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if len(path) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return path, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// executePlugin implements PluginHandler and executes a plugin found
|
||||
func (h *DefaultPluginHandler) executePlugin(executablePath string, cmdArgs, environment []string) error {
|
||||
cmd := h.command(executablePath, cmdArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Env = environment
|
||||
|
||||
err := h.run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// command creates a new command for all OSs
|
||||
func (h *DefaultPluginHandler) command(name string, arg ...string) *exec.Cmd {
|
||||
cmd := &exec.Cmd{
|
||||
Path: name,
|
||||
Args: append([]string{name}, arg...),
|
||||
}
|
||||
if filepath.Base(name) == name {
|
||||
lp, err := h.lookPath(name)
|
||||
if lp != "" && err != nil {
|
||||
// Update cmd.Path even if err is non-nil.
|
||||
// If err is ErrDot (especially on Windows), lp may include a resolved
|
||||
// extension (like .exe or .bat) that should be preserved.
|
||||
cmd.Path = lp
|
||||
}
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
311
cmd/argocd/commands/plugin_test.go
Normal file
311
cmd/argocd/commands/plugin_test.go
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// setupPluginPath sets the PATH to the directory where plugins are stored for testing purpose
|
||||
func setupPluginPath(t *testing.T) {
|
||||
t.Helper()
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
testdataPath := filepath.Join(wd, "testdata")
|
||||
t.Setenv("PATH", testdataPath)
|
||||
}
|
||||
|
||||
// TestNormalCommandWithPlugin ensures that a standard ArgoCD command executes correctly
|
||||
// even when a plugin with the same name exists in the PATH
|
||||
func TestNormalCommandWithPlugin(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
_ = NewDefaultPluginHandler([]string{"argocd"})
|
||||
args := []string{"argocd", "version", "--short", "--client"}
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewVersionCmd(&argocdclient.ClientOptions{}, nil)
|
||||
cmd.SetArgs(args[1:])
|
||||
cmd.SetOut(buf)
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Equal(t, "argocd: v99.99.99+unknown\n", output)
|
||||
}
|
||||
|
||||
// TestPluginExecution verifies that a plugin found in the PATH executes successfully following the correct naming conventions
|
||||
func TestPluginExecution(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedPluginErr string
|
||||
}{
|
||||
{
|
||||
name: "'argocd-foo' binary exists in the PATH",
|
||||
args: []string{"argocd", "foo"},
|
||||
expectedPluginErr: "",
|
||||
},
|
||||
{
|
||||
name: "'argocd-demo_plugin' binary exists in the PATH",
|
||||
args: []string{"argocd", "demo_plugin"},
|
||||
expectedPluginErr: "",
|
||||
},
|
||||
{
|
||||
name: "'my-plugin' binary exists in the PATH",
|
||||
args: []string{"argocd", "my-plugin"},
|
||||
expectedPluginErr: "unknown command \"my-plugin\" for \"argocd\"",
|
||||
},
|
||||
{
|
||||
name: "'argocd_my-plugin' binary exists in the PATH",
|
||||
args: []string{"argocd", "my-plugin"},
|
||||
expectedPluginErr: "unknown command \"my-plugin\" for \"argocd\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd.SetArgs(tt.args[1:])
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
// since the command is not a valid argocd command, check for plugin execution
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, tt.args)
|
||||
if tt.expectedPluginErr == "" {
|
||||
require.NoError(t, pluginErr)
|
||||
} else {
|
||||
require.EqualError(t, pluginErr, tt.expectedPluginErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalCommandError checks for an error when executing a normal ArgoCD command with invalid flags
|
||||
func TestNormalCommandError(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
args := []string{"argocd", "version", "--non-existent-flag"}
|
||||
cmd := NewVersionCmd(&argocdclient.ClientOptions{}, nil)
|
||||
cmd.SetArgs(args[1:])
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
|
||||
assert.EqualError(t, pluginErr, "unknown flag: --non-existent-flag")
|
||||
}
|
||||
|
||||
// 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"})
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
args := []string{"argocd", "non-existent"}
|
||||
cmd.SetArgs(args[1:])
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
|
||||
require.Error(t, pluginErr)
|
||||
assert.Equal(t, err, pluginErr)
|
||||
}
|
||||
|
||||
// TestPluginNoExecutePermission verifies the behavior when a plugin doesn't have executable permissions
|
||||
func TestPluginNoExecutePermission(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
args := []string{"argocd", "no-permission"}
|
||||
cmd.SetArgs(args[1:])
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
|
||||
require.Error(t, pluginErr)
|
||||
assert.EqualError(t, pluginErr, "unknown command \"no-permission\" for \"argocd\"")
|
||||
}
|
||||
|
||||
// TestPluginExecutionError checks for errors that occur during plugin execution
|
||||
func TestPluginExecutionError(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
args := []string{"argocd", "error"}
|
||||
cmd.SetArgs(args[1:])
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
|
||||
require.Error(t, pluginErr)
|
||||
assert.EqualError(t, pluginErr, "exit status 1")
|
||||
}
|
||||
|
||||
// TestPluginInRelativePathIgnored ensures that plugins in a relative path, even if the path is included in PATH,
|
||||
// are ignored and not executed.
|
||||
func TestPluginInRelativePathIgnored(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
relativePath := "./relative-plugins"
|
||||
err := os.MkdirAll(relativePath, 0o755)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(relativePath)
|
||||
|
||||
relativePluginPath := filepath.Join(relativePath, "argocd-ignore-plugin")
|
||||
err = os.WriteFile(relativePluginPath, []byte("#!/bin/bash\necho 'This should not execute'\n"), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv("PATH", os.Getenv("PATH")+string(os.PathListSeparator)+relativePath)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
args := []string{"argocd", "ignore-plugin"}
|
||||
cmd.SetArgs(args[1:])
|
||||
|
||||
err = cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
|
||||
require.Error(t, pluginErr)
|
||||
assert.EqualError(t, pluginErr, "unknown command \"ignore-plugin\" for \"argocd\"")
|
||||
}
|
||||
|
||||
// TestPluginFlagParsing checks that the flags are parsed correctly by the plugin handler
|
||||
func TestPluginFlagParsing(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
shouldFail bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "Valid flags",
|
||||
args: []string{"argocd", "test-plugin", "--flag1", "value1", "--flag2", "value2"},
|
||||
shouldFail: false,
|
||||
expectedErrMsg: "",
|
||||
},
|
||||
{
|
||||
name: "Unknown flag",
|
||||
args: []string{"argocd", "test-plugin", "--flag3", "invalid"},
|
||||
shouldFail: true,
|
||||
expectedErrMsg: "exit status 1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
cmd.SetArgs(tt.args[1:])
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, tt.args)
|
||||
|
||||
if tt.shouldFail {
|
||||
require.Error(t, pluginErr)
|
||||
assert.Equal(t, tt.expectedErrMsg, pluginErr.Error(), "Unexpected error message")
|
||||
} else {
|
||||
require.NoError(t, pluginErr, "Expected no error for valid flags")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPluginStatusCode checks for a correct status code that a plugin binary would generate
|
||||
func TestPluginStatusCode(t *testing.T) {
|
||||
setupPluginPath(t)
|
||||
|
||||
pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantStatus int
|
||||
throwErr bool
|
||||
}{
|
||||
{
|
||||
name: "plugin generates the successful exit code",
|
||||
args: []string{"argocd", "status-code-plugin", "--flag1", "value1"},
|
||||
wantStatus: 0,
|
||||
throwErr: false,
|
||||
},
|
||||
{
|
||||
name: "plugin generates an error status code",
|
||||
args: []string{"argocd", "status-code-plugin", "--flag3", "value3"},
|
||||
wantStatus: 1,
|
||||
throwErr: true,
|
||||
},
|
||||
{
|
||||
name: "plugin generates a status code for an invalid command",
|
||||
args: []string{"argocd", "status-code-plugin", "invalid"},
|
||||
wantStatus: 127,
|
||||
throwErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := NewCommand()
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
cmd.SetArgs(tt.args[1:])
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
|
||||
pluginErr := pluginHandler.HandleCommandExecutionError(err, true, tt.args)
|
||||
if !tt.throwErr {
|
||||
require.NoError(t, pluginErr)
|
||||
} else {
|
||||
require.Error(t, pluginErr)
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(pluginErr, &exitErr) {
|
||||
assert.Equal(t, tt.wantStatus, exitErr.ExitCode(), "unexpected exit code")
|
||||
} else {
|
||||
t.Fatalf("expected an exit error, got: %v", pluginErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
15
cmd/argocd/commands/testdata/argocd-demo_plugin
vendored
Executable file
15
cmd/argocd/commands/testdata/argocd-demo_plugin
vendored
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "version" ]]
|
||||
then
|
||||
echo "1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == "config" ]]
|
||||
then
|
||||
echo "$KUBECONFIG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "I am a plugin named argocd-demo_plugin"
|
||||
17
cmd/argocd/commands/testdata/argocd-error
vendored
Executable file
17
cmd/argocd/commands/testdata/argocd-error
vendored
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "version" ]]
|
||||
then
|
||||
echo "1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == "config" ]]
|
||||
then
|
||||
echo "$KUBECONFIG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Default behavior: simulate an error
|
||||
echo "Error: I am a plugin named argocd-error, and I encountered an error." >&2
|
||||
exit 1
|
||||
15
cmd/argocd/commands/testdata/argocd-foo
vendored
Executable file
15
cmd/argocd/commands/testdata/argocd-foo
vendored
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "version" ]]
|
||||
then
|
||||
echo "1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == "config" ]]
|
||||
then
|
||||
echo "$KUBECONFIG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "I am a plugin named argocd-foo"
|
||||
3
cmd/argocd/commands/testdata/argocd-no-permission
vendored
Normal file
3
cmd/argocd/commands/testdata/argocd-no-permission
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "I can't be executed since I don't have executable permissions"
|
||||
16
cmd/argocd/commands/testdata/argocd-status-code-plugin
vendored
Executable file
16
cmd/argocd/commands/testdata/argocd-status-code-plugin
vendored
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
case "$1" in
|
||||
"--flag1")
|
||||
echo "Flag1 detected: $2"
|
||||
exit 0
|
||||
;;
|
||||
"--flag3")
|
||||
echo "Unknown argument: --flag3" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Plugin not found or invalid command" >&2
|
||||
exit 127
|
||||
;;
|
||||
esac
|
||||
16
cmd/argocd/commands/testdata/argocd-test-plugin
vendored
Executable file
16
cmd/argocd/commands/testdata/argocd-test-plugin
vendored
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Ensure we receive the expected flags and values
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--flag1) FLAG1="$2"; shift ;;
|
||||
--flag2) FLAG2="$2"; shift ;;
|
||||
--help) echo "Usage: argocd test-plugin --flag1 value1 --flag2 value2"; exit 0 ;;
|
||||
*) echo "Unknown argument: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Output the parsed flag values
|
||||
echo "Flag1: ${FLAG1}"
|
||||
echo "Flag2: ${FLAG2}"
|
||||
4
cmd/argocd/commands/testdata/argocd-version
vendored
Executable file
4
cmd/argocd/commands/testdata/argocd-version
vendored
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Fetching the version of Argo CD..."
|
||||
argocd version --short --client
|
||||
7
cmd/argocd/commands/testdata/argocd_my-plugin
vendored
Executable file
7
cmd/argocd/commands/testdata/argocd_my-plugin
vendored
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Hello from argocd_my-plugin"
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
echo "Arguments received: $@"
|
||||
fi
|
||||
7
cmd/argocd/commands/testdata/my-plugin
vendored
Executable file
7
cmd/argocd/commands/testdata/my-plugin
vendored
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Hello from my-plugin"
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
echo "Arguments received: $@"
|
||||
fi
|
||||
43
cmd/main.go
43
cmd/main.go
|
|
@ -1,7 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -39,11 +41,12 @@ func main() {
|
|||
binaryName = val
|
||||
}
|
||||
|
||||
isCLI := false
|
||||
isArgocdCLI := false
|
||||
|
||||
switch binaryName {
|
||||
case "argocd", "argocd-linux-amd64", "argocd-darwin-amd64", "argocd-windows-amd64.exe":
|
||||
command = cli.NewCommand()
|
||||
isCLI = true
|
||||
isArgocdCLI = true
|
||||
case "argocd-server":
|
||||
command = apiserver.NewCommand()
|
||||
case "argocd-application-controller":
|
||||
|
|
@ -52,7 +55,7 @@ func main() {
|
|||
command = reposerver.NewCommand()
|
||||
case "argocd-cmp-server":
|
||||
command = cmpserver.NewCommand()
|
||||
isCLI = true
|
||||
isArgocdCLI = true
|
||||
case "argocd-commit-server":
|
||||
command = commitserver.NewCommand()
|
||||
case "argocd-dex":
|
||||
|
|
@ -61,19 +64,41 @@ func main() {
|
|||
command = notification.NewCommand()
|
||||
case "argocd-git-ask-pass":
|
||||
command = gitaskpass.NewCommand()
|
||||
isCLI = true
|
||||
isArgocdCLI = true
|
||||
case "argocd-applicationset-controller":
|
||||
command = applicationset.NewCommand()
|
||||
case "argocd-k8s-auth":
|
||||
command = k8sauth.NewCommand()
|
||||
isCLI = true
|
||||
isArgocdCLI = true
|
||||
default:
|
||||
command = cli.NewCommand()
|
||||
isCLI = true
|
||||
isArgocdCLI = true
|
||||
}
|
||||
util.SetAutoMaxProcs(isCLI)
|
||||
util.SetAutoMaxProcs(isArgocdCLI)
|
||||
|
||||
if err := command.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
if isArgocdCLI {
|
||||
// silence errors and usages since we'll be printing them manually.
|
||||
// This is because if we execute a plugin, the initial
|
||||
// errors and usage are always going to get printed that we don't want.
|
||||
command.SilenceErrors = true
|
||||
command.SilenceUsage = true
|
||||
}
|
||||
|
||||
err := command.Execute()
|
||||
// if the err is non-nil, 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)
|
||||
if pluginErr != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(pluginErr, &exitErr) {
|
||||
// Return the actual plugin exit code
|
||||
os.Exit(exitErr.ExitCode())
|
||||
}
|
||||
// Fallback to exit code 1 if the error isn't an exec.ExitError
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
201
docs/user-guide/plugins.md
Normal file
201
docs/user-guide/plugins.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Plugins
|
||||
|
||||
## Overview
|
||||
|
||||
This guide demonstrates how to write plugins for the
|
||||
`argocd` CLI tool. Plugins are a way to extend `argocd` CLI with new sub-commands,
|
||||
allowing for custom features which are not part of the default distribution
|
||||
of the `argocd` CLI..
|
||||
|
||||
If you would like to take a look at the original proposal, head over to this [enhancement proposal](../proposals/argocd-cli-pluin.md).
|
||||
It covers how the plugin mechanism works, its benefits, motivations, and the goals it aims to achieve.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need to have a working `argocd` binary installed locally. You can follow
|
||||
the [cli installation documentation](https://argo-cd.readthedocs.io/en/stable/cli_installation/) to install the binary.
|
||||
|
||||
## Create `argocd` plugins
|
||||
|
||||
A plugin is a standalone executable file whose name begins with argocd-.
|
||||
To install a plugin, move its executable file to any directory included in your PATH.
|
||||
Ensure that the PATH configuration specifies the full absolute path to the executable,
|
||||
not a relative path. `argocd` allows plugins to add custom commands such as
|
||||
`argocd my-plugin arg1 arg2 --flag1` by executing a `argocd-my-plugin` binary in the PATH.
|
||||
|
||||
## Limitations
|
||||
|
||||
1. It is currently not possible to create plugins that overwrite existing
|
||||
`argocd` commands. For example, creating a plugin such as `argocd-version`
|
||||
will cause the plugin to never get executed, as the existing `argocd version`
|
||||
command will always take precedence over it. Due to this limitation, it is
|
||||
also not possible to use plugins to add new subcommands to existing `argocd` commands.
|
||||
For example, adding a subcommand `argocd cluster upgrade` by naming your plugin
|
||||
`argocd-cluster` will cause the plugin to be ignored.
|
||||
|
||||
2. It is currently not possible to parse the global flags set by `argocd` CLI. For example,
|
||||
if you have set any global flag value such as `--logformat` value to `text`, the plugin will
|
||||
not parse the global flags and pass the default value to the `--logformat` flag which is `json`.
|
||||
The flag parsing will work exactly the same way for existing `argocd` commands which means executing a
|
||||
existing argocd command such as `argocd cluster list` will correctly parse the flag value as `text`.
|
||||
|
||||
## Conditions for an `argocd` plugin
|
||||
|
||||
Any binary that you would want to execute as an `argocd` plugin need to satisfy the following three conditions:
|
||||
|
||||
1. The binary should start with `argocd-` as the prefix name. For example,
|
||||
`argocd-demo-plugin` or `argocd-demo_plugin` is a valid binary name but not
|
||||
`argocd_demo-plugin` or `argocd_demo_plugin`.
|
||||
2. The binary should have executable permissions otherwise it will be ignored.
|
||||
3. The binary should reside anywhere in the system's absolute PATH.
|
||||
|
||||
## Writing `argocd` plugins
|
||||
|
||||
### Naming a plugin
|
||||
|
||||
An Argo CD plugin’s filename must start with `argocd-`. The subcommands implemented
|
||||
by the plugin are determined by the portion of the filename after the `argocd-` prefix.
|
||||
Anything after `argocd-` will become a subcommand for `argocd`.
|
||||
|
||||
For example, A plugin named `argocd-demo-plugin` is invoked when the user types:
|
||||
```bash
|
||||
argocd demo-plugin [args] [flags]
|
||||
```
|
||||
|
||||
The `argocd` CLI determines which plugin to invoke based on the subcommands provided.
|
||||
|
||||
For example, executing the following command:
|
||||
```bash
|
||||
argocd my-custom-command [args] [flags]
|
||||
```
|
||||
will lead to the execution of plugin named `argocd-my-custom-command` if it is present in the PATH.
|
||||
|
||||
### Writing a plugin
|
||||
|
||||
A plugin can be written in any programming language or script that allows you to write command-line commands.
|
||||
|
||||
A plugin determines which command path it wishes to implement based on its name.
|
||||
|
||||
For example, If a binary named `argocd-demo-plugin` is available in your system's absolute PATH, and the user runs the following command:
|
||||
|
||||
```bash
|
||||
argocd demo-plugin subcommand1 --flag=true
|
||||
```
|
||||
|
||||
Argo CD will translate and execute the corresponding plugin with the following command:
|
||||
|
||||
```bash
|
||||
argocd-demo-plugin subcommand1 --flag=true
|
||||
```
|
||||
|
||||
Similarly, if a plugin named `argocd-demo-demo-plugin` is found in the absolute PATH, and the user invokes:
|
||||
|
||||
```bash
|
||||
argocd demo-demo-plugin subcommand2 subcommand3 --flag=true
|
||||
```
|
||||
|
||||
Argo CD will execute the plugin as:
|
||||
|
||||
```bash
|
||||
argocd-demo-demo-plugin subcommand2 subcommand3 --flag=true
|
||||
```
|
||||
|
||||
### Example plugin
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the argocd CLI is installed
|
||||
if ! command -v argocd &> /dev/null; then
|
||||
echo "Error: Argo CD CLI (argocd) is not installed. Please install it first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "version" ]]
|
||||
then
|
||||
echo "displaying argocd version..."
|
||||
argocd version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
echo "I am a plugin named argocd-foo"
|
||||
```
|
||||
|
||||
### Using a plugin
|
||||
|
||||
To use a plugin, make the plugin executable:
|
||||
```bash
|
||||
sudo chmod +x ./argocd-foo
|
||||
```
|
||||
|
||||
and place it anywhere in your `PATH`:
|
||||
```bash
|
||||
sudo mv ./argocd-foo /usr/local/bin
|
||||
```
|
||||
|
||||
You may now invoke your plugin as a argocd command:
|
||||
```bash
|
||||
argocd foo
|
||||
```
|
||||
|
||||
This would give the following output
|
||||
```bash
|
||||
I am a plugin named argocd-foo
|
||||
```
|
||||
|
||||
All args and flags are passed as-is to the executable:
|
||||
```bash
|
||||
argocd foo version
|
||||
```
|
||||
|
||||
This would give the following output
|
||||
```bash
|
||||
DEBU[0000] command does not exist, looking for a plugin...
|
||||
displaying argocd version...
|
||||
2025/01/16 13:24:36 maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
|
||||
argocd: v2.13.0-rc2+0f083c9
|
||||
BuildDate: 2024-09-20T11:59:25Z
|
||||
GitCommit: 0f083c9e58638fc292cf064e294a1aa53caa5630
|
||||
GitTreeState: clean
|
||||
GoVersion: go1.22.7
|
||||
Compiler: gc
|
||||
Platform: linux/amd64
|
||||
argocd-server: v2.13.0-rc2+0f083c9
|
||||
BuildDate: 2024-09-20T11:59:25Z
|
||||
GitCommit: 0f083c9e58638fc292cf064e294a1aa53caa5630
|
||||
GitTreeState: clean
|
||||
GoVersion: go1.22.7
|
||||
Compiler: gc
|
||||
Platform: linux/amd64
|
||||
Kustomize Version: v5.4.3 2024-07-19T16:40:33Z
|
||||
Helm Version: v3.15.2+g1a500d5
|
||||
Kubectl Version: v0.31.0
|
||||
Jsonnet Version: v0.20.0
|
||||
```
|
||||
|
||||
## Distributing `argocd` plugins
|
||||
|
||||
If you’ve developed an Argo CD plugin for others to use,
|
||||
you should carefully consider how to package, distribute, and
|
||||
deliver updates to ensure a smooth installation and upgrade process
|
||||
for your users.
|
||||
|
||||
### Native / platform specific package management
|
||||
|
||||
You can distribute your plugin using traditional package managers,
|
||||
such as `apt` or `yum` for Linux, `Chocolatey` for Windows, and `Homebrew` for macOS.
|
||||
These package managers are well-suited for distributing plugins as they can
|
||||
place executables directly into the user's PATH, making them easily accessible.
|
||||
|
||||
However, as a plugin author, choosing this approach comes with the responsibility of
|
||||
maintaining and updating the plugin's distribution packages across multiple platforms
|
||||
for every release. This includes testing for compatibility, ensuring timely updates,
|
||||
and managing versioning to provide a seamless experience for your users.
|
||||
|
||||
### Source code
|
||||
|
||||
You can publish the source code of your plugin, for example,
|
||||
in a Git repository. This allows users to access and inspect
|
||||
the code directly. Users who want to install the plugin will need
|
||||
to fetch the code, set up a suitable build environment (if the plugin requires compiling),
|
||||
and manually deploy it.
|
||||
|
|
@ -166,6 +166,7 @@ nav:
|
|||
- user-guide/tool_detection.md
|
||||
- user-guide/projects.md
|
||||
- user-guide/private-repositories.md
|
||||
- user-guide/plugins.md
|
||||
- user-guide/multiple_sources.md
|
||||
- GnuPG verification: user-guide/gpg-verification.md
|
||||
- user-guide/auto_sync.md
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
|
|
@ -13,6 +15,25 @@ import (
|
|||
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app"
|
||||
)
|
||||
|
||||
// createTestPlugin creates a temporary Argo CD CLI plugin script for testing purposes.
|
||||
// The script is written to a temporary directory with executable permissions.
|
||||
func createTestPlugin(t *testing.T, name, content string) string {
|
||||
t.Helper()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
pluginPath := filepath.Join(tmpDir, "argocd-"+name)
|
||||
|
||||
require.NoError(t, os.WriteFile(pluginPath, []byte(content), 0o755))
|
||||
|
||||
// Ensure the plugin is cleaned up properly
|
||||
t.Cleanup(func() {
|
||||
_ = os.Remove(pluginPath)
|
||||
})
|
||||
|
||||
return pluginPath
|
||||
}
|
||||
|
||||
// TestCliAppCommand verifies the basic Argo CD CLI commands for app synchronization and listing.
|
||||
func TestCliAppCommand(t *testing.T) {
|
||||
Given(t).
|
||||
Path("hook").
|
||||
|
|
@ -38,3 +59,209 @@ func TestCliAppCommand(t *testing.T) {
|
|||
assert.Contains(t, NormalizeOutput(output), expected)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNormalArgoCDCommandsExecuteOverPluginsWithSameName verifies that normal Argo CD CLI commands
|
||||
// take precedence over plugins with the same name when both exist in the path.
|
||||
func TestNormalArgoCDCommandsExecuteOverPluginsWithSameName(t *testing.T) {
|
||||
pluginScript := `#!/bin/bash
|
||||
echo "I am a plugin, not Argo CD!"
|
||||
exit 0`
|
||||
|
||||
pluginPath := createTestPlugin(t, "app", pluginScript)
|
||||
|
||||
origPath := os.Getenv("PATH")
|
||||
t.Cleanup(func() {
|
||||
t.Setenv("PATH", origPath)
|
||||
})
|
||||
t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath)
|
||||
|
||||
Given(t).
|
||||
Path("hook").
|
||||
When().
|
||||
CreateApp().
|
||||
And(func() {
|
||||
output, err := RunCli("app", "sync", Name(), "--timeout", "90")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotContains(t, NormalizeOutput(output), "I am a plugin, not Argo CD!")
|
||||
|
||||
vars := map[string]any{"Name": Name(), "Namespace": DeploymentNamespace()}
|
||||
assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} pod Synced Progressing pod/pod created`, vars))
|
||||
assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} hook Succeeded Sync pod/hook created`, vars))
|
||||
}).
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
And(func(_ *Application) {
|
||||
output, err := RunCli("app", "list")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotContains(t, NormalizeOutput(output), "I am a plugin, not Argo CD!")
|
||||
|
||||
expected := Tmpl(
|
||||
t,
|
||||
`{{.Name}} https://kubernetes.default.svc {{.Namespace}} default Synced Healthy Manual <none>`,
|
||||
map[string]any{"Name": Name(), "Namespace": DeploymentNamespace()})
|
||||
assert.Contains(t, NormalizeOutput(output), expected)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCliPluginExecution tests the execution of a valid Argo CD CLI plugin.
|
||||
func TestCliPluginExecution(t *testing.T) {
|
||||
pluginScript := `#!/bin/bash
|
||||
echo "Hello from myplugin"
|
||||
exit 0`
|
||||
pluginPath := createTestPlugin(t, "myplugin", pluginScript)
|
||||
|
||||
origPath := os.Getenv("PATH")
|
||||
t.Cleanup(func() {
|
||||
t.Setenv("PATH", origPath)
|
||||
})
|
||||
t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath)
|
||||
|
||||
output, err := RunPluginCli("", "myplugin")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, NormalizeOutput(output), "Hello from myplugin")
|
||||
}
|
||||
|
||||
// TestCliPluginExecutionConditions tests for plugin execution conditions
|
||||
func TestCliPluginExecutionConditions(t *testing.T) {
|
||||
createValidPlugin := func(t *testing.T, name string, executable bool) string {
|
||||
t.Helper()
|
||||
|
||||
script := `#!/bin/bash
|
||||
echo "Hello from $0"
|
||||
exit 0
|
||||
`
|
||||
|
||||
pluginPath := createTestPlugin(t, name, script)
|
||||
|
||||
if executable {
|
||||
require.NoError(t, os.Chmod(pluginPath, 0o755))
|
||||
} else {
|
||||
require.NoError(t, os.Chmod(pluginPath, 0o644))
|
||||
}
|
||||
|
||||
return pluginPath
|
||||
}
|
||||
|
||||
createInvalidPlugin := func(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
|
||||
script := `#!/bin/bash
|
||||
echo "Hello from $0"
|
||||
exit 0
|
||||
`
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
pluginPath := filepath.Join(tmpDir, "argocd_"+name) // this is an invalid plugin name format
|
||||
require.NoError(t, os.WriteFile(pluginPath, []byte(script), 0o755))
|
||||
|
||||
return pluginPath
|
||||
}
|
||||
|
||||
// 'argocd-valid-plugin' is a valid plugin name
|
||||
validPlugin := createValidPlugin(t, "valid-plugin", true)
|
||||
// 'argocd_invalid-plugin' is an invalid plugin name
|
||||
invalidPlugin := createInvalidPlugin(t, "invalid-plugin")
|
||||
// 'argocd-nonexec-plugin' is a valid plugin name but lacks executable permissions
|
||||
noExecPlugin := createValidPlugin(t, "noexec-plugin", false)
|
||||
|
||||
origPath := os.Getenv("PATH")
|
||||
defer func() {
|
||||
t.Setenv("PATH", origPath)
|
||||
}()
|
||||
t.Setenv("PATH", filepath.Dir(validPlugin)+":"+filepath.Dir(invalidPlugin)+":"+filepath.Dir(noExecPlugin)+":"+origPath)
|
||||
|
||||
output, err := RunPluginCli("", "valid-plugin")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, NormalizeOutput(output), "Hello from")
|
||||
|
||||
_, err = RunPluginCli("", "invalid-plugin")
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = RunPluginCli("", "noexec-plugin")
|
||||
// expects error since plugin lacks executable permissions
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestCliPluginStatusCodes verifies that a plugin returns the correct exit codes based on its execution.
|
||||
func TestCliPluginStatusCodes(t *testing.T) {
|
||||
pluginScript := `#!/bin/bash
|
||||
case "$1" in
|
||||
"success") exit 0 ;;
|
||||
"error1") exit 1 ;;
|
||||
"error2") exit 2 ;;
|
||||
*) echo "Unknown argument: $1"; exit 3 ;;
|
||||
esac`
|
||||
|
||||
pluginPath := createTestPlugin(t, "error-plugin", pluginScript)
|
||||
|
||||
origPath := os.Getenv("PATH")
|
||||
t.Cleanup(func() {
|
||||
t.Setenv("PATH", origPath)
|
||||
})
|
||||
t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath)
|
||||
|
||||
output, err := RunPluginCli("", "error-plugin", "success")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, NormalizeOutput(output), "")
|
||||
|
||||
_, err = RunPluginCli("", "error-plugin", "error1")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exit status 1")
|
||||
|
||||
_, err = RunPluginCli("", "error-plugin", "error2")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exit status 2")
|
||||
|
||||
_, err = RunPluginCli("", "error-plugin", "unknown")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exit status 3")
|
||||
}
|
||||
|
||||
// TestCliPluginStdinHandling verifies that a CLI plugin correctly handles input from stdin.
|
||||
func TestCliPluginStdinHandling(t *testing.T) {
|
||||
pluginScript := `#!/bin/bash
|
||||
input=$(cat)
|
||||
echo "Received: $input"
|
||||
exit 0`
|
||||
|
||||
pluginPath := createTestPlugin(t, "stdin-plugin", pluginScript)
|
||||
|
||||
origPath := os.Getenv("PATH")
|
||||
t.Cleanup(func() {
|
||||
t.Setenv("PATH", origPath)
|
||||
})
|
||||
t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
stdin string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"Single line input",
|
||||
"Hello, ArgoCD!",
|
||||
"Received: Hello, ArgoCD!",
|
||||
},
|
||||
{
|
||||
"Multiline input",
|
||||
"Line1\nLine2\nLine3",
|
||||
"Received: Line1\nLine2\nLine3",
|
||||
},
|
||||
{
|
||||
"Empty input",
|
||||
"",
|
||||
"Received:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
output, err := RunPluginCli(tc.stdin, "stdin-plugin")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, NormalizeOutput(output), tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1006,6 +1006,7 @@ func EnsureCleanState(t *testing.T, opts ...TestOption) {
|
|||
}).Info("clean state")
|
||||
}
|
||||
|
||||
// RunCliWithRetry executes an Argo CD CLI command with retry logic.
|
||||
func RunCliWithRetry(maxRetries int, args ...string) (string, error) {
|
||||
var out string
|
||||
var err error
|
||||
|
|
@ -1019,10 +1020,12 @@ func RunCliWithRetry(maxRetries int, args ...string) (string, error) {
|
|||
return out, err
|
||||
}
|
||||
|
||||
// RunCli executes an Argo CD CLI command with no stdin input and default server authentication.
|
||||
func RunCli(args ...string) (string, error) {
|
||||
return RunCliWithStdin("", false, args...)
|
||||
}
|
||||
|
||||
// RunCliWithStdin executes an Argo CD CLI command with optional stdin input and authentication.
|
||||
func RunCliWithStdin(stdin string, isKubeConextOnlyCli bool, args ...string) (string, error) {
|
||||
if plainText {
|
||||
args = append(args, "--plaintext")
|
||||
|
|
@ -1038,6 +1041,11 @@ func RunCliWithStdin(stdin string, isKubeConextOnlyCli bool, args ...string) (st
|
|||
return RunWithStdin(stdin, "", "../../dist/argocd", args...)
|
||||
}
|
||||
|
||||
// RunPluginCli executes an Argo CD CLI plugin with optional stdin input.
|
||||
func RunPluginCli(stdin string, args ...string) (string, error) {
|
||||
return RunWithStdin(stdin, "", "../../dist/argocd", args...)
|
||||
}
|
||||
|
||||
func Patch(t *testing.T, path string, jsonPatch string) {
|
||||
t.Helper()
|
||||
log.WithFields(log.Fields{"path": path, "jsonPatch": jsonPatch}).Info("patching")
|
||||
|
|
|
|||
Loading…
Reference in a new issue