templates.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. package tpl
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "regexp"
  6. "strings"
  7. "text/template"
  8. "github.com/OliveTin/OliveTin/internal/entities"
  9. "github.com/OliveTin/OliveTin/internal/env"
  10. "github.com/OliveTin/OliveTin/internal/installationinfo"
  11. log "github.com/sirupsen/logrus"
  12. )
  13. func jsonFunc(v any) (string, error) {
  14. if v == nil {
  15. return "null", nil
  16. }
  17. data, err := json.Marshal(v)
  18. if err != nil {
  19. return "", err
  20. }
  21. return string(data), nil
  22. }
  23. // Root template (funcs/options). parseTemplate clones before Parse — text/template
  24. // must not receive concurrent Parse calls on the same instance.
  25. var tpl = template.New("tpl").
  26. Option("missingkey=error").
  27. Funcs(template.FuncMap{"Json": jsonFunc})
  28. type olivetinInfo struct {
  29. Build *installationinfo.BuildInfo
  30. Runtime *installationinfo.RuntimeInfo
  31. }
  32. var legacyArgumentRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\s*}}`)
  33. var legacyEntityPropertiesRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+)\s*}}`)
  34. type generalTemplateContext struct {
  35. OliveTin olivetinInfo
  36. Env map[string]string
  37. }
  38. type actionTemplateContext struct {
  39. CurrentEntity interface{}
  40. Arguments map[string]string
  41. // These are deliberately repeated because embedding structs
  42. // won't work in text/template.
  43. OliveTin olivetinInfo
  44. Env map[string]string
  45. }
  46. var (
  47. cachedOliveTinInfo olivetinInfo
  48. cachedEnvMap map[string]string
  49. )
  50. func init() {
  51. cachedOliveTinInfo = olivetinInfo{
  52. Build: installationinfo.Build,
  53. Runtime: installationinfo.Runtime,
  54. }
  55. cachedEnvMap = env.BuildEnvMap()
  56. }
  57. func GetNewGeneralTemplateContext() *generalTemplateContext {
  58. return &generalTemplateContext{
  59. OliveTin: cachedOliveTinInfo,
  60. Env: cachedEnvMap,
  61. }
  62. }
  63. func migrateLegacyEntityProperties(rawShellCommand string) string {
  64. foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)
  65. for _, match := range foundArgumentNames {
  66. entityName := match[1]
  67. argName := match[2]
  68. fullMatch := match[0] // The entire matched string like "{{ server.hostname }}"
  69. if strings.Contains(argName, ".") {
  70. replacement := "{{ .CurrentEntity." + argName + " }}"
  71. rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
  72. log.WithFields(log.Fields{
  73. "old": entityName,
  74. "new": ".CurrentEntity",
  75. }).Debugf("Legacy entity variable name found, changing to CurrentEntity")
  76. continue
  77. }
  78. if !strings.HasPrefix(argName, ".Arguments.") {
  79. replacement := "{{ .CurrentEntity." + argName + " }}"
  80. rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
  81. log.WithFields(log.Fields{
  82. "old": argName,
  83. "new": ".CurrentEntity." + argName,
  84. }).Debugf("Legacy variable name found, changing to CurrentEntity")
  85. }
  86. }
  87. return rawShellCommand
  88. }
  89. func migrateLegacyArgumentNames(rawShellCommand string) string {
  90. matches := legacyArgumentRegex.FindAllStringSubmatchIndex(rawShellCommand, -1)
  91. for i := len(matches) - 1; i >= 0; i-- {
  92. match := matches[i]
  93. fullMatchStart := match[0]
  94. fullMatchEnd := match[1]
  95. argNameStart := match[2]
  96. argNameEnd := match[3]
  97. argName := rawShellCommand[argNameStart:argNameEnd]
  98. log.WithFields(log.Fields{
  99. "old": argName,
  100. "new": ".Arguments." + argName,
  101. }).Debugf("Legacy variable name found, changing to Argument")
  102. replacement := "{{ .Arguments." + argName + " }}"
  103. rawShellCommand = rawShellCommand[:fullMatchStart] + replacement + rawShellCommand[fullMatchEnd:]
  104. }
  105. return rawShellCommand
  106. }
  107. func ParseTemplateWithActionContext(source string, ent *entities.Entity, args map[string]string) (string, error) {
  108. source = migrateLegacyArgumentNames(source)
  109. source = migrateLegacyEntityProperties(source)
  110. var entdata any
  111. if ent != nil {
  112. entdata = ent.Data
  113. }
  114. templateVariables := &actionTemplateContext{
  115. OliveTin: cachedOliveTinInfo,
  116. Env: cachedEnvMap,
  117. Arguments: args,
  118. CurrentEntity: entdata,
  119. }
  120. result, err := parseTemplate(source, templateVariables)
  121. if isMissingArgumentError, argName := checkMissingArgumentError(err); isMissingArgumentError {
  122. return "", fmt.Errorf("required arg not provided: %s", argName)
  123. }
  124. if err != nil {
  125. return "", err
  126. }
  127. return result, nil
  128. }
  129. func checkMissingArgumentError(err error) (bool, string) {
  130. if err == nil {
  131. return false, ""
  132. }
  133. if strings.Contains(err.Error(), "map has no entry for key") {
  134. re := regexp.MustCompile(`\.Arguments\.(\w+)`)
  135. match := re.FindStringSubmatch(err.Error())
  136. if len(match) > 1 {
  137. return true, match[1]
  138. }
  139. }
  140. return false, ""
  141. }
  142. func parseTemplate(source string, data any) (string, error) {
  143. clone, err := tpl.Clone()
  144. if err != nil {
  145. return "", err
  146. }
  147. t, err := clone.Parse(source)
  148. if err != nil {
  149. return "", err
  150. }
  151. var sb strings.Builder
  152. err = t.Execute(&sb, data)
  153. if err != nil {
  154. log.WithFields(log.Fields{
  155. "source": source,
  156. "err": err,
  157. }).Errorf("Error executing template")
  158. return "", err
  159. } else {
  160. return sb.String(), nil
  161. }
  162. }
  163. func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string {
  164. result, err := ParseTemplateWithActionContext(source, ent, nil)
  165. if err != nil {
  166. log.WithFields(log.Fields{
  167. "source": source,
  168. "err": err,
  169. }).Errorf("Error parsing template of action before exec")
  170. return ""
  171. }
  172. return result
  173. }
  174. /*
  175. func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
  176. source = strings.TrimSpace(source)
  177. tplBool := ParseTemplateOfActionBeforeExec(source, ent)
  178. return tplBool == "true"
  179. }
  180. */