executor.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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. "io"
  15. "io/ioutil"
  16. "os"
  17. "os/exec"
  18. "path"
  19. "runtime"
  20. "strings"
  21. "sync"
  22. "time"
  23. )
  24. var (
  25. metricActionsRequested = promauto.NewGauge(prometheus.GaugeOpts{
  26. Name: "olivetin_actions_requested_count",
  27. Help: "The actions requested count",
  28. })
  29. )
  30. // Executor represents a helper class for executing commands. It's main method
  31. // is ExecRequest
  32. type Executor struct {
  33. Logs map[string]*InternalLogEntry
  34. LogsByActionId map[string][]*InternalLogEntry
  35. listeners []listener
  36. chainOfCommand []executorStepFunc
  37. }
  38. // ExecutionRequest is a request to execute an action. It's passed to an
  39. // Executor. They're created from the grpcapi.
  40. type ExecutionRequest struct {
  41. ActionTitle string
  42. Action *config.Action
  43. Arguments map[string]string
  44. TrackingID string
  45. Tags []string
  46. Cfg *config.Config
  47. AuthenticatedUser *acl.AuthenticatedUser
  48. EntityPrefix string
  49. logEntry *InternalLogEntry
  50. finalParsedCommand string
  51. executor *Executor
  52. }
  53. // InternalLogEntry objects are created by an Executor, and represent the final
  54. // state of execution (even if the command is not executed). It's designed to be
  55. // easily serializable.
  56. type InternalLogEntry struct {
  57. DatetimeStarted time.Time
  58. DatetimeFinished time.Time
  59. Stdout string
  60. Stderr string
  61. StdoutBuffer io.ReadCloser
  62. StderrBuffer io.ReadCloser
  63. TimedOut bool
  64. Blocked bool
  65. ExitCode int32
  66. Tags []string
  67. ExecutionStarted bool
  68. ExecutionFinished bool
  69. ExecutionTrackingID string
  70. Process *os.Process
  71. /*
  72. The following 3 properties are obviously on Action normally, but it's useful
  73. that logs are lightweight (so we don't need to have an action associated to
  74. logs, etc. Therefore, we duplicate those values here.
  75. */
  76. ActionTitle string
  77. ActionIcon string
  78. ActionId string
  79. }
  80. type executorStepFunc func(*ExecutionRequest) bool
  81. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  82. // executing actions.
  83. func DefaultExecutor() *Executor {
  84. e := Executor{}
  85. e.Logs = make(map[string]*InternalLogEntry)
  86. e.LogsByActionId = make(map[string][]*InternalLogEntry)
  87. e.chainOfCommand = []executorStepFunc{
  88. stepRequestAction,
  89. stepConcurrencyCheck,
  90. stepRateCheck,
  91. stepACLCheck,
  92. stepParseArgs,
  93. stepLogStart,
  94. stepExec,
  95. stepExecAfter,
  96. stepLogFinish,
  97. stepSaveLog,
  98. stepTrigger,
  99. }
  100. return &e
  101. }
  102. type listener interface {
  103. OnExecutionStarted(actionTitle string)
  104. OnExecutionFinished(logEntry *InternalLogEntry)
  105. }
  106. func (e *Executor) AddListener(m listener) {
  107. e.listeners = append(e.listeners, m)
  108. }
  109. // ExecRequest processes an ExecutionRequest
  110. func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
  111. req.executor = e
  112. // req.UUID is now set by the client, so that they can track the request
  113. // from start to finish. This means that a malicious client could send
  114. // duplicate UUIDs (or just random strings), but this is the only way.
  115. req.logEntry = &InternalLogEntry{
  116. DatetimeStarted: time.Now(),
  117. ExecutionTrackingID: req.TrackingID,
  118. Stdout: "",
  119. Stderr: "",
  120. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  121. ExecutionStarted: false,
  122. ExecutionFinished: false,
  123. ActionId: "",
  124. ActionTitle: "notfound",
  125. ActionIcon: "💩",
  126. }
  127. _, foundLog := e.Logs[req.TrackingID]
  128. if foundLog || req.TrackingID == "" {
  129. req.TrackingID = uuid.NewString()
  130. }
  131. e.Logs[req.TrackingID] = req.logEntry
  132. wg := new(sync.WaitGroup)
  133. wg.Add(1)
  134. go func() {
  135. e.execChain(req)
  136. defer wg.Done()
  137. }()
  138. return wg, req.TrackingID
  139. }
  140. func (e *Executor) execChain(req *ExecutionRequest) {
  141. for _, step := range e.chainOfCommand {
  142. if !step(req) {
  143. break
  144. }
  145. }
  146. req.logEntry.ExecutionFinished = true
  147. // This isn't a step, because we want to notify all listeners, irrespective
  148. // of how many steps were actually executed.
  149. notifyListeners(req)
  150. }
  151. func getConcurrentCount(req *ExecutionRequest) int {
  152. concurrentCount := 0
  153. for _, log := range req.executor.LogsByActionId[req.Action.ID] {
  154. if !log.ExecutionFinished {
  155. concurrentCount += 1
  156. }
  157. }
  158. return concurrentCount
  159. }
  160. func stepConcurrencyCheck(req *ExecutionRequest) bool {
  161. concurrentCount := getConcurrentCount(req)
  162. // Note that the current execution is counted int the logs, so when checking we +1
  163. if concurrentCount >= (req.Action.MaxConcurrent + 1) {
  164. msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.Action.MaxConcurrent)
  165. log.WithFields(log.Fields{
  166. "actionTitle": req.logEntry.ActionTitle,
  167. }).Warnf(msg)
  168. req.logEntry.Stdout = msg
  169. req.logEntry.Blocked = true
  170. return false
  171. }
  172. return true
  173. }
  174. func parseDuration(rate config.RateSpec) time.Duration {
  175. duration, err := time.ParseDuration(rate.Duration)
  176. if err != nil {
  177. log.Warnf("Could not parse duration: %v", rate.Duration)
  178. return -1 * time.Minute
  179. }
  180. return duration
  181. }
  182. func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
  183. executions := -1 // Because we will find ourself when checking execution logs
  184. duration := parseDuration(rate)
  185. then := time.Now().Add(-duration)
  186. for _, logEntry := range req.executor.LogsByActionId[req.Action.ID] {
  187. if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
  188. executions += 1
  189. }
  190. }
  191. return executions
  192. }
  193. func stepRateCheck(req *ExecutionRequest) bool {
  194. for _, rate := range req.Action.MaxRate {
  195. executions := getExecutionsCount(rate, req)
  196. if executions >= rate.Limit {
  197. msg := fmt.Sprintf("Blocked from executing. This action has run %d out of %d allowed times in the last %s.", executions, rate.Limit, rate.Duration)
  198. log.WithFields(log.Fields{
  199. "actionTitle": req.logEntry.ActionTitle,
  200. }).Infof(msg)
  201. req.logEntry.Stdout = msg
  202. req.logEntry.Blocked = true
  203. return false
  204. }
  205. }
  206. return true
  207. }
  208. func stepACLCheck(req *ExecutionRequest) bool {
  209. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
  210. }
  211. func stepParseArgs(req *ExecutionRequest) bool {
  212. var err error
  213. req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
  214. if err != nil {
  215. req.logEntry.Stdout = err.Error()
  216. log.Warnf(err.Error())
  217. return false
  218. }
  219. return true
  220. }
  221. func stepRequestAction(req *ExecutionRequest) bool {
  222. // The grpc API always tries to find the action by ID, but it may
  223. if req.Action == nil {
  224. log.WithFields(log.Fields{
  225. "actionTitle": req.ActionTitle,
  226. }).Infof("Action finding by title")
  227. req.Action = req.Cfg.FindAction(req.ActionTitle)
  228. if req.Action == nil {
  229. log.WithFields(log.Fields{
  230. "actionTitle": req.ActionTitle,
  231. }).Warnf("Action requested, but not found")
  232. req.logEntry.Stderr = "Action not found: " + req.ActionTitle
  233. return false
  234. }
  235. }
  236. metricActionsRequested.Inc()
  237. req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
  238. req.logEntry.ActionIcon = req.Action.Icon
  239. req.logEntry.ActionId = req.Action.ID
  240. if _, containsKey := req.executor.LogsByActionId[req.Action.ID]; !containsKey {
  241. req.executor.LogsByActionId[req.Action.ID] = make([]*InternalLogEntry, 0)
  242. }
  243. req.executor.LogsByActionId[req.Action.ID] = append(req.executor.LogsByActionId[req.Action.ID], req.logEntry)
  244. log.WithFields(log.Fields{
  245. "actionTitle": req.logEntry.ActionTitle,
  246. "tags": req.Tags,
  247. }).Infof("Action requested")
  248. return true
  249. }
  250. func stepLogStart(req *ExecutionRequest) bool {
  251. log.WithFields(log.Fields{
  252. "actionTitle": req.logEntry.ActionTitle,
  253. "timeout": req.Action.Timeout,
  254. }).Infof("Action starting")
  255. return true
  256. }
  257. func stepLogFinish(req *ExecutionRequest) bool {
  258. req.logEntry.ExecutionFinished = true
  259. log.WithFields(log.Fields{
  260. "actionTitle": req.logEntry.ActionTitle,
  261. "stdout": req.logEntry.Stdout,
  262. "stderr": req.logEntry.Stderr,
  263. "timedOut": req.logEntry.TimedOut,
  264. "exit": req.logEntry.ExitCode,
  265. }).Infof("Action finished")
  266. return true
  267. }
  268. func notifyListeners(req *ExecutionRequest) {
  269. for _, listener := range req.executor.listeners {
  270. listener.OnExecutionFinished(req.logEntry)
  271. }
  272. }
  273. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  274. if runtime.GOOS == "windows" {
  275. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  276. }
  277. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  278. }
  279. func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
  280. if err != nil {
  281. logEntry.Stderr = err.Error() + "\n\n" + logEntry.Stderr
  282. }
  283. }
  284. func buildEnv(req *ExecutionRequest) []string {
  285. ret := append(os.Environ(), "OLIVETIN=1")
  286. for k, v := range req.Arguments {
  287. ret = append(ret, fmt.Sprintf("%v=%v", strings.ToUpper(k), v))
  288. }
  289. return ret
  290. }
  291. func stepExec(req *ExecutionRequest) bool {
  292. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  293. defer cancel()
  294. var stdout bytes.Buffer
  295. var stderr bytes.Buffer
  296. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  297. cmd.Env = buildEnv(req)
  298. cmd.Stdout = &stdout
  299. cmd.Stderr = &stderr
  300. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  301. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  302. req.logEntry.ExecutionStarted = true
  303. runerr := cmd.Start()
  304. req.logEntry.Process = cmd.Process
  305. waiterr := cmd.Wait()
  306. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  307. req.logEntry.Stdout = stdout.String()
  308. req.logEntry.Stderr = stderr.String()
  309. appendErrorToStderr(runerr, req.logEntry)
  310. appendErrorToStderr(waiterr, req.logEntry)
  311. if ctx.Err() == context.DeadlineExceeded {
  312. req.logEntry.TimedOut = true
  313. }
  314. req.logEntry.Tags = req.Tags
  315. req.logEntry.DatetimeFinished = time.Now()
  316. return true
  317. }
  318. func stepExecAfter(req *ExecutionRequest) bool {
  319. if req.Action.ShellAfterCompleted == "" {
  320. return true
  321. }
  322. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
  323. defer cancel()
  324. var stdout bytes.Buffer
  325. var stderr bytes.Buffer
  326. args := map[string]string{
  327. "stdout": req.logEntry.Stdout,
  328. "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
  329. }
  330. finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
  331. cmd := wrapCommandInShell(ctx, finalParsedCommand)
  332. cmd.Stdout = &stdout
  333. cmd.Stderr = &stderr
  334. runerr := cmd.Start()
  335. waiterr := cmd.Wait()
  336. req.logEntry.Stdout += "---\n" + stdout.String()
  337. req.logEntry.Stderr += "---\n" + stderr.String()
  338. appendErrorToStderr(runerr, req.logEntry)
  339. appendErrorToStderr(waiterr, req.logEntry)
  340. if ctx.Err() == context.DeadlineExceeded {
  341. req.logEntry.Stderr += "Your shellAfterCommand command timed out."
  342. }
  343. req.logEntry.Stdout += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
  344. return true
  345. }
  346. func stepTrigger(req *ExecutionRequest) bool {
  347. if req.Action.Trigger != "" {
  348. trigger := &ExecutionRequest{
  349. ActionTitle: req.Action.Trigger,
  350. TrackingID: uuid.NewString(),
  351. Tags: []string{"trigger"},
  352. AuthenticatedUser: req.AuthenticatedUser,
  353. Cfg: req.Cfg,
  354. }
  355. req.executor.ExecRequest(trigger)
  356. }
  357. return true
  358. }
  359. func stepSaveLog(req *ExecutionRequest) bool {
  360. filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
  361. saveLogResults(req, filename)
  362. saveLogOutput(req, filename)
  363. return true
  364. }
  365. func firstNonEmpty(one, two string) string {
  366. if one != "" {
  367. return one
  368. }
  369. return two
  370. }
  371. func saveLogResults(req *ExecutionRequest, filename string) {
  372. dir := firstNonEmpty(req.Action.SaveLogs.ResultsDirectory, req.Cfg.SaveLogs.ResultsDirectory)
  373. if dir != "" {
  374. data, err := yaml.Marshal(req.logEntry)
  375. if err != nil {
  376. log.Warnf("%v", err)
  377. }
  378. filepath := path.Join(dir, filename+".yaml")
  379. err = ioutil.WriteFile(filepath, data, 0644)
  380. if err != nil {
  381. log.Warnf("%v", err)
  382. }
  383. }
  384. }
  385. func saveLogOutput(req *ExecutionRequest, filename string) {
  386. dir := firstNonEmpty(req.Action.SaveLogs.OutputDirectory, req.Cfg.SaveLogs.OutputDirectory)
  387. if dir != "" {
  388. data := req.logEntry.Stdout + "\n" + req.logEntry.Stderr
  389. filepath := path.Join(dir, filename+".log")
  390. err := ioutil.WriteFile(filepath, []byte(data), 0644)
  391. if err != nil {
  392. log.Warnf("%v", err)
  393. }
  394. }
  395. }