| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- package tpl
- import (
- "encoding/json"
- "fmt"
- "regexp"
- "strings"
- "text/template"
- "github.com/OliveTin/OliveTin/internal/entities"
- "github.com/OliveTin/OliveTin/internal/env"
- "github.com/OliveTin/OliveTin/internal/installationinfo"
- log "github.com/sirupsen/logrus"
- )
- func jsonFunc(v any) (string, error) {
- if v == nil {
- return "null", nil
- }
- data, err := json.Marshal(v)
- if err != nil {
- return "", err
- }
- return string(data), nil
- }
- // Root template (funcs/options). parseTemplate clones before Parse — text/template
- // must not receive concurrent Parse calls on the same instance.
- var tpl = template.New("tpl").
- Option("missingkey=error").
- Funcs(template.FuncMap{"Json": jsonFunc})
- type olivetinInfo struct {
- Build *installationinfo.BuildInfo
- Runtime *installationinfo.RuntimeInfo
- }
- var legacyArgumentRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\s*}}`)
- var legacyEntityPropertiesRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+)\s*}}`)
- type generalTemplateContext struct {
- OliveTin olivetinInfo
- Env map[string]string
- }
- type actionTemplateContext struct {
- CurrentEntity interface{}
- Arguments map[string]string
- // These are deliberately repeated because embedding structs
- // won't work in text/template.
- OliveTin olivetinInfo
- Env map[string]string
- }
- var (
- cachedOliveTinInfo olivetinInfo
- cachedEnvMap map[string]string
- )
- func init() {
- cachedOliveTinInfo = olivetinInfo{
- Build: installationinfo.Build,
- Runtime: installationinfo.Runtime,
- }
- cachedEnvMap = env.BuildEnvMap()
- }
- func GetNewGeneralTemplateContext() *generalTemplateContext {
- return &generalTemplateContext{
- OliveTin: cachedOliveTinInfo,
- Env: cachedEnvMap,
- }
- }
- func migrateLegacyEntityProperties(rawShellCommand string) string {
- foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)
- for _, match := range foundArgumentNames {
- entityName := match[1]
- argName := match[2]
- fullMatch := match[0] // The entire matched string like "{{ server.hostname }}"
- if strings.Contains(argName, ".") {
- replacement := "{{ .CurrentEntity." + argName + " }}"
- rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
- log.WithFields(log.Fields{
- "old": entityName,
- "new": ".CurrentEntity",
- }).Debugf("Legacy entity variable name found, changing to CurrentEntity")
- continue
- }
- if !strings.HasPrefix(argName, ".Arguments.") {
- replacement := "{{ .CurrentEntity." + argName + " }}"
- rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
- log.WithFields(log.Fields{
- "old": argName,
- "new": ".CurrentEntity." + argName,
- }).Debugf("Legacy variable name found, changing to CurrentEntity")
- }
- }
- return rawShellCommand
- }
- func migrateLegacyArgumentNames(rawShellCommand string) string {
- matches := legacyArgumentRegex.FindAllStringSubmatchIndex(rawShellCommand, -1)
- for i := len(matches) - 1; i >= 0; i-- {
- match := matches[i]
- fullMatchStart := match[0]
- fullMatchEnd := match[1]
- argNameStart := match[2]
- argNameEnd := match[3]
- argName := rawShellCommand[argNameStart:argNameEnd]
- log.WithFields(log.Fields{
- "old": argName,
- "new": ".Arguments." + argName,
- }).Debugf("Legacy variable name found, changing to Argument")
- replacement := "{{ .Arguments." + argName + " }}"
- rawShellCommand = rawShellCommand[:fullMatchStart] + replacement + rawShellCommand[fullMatchEnd:]
- }
- return rawShellCommand
- }
- func ParseTemplateWithActionContext(source string, ent *entities.Entity, args map[string]string) (string, error) {
- source = migrateLegacyArgumentNames(source)
- source = migrateLegacyEntityProperties(source)
- var entdata any
- if ent != nil {
- entdata = ent.Data
- }
- templateVariables := &actionTemplateContext{
- OliveTin: cachedOliveTinInfo,
- Env: cachedEnvMap,
- Arguments: args,
- CurrentEntity: entdata,
- }
- result, err := parseTemplate(source, templateVariables)
- if isMissingArgumentError, argName := checkMissingArgumentError(err); isMissingArgumentError {
- return "", fmt.Errorf("required arg not provided: %s", argName)
- }
- if err != nil {
- return "", err
- }
- return result, nil
- }
- func checkMissingArgumentError(err error) (bool, string) {
- if err == nil {
- return false, ""
- }
- if strings.Contains(err.Error(), "map has no entry for key") {
- re := regexp.MustCompile(`\.Arguments\.(\w+)`)
- match := re.FindStringSubmatch(err.Error())
- if len(match) > 1 {
- return true, match[1]
- }
- }
- return false, ""
- }
- func parseTemplate(source string, data any) (string, error) {
- clone, err := tpl.Clone()
- if err != nil {
- return "", err
- }
- t, err := clone.Parse(source)
- if err != nil {
- return "", err
- }
- var sb strings.Builder
- err = t.Execute(&sb, data)
- if err != nil {
- log.WithFields(log.Fields{
- "source": source,
- "err": err,
- }).Errorf("Error executing template")
- return "", err
- } else {
- return sb.String(), nil
- }
- }
- func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string {
- result, err := ParseTemplateWithActionContext(source, ent, nil)
- if err != nil {
- log.WithFields(log.Fields{
- "source": source,
- "err": err,
- }).Errorf("Error parsing template of action before exec")
- return ""
- }
- return result
- }
- /*
- func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
- source = strings.TrimSpace(source)
- tplBool := ParseTemplateOfActionBeforeExec(source, ent)
- return tplBool == "true"
- }
- */
|