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  }