executor.go 8.9 KB

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