executor.go 4.5 KB

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