executor.go 9.1 KB

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