executor.go 40 KB

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