executor.go 11 KB

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