executor.go 12 KB

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