executor.go 18 KB

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