executor.go 32 KB

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