executor.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. package executor
  2. import (
  3. pb "github.com/OliveTin/OliveTin/gen/grpc"
  4. acl "github.com/OliveTin/OliveTin/internal/acl"
  5. config "github.com/OliveTin/OliveTin/internal/config"
  6. log "github.com/sirupsen/logrus"
  7. "bytes"
  8. "context"
  9. "os/exec"
  10. "runtime"
  11. "time"
  12. )
  13. // ExecutionRequest is a request to execute an action. It's passed to an
  14. // Executor. They're created from the grpcapi.
  15. type ExecutionRequest struct {
  16. ActionName string
  17. Arguments map[string]string
  18. Tags []string
  19. action *config.Action
  20. Cfg *config.Config
  21. AuthenticatedUser *acl.AuthenticatedUser
  22. logEntry *InternalLogEntry
  23. finalParsedCommand string
  24. }
  25. // InternalLogEntry objects are created by an Executor, and represent the final
  26. // state of execution (even if the command is not executed). It's designed to be
  27. // easily serializable.
  28. type InternalLogEntry struct {
  29. Datetime string
  30. Stdout string
  31. Stderr string
  32. TimedOut bool
  33. ExitCode int32
  34. Tags []string
  35. /*
  36. The following two properties are obviously on Action normally, but it's useful
  37. that logs are lightweight (so we don't need to have an action associated to
  38. logs, etc. Therefore, we duplicate those values here.
  39. */
  40. ActionTitle string
  41. ActionIcon string
  42. }
  43. type executorStepFunc func(*ExecutionRequest) bool
  44. // Executor represents a helper class for executing commands. It's main method
  45. // is ExecRequest
  46. type Executor struct {
  47. Logs []InternalLogEntry
  48. chainOfCommand []executorStepFunc
  49. }
  50. // ExecRequest processes an ExecutionRequest
  51. func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
  52. req.logEntry = &InternalLogEntry{
  53. Datetime: time.Now().Format("2006-01-02 15:04:05"),
  54. ActionTitle: req.ActionName,
  55. Stdout: "",
  56. Stderr: "",
  57. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  58. }
  59. for _, step := range e.chainOfCommand {
  60. if !step(req) {
  61. break
  62. }
  63. }
  64. e.Logs = append(e.Logs, *req.logEntry)
  65. return &pb.StartActionResponse{
  66. LogEntry: &pb.LogEntry{
  67. ActionTitle: req.logEntry.ActionTitle,
  68. ActionIcon: req.logEntry.ActionIcon,
  69. Datetime: req.logEntry.Datetime,
  70. Stderr: req.logEntry.Stderr,
  71. Stdout: req.logEntry.Stdout,
  72. TimedOut: req.logEntry.TimedOut,
  73. ExitCode: req.logEntry.ExitCode,
  74. },
  75. }
  76. }
  77. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  78. // executing actions.
  79. func DefaultExecutor() *Executor {
  80. e := Executor{}
  81. e.chainOfCommand = []executorStepFunc{
  82. stepFindAction,
  83. stepACLCheck,
  84. stepParseArgs,
  85. stepLogStart,
  86. stepExec,
  87. stepLogFinish,
  88. }
  89. return &e
  90. }
  91. func stepFindAction(req *ExecutionRequest) bool {
  92. actualAction := req.Cfg.FindAction(req.ActionName)
  93. if actualAction == nil {
  94. log.WithFields(log.Fields{
  95. "actionName": req.ActionName,
  96. }).Warnf("Action not found")
  97. req.logEntry.Stderr = "Action not found"
  98. return false
  99. }
  100. req.action = actualAction
  101. req.logEntry.ActionIcon = actualAction.Icon
  102. return true
  103. }
  104. func stepACLCheck(req *ExecutionRequest) bool {
  105. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
  106. }
  107. func stepParseArgs(req *ExecutionRequest) bool {
  108. var err error
  109. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  110. if err != nil {
  111. req.logEntry.Stdout = err.Error()
  112. log.Warnf(err.Error())
  113. return false
  114. }
  115. return true
  116. }
  117. func stepLogStart(req *ExecutionRequest) bool {
  118. log.WithFields(log.Fields{
  119. "title": req.action.Title,
  120. "timeout": req.action.Timeout,
  121. }).Infof("Action starting")
  122. return true
  123. }
  124. func stepLogFinish(req *ExecutionRequest) bool {
  125. log.WithFields(log.Fields{
  126. "title": req.action.Title,
  127. "stdout": req.logEntry.Stdout,
  128. "stderr": req.logEntry.Stderr,
  129. "timedOut": req.logEntry.TimedOut,
  130. "exit": req.logEntry.ExitCode,
  131. }).Infof("Action finished")
  132. return true
  133. }
  134. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  135. if runtime.GOOS == "windows" {
  136. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  137. }
  138. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  139. }
  140. func stepExec(req *ExecutionRequest) bool {
  141. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  142. defer cancel()
  143. var stdout bytes.Buffer
  144. var stderr bytes.Buffer
  145. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  146. cmd.Stdout = &stdout
  147. cmd.Stderr = &stderr
  148. runerr := cmd.Run()
  149. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  150. req.logEntry.Stdout = stdout.String()
  151. req.logEntry.Stderr = stderr.String()
  152. if runerr != nil {
  153. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  154. }
  155. if ctx.Err() == context.DeadlineExceeded {
  156. req.logEntry.TimedOut = true
  157. }
  158. req.logEntry.Tags = req.Tags
  159. return true
  160. }