executor.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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. stepTrigger,
  81. }
  82. return &e
  83. }
  84. type listener interface {
  85. OnExecutionStarted(actionTitle string)
  86. OnExecutionFinished(logEntry *InternalLogEntry)
  87. }
  88. func (e *Executor) AddListener(m listener) {
  89. e.listeners = append(e.listeners, m)
  90. }
  91. // ExecRequest processes an ExecutionRequest
  92. func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
  93. req.executor = e
  94. // req.UUID is now set by the client, so that they can track the request
  95. // from start to finish. This means that a malicious client could send
  96. // duplicate UUIDs (or just random strings), but this is the only way.
  97. req.logEntry = &InternalLogEntry{
  98. DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
  99. ExecutionTrackingID: req.TrackingID,
  100. Stdout: "",
  101. Stderr: "",
  102. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  103. ExecutionStarted: false,
  104. ExecutionFinished: false,
  105. ActionId: "",
  106. ActionTitle: "notfound",
  107. ActionIcon: "💩",
  108. }
  109. _, foundLog := e.Logs[req.TrackingID]
  110. if foundLog || req.TrackingID == "" {
  111. req.TrackingID = uuid.NewString()
  112. }
  113. e.Logs[req.TrackingID] = req.logEntry
  114. wg := new(sync.WaitGroup)
  115. wg.Add(1)
  116. go func() {
  117. e.execChain(req)
  118. defer wg.Done()
  119. }()
  120. return wg, req.TrackingID
  121. }
  122. func (e *Executor) execChain(req *ExecutionRequest) {
  123. for _, step := range e.chainOfCommand {
  124. if !step(req) {
  125. break
  126. }
  127. }
  128. req.logEntry.ExecutionFinished = true
  129. // This isn't a step, because we want to notify all listeners, irrespective
  130. // of how many steps were actually executed.
  131. notifyListeners(req)
  132. }
  133. func getConcurrentCount(req *ExecutionRequest) int {
  134. concurrentCount := 0
  135. for _, log := range req.executor.Logs {
  136. if log.ActionId == req.Action.ID && !log.ExecutionFinished {
  137. concurrentCount += 1
  138. }
  139. }
  140. return concurrentCount
  141. }
  142. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  143. concurrentCount := getConcurrentCount(req)
  144. // Note that the current execution is counted int the logs, so when checking we +1
  145. if concurrentCount >= (req.Action.MaxConcurrent + 1) {
  146. 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)
  147. log.WithFields(log.Fields{
  148. "actionTitle": req.logEntry.ActionTitle,
  149. }).Warnf(msg)
  150. req.logEntry.Stdout = msg
  151. req.logEntry.Blocked = true
  152. return false
  153. }
  154. return true
  155. }
  156. func stepACLCheck(req *ExecutionRequest) bool {
  157. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
  158. }
  159. func stepParseArgs(req *ExecutionRequest) bool {
  160. var err error
  161. req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
  162. if err != nil {
  163. req.logEntry.Stdout = err.Error()
  164. log.Warnf(err.Error())
  165. return false
  166. }
  167. return true
  168. }
  169. func stepRequestAction(req *ExecutionRequest) bool {
  170. // The grpc API always tries to find the action by ID, but it may
  171. if req.Action == nil {
  172. log.WithFields(log.Fields{
  173. "actionTitle": req.ActionTitle,
  174. }).Infof("Action finding by title")
  175. req.Action = req.Cfg.FindAction(req.ActionTitle)
  176. if req.Action == nil {
  177. log.WithFields(log.Fields{
  178. "actionTitle": req.ActionTitle,
  179. }).Warnf("Action requested, but not found")
  180. req.logEntry.Stderr = "Action not found: " + req.ActionTitle
  181. return false
  182. }
  183. }
  184. req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
  185. req.logEntry.ActionIcon = req.Action.Icon
  186. req.logEntry.ActionId = req.Action.ID
  187. log.WithFields(log.Fields{
  188. "actionTitle": req.logEntry.ActionTitle,
  189. }).Infof("Action requested")
  190. return true
  191. }
  192. func stepLogStart(req *ExecutionRequest) bool {
  193. log.WithFields(log.Fields{
  194. "actionTitle": req.logEntry.ActionTitle,
  195. "timeout": req.Action.Timeout,
  196. }).Infof("Action starting")
  197. return true
  198. }
  199. func stepLogFinish(req *ExecutionRequest) bool {
  200. log.WithFields(log.Fields{
  201. "actionTitle": req.logEntry.ActionTitle,
  202. "stdout": req.logEntry.Stdout,
  203. "stderr": req.logEntry.Stderr,
  204. "timedOut": req.logEntry.TimedOut,
  205. "exit": req.logEntry.ExitCode,
  206. }).Infof("Action finished")
  207. return true
  208. }
  209. func notifyListeners(req *ExecutionRequest) {
  210. for _, listener := range req.executor.listeners {
  211. listener.OnExecutionFinished(req.logEntry)
  212. }
  213. }
  214. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  215. if runtime.GOOS == "windows" {
  216. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  217. }
  218. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  219. }
  220. func stepExec(req *ExecutionRequest) bool {
  221. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  222. defer cancel()
  223. var stdout bytes.Buffer
  224. var stderr bytes.Buffer
  225. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  226. cmd.Stdout = &stdout
  227. cmd.Stderr = &stderr
  228. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  229. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  230. req.logEntry.ExecutionStarted = true
  231. runerr := cmd.Start()
  232. cmd.Wait()
  233. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  234. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  235. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  236. req.logEntry.Stdout = stdout.String()
  237. req.logEntry.Stderr = stderr.String()
  238. if runerr != nil {
  239. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  240. }
  241. if ctx.Err() == context.DeadlineExceeded {
  242. req.logEntry.TimedOut = true
  243. }
  244. req.logEntry.Tags = req.Tags
  245. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  246. return true
  247. }
  248. func stepExecAfter(req *ExecutionRequest) bool {
  249. if req.Action.ShellAfterCompleted == "" {
  250. return true
  251. }
  252. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  253. defer cancel()
  254. var stdout bytes.Buffer
  255. var stderr bytes.Buffer
  256. args := map[string]string{
  257. "stdout": req.logEntry.Stdout,
  258. "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
  259. }
  260. finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
  261. cmd := wrapCommandInShell(ctx, finalParsedCommand)
  262. cmd.Stdout = &stdout
  263. cmd.Stderr = &stderr
  264. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  265. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  266. runerr := cmd.Start()
  267. cmd.Wait()
  268. req.logEntry.Stdout += "---\n" + stdout.String()
  269. req.logEntry.Stderr += "---\n" + stderr.String()
  270. if runerr != nil {
  271. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  272. }
  273. if ctx.Err() == context.DeadlineExceeded {
  274. req.logEntry.Stderr += "Your shellAfterCommand command timed out."
  275. }
  276. req.logEntry.Stdout += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
  277. return true
  278. }
  279. func stepTrigger(req *ExecutionRequest) bool {
  280. if req.Action.Trigger != "" {
  281. trigger := &ExecutionRequest{
  282. ActionTitle: req.Action.Trigger,
  283. TrackingID: uuid.NewString(),
  284. Tags: []string{"trigger"},
  285. AuthenticatedUser: req.AuthenticatedUser,
  286. Cfg: req.Cfg,
  287. }
  288. req.executor.ExecRequest(trigger)
  289. }
  290. return true
  291. }