executor.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. "fmt"
  10. "io"
  11. "os/exec"
  12. "runtime"
  13. "time"
  14. )
  15. // Executor represents a helper class for executing commands. It's main method
  16. // is ExecRequest
  17. type Executor struct {
  18. Logs map[string]*InternalLogEntry
  19. listeners []listener
  20. chainOfCommand []executorStepFunc
  21. }
  22. // ExecutionRequest is a request to execute an action. It's passed to an
  23. // Executor. They're created from the grpcapi.
  24. type ExecutionRequest struct {
  25. ActionName string
  26. Arguments map[string]string
  27. UUID string
  28. Tags []string
  29. action *config.Action
  30. Cfg *config.Config
  31. AuthenticatedUser *acl.AuthenticatedUser
  32. logEntry *InternalLogEntry
  33. finalParsedCommand string
  34. executor *Executor
  35. }
  36. // InternalLogEntry objects are created by an Executor, and represent the final
  37. // state of execution (even if the command is not executed). It's designed to be
  38. // easily serializable.
  39. type InternalLogEntry struct {
  40. DatetimeStarted string
  41. DatetimeFinished string
  42. Stdout string
  43. Stderr string
  44. StdoutBuffer io.ReadCloser
  45. StderrBuffer io.ReadCloser
  46. TimedOut bool
  47. Blocked bool
  48. ExitCode int32
  49. Tags []string
  50. ExecutionStarted bool
  51. ExecutionFinished bool
  52. /*
  53. The following 3 properties are obviously on Action normally, but it's useful
  54. that logs are lightweight (so we don't need to have an action associated to
  55. logs, etc. Therefore, we duplicate those values here.
  56. */
  57. ActionTitle string
  58. ActionIcon string
  59. UUID string
  60. }
  61. type executorStepFunc func(*ExecutionRequest) bool
  62. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  63. // executing actions.
  64. func DefaultExecutor() *Executor {
  65. e := Executor{}
  66. e.Logs = make(map[string]*InternalLogEntry)
  67. e.chainOfCommand = []executorStepFunc{
  68. stepLogRequested,
  69. stepFindAction,
  70. stepConcurrencyCheck,
  71. stepACLCheck,
  72. stepParseArgs,
  73. stepLogStart,
  74. stepExec,
  75. stepLogFinish,
  76. }
  77. return &e
  78. }
  79. type listener interface {
  80. OnExecutionStarted(actionName string)
  81. OnExecutionFinished(logEntry *InternalLogEntry)
  82. }
  83. func (e *Executor) AddListener(m listener) {
  84. e.listeners = append(e.listeners, m)
  85. }
  86. // ExecRequest processes an ExecutionRequest
  87. func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
  88. // req.UUID is now set by the client, so that they can track the request
  89. // from start to finish. This means that a malicious client could send
  90. // duplicate UUIDs (or just random strings), but this is the only way.
  91. req.executor = e
  92. req.logEntry = &InternalLogEntry{
  93. DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
  94. ActionTitle: req.ActionName,
  95. UUID: req.UUID,
  96. Stdout: "",
  97. Stderr: "",
  98. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  99. ExecutionStarted: false,
  100. ExecutionFinished: false,
  101. }
  102. e.Logs[req.UUID] = req.logEntry
  103. for _, listener := range e.listeners {
  104. listener.OnExecutionStarted(req.ActionName)
  105. }
  106. go e.execChain(req)
  107. return &pb.StartActionResponse{
  108. ExecutionUuid: req.UUID,
  109. }
  110. }
  111. func (e *Executor) execChain(req *ExecutionRequest) {
  112. for _, step := range e.chainOfCommand {
  113. if !step(req) {
  114. break
  115. }
  116. }
  117. req.logEntry.ExecutionFinished = true
  118. // This isn't a step, because we want to notify all listeners, irrespective
  119. // of how many steps were actually executed.
  120. notifyListeners(req)
  121. }
  122. func getConcurrentCount(req *ExecutionRequest) int {
  123. concurrentCount := 0
  124. for _, log := range req.executor.Logs {
  125. if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
  126. concurrentCount += 1
  127. }
  128. }
  129. return concurrentCount
  130. }
  131. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  132. concurrentCount := getConcurrentCount(req)
  133. // Note that the current execution is counted int the logs, so when checking we +1
  134. if concurrentCount >= (req.action.MaxConcurrent + 1) {
  135. msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.action.MaxConcurrent)
  136. log.Warnf(msg)
  137. req.logEntry.Stdout = msg
  138. req.logEntry.Blocked = true
  139. return false
  140. }
  141. return true
  142. }
  143. func stepFindAction(req *ExecutionRequest) bool {
  144. actualAction := req.Cfg.FindAction(req.ActionName)
  145. if actualAction == nil {
  146. log.WithFields(log.Fields{
  147. "actionName": req.ActionName,
  148. }).Warnf("Action not found")
  149. req.logEntry.Stderr = "Action not found"
  150. return false
  151. }
  152. req.action = actualAction
  153. req.logEntry.ActionIcon = actualAction.Icon
  154. return true
  155. }
  156. func stepACLCheck(req *ExecutionRequest) bool {
  157. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
  158. }
  159. func stepParseArgs(req *ExecutionRequest) bool {
  160. var err error
  161. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  162. if err != nil {
  163. req.logEntry.Stdout = err.Error()
  164. log.Warnf(err.Error())
  165. return false
  166. }
  167. return true
  168. }
  169. func stepLogRequested(req *ExecutionRequest) bool {
  170. log.WithFields(log.Fields{
  171. "actionTitle": req.ActionName,
  172. }).Infof("Action requested")
  173. return true
  174. }
  175. func stepLogStart(req *ExecutionRequest) bool {
  176. log.WithFields(log.Fields{
  177. "actionTitle": req.action.Title,
  178. "timeout": req.action.Timeout,
  179. }).Infof("Action starting")
  180. return true
  181. }
  182. func stepLogFinish(req *ExecutionRequest) bool {
  183. log.WithFields(log.Fields{
  184. "actionTitle": req.action.Title,
  185. "stdout": req.logEntry.Stdout,
  186. "stderr": req.logEntry.Stderr,
  187. "timedOut": req.logEntry.TimedOut,
  188. "exit": req.logEntry.ExitCode,
  189. }).Infof("Action finished")
  190. return true
  191. }
  192. func notifyListeners(req *ExecutionRequest) {
  193. for _, listener := range req.executor.listeners {
  194. listener.OnExecutionFinished(req.logEntry)
  195. }
  196. }
  197. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  198. if runtime.GOOS == "windows" {
  199. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  200. }
  201. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  202. }
  203. func stepExec(req *ExecutionRequest) bool {
  204. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  205. defer cancel()
  206. var stdout bytes.Buffer
  207. var stderr bytes.Buffer
  208. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  209. cmd.Stdout = &stdout
  210. cmd.Stderr = &stderr
  211. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  212. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  213. req.logEntry.ExecutionStarted = true
  214. runerr := cmd.Start()
  215. cmd.Wait()
  216. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  217. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  218. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  219. req.logEntry.Stdout = stdout.String()
  220. req.logEntry.Stderr = stderr.String()
  221. if runerr != nil {
  222. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  223. }
  224. if ctx.Err() == context.DeadlineExceeded {
  225. req.logEntry.TimedOut = true
  226. }
  227. req.logEntry.Tags = req.Tags
  228. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  229. return true
  230. }