executor.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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.WithFields(log.Fields{
  140. "actionTitle": req.ActionName,
  141. }).Warnf(msg)
  142. req.logEntry.Stdout = msg
  143. req.logEntry.Blocked = true
  144. return false
  145. }
  146. return true
  147. }
  148. func stepFindAction(req *ExecutionRequest) bool {
  149. actualAction := req.Cfg.FindAction(req.ActionName)
  150. if actualAction == nil {
  151. log.WithFields(log.Fields{
  152. "actionName": req.ActionName,
  153. }).Warnf("Action not found")
  154. req.logEntry.Stderr = "Action not found"
  155. return false
  156. }
  157. req.action = actualAction
  158. req.logEntry.ActionIcon = actualAction.Icon
  159. return true
  160. }
  161. func stepACLCheck(req *ExecutionRequest) bool {
  162. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
  163. }
  164. func stepParseArgs(req *ExecutionRequest) bool {
  165. var err error
  166. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  167. if err != nil {
  168. req.logEntry.Stdout = err.Error()
  169. log.Warnf(err.Error())
  170. return false
  171. }
  172. return true
  173. }
  174. func stepLogRequested(req *ExecutionRequest) bool {
  175. log.WithFields(log.Fields{
  176. "actionTitle": req.ActionName,
  177. }).Infof("Action requested")
  178. return true
  179. }
  180. func stepLogStart(req *ExecutionRequest) bool {
  181. log.WithFields(log.Fields{
  182. "actionTitle": req.action.Title,
  183. "timeout": req.action.Timeout,
  184. }).Infof("Action starting")
  185. return true
  186. }
  187. func stepLogFinish(req *ExecutionRequest) bool {
  188. log.WithFields(log.Fields{
  189. "actionTitle": req.action.Title,
  190. "stdout": req.logEntry.Stdout,
  191. "stderr": req.logEntry.Stderr,
  192. "timedOut": req.logEntry.TimedOut,
  193. "exit": req.logEntry.ExitCode,
  194. }).Infof("Action finished")
  195. return true
  196. }
  197. func notifyListeners(req *ExecutionRequest) {
  198. for _, listener := range req.executor.listeners {
  199. listener.OnExecutionFinished(req.logEntry)
  200. }
  201. }
  202. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  203. if runtime.GOOS == "windows" {
  204. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  205. }
  206. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  207. }
  208. func stepExec(req *ExecutionRequest) bool {
  209. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  210. defer cancel()
  211. var stdout bytes.Buffer
  212. var stderr bytes.Buffer
  213. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  214. cmd.Stdout = &stdout
  215. cmd.Stderr = &stderr
  216. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  217. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  218. req.logEntry.ExecutionStarted = true
  219. runerr := cmd.Start()
  220. cmd.Wait()
  221. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  222. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  223. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  224. req.logEntry.Stdout = stdout.String()
  225. req.logEntry.Stderr = stderr.String()
  226. if runerr != nil {
  227. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  228. }
  229. if ctx.Err() == context.DeadlineExceeded {
  230. req.logEntry.TimedOut = true
  231. }
  232. req.logEntry.Tags = req.Tags
  233. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  234. return true
  235. }