executor.go 36 KB

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