executor.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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. "gopkg.in/yaml.v3"
  11. "bytes"
  12. "context"
  13. "fmt"
  14. "os"
  15. "path"
  16. "strings"
  17. "sync"
  18. "time"
  19. )
  20. var (
  21. metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
  22. Name: "olivetin_actions_requested_count",
  23. Help: "The actions requested count",
  24. })
  25. )
  26. type ActionBinding struct {
  27. Action *config.Action
  28. EntityPrefix string
  29. ConfigOrder int
  30. }
  31. // Executor represents a helper class for executing commands. It's main method
  32. // is ExecRequest
  33. type Executor struct {
  34. logs map[string]*InternalLogEntry
  35. LogsByActionId map[string][]*InternalLogEntry
  36. logmutex sync.RWMutex
  37. MapActionIdToBinding map[string]*ActionBinding
  38. MapActionIdToBindingLock sync.RWMutex
  39. Cfg *config.Config
  40. listeners []listener
  41. chainOfCommand []executorStepFunc
  42. }
  43. // ExecutionRequest is a request to execute an action. It's passed to an
  44. // Executor. They're created from the grpcapi.
  45. type ExecutionRequest struct {
  46. ActionTitle string
  47. Action *config.Action
  48. Arguments map[string]string
  49. TrackingID string
  50. Tags []string
  51. Cfg *config.Config
  52. AuthenticatedUser *acl.AuthenticatedUser
  53. EntityPrefix string
  54. logEntry *InternalLogEntry
  55. finalParsedCommand string
  56. executor *Executor
  57. }
  58. // InternalLogEntry objects are created by an Executor, and represent the final
  59. // state of execution (even if the command is not executed). It's designed to be
  60. // easily serializable.
  61. type InternalLogEntry struct {
  62. DatetimeStarted time.Time
  63. DatetimeFinished time.Time
  64. Output string
  65. TimedOut bool
  66. Blocked bool
  67. ExitCode int32
  68. Tags []string
  69. ExecutionStarted bool
  70. ExecutionFinished bool
  71. ExecutionTrackingID string
  72. Process *os.Process
  73. Username string
  74. /*
  75. The following 3 properties are obviously on Action normally, but it's useful
  76. that logs are lightweight (so we don't need to have an action associated to
  77. logs, etc. Therefore, we duplicate those values here.
  78. */
  79. ActionTitle string
  80. ActionIcon string
  81. ActionId string
  82. }
  83. type executorStepFunc func(*ExecutionRequest) bool
  84. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  85. // executing actions.
  86. func DefaultExecutor(cfg *config.Config) *Executor {
  87. e := Executor{}
  88. e.Cfg = cfg
  89. e.logs = make(map[string]*InternalLogEntry)
  90. e.LogsByActionId = make(map[string][]*InternalLogEntry)
  91. e.MapActionIdToBinding = make(map[string]*ActionBinding)
  92. e.chainOfCommand = []executorStepFunc{
  93. stepRequestAction,
  94. stepConcurrencyCheck,
  95. stepRateCheck,
  96. stepACLCheck,
  97. stepParseArgs,
  98. stepLogStart,
  99. stepExec,
  100. stepExecAfter,
  101. stepLogFinish,
  102. stepSaveLog,
  103. stepTrigger,
  104. }
  105. return &e
  106. }
  107. type listener interface {
  108. OnExecutionStarted(logEntry *InternalLogEntry)
  109. OnExecutionFinished(logEntry *InternalLogEntry)
  110. OnOutputChunk(o []byte, executionTrackingId string)
  111. OnActionMapRebuilt()
  112. }
  113. func (e *Executor) AddListener(m listener) {
  114. e.listeners = append(e.listeners, m)
  115. }
  116. func (e *Executor) GetLogsCopy() map[string]*InternalLogEntry {
  117. e.logmutex.RLock()
  118. copy := make(map[string]*InternalLogEntry)
  119. for k, v := range e.logs {
  120. copy[k] = v
  121. }
  122. e.logmutex.RUnlock()
  123. return copy
  124. }
  125. func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
  126. e.logmutex.RLock()
  127. entry, found := e.logs[trackingID]
  128. e.logmutex.RUnlock()
  129. return entry, found
  130. }
  131. func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
  132. e.logmutex.RLock()
  133. logs, found := e.LogsByActionId[actionId]
  134. e.logmutex.RUnlock()
  135. if !found {
  136. return make([]*InternalLogEntry, 0)
  137. }
  138. return logs
  139. }
  140. func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
  141. e.logmutex.Lock()
  142. e.logs[trackingID] = entry
  143. e.logmutex.Unlock()
  144. }
  145. // ExecRequest processes an ExecutionRequest
  146. func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
  147. if req.AuthenticatedUser == nil {
  148. req.AuthenticatedUser = acl.UserGuest(req.Cfg)
  149. }
  150. req.executor = e
  151. req.logEntry = &InternalLogEntry{
  152. DatetimeStarted: time.Now(),
  153. ExecutionTrackingID: req.TrackingID,
  154. Output: "",
  155. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  156. ExecutionStarted: false,
  157. ExecutionFinished: false,
  158. ActionId: "",
  159. ActionTitle: "notfound",
  160. ActionIcon: "💩",
  161. Username: req.AuthenticatedUser.Username,
  162. }
  163. _, isDuplicate := e.GetLog(req.TrackingID)
  164. if isDuplicate || req.TrackingID == "" {
  165. req.TrackingID = uuid.NewString()
  166. }
  167. log.Tracef("executor.ExecRequest(): %v", req)
  168. e.SetLog(req.TrackingID, req.logEntry)
  169. wg := new(sync.WaitGroup)
  170. wg.Add(1)
  171. go func() {
  172. e.execChain(req)
  173. defer wg.Done()
  174. }()
  175. return wg, req.TrackingID
  176. }
  177. func (e *Executor) execChain(req *ExecutionRequest) {
  178. for _, step := range e.chainOfCommand {
  179. if !step(req) {
  180. break
  181. }
  182. }
  183. req.logEntry.ExecutionFinished = true
  184. // This isn't a step, because we want to notify all listeners, irrespective
  185. // of how many steps were actually executed.
  186. notifyListenersFinished(req)
  187. }
  188. func getConcurrentCount(req *ExecutionRequest) int {
  189. concurrentCount := 0
  190. req.executor.logmutex.RLock()
  191. for _, log := range req.executor.GetLogsByActionId(req.Action.ID) {
  192. if !log.ExecutionFinished {
  193. concurrentCount += 1
  194. }
  195. }
  196. req.executor.logmutex.RUnlock()
  197. return concurrentCount
  198. }
  199. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  200. concurrentCount := getConcurrentCount(req)
  201. // Note that the current execution is counted int the logs, so when checking we +1
  202. if concurrentCount >= (req.Action.MaxConcurrent + 1) {
  203. 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)
  204. log.WithFields(log.Fields{
  205. "actionTitle": req.logEntry.ActionTitle,
  206. }).Warnf(msg)
  207. req.logEntry.Output = msg
  208. req.logEntry.Blocked = true
  209. return false
  210. }
  211. return true
  212. }
  213. func parseDuration(rate config.RateSpec) time.Duration {
  214. duration, err := time.ParseDuration(rate.Duration)
  215. if err != nil {
  216. log.Warnf("Could not parse duration: %v", rate.Duration)
  217. return -1 * time.Minute
  218. }
  219. return duration
  220. }
  221. func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
  222. executions := -1 // Because we will find ourself when checking execution logs
  223. duration := parseDuration(rate)
  224. then := time.Now().Add(-duration)
  225. for _, logEntry := range req.executor.GetLogsByActionId(req.Action.ID) {
  226. if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
  227. executions += 1
  228. }
  229. }
  230. return executions
  231. }
  232. func stepRateCheck(req *ExecutionRequest) bool {
  233. for _, rate := range req.Action.MaxRate {
  234. executions := getExecutionsCount(rate, req)
  235. if executions >= rate.Limit {
  236. 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)
  237. log.WithFields(log.Fields{
  238. "actionTitle": req.logEntry.ActionTitle,
  239. }).Infof(msg)
  240. req.logEntry.Output = msg
  241. req.logEntry.Blocked = true
  242. return false
  243. }
  244. }
  245. return true
  246. }
  247. func stepACLCheck(req *ExecutionRequest) bool {
  248. canExec := acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
  249. if !canExec {
  250. req.logEntry.Output = "ACL check failed. Blocked from executing."
  251. req.logEntry.Blocked = true
  252. log.WithFields(log.Fields{
  253. "actionTitle": req.logEntry.ActionTitle,
  254. }).Warnf("ACL check failed. Blocked from executing.")
  255. }
  256. return canExec
  257. }
  258. func stepParseArgs(req *ExecutionRequest) bool {
  259. var err error
  260. if req.Arguments == nil {
  261. req.Arguments = make(map[string]string)
  262. }
  263. req.Arguments["ot_executionTrackingId"] = req.TrackingID
  264. req.Arguments["ot_username"] = req.AuthenticatedUser.Username
  265. req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
  266. if err != nil {
  267. req.logEntry.Output = err.Error()
  268. log.Warnf(err.Error())
  269. return false
  270. }
  271. return true
  272. }
  273. func stepRequestAction(req *ExecutionRequest) bool {
  274. // The grpc API always tries to find the action by ID, but it may
  275. if req.Action == nil {
  276. log.WithFields(log.Fields{
  277. "actionTitle": req.ActionTitle,
  278. }).Infof("Action finding by title")
  279. req.Action = req.Cfg.FindAction(req.ActionTitle)
  280. if req.Action == nil {
  281. log.WithFields(log.Fields{
  282. "actionTitle": req.ActionTitle,
  283. }).Warnf("Action requested, but not found")
  284. req.logEntry.Output = "Action not found: " + req.ActionTitle
  285. return false
  286. }
  287. }
  288. metricActionsRequested.Inc()
  289. req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
  290. req.logEntry.ActionIcon = req.Action.Icon
  291. req.logEntry.ActionId = req.Action.ID
  292. req.logEntry.Tags = req.Tags
  293. req.executor.logmutex.Lock()
  294. if _, containsKey := req.executor.LogsByActionId[req.Action.ID]; !containsKey {
  295. req.executor.LogsByActionId[req.Action.ID] = make([]*InternalLogEntry, 0)
  296. }
  297. req.executor.LogsByActionId[req.Action.ID] = append(req.executor.LogsByActionId[req.Action.ID], req.logEntry)
  298. req.executor.logmutex.Unlock()
  299. log.WithFields(log.Fields{
  300. "actionTitle": req.logEntry.ActionTitle,
  301. "tags": req.Tags,
  302. }).Infof("Action requested")
  303. notifyListenersStarted(req)
  304. return true
  305. }
  306. func stepLogStart(req *ExecutionRequest) bool {
  307. log.WithFields(log.Fields{
  308. "actionTitle": req.logEntry.ActionTitle,
  309. "timeout": req.Action.Timeout,
  310. }).Infof("Action started")
  311. return true
  312. }
  313. func stepLogFinish(req *ExecutionRequest) bool {
  314. req.logEntry.ExecutionFinished = true
  315. log.WithFields(log.Fields{
  316. "actionTitle": req.logEntry.ActionTitle,
  317. "outputLength": len(req.logEntry.Output),
  318. "timedOut": req.logEntry.TimedOut,
  319. "exit": req.logEntry.ExitCode,
  320. }).Infof("Action finished")
  321. return true
  322. }
  323. func notifyListenersFinished(req *ExecutionRequest) {
  324. for _, listener := range req.executor.listeners {
  325. listener.OnExecutionFinished(req.logEntry)
  326. }
  327. }
  328. func notifyListenersStarted(req *ExecutionRequest) {
  329. for _, listener := range req.executor.listeners {
  330. listener.OnExecutionStarted(req.logEntry)
  331. }
  332. }
  333. func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
  334. if err != nil {
  335. logEntry.Output = err.Error() + "\n\n" + logEntry.Output
  336. }
  337. }
  338. type OutputStreamer struct {
  339. Req *ExecutionRequest
  340. output bytes.Buffer
  341. }
  342. func (ost *OutputStreamer) Write(o []byte) (n int, err error) {
  343. for _, listener := range ost.Req.executor.listeners {
  344. listener.OnOutputChunk(o, ost.Req.TrackingID)
  345. }
  346. return ost.output.Write(o)
  347. }
  348. func (ost *OutputStreamer) String() string {
  349. return ost.output.String()
  350. }
  351. func buildEnv(args map[string]string) []string {
  352. ret := append(os.Environ(), "OLIVETIN=1")
  353. for k, v := range args {
  354. varName := fmt.Sprintf("%v", strings.TrimSpace(strings.ToUpper(k)))
  355. // Skip arguments that might not have a name (eg, confirmation), as this causes weird bugs on Windows.
  356. if varName == "" {
  357. continue
  358. }
  359. ret = append(ret, fmt.Sprintf("%v=%v", varName, v))
  360. }
  361. return ret
  362. }
  363. func stepExec(req *ExecutionRequest) bool {
  364. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  365. defer cancel()
  366. streamer := &OutputStreamer{Req: req}
  367. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  368. cmd.Stdout = streamer
  369. cmd.Stderr = streamer
  370. cmd.Env = buildEnv(req.Arguments)
  371. req.logEntry.ExecutionStarted = true
  372. runerr := cmd.Start()
  373. req.logEntry.Process = cmd.Process
  374. waiterr := cmd.Wait()
  375. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  376. req.logEntry.Output = streamer.String()
  377. appendErrorToStderr(runerr, req.logEntry)
  378. appendErrorToStderr(waiterr, req.logEntry)
  379. if ctx.Err() == context.DeadlineExceeded {
  380. log.WithFields(log.Fields{
  381. "actionTitle": req.logEntry.ActionTitle,
  382. }).Warnf("Action timed out")
  383. // The context timeout should kill the process, but let's make sure.
  384. req.executor.Kill(req.logEntry)
  385. req.logEntry.TimedOut = true
  386. req.logEntry.Output += "OliveTin::timeout - this action timed out after " + fmt.Sprintf("%v", req.Action.Timeout) + " seconds. If you need more time for this action, set a longer timeout. See https://docs.olivetin.app/timeout.html for more help."
  387. }
  388. req.logEntry.DatetimeFinished = time.Now()
  389. return true
  390. }
  391. func stepExecAfter(req *ExecutionRequest) bool {
  392. if req.Action.ShellAfterCompleted == "" {
  393. return true
  394. }
  395. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  396. defer cancel()
  397. var stdout bytes.Buffer
  398. var stderr bytes.Buffer
  399. args := map[string]string{
  400. "output": req.logEntry.Output,
  401. "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
  402. "ot_executionTrackingId": req.TrackingID,
  403. "ot_username": req.AuthenticatedUser.Username,
  404. }
  405. finalParsedCommand, _, err := parseCommandForReplacements(req.Action.ShellAfterCompleted, args)
  406. if err != nil {
  407. msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
  408. req.logEntry.Output += msg
  409. log.Warnf(msg)
  410. return true
  411. }
  412. cmd := wrapCommandInShell(ctx, finalParsedCommand)
  413. cmd.Stdout = &stdout
  414. cmd.Stderr = &stderr
  415. cmd.Env = buildEnv(args)
  416. runerr := cmd.Start()
  417. waiterr := cmd.Wait()
  418. req.logEntry.Output += "\n"
  419. req.logEntry.Output += "OliveTin::shellAfterCompleted stdout\n"
  420. req.logEntry.Output += stdout.String()
  421. req.logEntry.Output += "OliveTin::shellAfterCompleted stderr\n"
  422. req.logEntry.Output += stderr.String()
  423. req.logEntry.Output += "OliveTin::shellAfterCompleted errors and summary\n"
  424. appendErrorToStderr(runerr, req.logEntry)
  425. appendErrorToStderr(waiterr, req.logEntry)
  426. if ctx.Err() == context.DeadlineExceeded {
  427. req.logEntry.Output += "Your shellAfterCompleted command timed out."
  428. }
  429. req.logEntry.Output += fmt.Sprintf("Your shellAfterCompleted exited with code %v\n", cmd.ProcessState.ExitCode())
  430. req.logEntry.Output += "OliveTin::shellAfterCompleted output complete\n"
  431. return true
  432. }
  433. func stepTrigger(req *ExecutionRequest) bool {
  434. if req.Action.Trigger != "" {
  435. trigger := &ExecutionRequest{
  436. ActionTitle: req.Action.Trigger,
  437. TrackingID: uuid.NewString(),
  438. Tags: []string{"trigger"},
  439. AuthenticatedUser: req.AuthenticatedUser,
  440. Cfg: req.Cfg,
  441. }
  442. req.executor.ExecRequest(trigger)
  443. }
  444. return true
  445. }
  446. func stepSaveLog(req *ExecutionRequest) bool {
  447. filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
  448. saveLogResults(req, filename)
  449. saveLogOutput(req, filename)
  450. return true
  451. }
  452. func firstNonEmpty(one, two string) string {
  453. if one != "" {
  454. return one
  455. }
  456. return two
  457. }
  458. func saveLogResults(req *ExecutionRequest, filename string) {
  459. dir := firstNonEmpty(req.Action.SaveLogs.ResultsDirectory, req.Cfg.SaveLogs.ResultsDirectory)
  460. if dir != "" {
  461. data, err := yaml.Marshal(req.logEntry)
  462. if err != nil {
  463. log.Warnf("%v", err)
  464. }
  465. filepath := path.Join(dir, filename+".yaml")
  466. err = os.WriteFile(filepath, data, 0644)
  467. if err != nil {
  468. log.Warnf("%v", err)
  469. }
  470. }
  471. }
  472. func saveLogOutput(req *ExecutionRequest, filename string) {
  473. dir := firstNonEmpty(req.Action.SaveLogs.OutputDirectory, req.Cfg.SaveLogs.OutputDirectory)
  474. if dir != "" {
  475. data := req.logEntry.Output
  476. filepath := path.Join(dir, filename+".log")
  477. err := os.WriteFile(filepath, []byte(data), 0644)
  478. if err != nil {
  479. log.Warnf("%v", err)
  480. }
  481. }
  482. }