github.com/argoproj/argo-cd/v3@v3.2.1/util/exec/exec_test.go (about)

     1  package exec
     2  
     3  import (
     4  	"os/exec"
     5  	"regexp"
     6  	"syscall"
     7  	"testing"
     8  	"time"
     9  
    10  	log "github.com/sirupsen/logrus"
    11  	"github.com/sirupsen/logrus/hooks/test"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func Test_timeout(t *testing.T) {
    17  	t.Run("Default", func(t *testing.T) {
    18  		initTimeout()
    19  		assert.Equal(t, 90*time.Second, timeout)
    20  		assert.Equal(t, 10*time.Second, fatalTimeout)
    21  	})
    22  	t.Run("Default", func(t *testing.T) {
    23  		t.Setenv("ARGOCD_EXEC_TIMEOUT", "1s")
    24  		t.Setenv("ARGOCD_EXEC_FATAL_TIMEOUT", "2s")
    25  		initTimeout()
    26  		assert.Equal(t, 1*time.Second, timeout)
    27  		assert.Equal(t, 2*time.Second, fatalTimeout)
    28  	})
    29  }
    30  
    31  func TestRun(t *testing.T) {
    32  	out, err := Run(exec.Command("ls"))
    33  	require.NoError(t, err)
    34  	assert.NotEmpty(t, out)
    35  }
    36  
    37  func TestHideUsernamePassword(t *testing.T) {
    38  	_, err := RunWithRedactor(exec.Command("helm registry login https://charts.bitnami.com/bitnami", "--username", "foo", "--password", "bar"), nil)
    39  	require.Error(t, err)
    40  
    41  	redactor := func(text string) string {
    42  		return regexp.MustCompile("(--username|--password) [^ ]*").ReplaceAllString(text, "$1 ******")
    43  	}
    44  	_, err = RunWithRedactor(exec.Command("helm registry login https://charts.bitnami.com/bitnami", "--username", "foo", "--password", "bar"), redactor)
    45  	require.Error(t, err)
    46  }
    47  
    48  // This tests a cmd that properly handles a SIGTERM signal
    49  func TestRunWithExecRunOpts(t *testing.T) {
    50  	t.Setenv("ARGOCD_EXEC_TIMEOUT", "200ms")
    51  	initTimeout()
    52  
    53  	opts := ExecRunOpts{
    54  		TimeoutBehavior: TimeoutBehavior{
    55  			Signal:     syscall.SIGTERM,
    56  			ShouldWait: true,
    57  		},
    58  	}
    59  	_, err := RunWithExecRunOpts(exec.Command("sh", "-c", "trap 'trap - 15 && echo captured && exit' 15 && sleep 2"), opts)
    60  	assert.ErrorContains(t, err, "failed timeout after 200ms")
    61  }
    62  
    63  // This tests a mis-behaved cmd that stalls on SIGTERM and requires a SIGKILL
    64  func TestRunWithExecRunOptsFatal(t *testing.T) {
    65  	t.Setenv("ARGOCD_EXEC_TIMEOUT", "200ms")
    66  	t.Setenv("ARGOCD_EXEC_FATAL_TIMEOUT", "100ms")
    67  
    68  	initTimeout()
    69  
    70  	opts := ExecRunOpts{
    71  		TimeoutBehavior: TimeoutBehavior{
    72  			Signal:     syscall.SIGTERM,
    73  			ShouldWait: true,
    74  		},
    75  	}
    76  	// The returned error string in this case should contain a "fatal" in this case
    77  	_, err := RunWithExecRunOpts(exec.Command("sh", "-c", "trap 'trap - 15 && echo captured && sleep 10000' 15 && sleep 2"), opts)
    78  	// The expected timeout is ARGOCD_EXEC_TIMEOUT + ARGOCD_EXEC_FATAL_TIMEOUT = 200ms + 100ms = 300ms
    79  	assert.ErrorContains(t, err, "failed fatal timeout after 300ms")
    80  }
    81  
    82  func Test_getCommandArgsToLog(t *testing.T) {
    83  	t.Parallel()
    84  
    85  	testCases := []struct {
    86  		name     string
    87  		args     []string
    88  		expected string
    89  	}{
    90  		{
    91  			name:     "no spaces",
    92  			args:     []string{"sh", "-c", "cat"},
    93  			expected: "sh -c cat",
    94  		},
    95  		{
    96  			name:     "spaces",
    97  			args:     []string{"sh", "-c", `echo "hello world"`},
    98  			expected: `sh -c "echo \"hello world\""`,
    99  		},
   100  		{
   101  			name:     "empty string arg",
   102  			args:     []string{"sh", "-c", ""},
   103  			expected: `sh -c ""`,
   104  		},
   105  	}
   106  
   107  	for _, tc := range testCases {
   108  		tcc := tc
   109  		t.Run(tcc.name, func(t *testing.T) {
   110  			t.Parallel()
   111  			assert.Equal(t, tcc.expected, GetCommandArgsToLog(exec.Command(tcc.args[0], tcc.args[1:]...)))
   112  		})
   113  	}
   114  }
   115  
   116  func TestRunCommand(t *testing.T) {
   117  	hook := test.NewGlobal()
   118  	log.SetLevel(log.DebugLevel)
   119  	defer log.SetLevel(log.InfoLevel)
   120  
   121  	message, err := RunCommand("echo", CmdOpts{Redactor: Redact([]string{"world"})}, "hello world")
   122  	require.NoError(t, err)
   123  	assert.Equal(t, "hello world", message)
   124  
   125  	assert.Len(t, hook.Entries, 2)
   126  
   127  	entry := hook.Entries[0]
   128  	assert.Equal(t, log.InfoLevel, entry.Level)
   129  	assert.Equal(t, "echo hello ******", entry.Message)
   130  	assert.Contains(t, entry.Data, "dir")
   131  	assert.Contains(t, entry.Data, "execID")
   132  
   133  	entry = hook.Entries[1]
   134  	assert.Equal(t, log.DebugLevel, entry.Level)
   135  	assert.Equal(t, "hello ******\n", entry.Message)
   136  	assert.Contains(t, entry.Data, "duration")
   137  	assert.Contains(t, entry.Data, "execID")
   138  }
   139  
   140  func TestRunCommandSignal(t *testing.T) {
   141  	hook := test.NewGlobal()
   142  	log.SetLevel(log.DebugLevel)
   143  	defer log.SetLevel(log.InfoLevel)
   144  
   145  	timeoutBehavior := TimeoutBehavior{Signal: syscall.SIGTERM, ShouldWait: true}
   146  	output, err := RunCommand("sh", CmdOpts{Timeout: 200 * time.Millisecond, TimeoutBehavior: timeoutBehavior}, "-c", "trap 'trap - 15 && echo captured && exit' 15 && sleep 2")
   147  	assert.Equal(t, "captured", output)
   148  	require.EqualError(t, err, "`sh -c trap 'trap - 15 && echo captured && exit' 15 && sleep 2` failed timeout after 200ms")
   149  
   150  	assert.Len(t, hook.Entries, 3)
   151  }
   152  
   153  func TestTrimmedOutput(t *testing.T) {
   154  	message, err := RunCommand("printf", CmdOpts{}, "hello world")
   155  	require.NoError(t, err)
   156  	assert.Equal(t, "hello world", message)
   157  }
   158  
   159  func TestRunCommandExitErr(t *testing.T) {
   160  	hook := test.NewGlobal()
   161  	log.SetLevel(log.DebugLevel)
   162  	defer log.SetLevel(log.InfoLevel)
   163  
   164  	output, err := RunCommand("sh", CmdOpts{Redactor: Redact([]string{"world"})}, "-c", "echo hello world && echo my-error >&2 && exit 1")
   165  	assert.Equal(t, "hello world", output)
   166  	require.EqualError(t, err, "`sh -c echo hello ****** && echo my-error >&2 && exit 1` failed exit status 1: my-error")
   167  
   168  	assert.Len(t, hook.Entries, 3)
   169  
   170  	entry := hook.Entries[0]
   171  	assert.Equal(t, log.InfoLevel, entry.Level)
   172  	assert.Equal(t, "sh -c echo hello ****** && echo my-error >&2 && exit 1", entry.Message)
   173  	assert.Contains(t, entry.Data, "dir")
   174  	assert.Contains(t, entry.Data, "execID")
   175  
   176  	entry = hook.Entries[1]
   177  	assert.Equal(t, log.DebugLevel, entry.Level)
   178  	assert.Equal(t, "hello ******\n", entry.Message)
   179  	assert.Contains(t, entry.Data, "duration")
   180  	assert.Contains(t, entry.Data, "execID")
   181  
   182  	entry = hook.Entries[2]
   183  	assert.Equal(t, log.ErrorLevel, entry.Level)
   184  	assert.Equal(t, "`sh -c echo hello ****** && echo my-error >&2 && exit 1` failed exit status 1: my-error", entry.Message)
   185  	assert.Contains(t, entry.Data, "execID")
   186  }
   187  
   188  func TestRunCommandErr(t *testing.T) {
   189  	log.SetLevel(log.DebugLevel)
   190  	defer log.SetLevel(log.InfoLevel)
   191  
   192  	output, err := RunCommand("sh", CmdOpts{Redactor: Redact([]string{"world"})}, "-c", ">&2 echo 'failure'; false")
   193  	assert.Empty(t, output)
   194  	assert.EqualError(t, err, "`sh -c >&2 echo 'failure'; false` failed exit status 1: failure")
   195  }
   196  
   197  func TestRunInDir(t *testing.T) {
   198  	cmd := exec.Command("pwd")
   199  	cmd.Dir = "/"
   200  	message, err := RunCommandExt(cmd, CmdOpts{})
   201  	require.NoError(t, err)
   202  	assert.Equal(t, "/", message)
   203  }
   204  
   205  func TestRedact(t *testing.T) {
   206  	assert.Empty(t, Redact(nil)(""))
   207  	assert.Empty(t, Redact([]string{})(""))
   208  	assert.Empty(t, Redact([]string{"foo"})(""))
   209  	assert.Equal(t, "foo", Redact([]string{})("foo"))
   210  	assert.Equal(t, "******", Redact([]string{"foo"})("foo"))
   211  	assert.Equal(t, "****** ******", Redact([]string{"foo", "bar"})("foo bar"))
   212  	assert.Equal(t, "****** ******", Redact([]string{"foo"})("foo foo"))
   213  }
   214  
   215  func TestRunCaptureStderr(t *testing.T) {
   216  	output, err := RunCommand("sh", CmdOpts{CaptureStderr: true}, "-c", "echo hello world && echo my-error >&2 && exit 0")
   217  	assert.Equal(t, "hello world\nmy-error", output)
   218  	assert.NoError(t, err)
   219  }
   220  
   221  func TestRunWithExecRunOptsCaptureStderr(t *testing.T) {
   222  	ctx := t.Context()
   223  	cmd := exec.CommandContext(ctx, "sh", "-c", "echo hello world && echo my-error >&2 && exit 0")
   224  	output, err := RunWithExecRunOpts(cmd, ExecRunOpts{CaptureStderr: true})
   225  	assert.Equal(t, "hello world\nmy-error", output)
   226  	assert.NoError(t, err)
   227  }