github.com/argoproj/argo-cd/v3@v3.2.1/test/e2e/cli_test.go (about) 1 package e2e 2 3 import ( 4 "os" 5 "path/filepath" 6 "testing" 7 8 "github.com/argoproj/gitops-engine/pkg/health" 9 . "github.com/argoproj/gitops-engine/pkg/sync/common" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 . "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 14 . "github.com/argoproj/argo-cd/v3/test/e2e/fixture" 15 . "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app" 16 ) 17 18 // createTestPlugin creates a temporary Argo CD CLI plugin script for testing purposes. 19 // The script is written to a temporary directory with executable permissions. 20 func createTestPlugin(t *testing.T, name, content string) string { 21 t.Helper() 22 23 tmpDir := t.TempDir() 24 pluginPath := filepath.Join(tmpDir, "argocd-"+name) 25 26 require.NoError(t, os.WriteFile(pluginPath, []byte(content), 0o755)) 27 28 // Ensure the plugin is cleaned up properly 29 t.Cleanup(func() { 30 _ = os.Remove(pluginPath) 31 }) 32 33 return pluginPath 34 } 35 36 // TestCliAppCommand verifies the basic Argo CD CLI commands for app synchronization and listing. 37 func TestCliAppCommand(t *testing.T) { 38 Given(t). 39 Path("hook"). 40 When(). 41 CreateApp(). 42 And(func() { 43 output, err := RunCli("app", "sync", Name(), "--timeout", "90") 44 require.NoError(t, err) 45 vars := map[string]any{"Name": Name(), "Namespace": DeploymentNamespace()} 46 assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} pod Synced Progressing pod/pod created`, vars)) 47 assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} hook Succeeded Sync pod/hook created`, vars)) 48 }). 49 Then(). 50 Expect(OperationPhaseIs(OperationSucceeded)). 51 Expect(HealthIs(health.HealthStatusHealthy)). 52 And(func(_ *Application) { 53 output, err := RunCli("app", "list") 54 require.NoError(t, err) 55 expected := Tmpl( 56 t, 57 `{{.Name}} https://kubernetes.default.svc {{.Namespace}} default Synced Healthy Manual <none>`, 58 map[string]any{"Name": Name(), "Namespace": DeploymentNamespace()}) 59 assert.Contains(t, NormalizeOutput(output), expected) 60 }) 61 } 62 63 // TestNormalArgoCDCommandsExecuteOverPluginsWithSameName verifies that normal Argo CD CLI commands 64 // take precedence over plugins with the same name when both exist in the path. 65 func TestNormalArgoCDCommandsExecuteOverPluginsWithSameName(t *testing.T) { 66 pluginScript := `#!/bin/bash 67 echo "I am a plugin, not Argo CD!" 68 exit 0` 69 70 pluginPath := createTestPlugin(t, "app", pluginScript) 71 72 origPath := os.Getenv("PATH") 73 t.Cleanup(func() { 74 t.Setenv("PATH", origPath) 75 }) 76 t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) 77 78 Given(t). 79 Path("hook"). 80 When(). 81 CreateApp(). 82 And(func() { 83 output, err := RunCli("app", "sync", Name(), "--timeout", "90") 84 require.NoError(t, err) 85 86 assert.NotContains(t, NormalizeOutput(output), "I am a plugin, not Argo CD!") 87 88 vars := map[string]any{"Name": Name(), "Namespace": DeploymentNamespace()} 89 assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} pod Synced Progressing pod/pod created`, vars)) 90 assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} hook Succeeded Sync pod/hook created`, vars)) 91 }). 92 Then(). 93 Expect(OperationPhaseIs(OperationSucceeded)). 94 Expect(HealthIs(health.HealthStatusHealthy)). 95 And(func(_ *Application) { 96 output, err := RunCli("app", "list") 97 require.NoError(t, err) 98 99 assert.NotContains(t, NormalizeOutput(output), "I am a plugin, not Argo CD!") 100 101 expected := Tmpl( 102 t, 103 `{{.Name}} https://kubernetes.default.svc {{.Namespace}} default Synced Healthy Manual <none>`, 104 map[string]any{"Name": Name(), "Namespace": DeploymentNamespace()}) 105 assert.Contains(t, NormalizeOutput(output), expected) 106 }) 107 } 108 109 // TestCliPluginExecution tests the execution of a valid Argo CD CLI plugin. 110 func TestCliPluginExecution(t *testing.T) { 111 pluginScript := `#!/bin/bash 112 echo "Hello from myplugin" 113 exit 0` 114 pluginPath := createTestPlugin(t, "myplugin", pluginScript) 115 116 origPath := os.Getenv("PATH") 117 t.Cleanup(func() { 118 t.Setenv("PATH", origPath) 119 }) 120 t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) 121 122 output, err := RunPluginCli("", "myplugin") 123 require.NoError(t, err) 124 assert.Contains(t, NormalizeOutput(output), "Hello from myplugin") 125 } 126 127 // TestCliPluginExecutionConditions tests for plugin execution conditions 128 func TestCliPluginExecutionConditions(t *testing.T) { 129 createValidPlugin := func(t *testing.T, name string, executable bool) string { 130 t.Helper() 131 132 script := `#!/bin/bash 133 echo "Hello from $0" 134 exit 0 135 ` 136 137 pluginPath := createTestPlugin(t, name, script) 138 139 if executable { 140 require.NoError(t, os.Chmod(pluginPath, 0o755)) 141 } else { 142 require.NoError(t, os.Chmod(pluginPath, 0o644)) 143 } 144 145 return pluginPath 146 } 147 148 createInvalidPlugin := func(t *testing.T, name string) string { 149 t.Helper() 150 151 script := `#!/bin/bash 152 echo "Hello from $0" 153 exit 0 154 ` 155 156 tmpDir := t.TempDir() 157 pluginPath := filepath.Join(tmpDir, "argocd_"+name) // this is an invalid plugin name format 158 require.NoError(t, os.WriteFile(pluginPath, []byte(script), 0o755)) 159 160 return pluginPath 161 } 162 163 // 'argocd-valid-plugin' is a valid plugin name 164 validPlugin := createValidPlugin(t, "valid-plugin", true) 165 // 'argocd_invalid-plugin' is an invalid plugin name 166 invalidPlugin := createInvalidPlugin(t, "invalid-plugin") 167 // 'argocd-nonexec-plugin' is a valid plugin name but lacks executable permissions 168 noExecPlugin := createValidPlugin(t, "noexec-plugin", false) 169 170 origPath := os.Getenv("PATH") 171 defer func() { 172 t.Setenv("PATH", origPath) 173 }() 174 t.Setenv("PATH", filepath.Dir(validPlugin)+":"+filepath.Dir(invalidPlugin)+":"+filepath.Dir(noExecPlugin)+":"+origPath) 175 176 output, err := RunPluginCli("", "valid-plugin") 177 require.NoError(t, err) 178 assert.Contains(t, NormalizeOutput(output), "Hello from") 179 180 _, err = RunPluginCli("", "invalid-plugin") 181 require.Error(t, err) 182 183 _, err = RunPluginCli("", "noexec-plugin") 184 // expects error since plugin lacks executable permissions 185 require.Error(t, err) 186 } 187 188 // TestCliPluginStatusCodes verifies that a plugin returns the correct exit codes based on its execution. 189 func TestCliPluginStatusCodes(t *testing.T) { 190 pluginScript := `#!/bin/bash 191 case "$1" in 192 "success") exit 0 ;; 193 "error1") exit 1 ;; 194 "error2") exit 2 ;; 195 *) echo "Unknown argument: $1"; exit 3 ;; 196 esac` 197 198 pluginPath := createTestPlugin(t, "error-plugin", pluginScript) 199 200 origPath := os.Getenv("PATH") 201 t.Cleanup(func() { 202 t.Setenv("PATH", origPath) 203 }) 204 t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) 205 206 output, err := RunPluginCli("", "error-plugin", "success") 207 require.NoError(t, err) 208 assert.Contains(t, NormalizeOutput(output), "") 209 210 _, err = RunPluginCli("", "error-plugin", "error1") 211 require.Error(t, err) 212 assert.Contains(t, err.Error(), "exit status 1") 213 214 _, err = RunPluginCli("", "error-plugin", "error2") 215 require.Error(t, err) 216 assert.Contains(t, err.Error(), "exit status 2") 217 218 _, err = RunPluginCli("", "error-plugin", "unknown") 219 require.Error(t, err) 220 assert.Contains(t, err.Error(), "exit status 3") 221 } 222 223 // TestCliPluginStdinHandling verifies that a CLI plugin correctly handles input from stdin. 224 func TestCliPluginStdinHandling(t *testing.T) { 225 pluginScript := `#!/bin/bash 226 input=$(cat) 227 echo "Received: $input" 228 exit 0` 229 230 pluginPath := createTestPlugin(t, "stdin-plugin", pluginScript) 231 232 origPath := os.Getenv("PATH") 233 t.Cleanup(func() { 234 t.Setenv("PATH", origPath) 235 }) 236 t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) 237 238 testCases := []struct { 239 name string 240 stdin string 241 expected string 242 }{ 243 { 244 "Single line input", 245 "Hello, ArgoCD!", 246 "Received: Hello, ArgoCD!", 247 }, 248 { 249 "Multiline input", 250 "Line1\nLine2\nLine3", 251 "Received: Line1\nLine2\nLine3", 252 }, 253 { 254 "Empty input", 255 "", 256 "Received:", 257 }, 258 } 259 260 for _, tc := range testCases { 261 t.Run(tc.name, func(t *testing.T) { 262 output, err := RunPluginCli(tc.stdin, "stdin-plugin") 263 require.NoError(t, err) 264 assert.Contains(t, NormalizeOutput(output), tc.expected) 265 }) 266 } 267 }