templates.go 5.2 KB

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