executor.go 4.4 KB

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