templates.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. const maxExecToolConfigDepth = 64
  158. // ApplyTemplatesToExecToolConfig recursively applies action templates to all string values in config.
  159. // Depth-first over maps and slices; only string values are templated. Returns an error if any template fails.
  160. func ApplyTemplatesToExecToolConfig(config map[string]any, ent *entities.Entity, args map[string]string) (map[string]any, error) {
  161. out, err := applyTemplatesToValue(config, ent, args, 0)
  162. if err != nil {
  163. return nil, err
  164. }
  165. if out == nil {
  166. return nil, nil
  167. }
  168. return out.(map[string]any), nil
  169. }
  170. func applyTemplatesToValue(v any, ent *entities.Entity, args map[string]string, depth int) (any, error) {
  171. if depth > maxExecToolConfigDepth {
  172. return nil, fmt.Errorf("execTool config nested too deeply")
  173. }
  174. if s, ok := v.(string); ok {
  175. return ParseTemplateWithActionContext(s, ent, args)
  176. }
  177. return applyTemplatesToComposite(v, ent, args, depth)
  178. }
  179. func applyTemplatesToComposite(v any, ent *entities.Entity, args map[string]string, depth int) (any, error) {
  180. if m, ok := v.(map[string]any); ok {
  181. return applyTemplatesToMap(m, ent, args, depth)
  182. }
  183. if items, ok := v.([]any); ok {
  184. return applyTemplatesToSliceAny(items, ent, args, depth)
  185. }
  186. if strItems, ok := v.([]string); ok {
  187. return applyTemplatesToSliceAny(stringSliceAsAnySlice(strItems), ent, args, depth)
  188. }
  189. return v, nil
  190. }
  191. func stringSliceAsAnySlice(val []string) []any {
  192. out := make([]any, len(val))
  193. for i, s := range val {
  194. out[i] = s
  195. }
  196. return out
  197. }
  198. func applyTemplatesToMap(val map[string]any, ent *entities.Entity, args map[string]string, depth int) (map[string]any, error) {
  199. result := make(map[string]any, len(val))
  200. for k, elem := range val {
  201. transformed, err := applyTemplatesToValue(elem, ent, args, depth+1)
  202. if err != nil {
  203. return nil, err
  204. }
  205. result[k] = transformed
  206. }
  207. return result, nil
  208. }
  209. func applyTemplatesToSliceAny(val []any, ent *entities.Entity, args map[string]string, depth int) ([]any, error) {
  210. result := make([]any, len(val))
  211. for i, elem := range val {
  212. transformed, err := applyTemplatesToValue(elem, ent, args, depth+1)
  213. if err != nil {
  214. return nil, err
  215. }
  216. result[i] = transformed
  217. }
  218. return result, nil
  219. }
  220. func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string {
  221. result, err := ParseTemplateWithActionContext(source, ent, nil)
  222. if err != nil {
  223. log.WithFields(log.Fields{
  224. "source": source,
  225. "err": err,
  226. }).Errorf("Error parsing template of action before exec")
  227. return ""
  228. }
  229. return result
  230. }
  231. /*
  232. func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
  233. source = strings.TrimSpace(source)
  234. tplBool := ParseTemplateOfActionBeforeExec(source, ent)
  235. return tplBool == "true"
  236. }
  237. */