executor.go 11 KB

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