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 }