executor.go 36 KB

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