executor.go 28 KB

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