github.com/argoproj/argo-cd/v3@v3.2.1/util/exec/exec.go (about) 1 package exec 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "os/exec" 9 "strconv" 10 "strings" 11 "syscall" 12 "time" 13 "unicode" 14 15 "github.com/argoproj/gitops-engine/pkg/utils/tracing" 16 "github.com/sirupsen/logrus" 17 18 "github.com/argoproj/argo-cd/v3/util/log" 19 "github.com/argoproj/argo-cd/v3/util/rand" 20 ) 21 22 var ( 23 timeout time.Duration 24 fatalTimeout time.Duration 25 Unredacted = Redact(nil) 26 ) 27 28 type ExecRunOpts struct { 29 // Redactor redacts tokens from the output 30 Redactor func(text string) string 31 // TimeoutBehavior configures what to do in case of timeout 32 TimeoutBehavior TimeoutBehavior 33 // SkipErrorLogging determines whether to skip logging of execution errors (rc > 0) 34 SkipErrorLogging bool 35 // CaptureStderr determines whether to capture stderr in addition to stdout 36 CaptureStderr bool 37 } 38 39 func init() { 40 initTimeout() 41 } 42 43 func initTimeout() { 44 var err error 45 timeout, err = time.ParseDuration(os.Getenv("ARGOCD_EXEC_TIMEOUT")) 46 if err != nil { 47 timeout = 90 * time.Second 48 } 49 fatalTimeout, err = time.ParseDuration(os.Getenv("ARGOCD_EXEC_FATAL_TIMEOUT")) 50 if err != nil { 51 fatalTimeout = 10 * time.Second 52 } 53 } 54 55 func Run(cmd *exec.Cmd) (string, error) { 56 return RunWithRedactor(cmd, nil) 57 } 58 59 func RunWithRedactor(cmd *exec.Cmd, redactor func(text string) string) (string, error) { 60 opts := ExecRunOpts{Redactor: redactor} 61 return RunWithExecRunOpts(cmd, opts) 62 } 63 64 func RunWithExecRunOpts(cmd *exec.Cmd, opts ExecRunOpts) (string, error) { 65 cmdOpts := CmdOpts{Timeout: timeout, FatalTimeout: fatalTimeout, Redactor: opts.Redactor, TimeoutBehavior: opts.TimeoutBehavior, SkipErrorLogging: opts.SkipErrorLogging, CaptureStderr: opts.CaptureStderr} 66 span := tracing.NewLoggingTracer(log.NewLogrusLogger(log.NewWithCurrentConfig())).StartSpan(fmt.Sprintf("exec %v", cmd.Args[0])) 67 span.SetBaggageItem("dir", cmd.Dir) 68 if cmdOpts.Redactor != nil { 69 span.SetBaggageItem("args", opts.Redactor(fmt.Sprintf("%v", cmd.Args))) 70 } else { 71 span.SetBaggageItem("args", fmt.Sprintf("%v", cmd.Args)) 72 } 73 defer span.Finish() 74 return RunCommandExt(cmd, cmdOpts) 75 } 76 77 // GetCommandArgsToLog represents the given command in a way that we can copy-and-paste into a terminal 78 func GetCommandArgsToLog(cmd *exec.Cmd) string { 79 var argsToLog []string 80 for _, arg := range cmd.Args { 81 if arg == "" { 82 argsToLog = append(argsToLog, `""`) 83 continue 84 } 85 86 containsSpace := false 87 for _, r := range arg { 88 if unicode.IsSpace(r) { 89 containsSpace = true 90 break 91 } 92 } 93 if containsSpace { 94 // add quotes and escape any internal quotes 95 argsToLog = append(argsToLog, strconv.Quote(arg)) 96 } else { 97 argsToLog = append(argsToLog, arg) 98 } 99 } 100 args := strings.Join(argsToLog, " ") 101 return args 102 } 103 104 type CmdError struct { 105 Args string 106 Stderr string 107 Cause error 108 } 109 110 func (ce *CmdError) Error() string { 111 res := fmt.Sprintf("`%v` failed %v", ce.Args, ce.Cause) 112 if ce.Stderr != "" { 113 res = fmt.Sprintf("%s: %s", res, ce.Stderr) 114 } 115 return res 116 } 117 118 func (ce *CmdError) String() string { 119 return ce.Error() 120 } 121 122 func newCmdError(args string, cause error, stderr string) *CmdError { 123 return &CmdError{Args: args, Stderr: stderr, Cause: cause} 124 } 125 126 // TimeoutBehavior defines behavior for when the command takes longer than the passed in timeout to exit 127 // By default, SIGKILL is sent to the process and it is not waited upon 128 type TimeoutBehavior struct { 129 // Signal determines the signal to send to the process 130 Signal syscall.Signal 131 // ShouldWait determines whether to wait for the command to exit once timeout is reached 132 ShouldWait bool 133 } 134 135 type CmdOpts struct { 136 // Timeout determines how long to wait for the command to exit 137 Timeout time.Duration 138 // FatalTimeout is the amount of additional time to wait after Timeout before fatal SIGKILL 139 FatalTimeout time.Duration 140 // Redactor redacts tokens from the output 141 Redactor func(text string) string 142 // TimeoutBehavior configures what to do in case of timeout 143 TimeoutBehavior TimeoutBehavior 144 // SkipErrorLogging defines whether to skip logging of execution errors (rc > 0) 145 SkipErrorLogging bool 146 // CaptureStderr defines whether to capture stderr in addition to stdout 147 CaptureStderr bool 148 } 149 150 var DefaultCmdOpts = CmdOpts{ 151 Timeout: time.Duration(0), 152 FatalTimeout: time.Duration(0), 153 Redactor: Unredacted, 154 TimeoutBehavior: TimeoutBehavior{syscall.SIGKILL, false}, 155 SkipErrorLogging: false, 156 CaptureStderr: false, 157 } 158 159 func Redact(items []string) func(text string) string { 160 return func(text string) string { 161 for _, item := range items { 162 text = strings.ReplaceAll(text, item, "******") 163 } 164 return text 165 } 166 } 167 168 // RunCommandExt is a convenience function to run/log a command and return/log stderr in an error upon 169 // failure. 170 func RunCommandExt(cmd *exec.Cmd, opts CmdOpts) (string, error) { 171 execId, err := rand.RandHex(5) 172 if err != nil { 173 return "", err 174 } 175 logCtx := logrus.WithFields(logrus.Fields{"execID": execId}) 176 177 redactor := DefaultCmdOpts.Redactor 178 if opts.Redactor != nil { 179 redactor = opts.Redactor 180 } 181 182 // log in a way we can copy-and-paste into a terminal 183 args := strings.Join(cmd.Args, " ") 184 logCtx.WithFields(logrus.Fields{"dir": cmd.Dir}).Info(redactor(args)) 185 186 var stdout bytes.Buffer 187 var stderr bytes.Buffer 188 cmd.Stdout = &stdout 189 cmd.Stderr = &stderr 190 191 start := time.Now() 192 err = cmd.Start() 193 if err != nil { 194 return "", err 195 } 196 197 done := make(chan error) 198 go func() { done <- cmd.Wait() }() 199 200 // Start timers for timeout 201 timeout := DefaultCmdOpts.Timeout 202 fatalTimeout := DefaultCmdOpts.FatalTimeout 203 204 if opts.Timeout != time.Duration(0) { 205 timeout = opts.Timeout 206 } 207 208 if opts.FatalTimeout != time.Duration(0) { 209 fatalTimeout = opts.FatalTimeout 210 } 211 212 var timoutCh <-chan time.Time 213 if timeout != 0 { 214 timoutCh = time.NewTimer(timeout).C 215 } 216 217 var fatalTimeoutCh <-chan time.Time 218 if fatalTimeout != 0 { 219 fatalTimeoutCh = time.NewTimer(timeout + fatalTimeout).C 220 } 221 222 timeoutBehavior := DefaultCmdOpts.TimeoutBehavior 223 fatalTimeoutBehaviour := syscall.SIGKILL 224 if opts.TimeoutBehavior.Signal != syscall.Signal(0) { 225 timeoutBehavior = opts.TimeoutBehavior 226 } 227 228 select { 229 // noinspection ALL 230 case <-timoutCh: 231 // send timeout signal 232 _ = cmd.Process.Signal(timeoutBehavior.Signal) 233 // wait on timeout signal and fallback to fatal timeout signal 234 if timeoutBehavior.ShouldWait { 235 select { 236 case <-done: 237 case <-fatalTimeoutCh: 238 // upgrades to SIGKILL if cmd does not respect SIGTERM 239 _ = cmd.Process.Signal(fatalTimeoutBehaviour) 240 // now original cmd should exit immediately after SIGKILL 241 <-done 242 // return error with a marker indicating that cmd exited only after fatal SIGKILL 243 output := stdout.String() 244 if opts.CaptureStderr { 245 output += stderr.String() 246 } 247 logCtx.WithFields(logrus.Fields{"duration": time.Since(start)}).Debug(redactor(output)) 248 err = newCmdError(redactor(args), fmt.Errorf("fatal timeout after %v", timeout+fatalTimeout), "") 249 logCtx.Error(err.Error()) 250 return strings.TrimSuffix(output, "\n"), err 251 } 252 } 253 // either did not wait for timeout or cmd did respect SIGTERM 254 output := stdout.String() 255 if opts.CaptureStderr { 256 output += stderr.String() 257 } 258 logCtx.WithFields(logrus.Fields{"duration": time.Since(start)}).Debug(redactor(output)) 259 err = newCmdError(redactor(args), fmt.Errorf("timeout after %v", timeout), "") 260 logCtx.Error(err.Error()) 261 return strings.TrimSuffix(output, "\n"), err 262 case err := <-done: 263 if err != nil { 264 output := stdout.String() 265 if opts.CaptureStderr { 266 output += stderr.String() 267 } 268 logCtx.WithFields(logrus.Fields{"duration": time.Since(start)}).Debug(redactor(output)) 269 err := newCmdError(redactor(args), errors.New(redactor(err.Error())), strings.TrimSpace(redactor(stderr.String()))) 270 if !opts.SkipErrorLogging { 271 logCtx.Error(err.Error()) 272 } 273 return strings.TrimSuffix(output, "\n"), err 274 } 275 } 276 output := stdout.String() 277 if opts.CaptureStderr { 278 output += stderr.String() 279 } 280 logCtx.WithFields(logrus.Fields{"duration": time.Since(start)}).Debug(redactor(output)) 281 282 return strings.TrimSuffix(output, "\n"), nil 283 } 284 285 func RunCommand(name string, opts CmdOpts, arg ...string) (string, error) { 286 return RunCommandExt(exec.Command(name, arg...), opts) 287 }