4
0

executor.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. package executor
  2. import (
  3. acl "github.com/OliveTin/OliveTin/internal/acl"
  4. config "github.com/OliveTin/OliveTin/internal/config"
  5. sv "github.com/OliveTin/OliveTin/internal/stringvariables"
  6. "github.com/google/uuid"
  7. log "github.com/sirupsen/logrus"
  8. "github.com/prometheus/client_golang/prometheus"
  9. "github.com/prometheus/client_golang/prometheus/promauto"
  10. "gopkg.in/yaml.v3"
  11. "bytes"
  12. "context"
  13. "fmt"
  14. "os"
  15. "os/exec"
  16. "path"
  17. "strings"
  18. "sync"
  19. "time"
  20. )
  21. var (
  22. metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
  23. Name: "olivetin_actions_requested_count",
  24. Help: "The actions requested count",
  25. })
  26. )
  27. type ActionBinding struct {
  28. Action *config.Action
  29. EntityPrefix string
  30. ConfigOrder int
  31. }
  32. // Executor represents a helper class for executing commands. It's main method
  33. // is ExecRequest
  34. type Executor struct {
  35. logs map[string]*InternalLogEntry
  36. logsTrackingIdsByDate []string
  37. LogsByActionId map[string][]*InternalLogEntry
  38. logmutex sync.RWMutex
  39. MapActionIdToBinding map[string]*ActionBinding
  40. MapActionIdToBindingLock sync.RWMutex
  41. Cfg *config.Config
  42. listeners []listener
  43. chainOfCommand []executorStepFunc
  44. }
  45. // ExecutionRequest is a request to execute an action. It's passed to an
  46. // Executor. They're created from the grpcapi.
  47. type ExecutionRequest struct {
  48. ActionTitle string
  49. Action *config.Action
  50. Arguments map[string]string
  51. TrackingID string
  52. Tags []string
  53. Cfg *config.Config
  54. AuthenticatedUser *acl.AuthenticatedUser
  55. EntityPrefix string
  56. logEntry *InternalLogEntry
  57. finalParsedCommand string
  58. execArgs []string
  59. useDirectExec bool
  60. executor *Executor
  61. }
  62. // InternalLogEntry objects are created by an Executor, and represent the final
  63. // state of execution (even if the command is not executed). It's designed to be
  64. // easily serializable.
  65. type InternalLogEntry struct {
  66. DatetimeStarted time.Time
  67. DatetimeFinished time.Time
  68. Output string
  69. TimedOut bool
  70. Blocked bool
  71. ExitCode int32
  72. Tags []string
  73. ExecutionStarted bool
  74. ExecutionFinished bool
  75. ExecutionTrackingID string
  76. Process *os.Process
  77. Username string
  78. Index int64
  79. EntityPrefix string
  80. /*
  81. The following 3 properties are obviously on Action normally, but it's useful
  82. that logs are lightweight (so we don't need to have an action associated to
  83. logs, etc. Therefore, we duplicate those values here.
  84. */
  85. ActionTitle string
  86. ActionIcon string
  87. ActionId string
  88. }
  89. type executorStepFunc func(*ExecutionRequest) bool
  90. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  91. // executing actions.
  92. func DefaultExecutor(cfg *config.Config) *Executor {
  93. e := Executor{}
  94. e.Cfg = cfg
  95. e.logs = make(map[string]*InternalLogEntry)
  96. e.logsTrackingIdsByDate = make([]string, 0)
  97. e.LogsByActionId = make(map[string][]*InternalLogEntry)
  98. e.MapActionIdToBinding = make(map[string]*ActionBinding)
  99. e.chainOfCommand = []executorStepFunc{
  100. stepRequestAction,
  101. stepConcurrencyCheck,
  102. stepRateCheck,
  103. stepACLCheck,
  104. stepParseArgs,
  105. stepLogStart,
  106. stepExec,
  107. stepExecAfter,
  108. stepLogFinish,
  109. stepSaveLog,
  110. stepTrigger,
  111. }
  112. return &e
  113. }
  114. type listener interface {
  115. OnExecutionStarted(logEntry *InternalLogEntry, action *config.Action)
  116. OnExecutionFinished(logEntry *InternalLogEntry, action *config.Action)
  117. OnOutputChunk(o []byte, executionTrackingId string, logEntry *InternalLogEntry, action *config.Action)
  118. OnActionMapRebuilt()
  119. }
  120. func (e *Executor) AddListener(m listener) {
  121. e.listeners = append(e.listeners, m)
  122. }
  123. // getPagingStartIndex calculates the starting index for log pagination.
  124. // Parameters:
  125. //
  126. // startOffset: The offset from the most recent log (0 means start from the most recent)
  127. // totalLogCount: Total number of logs available
  128. // count: Number of logs to retrieve
  129. //
  130. // Returns: The calculated starting index for pagination
  131. func getPagingStartIndex(startOffset int64, totalLogCount int64) int64 {
  132. var startIndex int64
  133. if startOffset <= 0 {
  134. startIndex = totalLogCount
  135. } else {
  136. startIndex = (totalLogCount - startOffset)
  137. if startIndex < 0 {
  138. startIndex = 1
  139. }
  140. }
  141. return startIndex - 1
  142. }
  143. func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*InternalLogEntry, int64) {
  144. e.logmutex.RLock()
  145. totalLogCount := int64(len(e.logsTrackingIdsByDate))
  146. startIndex := getPagingStartIndex(startOffset, totalLogCount)
  147. pageCount = min(totalLogCount, pageCount)
  148. endIndex := max(0, (startIndex-pageCount)+1)
  149. log.WithFields(log.Fields{
  150. "startOffset": startOffset,
  151. "pageCount": pageCount,
  152. "total": totalLogCount,
  153. "startIndex": startIndex,
  154. "endIndex": endIndex,
  155. }).Tracef("GetLogTrackingIds")
  156. trackingIds := make([]*InternalLogEntry, 0, pageCount)
  157. if totalLogCount > 0 {
  158. for i := endIndex; i <= startIndex; i++ {
  159. trackingIds = append(trackingIds, e.logs[e.logsTrackingIdsByDate[i]])
  160. }
  161. }
  162. e.logmutex.RUnlock()
  163. remainingLogs := endIndex
  164. return trackingIds, remainingLogs
  165. }
  166. func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
  167. e.logmutex.RLock()
  168. entry, found := e.logs[trackingID]
  169. e.logmutex.RUnlock()
  170. return entry, found
  171. }
  172. func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
  173. e.logmutex.RLock()
  174. logs, found := e.LogsByActionId[actionId]
  175. e.logmutex.RUnlock()
  176. if !found {
  177. return make([]*InternalLogEntry, 0)
  178. }
  179. return logs
  180. }
  181. func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
  182. e.logmutex.Lock()
  183. entry.Index = int64(len(e.logsTrackingIdsByDate))
  184. e.logs[trackingID] = entry
  185. e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, trackingID)
  186. e.logmutex.Unlock()
  187. }
  188. // ExecRequest processes an ExecutionRequest
  189. func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
  190. if req.AuthenticatedUser == nil {
  191. req.AuthenticatedUser = acl.UserGuest(req.Cfg)
  192. }
  193. req.executor = e
  194. req.logEntry = &InternalLogEntry{
  195. DatetimeStarted: time.Now(),
  196. ExecutionTrackingID: req.TrackingID,
  197. Output: "",
  198. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  199. ExecutionStarted: false,
  200. ExecutionFinished: false,
  201. ActionId: "",
  202. ActionTitle: "notfound",
  203. ActionIcon: "&#x1f4a9;",
  204. Username: req.AuthenticatedUser.Username,
  205. EntityPrefix: req.EntityPrefix,
  206. }
  207. _, isDuplicate := e.GetLog(req.TrackingID)
  208. if isDuplicate || req.TrackingID == "" {
  209. req.TrackingID = uuid.NewString()
  210. }
  211. // Update the log entry with the final tracking ID
  212. req.logEntry.ExecutionTrackingID = req.TrackingID
  213. log.Tracef("executor.ExecRequest(): %v", req)
  214. e.SetLog(req.TrackingID, req.logEntry)
  215. wg := new(sync.WaitGroup)
  216. wg.Add(1)
  217. go func() {
  218. e.execChain(req)
  219. defer wg.Done()
  220. }()
  221. return wg, req.TrackingID
  222. }
  223. func (e *Executor) execChain(req *ExecutionRequest) {
  224. for _, step := range e.chainOfCommand {
  225. if !step(req) {
  226. break
  227. }
  228. }
  229. req.logEntry.ExecutionFinished = true
  230. // This isn't a step, because we want to notify all listeners, irrespective
  231. // of how many steps were actually executed.
  232. notifyListenersFinished(req)
  233. }
  234. func getConcurrentCount(req *ExecutionRequest) int {
  235. concurrentCount := 0
  236. req.executor.logmutex.RLock()
  237. for _, log := range req.executor.GetLogsByActionId(req.Action.ID) {
  238. if !log.ExecutionFinished {
  239. concurrentCount += 1
  240. }
  241. }
  242. req.executor.logmutex.RUnlock()
  243. return concurrentCount
  244. }
  245. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  246. concurrentCount := getConcurrentCount(req)
  247. // Note that the current execution is counted int the logs, so when checking we +1
  248. if concurrentCount >= (req.Action.MaxConcurrent + 1) {
  249. log.WithFields(log.Fields{
  250. "actionTitle": req.logEntry.ActionTitle,
  251. "concurrentCount": concurrentCount,
  252. "maxConcurrent": req.Action.MaxConcurrent,
  253. }).Warnf("Blocked from executing due to concurrency limit")
  254. req.logEntry.Output = "Blocked from executing due to concurrency limit"
  255. req.logEntry.Blocked = true
  256. return false
  257. }
  258. return true
  259. }
  260. func parseDuration(rate config.RateSpec) time.Duration {
  261. duration, err := time.ParseDuration(rate.Duration)
  262. if err != nil {
  263. log.Warnf("Could not parse duration: %v", rate.Duration)
  264. return -1 * time.Minute
  265. }
  266. return duration
  267. }
  268. //gocyclo:ignore
  269. func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
  270. executions := -1 // Because we will find ourself when checking execution logs
  271. duration := parseDuration(rate)
  272. then := time.Now().Add(-duration)
  273. for _, logEntry := range req.executor.GetLogsByActionId(req.Action.ID) {
  274. if logEntry.EntityPrefix != req.EntityPrefix {
  275. continue
  276. }
  277. if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
  278. executions += 1
  279. }
  280. }
  281. return executions
  282. }
  283. func stepRateCheck(req *ExecutionRequest) bool {
  284. for _, rate := range req.Action.MaxRate {
  285. executions := getExecutionsCount(rate, req)
  286. if executions >= rate.Limit {
  287. log.WithFields(log.Fields{
  288. "actionTitle": req.logEntry.ActionTitle,
  289. "executions": executions,
  290. "limit": rate.Limit,
  291. "duration": rate.Duration,
  292. }).Infof("Blocked from executing due to rate limit")
  293. req.logEntry.Output = "Blocked from executing due to rate limit"
  294. req.logEntry.Blocked = true
  295. return false
  296. }
  297. }
  298. return true
  299. }
  300. func stepACLCheck(req *ExecutionRequest) bool {
  301. canExec := acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
  302. if !canExec {
  303. req.logEntry.Output = "ACL check failed. Blocked from executing."
  304. req.logEntry.Blocked = true
  305. log.WithFields(log.Fields{
  306. "actionTitle": req.logEntry.ActionTitle,
  307. }).Warnf("ACL check failed. Blocked from executing.")
  308. }
  309. return canExec
  310. }
  311. func stepParseArgs(req *ExecutionRequest) bool {
  312. var err error
  313. if req.Arguments == nil {
  314. req.Arguments = make(map[string]string)
  315. }
  316. req.Arguments["ot_executionTrackingId"] = req.TrackingID
  317. req.Arguments["ot_username"] = req.AuthenticatedUser.Username
  318. mangleInvalidArgumentValues(req)
  319. if len(req.Action.Exec) > 0 {
  320. req.useDirectExec = true
  321. req.execArgs, err = parseActionExec(req.Arguments, req.Action, req.EntityPrefix)
  322. } else {
  323. req.useDirectExec = false
  324. err = checkShellArgumentSafety(req.Action)
  325. if err != nil {
  326. req.logEntry.Output = err.Error()
  327. log.Warn(err.Error())
  328. return false
  329. }
  330. req.finalParsedCommand, err = parseActionArguments(req.Arguments, req.Action, req.EntityPrefix)
  331. }
  332. if err != nil {
  333. req.logEntry.Output = err.Error()
  334. log.Warn(err.Error())
  335. return false
  336. }
  337. return true
  338. }
  339. func stepRequestAction(req *ExecutionRequest) bool {
  340. // The grpc API always tries to find the action by ID, but it may
  341. if req.Action == nil {
  342. log.WithFields(log.Fields{
  343. "actionTitle": req.ActionTitle,
  344. }).Infof("Action finding by title")
  345. req.Action = req.Cfg.FindAction(req.ActionTitle)
  346. if req.Action == nil {
  347. log.WithFields(log.Fields{
  348. "actionTitle": req.ActionTitle,
  349. }).Warnf("Action requested, but not found")
  350. req.logEntry.Output = "Action not found: " + req.ActionTitle
  351. return false
  352. }
  353. }
  354. metricActionsRequested.Inc()
  355. req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
  356. req.logEntry.ActionIcon = req.Action.Icon
  357. req.logEntry.ActionId = req.Action.ID
  358. req.logEntry.Tags = req.Tags
  359. req.executor.logmutex.Lock()
  360. if _, containsKey := req.executor.LogsByActionId[req.Action.ID]; !containsKey {
  361. req.executor.LogsByActionId[req.Action.ID] = make([]*InternalLogEntry, 0)
  362. }
  363. req.executor.LogsByActionId[req.Action.ID] = append(req.executor.LogsByActionId[req.Action.ID], req.logEntry)
  364. req.executor.logmutex.Unlock()
  365. log.WithFields(log.Fields{
  366. "actionTitle": req.logEntry.ActionTitle,
  367. "tags": req.Tags,
  368. }).Infof("Action requested")
  369. notifyListenersStarted(req)
  370. return true
  371. }
  372. func stepLogStart(req *ExecutionRequest) bool {
  373. log.WithFields(log.Fields{
  374. "actionTitle": req.logEntry.ActionTitle,
  375. "timeout": req.Action.Timeout,
  376. }).Infof("Action started")
  377. return true
  378. }
  379. func stepLogFinish(req *ExecutionRequest) bool {
  380. req.logEntry.ExecutionFinished = true
  381. log.WithFields(log.Fields{
  382. "actionTitle": req.logEntry.ActionTitle,
  383. "outputLength": len(req.logEntry.Output),
  384. "timedOut": req.logEntry.TimedOut,
  385. "exit": req.logEntry.ExitCode,
  386. }).Infof("Action finished")
  387. return true
  388. }
  389. func notifyListenersFinished(req *ExecutionRequest) {
  390. for _, listener := range req.executor.listeners {
  391. listener.OnExecutionFinished(req.logEntry, req.Action)
  392. }
  393. }
  394. func notifyListenersStarted(req *ExecutionRequest) {
  395. for _, listener := range req.executor.listeners {
  396. listener.OnExecutionStarted(req.logEntry, req.Action)
  397. }
  398. }
  399. func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
  400. if err != nil {
  401. logEntry.Output = err.Error() + "\n\n" + logEntry.Output
  402. }
  403. }
  404. type OutputStreamer struct {
  405. Req *ExecutionRequest
  406. output bytes.Buffer
  407. }
  408. func (ost *OutputStreamer) Write(o []byte) (n int, err error) {
  409. for _, listener := range ost.Req.executor.listeners {
  410. listener.OnOutputChunk(o, ost.Req.TrackingID, ost.Req.logEntry, ost.Req.Action)
  411. }
  412. return ost.output.Write(o)
  413. }
  414. func (ost *OutputStreamer) String() string {
  415. return ost.output.String()
  416. }
  417. func buildEnv(args map[string]string) []string {
  418. ret := append(os.Environ(), "OLIVETIN=1")
  419. for k, v := range args {
  420. varName := fmt.Sprintf("%v", strings.TrimSpace(strings.ToUpper(k)))
  421. // Skip arguments that might not have a name (eg, confirmation), as this causes weird bugs on Windows.
  422. if varName == "" {
  423. continue
  424. }
  425. ret = append(ret, fmt.Sprintf("%v=%v", varName, v))
  426. }
  427. return ret
  428. }
  429. func stepExec(req *ExecutionRequest) bool {
  430. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  431. defer cancel()
  432. streamer := &OutputStreamer{Req: req}
  433. var cmd *exec.Cmd
  434. if req.useDirectExec {
  435. cmd = wrapCommandDirect(ctx, req.execArgs)
  436. } else {
  437. cmd = wrapCommandInShell(ctx, req.finalParsedCommand)
  438. }
  439. cmd.Stdout = streamer
  440. cmd.Stderr = streamer
  441. cmd.Env = buildEnv(req.Arguments)
  442. req.logEntry.ExecutionStarted = true
  443. runerr := cmd.Start()
  444. req.logEntry.Process = cmd.Process
  445. waiterr := cmd.Wait()
  446. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  447. req.logEntry.Output = streamer.String()
  448. appendErrorToStderr(runerr, req.logEntry)
  449. appendErrorToStderr(waiterr, req.logEntry)
  450. if ctx.Err() == context.DeadlineExceeded {
  451. log.WithFields(log.Fields{
  452. "actionTitle": req.logEntry.ActionTitle,
  453. }).Warnf("Action timed out")
  454. // The context timeout should kill the process, but let's make sure.
  455. err := req.executor.Kill(req.logEntry)
  456. if err != nil {
  457. log.WithFields(log.Fields{
  458. "actionTitle": req.logEntry.ActionTitle,
  459. }).Warnf("could not kill process: %v", err)
  460. }
  461. req.logEntry.TimedOut = true
  462. req.logEntry.Output += "OliveTin::timeout - this action timed out after " + fmt.Sprintf("%v", req.Action.Timeout) + " seconds. If you need more time for this action, set a longer timeout. See https://docs.olivetin.app/timeout.html for more help."
  463. }
  464. req.logEntry.DatetimeFinished = time.Now()
  465. return true
  466. }
  467. func stepExecAfter(req *ExecutionRequest) bool {
  468. if req.Action.ShellAfterCompleted == "" {
  469. return true
  470. }
  471. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  472. defer cancel()
  473. var stdout bytes.Buffer
  474. var stderr bytes.Buffer
  475. args := map[string]string{
  476. "output": req.logEntry.Output,
  477. "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
  478. "ot_executionTrackingId": req.TrackingID,
  479. "ot_username": req.AuthenticatedUser.Username,
  480. "actionTitle": req.logEntry.ActionTitle,
  481. }
  482. finalParsedCommand, err := parseCommandForReplacements(req.Action.ShellAfterCompleted, args)
  483. if err != nil {
  484. msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
  485. req.logEntry.Output += msg
  486. log.Warn(msg)
  487. return true
  488. }
  489. cmd := wrapCommandInShell(ctx, finalParsedCommand)
  490. cmd.Stdout = &stdout
  491. cmd.Stderr = &stderr
  492. cmd.Env = buildEnv(args)
  493. runerr := cmd.Start()
  494. waiterr := cmd.Wait()
  495. req.logEntry.Output += "\n"
  496. req.logEntry.Output += "OliveTin::shellAfterCompleted stdout\n"
  497. req.logEntry.Output += stdout.String()
  498. req.logEntry.Output += "OliveTin::shellAfterCompleted stderr\n"
  499. req.logEntry.Output += stderr.String()
  500. req.logEntry.Output += "OliveTin::shellAfterCompleted errors and summary\n"
  501. appendErrorToStderr(runerr, req.logEntry)
  502. appendErrorToStderr(waiterr, req.logEntry)
  503. if ctx.Err() == context.DeadlineExceeded {
  504. req.logEntry.Output += "Your shellAfterCompleted command timed out."
  505. }
  506. req.logEntry.Output += fmt.Sprintf("Your shellAfterCompleted exited with code %v\n", cmd.ProcessState.ExitCode())
  507. req.logEntry.Output += "OliveTin::shellAfterCompleted output complete\n"
  508. return true
  509. }
  510. func stepTrigger(req *ExecutionRequest) bool {
  511. if req.Action.Triggers == nil {
  512. return true
  513. }
  514. if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
  515. log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
  516. }
  517. triggerLoop(req)
  518. return true
  519. }
  520. func triggerLoop(req *ExecutionRequest) {
  521. for _, triggerReq := range req.Action.Triggers {
  522. trigger := &ExecutionRequest{
  523. ActionTitle: triggerReq,
  524. TrackingID: uuid.NewString(),
  525. Tags: []string{"trigger"},
  526. AuthenticatedUser: req.AuthenticatedUser,
  527. Arguments: req.Arguments,
  528. Cfg: req.Cfg,
  529. }
  530. req.executor.ExecRequest(trigger)
  531. }
  532. }
  533. func stepSaveLog(req *ExecutionRequest) bool {
  534. filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
  535. saveLogResults(req, filename)
  536. saveLogOutput(req, filename)
  537. return true
  538. }
  539. func firstNonEmpty(one, two string) string {
  540. if one != "" {
  541. return one
  542. }
  543. return two
  544. }
  545. func saveLogResults(req *ExecutionRequest, filename string) {
  546. dir := firstNonEmpty(req.Action.SaveLogs.ResultsDirectory, req.Cfg.SaveLogs.ResultsDirectory)
  547. if dir != "" {
  548. data, err := yaml.Marshal(req.logEntry)
  549. if err != nil {
  550. log.Warnf("%v", err)
  551. }
  552. filepath := path.Join(dir, filename+".yaml")
  553. err = os.WriteFile(filepath, data, 0644)
  554. if err != nil {
  555. log.Warnf("%v", err)
  556. }
  557. }
  558. }
  559. func saveLogOutput(req *ExecutionRequest, filename string) {
  560. dir := firstNonEmpty(req.Action.SaveLogs.OutputDirectory, req.Cfg.SaveLogs.OutputDirectory)
  561. if dir != "" {
  562. data := req.logEntry.Output
  563. filepath := path.Join(dir, filename+".log")
  564. err := os.WriteFile(filepath, []byte(data), 0644)
  565. if err != nil {
  566. log.Warnf("%v", err)
  567. }
  568. }
  569. }