executor.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. package executor
  2. import (
  3. acl "github.com/OliveTin/OliveTin/internal/acl"
  4. config "github.com/OliveTin/OliveTin/internal/config"
  5. log "github.com/sirupsen/logrus"
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "io"
  10. "os/exec"
  11. "runtime"
  12. "sync"
  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) (*sync.WaitGroup, string) {
  88. req.executor = e
  89. // req.UUID is now set by the client, so that they can track the request
  90. // from start to finish. This means that a malicious client could send
  91. // duplicate UUIDs (or just random strings), but this is the only way.
  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. wg := new(sync.WaitGroup)
  107. wg.Add(1)
  108. go func() {
  109. e.execChain(req)
  110. defer wg.Done()
  111. }()
  112. return wg, req.UUID
  113. }
  114. func (e *Executor) execChain(req *ExecutionRequest) {
  115. for _, step := range e.chainOfCommand {
  116. if !step(req) {
  117. break
  118. }
  119. }
  120. req.logEntry.ExecutionFinished = true
  121. // This isn't a step, because we want to notify all listeners, irrespective
  122. // of how many steps were actually executed.
  123. notifyListeners(req)
  124. }
  125. func getConcurrentCount(req *ExecutionRequest) int {
  126. concurrentCount := 0
  127. for _, log := range req.executor.Logs {
  128. if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
  129. concurrentCount += 1
  130. }
  131. }
  132. return concurrentCount
  133. }
  134. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  135. concurrentCount := getConcurrentCount(req)
  136. // Note that the current execution is counted int the logs, so when checking we +1
  137. if concurrentCount >= (req.action.MaxConcurrent + 1) {
  138. 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)
  139. log.Warnf(msg)
  140. req.logEntry.Stdout = msg
  141. req.logEntry.Blocked = true
  142. return false
  143. }
  144. return true
  145. }
  146. func stepFindAction(req *ExecutionRequest) bool {
  147. actualAction := req.Cfg.FindAction(req.ActionName)
  148. if actualAction == nil {
  149. log.WithFields(log.Fields{
  150. "actionName": req.ActionName,
  151. }).Warnf("Action not found")
  152. req.logEntry.Stderr = "Action not found"
  153. return false
  154. }
  155. req.action = actualAction
  156. req.logEntry.ActionIcon = actualAction.Icon
  157. return true
  158. }
  159. func stepACLCheck(req *ExecutionRequest) bool {
  160. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
  161. }
  162. func stepParseArgs(req *ExecutionRequest) bool {
  163. var err error
  164. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  165. if err != nil {
  166. req.logEntry.Stdout = err.Error()
  167. log.Warnf(err.Error())
  168. return false
  169. }
  170. return true
  171. }
  172. func stepLogRequested(req *ExecutionRequest) bool {
  173. log.WithFields(log.Fields{
  174. "actionTitle": req.ActionName,
  175. }).Infof("Action requested")
  176. return true
  177. }
  178. func stepLogStart(req *ExecutionRequest) bool {
  179. log.WithFields(log.Fields{
  180. "actionTitle": req.action.Title,
  181. "timeout": req.action.Timeout,
  182. }).Infof("Action starting")
  183. return true
  184. }
  185. func stepLogFinish(req *ExecutionRequest) bool {
  186. log.WithFields(log.Fields{
  187. "actionTitle": req.action.Title,
  188. "stdout": req.logEntry.Stdout,
  189. "stderr": req.logEntry.Stderr,
  190. "timedOut": req.logEntry.TimedOut,
  191. "exit": req.logEntry.ExitCode,
  192. }).Infof("Action finished")
  193. return true
  194. }
  195. func notifyListeners(req *ExecutionRequest) {
  196. for _, listener := range req.executor.listeners {
  197. listener.OnExecutionFinished(req.logEntry)
  198. }
  199. }
  200. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  201. if runtime.GOOS == "windows" {
  202. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  203. }
  204. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  205. }
  206. func stepExec(req *ExecutionRequest) bool {
  207. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  208. defer cancel()
  209. var stdout bytes.Buffer
  210. var stderr bytes.Buffer
  211. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  212. cmd.Stdout = &stdout
  213. cmd.Stderr = &stderr
  214. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  215. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  216. req.logEntry.ExecutionStarted = true
  217. runerr := cmd.Start()
  218. cmd.Wait()
  219. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  220. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  221. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  222. req.logEntry.Stdout = stdout.String()
  223. req.logEntry.Stderr = stderr.String()
  224. if runerr != nil {
  225. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  226. }
  227. if ctx.Err() == context.DeadlineExceeded {
  228. req.logEntry.TimedOut = true
  229. }
  230. req.logEntry.Tags = req.Tags
  231. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  232. return true
  233. }