executor.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. "time"
  13. "sync"
  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.uuid = uuid.New().String()
  93. req.logEntry = &InternalLogEntry{
  94. DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
  95. ActionTitle: req.ActionName,
  96. UUID: req.UUID,
  97. Stdout: "",
  98. Stderr: "",
  99. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  100. ExecutionStarted: false,
  101. ExecutionFinished: false,
  102. }
  103. e.Logs[req.UUID] = req.logEntry
  104. for _, listener := range e.listeners {
  105. listener.OnExecutionStarted(req.ActionName)
  106. }
  107. wg := new(sync.WaitGroup)
  108. wg.Add(1);
  109. go func() {
  110. e.execChain(req)
  111. defer wg.Done();
  112. }()
  113. return wg, req.uuid;
  114. }
  115. func (e *Executor) execChain(req *ExecutionRequest) {
  116. for _, step := range e.chainOfCommand {
  117. if !step(req) {
  118. break
  119. }
  120. }
  121. req.logEntry.ExecutionFinished = true
  122. // This isn't a step, because we want to notify all listeners, irrespective
  123. // of how many steps were actually executed.
  124. notifyListeners(req)
  125. }
  126. func getConcurrentCount(req *ExecutionRequest) int {
  127. concurrentCount := 0
  128. for _, log := range req.executor.Logs {
  129. if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
  130. concurrentCount += 1
  131. }
  132. }
  133. return concurrentCount
  134. }
  135. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  136. concurrentCount := getConcurrentCount(req)
  137. // Note that the current execution is counted int the logs, so when checking we +1
  138. if concurrentCount >= (req.action.MaxConcurrent + 1) {
  139. 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)
  140. log.Warnf(msg)
  141. req.logEntry.Stdout = msg
  142. req.logEntry.Blocked = true
  143. return false
  144. }
  145. return true
  146. }
  147. func stepFindAction(req *ExecutionRequest) bool {
  148. actualAction := req.Cfg.FindAction(req.ActionName)
  149. if actualAction == nil {
  150. log.WithFields(log.Fields{
  151. "actionName": req.ActionName,
  152. }).Warnf("Action not found")
  153. req.logEntry.Stderr = "Action not found"
  154. return false
  155. }
  156. req.action = actualAction
  157. req.logEntry.ActionIcon = actualAction.Icon
  158. return true
  159. }
  160. func stepACLCheck(req *ExecutionRequest) bool {
  161. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
  162. }
  163. func stepParseArgs(req *ExecutionRequest) bool {
  164. var err error
  165. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  166. if err != nil {
  167. req.logEntry.Stdout = err.Error()
  168. log.Warnf(err.Error())
  169. return false
  170. }
  171. return true
  172. }
  173. func stepLogRequested(req *ExecutionRequest) bool {
  174. log.WithFields(log.Fields{
  175. "actionTitle": req.ActionName,
  176. }).Infof("Action requested")
  177. return true
  178. }
  179. func stepLogStart(req *ExecutionRequest) bool {
  180. log.WithFields(log.Fields{
  181. "actionTitle": req.action.Title,
  182. "timeout": req.action.Timeout,
  183. }).Infof("Action starting")
  184. return true
  185. }
  186. func stepLogFinish(req *ExecutionRequest) bool {
  187. log.WithFields(log.Fields{
  188. "actionTitle": req.action.Title,
  189. "stdout": req.logEntry.Stdout,
  190. "stderr": req.logEntry.Stderr,
  191. "timedOut": req.logEntry.TimedOut,
  192. "exit": req.logEntry.ExitCode,
  193. }).Infof("Action finished")
  194. return true
  195. }
  196. func notifyListeners(req *ExecutionRequest) {
  197. for _, listener := range req.executor.listeners {
  198. listener.OnExecutionFinished(req.logEntry)
  199. }
  200. }
  201. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  202. if runtime.GOOS == "windows" {
  203. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  204. }
  205. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  206. }
  207. func stepExec(req *ExecutionRequest) bool {
  208. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  209. defer cancel()
  210. var stdout bytes.Buffer
  211. var stderr bytes.Buffer
  212. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  213. cmd.Stdout = &stdout
  214. cmd.Stderr = &stderr
  215. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  216. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  217. req.logEntry.ExecutionStarted = true
  218. runerr := cmd.Start()
  219. cmd.Wait()
  220. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  221. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  222. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  223. req.logEntry.Stdout = stdout.String()
  224. req.logEntry.Stderr = stderr.String()
  225. if runerr != nil {
  226. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  227. }
  228. if ctx.Err() == context.DeadlineExceeded {
  229. req.logEntry.TimedOut = true
  230. }
  231. req.logEntry.Tags = req.Tags
  232. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  233. return true
  234. }