executor.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package executor
  2. import (
  3. pb "github.com/OliveTin/OliveTin/gen/grpc"
  4. acl "github.com/OliveTin/OliveTin/internal/acl"
  5. config "github.com/OliveTin/OliveTin/internal/config"
  6. log "github.com/sirupsen/logrus"
  7. "bytes"
  8. "context"
  9. "io"
  10. "os/exec"
  11. "runtime"
  12. "time"
  13. )
  14. // Executor represents a helper class for executing commands. It's main method
  15. // is ExecRequest
  16. type Executor struct {
  17. Logs map[string]*InternalLogEntry
  18. listeners []listener
  19. chainOfCommand []executorStepFunc
  20. }
  21. // ExecutionRequest is a request to execute an action. It's passed to an
  22. // Executor. They're created from the grpcapi.
  23. type ExecutionRequest struct {
  24. ActionName string
  25. Arguments map[string]string
  26. UUID string
  27. Tags []string
  28. action *config.Action
  29. Cfg *config.Config
  30. AuthenticatedUser *acl.AuthenticatedUser
  31. logEntry *InternalLogEntry
  32. finalParsedCommand string
  33. executor *Executor
  34. }
  35. // InternalLogEntry objects are created by an Executor, and represent the final
  36. // state of execution (even if the command is not executed). It's designed to be
  37. // easily serializable.
  38. type InternalLogEntry struct {
  39. DatetimeStarted string
  40. DatetimeFinished string
  41. Stdout string
  42. Stderr string
  43. StdoutBuffer io.ReadCloser
  44. StderrBuffer io.ReadCloser
  45. TimedOut bool
  46. ExitCode int32
  47. Tags []string
  48. ExecutionStarted bool
  49. ExecutionCompleted bool
  50. /*
  51. The following 3 properties are obviously on Action normally, but it's useful
  52. that logs are lightweight (so we don't need to have an action associated to
  53. logs, etc. Therefore, we duplicate those values here.
  54. */
  55. ActionTitle string
  56. ActionIcon string
  57. UUID string
  58. }
  59. type executorStepFunc func(*ExecutionRequest) bool
  60. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  61. // executing actions.
  62. func DefaultExecutor() *Executor {
  63. e := Executor{}
  64. e.Logs = make(map[string]*InternalLogEntry)
  65. e.chainOfCommand = []executorStepFunc{
  66. stepLogRequested,
  67. stepFindAction,
  68. stepACLCheck,
  69. stepParseArgs,
  70. stepLogStart,
  71. stepExec,
  72. stepNotifyListeners,
  73. stepLogFinish,
  74. }
  75. return &e
  76. }
  77. type listener interface {
  78. OnExecutionStarted(actionName string)
  79. OnExecutionFinished(logEntry *InternalLogEntry)
  80. }
  81. func (e *Executor) AddListener(m listener) {
  82. e.listeners = append(e.listeners, m)
  83. }
  84. // ExecRequest processes an ExecutionRequest
  85. func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
  86. // req.UUID is now set by the client, so that they can track the request
  87. // from start to finish. This means that a malicious client could send
  88. // duplicate UUIDs (or just random strings), but this is the only way.
  89. req.executor = e
  90. req.logEntry = &InternalLogEntry{
  91. DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
  92. ActionTitle: req.ActionName,
  93. UUID: req.UUID,
  94. Stdout: "",
  95. Stderr: "",
  96. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  97. ExecutionStarted: false,
  98. ExecutionCompleted: false,
  99. }
  100. e.Logs[req.UUID] = req.logEntry
  101. for _, listener := range e.listeners {
  102. listener.OnExecutionStarted(req.ActionName)
  103. }
  104. go e.execChain(req)
  105. return &pb.StartActionResponse{
  106. ExecutionUuid: req.UUID,
  107. }
  108. }
  109. func (e *Executor) execChain(req *ExecutionRequest) {
  110. for _, step := range e.chainOfCommand {
  111. if !step(req) {
  112. break
  113. }
  114. }
  115. }
  116. func stepFindAction(req *ExecutionRequest) bool {
  117. actualAction := req.Cfg.FindAction(req.ActionName)
  118. if actualAction == nil {
  119. log.WithFields(log.Fields{
  120. "actionName": req.ActionName,
  121. }).Warnf("Action not found")
  122. req.logEntry.Stderr = "Action not found"
  123. return false
  124. }
  125. req.action = actualAction
  126. req.logEntry.ActionIcon = actualAction.Icon
  127. return true
  128. }
  129. func stepACLCheck(req *ExecutionRequest) bool {
  130. return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
  131. }
  132. func stepParseArgs(req *ExecutionRequest) bool {
  133. var err error
  134. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  135. if err != nil {
  136. req.logEntry.Stdout = err.Error()
  137. log.Warnf(err.Error())
  138. return false
  139. }
  140. return true
  141. }
  142. func stepLogRequested(req *ExecutionRequest) bool {
  143. log.WithFields(log.Fields{
  144. "actionTitle": req.ActionName,
  145. }).Infof("Action requested")
  146. return true
  147. }
  148. func stepLogStart(req *ExecutionRequest) bool {
  149. log.WithFields(log.Fields{
  150. "actionTitle": req.action.Title,
  151. "timeout": req.action.Timeout,
  152. }).Infof("Action starting")
  153. return true
  154. }
  155. func stepLogFinish(req *ExecutionRequest) bool {
  156. log.WithFields(log.Fields{
  157. "actionTitle": req.action.Title,
  158. "stdout": req.logEntry.Stdout,
  159. "stderr": req.logEntry.Stderr,
  160. "timedOut": req.logEntry.TimedOut,
  161. "exit": req.logEntry.ExitCode,
  162. }).Infof("Action finished")
  163. return true
  164. }
  165. func stepNotifyListeners(req *ExecutionRequest) bool {
  166. for _, listener := range req.executor.listeners {
  167. listener.OnExecutionFinished(req.logEntry)
  168. }
  169. return true
  170. }
  171. func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
  172. if runtime.GOOS == "windows" {
  173. return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
  174. }
  175. return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
  176. }
  177. func stepExec(req *ExecutionRequest) bool {
  178. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  179. defer cancel()
  180. var stdout bytes.Buffer
  181. var stderr bytes.Buffer
  182. cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
  183. cmd.Stdout = &stdout
  184. cmd.Stderr = &stderr
  185. req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
  186. req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
  187. req.logEntry.ExecutionStarted = true
  188. runerr := cmd.Start()
  189. cmd.Wait()
  190. // req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
  191. // req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
  192. req.logEntry.ExecutionCompleted = true
  193. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  194. req.logEntry.Stdout = stdout.String()
  195. req.logEntry.Stderr = stderr.String()
  196. if runerr != nil {
  197. req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
  198. }
  199. if ctx.Err() == context.DeadlineExceeded {
  200. req.logEntry.TimedOut = true
  201. }
  202. req.logEntry.Tags = req.Tags
  203. req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
  204. return true
  205. }