/pkg/executors/executor.go

https://github.com/lunarway/shuttle · Go · 192 lines · 161 code · 23 blank · 8 comment · 25 complexity · 8ea1eeeae3f59826604e113dce95a072 MD5 · raw file

  1. package executors
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "github.com/lunarway/shuttle/pkg/config"
  8. "github.com/lunarway/shuttle/pkg/errors"
  9. )
  10. // ScriptExecutionContext gives context to the execution of a plan script
  11. type ScriptExecutionContext struct {
  12. ScriptName string
  13. Script config.ShuttlePlanScript
  14. Project config.ShuttleProjectContext
  15. Args map[string]string
  16. }
  17. // ActionExecutionContext gives context to the execution of Actions in a script
  18. type ActionExecutionContext struct {
  19. ScriptContext ScriptExecutionContext
  20. Action config.ShuttleAction
  21. ActionIndex int
  22. }
  23. // Execute is the command executor for the plan files
  24. func Execute(ctx context.Context, p config.ShuttleProjectContext, command string, args []string, validateArgs bool) error {
  25. script, ok := p.Scripts[command]
  26. if !ok {
  27. return errors.NewExitCode(2, "Script '%s' not found", command)
  28. }
  29. namedArgs, err := validateArguments(p, command, script.Args, args, validateArgs)
  30. if err != nil {
  31. return err
  32. }
  33. scriptContext := ScriptExecutionContext{
  34. ScriptName: command,
  35. Script: script,
  36. Project: p,
  37. Args: namedArgs,
  38. }
  39. for actionIndex, action := range script.Actions {
  40. actionContext := ActionExecutionContext{
  41. ScriptContext: scriptContext,
  42. Action: action,
  43. ActionIndex: actionIndex,
  44. }
  45. err := executeAction(ctx, actionContext)
  46. if err != nil {
  47. return err
  48. }
  49. }
  50. return nil
  51. }
  52. // validateArguments parses and validates args against available arguments in
  53. // scriptArgs.
  54. //
  55. // All detectable constraints are checked before reporting to the UI.
  56. func validateArguments(p config.ShuttleProjectContext, command string, scriptArgs []config.ShuttleScriptArgs, args []string, validateArgs bool) (map[string]string, error) {
  57. var validationErrors []validationError
  58. namedArgs, parsingErrors := validateArgFormat(args)
  59. validationErrors = append(validationErrors, parsingErrors...)
  60. if validateArgs {
  61. validationErrors = append(validationErrors, validateRequiredArgs(scriptArgs, namedArgs)...)
  62. validationErrors = append(validationErrors, validateUnknownArgs(scriptArgs, namedArgs)...)
  63. }
  64. if len(validationErrors) != 0 {
  65. sortValidationErrors(validationErrors)
  66. var s strings.Builder
  67. s.WriteString("Arguments not valid:\n")
  68. for _, e := range validationErrors {
  69. fmt.Fprintf(&s, " %s\n", e)
  70. }
  71. fmt.Fprintf(&s, "\n%s", expectedArgumentsHelp(command, scriptArgs))
  72. return nil, errors.NewExitCode(2, s.String())
  73. }
  74. return namedArgs, nil
  75. }
  76. type validationError struct {
  77. arg string
  78. err string
  79. }
  80. func (v validationError) String() string {
  81. return fmt.Sprintf("'%s' %s", v.arg, v.err)
  82. }
  83. func validateArgFormat(args []string) (map[string]string, []validationError) {
  84. var validationErrors []validationError
  85. namedArgs := map[string]string{}
  86. for _, arg := range args {
  87. parts := strings.SplitN(arg, "=", 2)
  88. if len(parts) < 2 {
  89. validationErrors = append(validationErrors, validationError{
  90. arg: arg,
  91. err: "not <argument>=<value>",
  92. })
  93. continue
  94. }
  95. namedArgs[parts[0]] = parts[1]
  96. }
  97. return namedArgs, validationErrors
  98. }
  99. func validateRequiredArgs(scriptArgs []config.ShuttleScriptArgs, args map[string]string) []validationError {
  100. var validationErrors []validationError
  101. for _, argSpec := range scriptArgs {
  102. if _, ok := args[argSpec.Name]; argSpec.Required && !ok {
  103. validationErrors = append(validationErrors, validationError{
  104. arg: argSpec.Name,
  105. err: "not supplied but is required",
  106. })
  107. }
  108. }
  109. return validationErrors
  110. }
  111. func validateUnknownArgs(scriptArgs []config.ShuttleScriptArgs, args map[string]string) []validationError {
  112. var validationErrors []validationError
  113. for namedArg := range args {
  114. found := false
  115. for _, arg := range scriptArgs {
  116. if arg.Name == namedArg {
  117. found = true
  118. break
  119. }
  120. }
  121. if !found {
  122. validationErrors = append(validationErrors, validationError{
  123. arg: namedArg,
  124. err: "unknown",
  125. })
  126. }
  127. }
  128. return validationErrors
  129. }
  130. func sortValidationErrors(errs []validationError) {
  131. sort.Slice(errs, func(i, j int) bool {
  132. return errs[i].arg < errs[j].arg
  133. })
  134. }
  135. func expectedArgumentsHelp(command string, args []config.ShuttleScriptArgs) string {
  136. var s strings.Builder
  137. fmt.Fprintf(&s, "Script '%s' accepts ", command)
  138. if len(args) == 0 {
  139. s.WriteString("no arguments.")
  140. return s.String()
  141. }
  142. s.WriteString("the following arguments:")
  143. for _, a := range args {
  144. fmt.Fprintf(&s, "\n %s", a)
  145. }
  146. return s.String()
  147. }
  148. func executeAction(ctx context.Context, context ActionExecutionContext) error {
  149. for _, executor := range executors {
  150. if executor.match(context.Action) {
  151. return executor.execute(ctx, context)
  152. }
  153. }
  154. panic(fmt.Sprintf("Could not find an executor for %v.actions[%v]!", context.ScriptContext.ScriptName, context.ActionIndex))
  155. }
  156. type actionMatchFunc = func(config.ShuttleAction) bool
  157. type actionExecutionFunc = func(context.Context, ActionExecutionContext) error
  158. type actionExecutor struct {
  159. match actionMatchFunc
  160. execute actionExecutionFunc
  161. }
  162. var executors = []actionExecutor{}
  163. // AddExecutor taps a new executor into the script execution pipeline
  164. func addExecutor(match actionMatchFunc, execute actionExecutionFunc) {
  165. executors = append(executors, actionExecutor{
  166. match: match,
  167. execute: execute,
  168. })
  169. }