executor.go 5.3 KB

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