executor.go 35 KB

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