executor.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. Action *config.Action
  27. Arguments map[string]string
  28. UUID string
  29. Tags []string
  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. stepExecAfter,
  76. stepLogFinish,
  77. }
  78. return &e
  79. }
  80. type listener interface {
  81. OnExecutionStarted(actionName string)
  82. OnExecutionFinished(logEntry *InternalLogEntry)
  83. }
  84. func (e *Executor) AddListener(m listener) {
  85. e.listeners = append(e.listeners, m)
  86. }
  87. // ExecRequest processes an ExecutionRequest
  88. func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
  89. req.executor = e
  90. // req.UUID is now set by the client, so that they can track the request
  91. // from start to finish. This means that a malicious client could send
  92. // duplicate UUIDs (or just random strings), but this is the only way.
  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.WithFields(log.Fields{
  141. "actionTitle": req.ActionName,
  142. }).Warnf(msg)
  143. req.logEntry.Stdout = msg
  144. req.logEntry.Blocked = true
  145. return false
  146. }
  147. return true
  148. }
  149. func stepFindAction(req *ExecutionRequest) bool {
  150. if req.Action != nil {
  151. return true
  152. }
  153. actualAction := req.Cfg.FindAction(req.ActionName)
  154. if actualAction == nil {
  155. log.WithFields(log.Fields{
  156. "actionName": req.ActionName,
  157. }).Warnf("Action not found")
  158. req.logEntry.Stderr = "Action not found"
  159. return false
  160. }
  161. req.Action = actualAction
  162. req.logEntry.ActionIcon = actualAction.Icon
  163. return true
  164. }
  165. func stepACLCheck(req *ExecutionRequest) bool {
  166. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
  167. }
  168. func stepParseArgs(req *ExecutionRequest) bool {
  169. var err error
  170. req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action)
  171. if err != nil {
  172. req.logEntry.Stdout = err.Error()
  173. log.Warnf(err.Error())
  174. return false
  175. }
  176. return true
  177. }
  178. func stepLogRequested(req *ExecutionRequest) bool {
  179. log.WithFields(log.Fields{
  180. "actionTitle": req.ActionName,
  181. }).Infof("Action requested")
  182. return true
  183. }
  184. func stepLogStart(req *ExecutionRequest) bool {
  185. log.WithFields(log.Fields{
  186. "actionTitle": req.Action.Title,
  187. "timeout": req.Action.Timeout,
  188. }).Infof("Action starting")
  189. return true
  190. }
  191. func stepLogFinish(req *ExecutionRequest) bool {
  192. log.WithFields(log.Fields{
  193. "actionTitle": req.Action.Title,
  194. "stdout": req.logEntry.Stdout,
  195. "stderr": req.logEntry.Stderr,
  196. "timedOut": req.logEntry.TimedOut,
  197. "exit": req.logEntry.ExitCode,
  198. }).Infof("Action finished")
  199. return true
  200. }
  201. func notifyListeners(req *ExecutionRequest) {
  202. for _, listener := range req.executor.listeners {
  203. listener.OnExecutionFinished(req.logEntry)
  204. }
  205. }
  206. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  207. if runtime.GOOS == "windows" {
  208. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  209. }
  210. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  211. }
  212. func stepExec(req *ExecutionRequest) bool {
  213. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  214. defer cancel()
  215. var stdout bytes.Buffer
  216. var stderr bytes.Buffer
  217. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  218. cmd.Stdout = &stdout
  219. cmd.Stderr = &stderr
  220. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  221. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  222. req.logEntry.ExecutionStarted = true
  223. runerr := cmd.Start()
  224. cmd.Wait()
  225. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  226. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  227. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  228. req.logEntry.Stdout = stdout.String()
  229. req.logEntry.Stderr = stderr.String()
  230. if runerr != nil {
  231. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  232. }
  233. if ctx.Err() == context.DeadlineExceeded {
  234. req.logEntry.TimedOut = true
  235. }
  236. req.logEntry.Tags = req.Tags
  237. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  238. return true
  239. }
  240. func stepExecAfter(req *ExecutionRequest) bool {
  241. if req.Action.ShellAfterCompleted == "" {
  242. return true
  243. }
  244. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  245. defer cancel()
  246. var stdout bytes.Buffer
  247. var stderr bytes.Buffer
  248. args := map[string]string{
  249. "stdout": req.logEntry.Stdout,
  250. "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
  251. }
  252. finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action)
  253. cmd := wrapCommandInShell(ctx, finalParsedCommand)
  254. cmd.Stdout = &stdout
  255. cmd.Stderr = &stderr
  256. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  257. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  258. runerr := cmd.Start()
  259. cmd.Wait()
  260. req.logEntry.Stdout += "---\n" + stdout.String()
  261. req.logEntry.Stderr += "---\n" + stderr.String()
  262. if runerr != nil {
  263. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  264. }
  265. if ctx.Err() == context.DeadlineExceeded {
  266. req.logEntry.Stderr += "Your shellAfterCommand command timed out."
  267. }
  268. req.logEntry.Stdout += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
  269. return true
  270. }