executor.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089
  1. package executor
  2. import (
  3. acl "github.com/OliveTin/OliveTin/internal/acl"
  4. "github.com/OliveTin/OliveTin/internal/auth"
  5. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  6. config "github.com/OliveTin/OliveTin/internal/config"
  7. "github.com/OliveTin/OliveTin/internal/entities"
  8. "github.com/OliveTin/OliveTin/internal/tpl"
  9. "github.com/google/uuid"
  10. log "github.com/sirupsen/logrus"
  11. "github.com/prometheus/client_golang/prometheus"
  12. "github.com/prometheus/client_golang/prometheus/promauto"
  13. "gopkg.in/yaml.v3"
  14. "bytes"
  15. "context"
  16. "fmt"
  17. "os"
  18. "os/exec"
  19. "path"
  20. "strings"
  21. "sync"
  22. "time"
  23. )
  24. const (
  25. DefaultExitCodeNotExecuted = -1337
  26. MaxTriggerDepth = 10
  27. )
  28. var (
  29. metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
  30. Name: "olivetin_actions_requested_count",
  31. Help: "The actions requested count",
  32. })
  33. )
  34. type ActionBinding struct {
  35. ID string
  36. Action *config.Action
  37. Entity *entities.Entity
  38. ConfigOrder int
  39. IsOnDashboard bool
  40. }
  41. // Executor represents a helper class for executing commands. It's main method
  42. // is ExecRequest
  43. type Executor struct {
  44. logs map[string]*InternalLogEntry
  45. logsTrackingIdsByDate []string
  46. LogsByBindingId map[string][]*InternalLogEntry
  47. logmutex sync.RWMutex
  48. MapActionBindings map[string]*ActionBinding
  49. MapActionBindingsLock sync.RWMutex
  50. Cfg *config.Config
  51. listeners []listener
  52. chainOfCommand []executorStepFunc
  53. }
  54. // ExecutionRequest is a request to execute an action. It's passed to an
  55. // Executor. They're created from the api.
  56. type ExecutionRequest struct {
  57. Binding *ActionBinding
  58. Arguments map[string]string
  59. TrackingID string
  60. Tags []string
  61. Cfg *config.Config
  62. AuthenticatedUser *authpublic.AuthenticatedUser
  63. TriggerDepth int
  64. logEntry *InternalLogEntry
  65. finalParsedCommand string
  66. execArgs []string
  67. useDirectExec bool
  68. executor *Executor
  69. }
  70. // InternalLogEntry objects are created by an Executor, and represent the final
  71. // state of execution (even if the command is not executed). It's designed to be
  72. // easily serializable.
  73. type InternalLogEntry struct {
  74. Binding *ActionBinding
  75. DatetimeStarted time.Time
  76. DatetimeFinished time.Time
  77. Output string
  78. TimedOut bool
  79. Blocked bool
  80. ExitCode int32
  81. Tags []string
  82. ExecutionStarted bool
  83. ExecutionFinished bool
  84. ExecutionTrackingID string
  85. Process *os.Process
  86. Username string
  87. Index int64
  88. EntityPrefix string
  89. ActionConfigTitle string // This is the title of the action as defined in the config, not the final parsed title.
  90. /*
  91. The following 3 properties are obviously on Action normally, but it's useful
  92. that logs are lightweight (so we don't need to have an action associated to
  93. logs, etc. Therefore, we duplicate those values here.
  94. */
  95. ActionTitle string
  96. ActionIcon string
  97. }
  98. // .Binding can be nil, so we need to handle that.
  99. func (e *InternalLogEntry) GetBindingId() string {
  100. if e.Binding == nil {
  101. return ""
  102. }
  103. return e.Binding.ID
  104. }
  105. type executorStepFunc func(*ExecutionRequest) bool
  106. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  107. // executing actions.
  108. func DefaultExecutor(cfg *config.Config) *Executor {
  109. e := Executor{}
  110. e.Cfg = cfg
  111. e.logs = make(map[string]*InternalLogEntry)
  112. e.logsTrackingIdsByDate = make([]string, 0)
  113. e.LogsByBindingId = make(map[string][]*InternalLogEntry)
  114. e.MapActionBindings = make(map[string]*ActionBinding)
  115. e.chainOfCommand = []executorStepFunc{
  116. stepRequestAction,
  117. stepConcurrencyCheck,
  118. stepRateCheck,
  119. stepACLCheck,
  120. stepParseArgs,
  121. stepLogStart,
  122. stepExec,
  123. stepExecAfter,
  124. stepLogFinish,
  125. stepSaveLog,
  126. stepTrigger,
  127. }
  128. return &e
  129. }
  130. type listener interface {
  131. OnExecutionStarted(logEntry *InternalLogEntry)
  132. OnExecutionFinished(logEntry *InternalLogEntry)
  133. OnOutputChunk(o []byte, executionTrackingId string)
  134. OnActionMapRebuilt()
  135. }
  136. func (e *Executor) AddListener(m listener) {
  137. e.listeners = append(e.listeners, m)
  138. }
  139. // getPagingStartIndex calculates the starting index for log pagination.
  140. // Parameters:
  141. //
  142. // startOffset: The offset from the most recent log (0 means start from the most recent)
  143. // totalLogCount: Total number of logs available
  144. // count: Number of logs to retrieve
  145. //
  146. // Returns: The calculated starting index for pagination
  147. func getPagingStartIndex(startOffset int64, totalLogCount int64) int64 {
  148. var startIndex int64
  149. if startOffset <= 0 {
  150. startIndex = totalLogCount
  151. } else {
  152. startIndex = (totalLogCount - startOffset)
  153. if startIndex < 0 {
  154. startIndex = 1
  155. }
  156. }
  157. return startIndex - 1
  158. }
  159. type PagingResult struct {
  160. CountRemaining int64
  161. PageSize int64
  162. TotalCount int64
  163. StartOffset int64
  164. }
  165. func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
  166. pagingResult := &PagingResult{
  167. CountRemaining: 0,
  168. PageSize: pageCount,
  169. TotalCount: 0,
  170. StartOffset: startOffset,
  171. }
  172. e.logmutex.RLock()
  173. totalLogCount := int64(len(e.logsTrackingIdsByDate))
  174. pagingResult.TotalCount = totalLogCount
  175. startIndex := getPagingStartIndex(startOffset, totalLogCount)
  176. pageCount = min(totalLogCount, pageCount)
  177. endIndex := max(0, (startIndex-pageCount)+1)
  178. log.WithFields(log.Fields{
  179. "startOffset": startOffset,
  180. "pageCount": pageCount,
  181. "total": totalLogCount,
  182. "startIndex": startIndex,
  183. "endIndex": endIndex,
  184. }).Tracef("GetLogTrackingIds")
  185. trackingIds := make([]*InternalLogEntry, 0, pageCount)
  186. if totalLogCount > 0 {
  187. for i := endIndex; i <= startIndex; i++ {
  188. trackingIds = append(trackingIds, e.logs[e.logsTrackingIdsByDate[i]])
  189. }
  190. }
  191. e.logmutex.RUnlock()
  192. pagingResult.CountRemaining = endIndex
  193. return trackingIds, pagingResult
  194. }
  195. func isValidLogEntryForACL(entry *InternalLogEntry) bool {
  196. return entry != nil && entry.Binding != nil && entry.Binding.Action != nil
  197. }
  198. func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry) bool {
  199. return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
  200. }
  201. func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, dateFilter string) []*InternalLogEntry {
  202. e.logmutex.RLock()
  203. defer e.logmutex.RUnlock()
  204. filtered := make([]*InternalLogEntry, 0, len(e.logsTrackingIdsByDate))
  205. filterDate, hasDateFilter := parseDateFilter(dateFilter)
  206. for _, trackingId := range e.logsTrackingIdsByDate {
  207. entry := e.logs[trackingId]
  208. if shouldIncludeLogEntry(cfg, user, entry, filterDate, hasDateFilter) {
  209. filtered = append(filtered, entry)
  210. }
  211. }
  212. return filtered
  213. }
  214. // parseDateFilter parses the date filter string and returns filter information.
  215. func parseDateFilter(dateFilter string) (filterDate time.Time, hasDateFilter bool) {
  216. if dateFilter == "" {
  217. return time.Time{}, false
  218. }
  219. parsedDate, err := time.Parse("2006-01-02", dateFilter)
  220. if err != nil {
  221. log.WithFields(log.Fields{
  222. "dateFilter": dateFilter,
  223. "error": err,
  224. }).Errorf("Failed to parse date filter, expected format YYYY-MM-DD")
  225. return time.Time{}, false
  226. }
  227. return parsedDate, true
  228. }
  229. // shouldIncludeLogEntry determines if a log entry should be included based on ACL and date filter.
  230. func shouldIncludeLogEntry(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry, filterDate time.Time, hasDateFilter bool) bool {
  231. if !isValidLogEntryForACL(entry) {
  232. return false
  233. }
  234. if !isLogEntryAllowedByACL(cfg, user, entry) {
  235. return false
  236. }
  237. return matchesDateFilter(entry, filterDate, hasDateFilter)
  238. }
  239. // matchesDateFilter checks if the log entry matches the date filter.
  240. func matchesDateFilter(entry *InternalLogEntry, filterDate time.Time, hasDateFilter bool) bool {
  241. if !hasDateFilter {
  242. return true
  243. }
  244. entryDate := entry.DatetimeStarted.UTC().Truncate(24 * time.Hour)
  245. filterDateUTC := filterDate.UTC().Truncate(24 * time.Hour)
  246. return entryDate.Equal(filterDateUTC)
  247. }
  248. // paginateFilteredLogs applies pagination to a filtered list of logs and returns
  249. // the paginated results along with pagination metadata.
  250. func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
  251. total := int64(len(filtered))
  252. paging := &PagingResult{PageSize: pageCount, TotalCount: total, StartOffset: startOffset}
  253. if total == 0 {
  254. paging.CountRemaining = 0
  255. return []*InternalLogEntry{}, paging
  256. }
  257. startIndex := getPagingStartIndex(startOffset, total)
  258. pageCount = min(total, pageCount)
  259. endIndex := max(0, (startIndex-pageCount)+1)
  260. out := make([]*InternalLogEntry, 0, pageCount)
  261. for i := endIndex; i <= startIndex && i < int64(len(filtered)); i++ {
  262. out = append(out, filtered[i])
  263. }
  264. paging.CountRemaining = endIndex
  265. return out, paging
  266. }
  267. // GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
  268. // paginated correctly based on the filtered set.
  269. // dateFilter is optional and should be in YYYY-MM-DD format. If empty, no date filtering is applied.
  270. func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64, dateFilter string) ([]*InternalLogEntry, *PagingResult) {
  271. filtered := e.filterLogsByACL(cfg, user, dateFilter)
  272. return paginateFilteredLogs(filtered, startOffset, pageCount)
  273. }
  274. func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
  275. e.logmutex.RLock()
  276. entry, found := e.logs[trackingID]
  277. e.logmutex.RUnlock()
  278. return entry, found
  279. }
  280. func (e *Executor) GetLogsByBindingId(bindingId string) []*InternalLogEntry {
  281. e.logmutex.RLock()
  282. logs, found := e.LogsByBindingId[bindingId]
  283. e.logmutex.RUnlock()
  284. if !found {
  285. return make([]*InternalLogEntry, 0)
  286. }
  287. return logs
  288. }
  289. // shouldCountExecution checks if a log entry should be counted for rate limiting.
  290. func shouldCountExecution(logEntry *InternalLogEntry, windowStart time.Time) bool {
  291. return !logEntry.Blocked && logEntry.DatetimeStarted.After(windowStart)
  292. }
  293. // updateOldestExecution updates the oldest execution time if this entry is older.
  294. func updateOldestExecution(oldestExecutionTime **time.Time, logEntry *InternalLogEntry) {
  295. if *oldestExecutionTime == nil {
  296. *oldestExecutionTime = &logEntry.DatetimeStarted
  297. } else if logEntry.DatetimeStarted.Before(**oldestExecutionTime) {
  298. *oldestExecutionTime = &logEntry.DatetimeStarted
  299. }
  300. }
  301. // findOldestExecutionInWindow finds the oldest execution within the time window and counts executions.
  302. // Returns the count of executions and the oldest execution time, or nil if none found.
  303. func findOldestExecutionInWindow(logs []*InternalLogEntry, windowStart time.Time) (int, *time.Time) {
  304. executions := 0
  305. var oldestExecutionTime *time.Time
  306. for _, logEntry := range logs {
  307. if !shouldCountExecution(logEntry, windowStart) {
  308. continue
  309. }
  310. executions++
  311. updateOldestExecution(&oldestExecutionTime, logEntry)
  312. }
  313. return executions, oldestExecutionTime
  314. }
  315. // calculateExpiryTime calculates when the oldest execution will fall outside the rate limit window.
  316. func calculateExpiryTime(oldestExecutionTime time.Time, duration time.Duration, now time.Time) time.Time {
  317. expiryTime := oldestExecutionTime.Add(duration)
  318. if !expiryTime.After(now) {
  319. return time.Time{}
  320. }
  321. return expiryTime
  322. }
  323. // updateMaxExpiryTime updates maxExpiryTime if expiryTime is later.
  324. func updateMaxExpiryTime(maxExpiryTime *time.Time, expiryTime time.Time) {
  325. if expiryTime.IsZero() {
  326. return
  327. }
  328. if maxExpiryTime.IsZero() || expiryTime.After(*maxExpiryTime) {
  329. *maxExpiryTime = expiryTime
  330. }
  331. }
  332. // calculateExpiryForRate calculates the expiry time for a single rate limit rule.
  333. // Returns the expiry time if the rate limit is exceeded, or zero time if not.
  334. func calculateExpiryForRate(rate config.RateSpec, logs []*InternalLogEntry, now time.Time) time.Time {
  335. duration := parseDuration(rate)
  336. if duration <= 0 {
  337. return time.Time{}
  338. }
  339. windowStart := now.Add(-duration)
  340. executions, oldestExecutionTime := findOldestExecutionInWindow(logs, windowStart)
  341. if executions < rate.Limit || oldestExecutionTime == nil {
  342. return time.Time{}
  343. }
  344. return calculateExpiryTime(*oldestExecutionTime, duration, now)
  345. }
  346. // getLogsForBinding retrieves logs for a binding ID.
  347. func (e *Executor) getLogsForBinding(bindingId string) []*InternalLogEntry {
  348. e.logmutex.RLock()
  349. logs, found := e.LogsByBindingId[bindingId]
  350. e.logmutex.RUnlock()
  351. if !found || len(logs) == 0 {
  352. return nil
  353. }
  354. return logs
  355. }
  356. // calculateMaxExpiryTimeFromRates calculates the maximum expiry time across all rate limit rules.
  357. func calculateMaxExpiryTimeFromRates(rates []config.RateSpec, logs []*InternalLogEntry, now time.Time) time.Time {
  358. var maxExpiryTime time.Time
  359. for _, rate := range rates {
  360. expiryTime := calculateExpiryForRate(rate, logs, now)
  361. updateMaxExpiryTime(&maxExpiryTime, expiryTime)
  362. }
  363. return maxExpiryTime
  364. }
  365. // GetTimeUntilAvailable calculates when an action will be available again based on rate limits.
  366. // Returns the Unix timestamp in seconds when the rate limit expires, or 0 if the action is available now.
  367. func (e *Executor) GetTimeUntilAvailable(binding *ActionBinding) int64 {
  368. if len(binding.Action.MaxRate) == 0 {
  369. return 0
  370. }
  371. logs := e.getLogsForBinding(binding.ID)
  372. if logs == nil {
  373. return 0
  374. }
  375. maxExpiryTime := calculateMaxExpiryTimeFromRates(binding.Action.MaxRate, logs, time.Now())
  376. if maxExpiryTime.IsZero() {
  377. return 0
  378. }
  379. return maxExpiryTime.Unix()
  380. }
  381. func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
  382. e.logmutex.Lock()
  383. entry.Index = int64(len(e.logsTrackingIdsByDate))
  384. e.logs[trackingID] = entry
  385. e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, trackingID)
  386. e.logmutex.Unlock()
  387. }
  388. // ExecRequest processes an ExecutionRequest
  389. func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
  390. if req.AuthenticatedUser == nil {
  391. req.AuthenticatedUser = auth.UserGuest(req.Cfg)
  392. }
  393. req.executor = e
  394. req.logEntry = &InternalLogEntry{
  395. Binding: req.Binding,
  396. DatetimeStarted: time.Now(),
  397. ExecutionTrackingID: req.TrackingID,
  398. Output: "",
  399. ExitCode: DefaultExitCodeNotExecuted,
  400. ExecutionStarted: false,
  401. ExecutionFinished: false,
  402. ActionTitle: "notfound",
  403. ActionIcon: "&#x1f4a9;",
  404. Username: req.AuthenticatedUser.Username,
  405. }
  406. _, isDuplicate := e.GetLog(req.TrackingID)
  407. if isDuplicate || req.TrackingID == "" {
  408. req.TrackingID = uuid.NewString()
  409. }
  410. // Update the log entry with the final tracking ID
  411. req.logEntry.ExecutionTrackingID = req.TrackingID
  412. log.Tracef("executor.ExecRequest(): %v", req)
  413. e.SetLog(req.TrackingID, req.logEntry)
  414. wg := new(sync.WaitGroup)
  415. wg.Add(1)
  416. go func() {
  417. e.execChain(req)
  418. defer wg.Done()
  419. }()
  420. return wg, req.TrackingID
  421. }
  422. func (e *Executor) execChain(req *ExecutionRequest) {
  423. for _, step := range e.chainOfCommand {
  424. if !step(req) {
  425. break
  426. }
  427. }
  428. // Ensure DatetimeFinished is set even if execution was blocked early
  429. if req.logEntry.DatetimeFinished.IsZero() {
  430. req.logEntry.DatetimeFinished = time.Now()
  431. }
  432. req.logEntry.ExecutionFinished = true
  433. // This isn't a step, because we want to notify all listeners, irrespective
  434. // of how many steps were actually executed.
  435. notifyListenersFinished(req)
  436. }
  437. func getConcurrentCount(req *ExecutionRequest) int {
  438. concurrentCount := 0
  439. req.executor.logmutex.RLock()
  440. for _, log := range req.executor.GetLogsByBindingId(req.Binding.ID) {
  441. if !log.ExecutionFinished {
  442. concurrentCount += 1
  443. }
  444. }
  445. req.executor.logmutex.RUnlock()
  446. return concurrentCount
  447. }
  448. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  449. concurrentCount := getConcurrentCount(req)
  450. // Note that the current execution is counted int the logs, so when checking we +1
  451. if concurrentCount >= (req.Binding.Action.MaxConcurrent + 1) {
  452. log.WithFields(log.Fields{
  453. "actionTitle": req.logEntry.ActionTitle,
  454. "concurrentCount": concurrentCount,
  455. "maxConcurrent": req.Binding.Action.MaxConcurrent,
  456. }).Warnf("Blocked from executing due to concurrency limit")
  457. req.logEntry.Output = "Blocked from executing due to concurrency limit"
  458. req.logEntry.Blocked = true
  459. return false
  460. }
  461. return true
  462. }
  463. func parseDuration(rate config.RateSpec) time.Duration {
  464. duration, err := time.ParseDuration(rate.Duration)
  465. if err != nil {
  466. log.Warnf("Could not parse duration: %v", rate.Duration)
  467. return -1 * time.Minute
  468. }
  469. return duration
  470. }
  471. //gocyclo:ignore
  472. func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
  473. executions := -1 // Because we will find ourself when checking execution logs
  474. duration := parseDuration(rate)
  475. then := time.Now().Add(-duration)
  476. currentEntityPrefix := ""
  477. if req.Binding != nil && req.Binding.Entity != nil {
  478. currentEntityPrefix = req.Binding.Entity.UniqueKey
  479. }
  480. for _, logEntry := range req.executor.GetLogsByBindingId(req.Binding.ID) {
  481. if logEntry.EntityPrefix != currentEntityPrefix {
  482. continue
  483. }
  484. if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
  485. executions += 1
  486. }
  487. }
  488. return executions
  489. }
  490. func stepRateCheck(req *ExecutionRequest) bool {
  491. for _, rate := range req.Binding.Action.MaxRate {
  492. executions := getExecutionsCount(rate, req)
  493. if executions >= rate.Limit {
  494. log.WithFields(log.Fields{
  495. "actionTitle": req.logEntry.ActionTitle,
  496. "executions": executions,
  497. "limit": rate.Limit,
  498. "duration": rate.Duration,
  499. }).Infof("Blocked from executing due to rate limit")
  500. req.logEntry.Output = "Blocked from executing due to rate limit"
  501. req.logEntry.Blocked = true
  502. return false
  503. }
  504. }
  505. return true
  506. }
  507. func stepACLCheck(req *ExecutionRequest) bool {
  508. canExec := acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Binding.Action)
  509. if !canExec {
  510. req.logEntry.Output = "ACL check failed. Blocked from executing."
  511. req.logEntry.Blocked = true
  512. log.WithFields(log.Fields{
  513. "actionTitle": req.logEntry.ActionTitle,
  514. }).Warnf("ACL check failed. Blocked from executing.")
  515. }
  516. return canExec
  517. }
  518. func stepParseArgs(req *ExecutionRequest) bool {
  519. ensureArgumentMap(req)
  520. injectSystemArgs(req)
  521. if !hasBindingAndAction(req) {
  522. return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
  523. }
  524. filterToDefinedArgumentsOnly(req)
  525. mangleInvalidArgumentValues(req)
  526. if hasExec(req) {
  527. return handleExecBranch(req)
  528. } else {
  529. return handleShellBranch(req)
  530. }
  531. }
  532. func handleExecBranch(req *ExecutionRequest) bool {
  533. args, err := parseActionExec(req.Arguments, req.Binding.Action, req.Binding.Entity)
  534. if err != nil {
  535. return fail(req, err)
  536. }
  537. req.useDirectExec = true
  538. req.execArgs = args
  539. return true
  540. }
  541. func handleShellBranch(req *ExecutionRequest) bool {
  542. if hasWebhookTag(req) {
  543. return fail(req, fmt.Errorf("webhooks cannot use Shell execution; use exec instead. See https://docs.olivetin.app/action_execution/shellvsexec.html"))
  544. }
  545. if err := checkShellArgumentSafety(req.Binding.Action); err != nil {
  546. return fail(req, err)
  547. }
  548. cmd, err := parseActionArguments(req)
  549. if err != nil {
  550. return fail(req, err)
  551. }
  552. req.useDirectExec = false
  553. req.finalParsedCommand = cmd
  554. return true
  555. }
  556. func ensureArgumentMap(req *ExecutionRequest) {
  557. if req.Arguments == nil {
  558. req.Arguments = make(map[string]string)
  559. }
  560. }
  561. func filterToDefinedArgumentsOnly(req *ExecutionRequest) {
  562. definedNames := make(map[string]struct{})
  563. for _, arg := range req.Binding.Action.Arguments {
  564. definedNames[arg.Name] = struct{}{}
  565. }
  566. filtered := make(map[string]string)
  567. for k, v := range req.Arguments {
  568. if keepArgument(k, definedNames) {
  569. filtered[k] = v
  570. }
  571. }
  572. req.Arguments = filtered
  573. }
  574. func keepArgument(name string, definedNames map[string]struct{}) bool {
  575. _, ok := definedNames[name]
  576. return ok || strings.HasPrefix(name, "ot_")
  577. }
  578. func hasWebhookTag(req *ExecutionRequest) bool {
  579. for _, tag := range req.Tags {
  580. if tag == "webhook" {
  581. return true
  582. }
  583. }
  584. return false
  585. }
  586. func injectSystemArgs(req *ExecutionRequest) {
  587. req.Arguments["ot_executionTrackingId"] = req.TrackingID
  588. req.Arguments["ot_username"] = req.AuthenticatedUser.Username
  589. }
  590. func hasBindingAndAction(req *ExecutionRequest) bool {
  591. return !(req.Binding == nil || req.Binding.Action == nil)
  592. }
  593. func hasExec(req *ExecutionRequest) bool {
  594. return len(req.Binding.Action.Exec) > 0
  595. }
  596. func fail(req *ExecutionRequest, err error) bool {
  597. req.logEntry.Output = err.Error()
  598. log.Warn(err.Error())
  599. return false
  600. }
  601. func stepRequestAction(req *ExecutionRequest) bool {
  602. metricActionsRequested.Inc()
  603. if !stepRequestActionHasBinding(req) {
  604. return false
  605. }
  606. stepRequestActionPopulateLogEntry(req)
  607. stepRequestActionRegisterLog(req)
  608. log.WithFields(log.Fields{
  609. "actionTitle": req.logEntry.ActionTitle,
  610. "tags": req.Tags,
  611. }).Infof("Action requested")
  612. notifyListenersStarted(req)
  613. return true
  614. }
  615. func stepRequestActionHasBinding(req *ExecutionRequest) bool {
  616. if req.Binding == nil || req.Binding.Action == nil {
  617. log.Warnf("Action request has no binding/action; skipping execution")
  618. return false
  619. }
  620. return true
  621. }
  622. func stepRequestActionPopulateLogEntry(req *ExecutionRequest) {
  623. req.logEntry.Binding = req.Binding
  624. req.logEntry.ActionConfigTitle = req.Binding.Action.Title
  625. req.logEntry.ActionTitle = tpl.ParseTemplateOfActionBeforeExec(req.Binding.Action.Title, req.Binding.Entity)
  626. req.logEntry.ActionIcon = req.Binding.Action.Icon
  627. req.logEntry.Tags = req.Tags
  628. if req.Binding.Entity != nil {
  629. req.logEntry.EntityPrefix = req.Binding.Entity.UniqueKey
  630. }
  631. }
  632. func stepRequestActionRegisterLog(req *ExecutionRequest) {
  633. req.executor.logmutex.Lock()
  634. defer req.executor.logmutex.Unlock()
  635. if _, containsKey := req.executor.LogsByBindingId[req.Binding.ID]; !containsKey {
  636. req.executor.LogsByBindingId[req.Binding.ID] = make([]*InternalLogEntry, 0)
  637. }
  638. req.executor.LogsByBindingId[req.Binding.ID] = append(req.executor.LogsByBindingId[req.Binding.ID], req.logEntry)
  639. }
  640. func stepLogStart(req *ExecutionRequest) bool {
  641. log.WithFields(log.Fields{
  642. "actionTitle": req.logEntry.ActionTitle,
  643. "timeout": req.Binding.Action.Timeout,
  644. }).Infof("Action started")
  645. return true
  646. }
  647. func stepLogFinish(req *ExecutionRequest) bool {
  648. req.logEntry.ExecutionFinished = true
  649. log.WithFields(log.Fields{
  650. "actionTitle": req.logEntry.ActionTitle,
  651. "outputLength": len(req.logEntry.Output),
  652. "timedOut": req.logEntry.TimedOut,
  653. "exit": req.logEntry.ExitCode,
  654. }).Infof("Action finished")
  655. return true
  656. }
  657. func notifyListenersFinished(req *ExecutionRequest) {
  658. for _, listener := range req.executor.listeners {
  659. listener.OnExecutionFinished(req.logEntry)
  660. }
  661. }
  662. func notifyListenersStarted(req *ExecutionRequest) {
  663. for _, listener := range req.executor.listeners {
  664. listener.OnExecutionStarted(req.logEntry)
  665. }
  666. }
  667. func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
  668. if err != nil {
  669. logEntry.Output = err.Error() + "\n\n" + logEntry.Output
  670. }
  671. }
  672. type OutputStreamer struct {
  673. Req *ExecutionRequest
  674. output bytes.Buffer
  675. }
  676. func (ost *OutputStreamer) Write(o []byte) (n int, err error) {
  677. for _, listener := range ost.Req.executor.listeners {
  678. listener.OnOutputChunk(o, ost.Req.TrackingID)
  679. }
  680. return ost.output.Write(o)
  681. }
  682. func (ost *OutputStreamer) String() string {
  683. return ost.output.String()
  684. }
  685. func buildEnv(args map[string]string) []string {
  686. ret := append(os.Environ(), "OLIVETIN=1")
  687. for k, v := range args {
  688. varName := fmt.Sprintf("%v", strings.TrimSpace(strings.ToUpper(k)))
  689. // Skip arguments that might not have a name (eg, confirmation), as this causes weird bugs on Windows.
  690. if varName == "" {
  691. continue
  692. }
  693. ret = append(ret, fmt.Sprintf("%v=%v", varName, v))
  694. }
  695. return ret
  696. }
  697. func stepExec(req *ExecutionRequest) bool {
  698. ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
  699. defer cancel()
  700. streamer := &OutputStreamer{Req: req}
  701. cmd := buildCommand(ctx, req)
  702. if cmd == nil {
  703. req.logEntry.Output = "Cannot execute: no command arguments provided"
  704. log.Warn("Cannot execute: no command arguments provided")
  705. return false
  706. }
  707. prepareCommand(cmd, streamer, req)
  708. runerr := cmd.Start()
  709. req.logEntry.Process = cmd.Process
  710. ctx.setProcess(cmd.Process)
  711. waiterr := cmd.Wait()
  712. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  713. req.logEntry.Output = streamer.String()
  714. appendErrorToStderr(runerr, req.logEntry)
  715. appendErrorToStderr(waiterr, req.logEntry)
  716. if ctx.Err() == context.DeadlineExceeded {
  717. log.WithFields(log.Fields{
  718. "actionTitle": req.logEntry.ActionTitle,
  719. }).Warnf("Action timed out")
  720. req.logEntry.TimedOut = true
  721. req.logEntry.Output += "OliveTin::timeout - this action timed out after " + fmt.Sprintf("%v", req.Binding.Action.Timeout) + " seconds. If you need more time for this action, set a longer timeout. See https://docs.olivetin.app/action_customization/timeouts.html for more help."
  722. }
  723. req.logEntry.DatetimeFinished = time.Now()
  724. return true
  725. }
  726. func buildCommand(ctx context.Context, req *ExecutionRequest) *exec.Cmd {
  727. if req.useDirectExec {
  728. return wrapCommandDirect(ctx, req.execArgs)
  729. }
  730. return wrapCommandInShell(ctx, req.finalParsedCommand)
  731. }
  732. func prepareCommand(cmd *exec.Cmd, streamer *OutputStreamer, req *ExecutionRequest) {
  733. cmd.Stdout = streamer
  734. cmd.Stderr = streamer
  735. cmd.Env = buildEnv(req.Arguments)
  736. req.logEntry.ExecutionStarted = true
  737. }
  738. func stepExecAfter(req *ExecutionRequest) bool {
  739. if req.Binding.Action.ShellAfterCompleted == "" {
  740. return true
  741. }
  742. ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
  743. defer cancel()
  744. var stdout bytes.Buffer
  745. var stderr bytes.Buffer
  746. args := map[string]string{
  747. "output": req.logEntry.Output,
  748. "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
  749. "ot_executionTrackingId": req.TrackingID,
  750. "ot_username": req.AuthenticatedUser.Username,
  751. }
  752. finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
  753. if err != nil {
  754. msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
  755. req.logEntry.Output += msg
  756. log.Warn(msg)
  757. return true
  758. }
  759. cmd := wrapCommandInShell(ctx, finalParsedCommand)
  760. cmd.Stdout = &stdout
  761. cmd.Stderr = &stderr
  762. cmd.Env = buildEnv(args)
  763. runerr := cmd.Start()
  764. ctx.setProcess(cmd.Process)
  765. waiterr := cmd.Wait()
  766. req.logEntry.Output += "\n"
  767. req.logEntry.Output += "OliveTin::shellAfterCompleted stdout\n"
  768. req.logEntry.Output += stdout.String()
  769. req.logEntry.Output += "OliveTin::shellAfterCompleted stderr\n"
  770. req.logEntry.Output += stderr.String()
  771. req.logEntry.Output += "OliveTin::shellAfterCompleted errors and summary\n"
  772. appendErrorToStderr(runerr, req.logEntry)
  773. appendErrorToStderr(waiterr, req.logEntry)
  774. if ctx.Err() == context.DeadlineExceeded {
  775. req.logEntry.Output += "Your shellAfterCompleted command timed out."
  776. }
  777. req.logEntry.Output += fmt.Sprintf("Your shellAfterCompleted exited with code %v\n", cmd.ProcessState.ExitCode())
  778. req.logEntry.Output += "OliveTin::shellAfterCompleted output complete\n"
  779. return true
  780. }
  781. //gocyclo:ignore
  782. func stepTrigger(req *ExecutionRequest) bool {
  783. if req.Binding.Action.Triggers == nil {
  784. return true
  785. }
  786. if req.TriggerDepth >= MaxTriggerDepth {
  787. log.WithFields(log.Fields{
  788. "actionTitle": req.logEntry.ActionTitle,
  789. "depth": req.TriggerDepth,
  790. }).Warnf("Trigger action reached maximum depth of %v. Not triggering further actions.", MaxTriggerDepth)
  791. req.logEntry.Output += fmt.Sprintf("OliveTin::trigger - this action reached maximum trigger depth of %v. Not triggering further actions.", MaxTriggerDepth)
  792. return true
  793. }
  794. if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
  795. log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
  796. }
  797. triggerLoop(req)
  798. return true
  799. }
  800. func triggerLoop(req *ExecutionRequest) {
  801. for _, triggerTitle := range req.Binding.Action.Triggers {
  802. binding := req.executor.findBindingByActionTitle(triggerTitle, "")
  803. if binding == nil {
  804. log.WithFields(log.Fields{
  805. "triggerTitle": triggerTitle,
  806. "fromAction": req.logEntry.ActionTitle,
  807. }).Warnf("Trigger references unknown action title; skipping")
  808. continue
  809. }
  810. trigger := &ExecutionRequest{
  811. Binding: binding,
  812. TrackingID: uuid.NewString(),
  813. Tags: []string{"trigger"},
  814. AuthenticatedUser: req.AuthenticatedUser,
  815. Arguments: req.Arguments,
  816. Cfg: req.Cfg,
  817. TriggerDepth: req.TriggerDepth + 1,
  818. }
  819. req.executor.ExecRequest(trigger)
  820. }
  821. }
  822. func stepSaveLog(req *ExecutionRequest) bool {
  823. filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
  824. saveLogResults(req, filename)
  825. saveLogOutput(req, filename)
  826. return true
  827. }
  828. func firstNonEmpty(one, two string) string {
  829. if one != "" {
  830. return one
  831. }
  832. return two
  833. }
  834. func saveLogResults(req *ExecutionRequest, filename string) {
  835. dir := firstNonEmpty(req.Binding.Action.SaveLogs.ResultsDirectory, req.Cfg.SaveLogs.ResultsDirectory)
  836. if dir != "" {
  837. data, err := yaml.Marshal(req.logEntry)
  838. if err != nil {
  839. log.Warnf("%v", err)
  840. }
  841. filepath := path.Join(dir, filename+".yaml")
  842. err = os.WriteFile(filepath, data, 0600)
  843. if err != nil {
  844. log.Warnf("%v", err)
  845. }
  846. }
  847. }
  848. func saveLogOutput(req *ExecutionRequest, filename string) {
  849. dir := firstNonEmpty(req.Binding.Action.SaveLogs.OutputDirectory, req.Cfg.SaveLogs.OutputDirectory)
  850. if dir != "" {
  851. data := req.logEntry.Output
  852. filepath := path.Join(dir, filename+".log")
  853. err := os.WriteFile(filepath, []byte(data), 0600)
  854. if err != nil {
  855. log.Warnf("%v", err)
  856. }
  857. }
  858. }