fix: Move templating functionality to global, making it possible to replace templates across the config

This commit is contained in:
jamesread 2026-01-25 10:32:22 +00:00
parent bcc2d50299
commit b37f035ea6
12 changed files with 145 additions and 91 deletions

View File

@ -27,6 +27,7 @@ import (
entities "github.com/OliveTin/OliveTin/internal/entities" entities "github.com/OliveTin/OliveTin/internal/entities"
executor "github.com/OliveTin/OliveTin/internal/executor" executor "github.com/OliveTin/OliveTin/internal/executor"
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo" installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
"github.com/OliveTin/OliveTin/internal/tpl"
connectproto "go.akshayshah.org/connectproto" connectproto "go.akshayshah.org/connectproto"
) )
@ -709,7 +710,7 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum
return connect.NewResponse(res), nil return connect.NewResponse(res), nil
} }
jsonstring, _ := json.MarshalIndent(entities.GetAll(), "", " ") jsonstring, _ := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ")
fmt.Printf("%s", &jsonstring) fmt.Printf("%s", &jsonstring)
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore" res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"

View File

@ -13,6 +13,7 @@ import (
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
entities "github.com/OliveTin/OliveTin/internal/entities" entities "github.com/OliveTin/OliveTin/internal/entities"
executor "github.com/OliveTin/OliveTin/internal/executor" executor "github.com/OliveTin/OliveTin/internal/executor"
"github.com/OliveTin/OliveTin/internal/tpl"
) )
type DashboardRenderRequest struct { type DashboardRenderRequest struct {
@ -66,7 +67,7 @@ func evaluateEnabledExpression(action *config.Action, entity *entities.Entity) b
return true return true
} }
result := entities.ParseTemplateWith(action.EnabledExpression, entity) result := tpl.ParseTemplateWith(action.EnabledExpression, entity)
result = strings.TrimSpace(result) result = strings.TrimSpace(result)
if result == "" { if result == "" {
@ -105,6 +106,16 @@ func evaluateResultValue(result string) bool {
return false return false
} }
func getDefaultValue(cfgArg config.ActionArgument, entity *entities.Entity) string {
defaultValue := cfgArg.Default
if defaultValue != "" {
defaultValue = tpl.ParseTemplateWith(defaultValue, entity)
}
return defaultValue
}
func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action { func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action {
action := actionBinding.Action action := actionBinding.Action
@ -120,8 +131,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
btn := apiv1.Action{ btn := apiv1.Action{
BindingId: actionBinding.ID, BindingId: actionBinding.ID,
Title: entities.ParseTemplateWith(action.Title, actionBinding.Entity), Title: tpl.ParseTemplateWith(action.Title, actionBinding.Entity),
Icon: entities.ParseTemplateWith(action.Icon, actionBinding.Entity), Icon: tpl.ParseTemplateWith(action.Icon, actionBinding.Entity),
CanExec: aclCanExec && enabledExprCanExec, CanExec: aclCanExec && enabledExprCanExec,
PopupOnStart: action.PopupOnStart, PopupOnStart: action.PopupOnStart,
Order: int32(actionBinding.ConfigOrder), Order: int32(actionBinding.ConfigOrder),
@ -135,7 +146,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
Title: cfgArg.Title, Title: cfgArg.Title,
Type: cfgArg.Type, Type: cfgArg.Type,
Description: cfgArg.Description, Description: cfgArg.Description,
DefaultValue: cfgArg.Default, DefaultValue: getDefaultValue(cfgArg, actionBinding.Entity),
Choices: buildChoices(cfgArg), Choices: buildChoices(cfgArg),
Suggestions: cfgArg.Suggestions, Suggestions: cfgArg.Suggestions,
SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey, SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey,
@ -162,8 +173,8 @@ func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle str
for _, ent := range entList { for _, ent := range entList {
ret = append(ret, &apiv1.ActionArgumentChoice{ ret = append(ret, &apiv1.ActionArgumentChoice{
Value: entities.ParseTemplateWith(firstChoice.Value, ent), Value: tpl.ParseTemplateWith(firstChoice.Value, ent),
Title: entities.ParseTemplateWith(firstChoice.Title, ent), Title: tpl.ParseTemplateWith(firstChoice.Title, ent),
}) })
} }

View File

@ -119,9 +119,9 @@ func TestGetEntities(t *testing.T) {
} }
func setupTestEntities() { func setupTestEntities() {
entities.ClearEntities("server") entities.ClearEntitiesOfType("server")
entities.ClearEntities("database") entities.ClearEntitiesOfType("database")
entities.ClearEntities("application") entities.ClearEntitiesOfType("application")
entities.AddEntity("server", "zebra", map[string]any{"title": "Server Zebra", "hostname": "zebra.example.com"}) entities.AddEntity("server", "zebra", map[string]any{"title": "Server Zebra", "hostname": "zebra.example.com"})
entities.AddEntity("server", "alpha", map[string]any{"title": "Server Alpha", "hostname": "alpha.example.com"}) entities.AddEntity("server", "alpha", map[string]any{"title": "Server Alpha", "hostname": "alpha.example.com"})

View File

@ -4,6 +4,7 @@ import (
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
entities "github.com/OliveTin/OliveTin/internal/entities" entities "github.com/OliveTin/OliveTin/internal/entities"
"github.com/OliveTin/OliveTin/internal/tpl"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -23,14 +24,14 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr
return ret return ret
} }
func buildEntityFieldset(tpl *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent { func buildEntityFieldset(component *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
return &apiv1.DashboardComponent{ return &apiv1.DashboardComponent{
Title: entities.ParseTemplateWith(tpl.Title, ent), Title: tpl.ParseTemplateWith(component.Title, ent),
Type: "fieldset", Type: "fieldset",
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, tpl.Entity, rr)), Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(component.Contents, ent, component.Entity, rr)),
CssClass: entities.ParseTemplateWith(tpl.CssClass, ent), CssClass: tpl.ParseTemplateWith(component.CssClass, ent),
Action: rr.findAction(tpl.Title), Action: rr.findAction(component.Title),
EntityType: tpl.Entity, EntityType: component.Entity,
EntityKey: ent.UniqueKey, EntityKey: ent.UniqueKey,
} }
} }
@ -68,7 +69,7 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent
func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent { func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
clone := &apiv1.DashboardComponent{} clone := &apiv1.DashboardComponent{}
clone.CssClass = entities.ParseTemplateWith(subitem.CssClass, ent) clone.CssClass = tpl.ParseTemplateWith(subitem.CssClass, ent)
if isLinkType(subitem.Type) { if isLinkType(subitem.Type) {
return cloneLinkItem(subitem, ent, clone, rr) return cloneLinkItem(subitem, ent, clone, rr)
@ -83,7 +84,7 @@ func isLinkType(itemType string) bool {
func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
clone.Type = "link" clone.Type = "link"
clone.Title = entities.ParseTemplateWith(subitem.Title, ent) clone.Title = tpl.ParseTemplateWith(subitem.Title, ent)
// Prefer an entity-specific action when available, but fall back to a // Prefer an entity-specific action when available, but fall back to a
// non-entity-scoped action with the same title. This allows inline actions // non-entity-scoped action with the same title. This allows inline actions
// defined inside entity dashboards to work without requiring an explicit // defined inside entity dashboards to work without requiring an explicit
@ -98,7 +99,7 @@ func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clo
} }
func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
clone.Title = entities.ParseTemplateWith(subitem.Title, ent) clone.Title = tpl.ParseTemplateWith(subitem.Title, ent)
clone.Type = subitem.Type clone.Type = subitem.Type
if isDirectoryWithEntity(clone.Type, ent, entityType) { if isDirectoryWithEntity(clone.Type, ent, entityType) {

View File

@ -6,6 +6,7 @@ import (
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
entities "github.com/OliveTin/OliveTin/internal/entities" entities "github.com/OliveTin/OliveTin/internal/entities"
"github.com/OliveTin/OliveTin/internal/tpl"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -236,7 +237,7 @@ func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent,
title := subitem.Title title := subitem.Title
if entity != nil { if entity != nil {
title = entities.ParseTemplateWith(subitem.Title, entity) title = tpl.ParseTemplateWith(subitem.Title, entity)
} }
newitem := &apiv1.DashboardComponent{ newitem := &apiv1.DashboardComponent{

View File

@ -171,6 +171,7 @@ type Config struct {
Include string `koanf:"include"` Include string `koanf:"include"`
sourceFiles []string sourceFiles []string
passwordTemplateParser func(string, interface{}) string
} }
type AuthLocalUsersConfig struct { type AuthLocalUsersConfig struct {

View File

@ -13,6 +13,7 @@ func (cfg *Config) Sanitize() {
cfg.sanitizeLogLevel() cfg.sanitizeLogLevel()
cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeAuthRequireGuestsToLogin()
cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLogHistoryPageSize()
cfg.sanitizeLocalUserPasswords()
// log.Infof("cfg %p", cfg) // log.Infof("cfg %p", cfg)
@ -172,6 +173,26 @@ func (cfg *Config) sanitizeLogHistoryPageSize() {
} }
} }
// SetPasswordTemplateParser sets the function to use for parsing password templates.
// This is called from main.go to avoid import cycles (config can't import entities).
func (cfg *Config) SetPasswordTemplateParser(parser func(string, interface{}) string) {
cfg.passwordTemplateParser = parser
}
func (cfg *Config) sanitizeLocalUserPasswords() {
if cfg.passwordTemplateParser == nil {
return
}
for _, user := range cfg.AuthLocalUsers.Users {
if user.Password != "" {
// Parse password as template to support environment variables and other template values
// Note: .CurrentEntity is nil in this context as local users are not entity-bound
user.Password = cfg.passwordTemplateParser(user.Password, nil)
}
}
}
func getActionID(action *Action) string { func getActionID(action *Action) string {
if action.ID == "" { if action.ID == "" {
return uuid.NewString() return uuid.NewString()

View File

@ -135,7 +135,7 @@ func loadEntityFileYaml(filename string, entityname string) {
} }
func updateSvFromFile(entityname string, data []map[string]any) { func updateSvFromFile(entityname string, data []map[string]any) {
ClearEntities(entityname) ClearEntitiesOfType(entityname)
for i, mapp := range data { for i, mapp := range data {
AddEntity(entityname, fmt.Sprintf("%d", i), mapp) AddEntity(entityname, fmt.Sprintf("%d", i), mapp)

View File

@ -10,72 +10,31 @@ package entities
*/ */
import ( import (
"os"
"strings" "strings"
"sync" "sync"
"github.com/OliveTin/OliveTin/internal/installationinfo"
) )
type entityInstancesByKey map[string]*Entity type entityInstancesByKey map[string]*Entity
type entitiesByClass map[string]entityInstancesByKey type EntitiesByClass map[string]entityInstancesByKey
type variableBase struct {
OliveTin installationInfo
Entities entitiesByClass
CurrentEntity interface{}
Arguments map[string]string
Env map[string]string
}
type installationInfo struct {
Build *installationinfo.BuildInfo
Runtime *installationinfo.RuntimeInfo
}
var ( var (
contents *variableBase
rwmutex = sync.RWMutex{} rwmutex = sync.RWMutex{}
Entities EntitiesByClass
) )
func init() { func init() {
rwmutex.Lock() rwmutex.Lock()
Entities = make(EntitiesByClass, 0)
envMap := make(map[string]string)
for _, env := range os.Environ() {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
contents = &variableBase{
OliveTin: installationInfo{
Build: installationinfo.Build,
Runtime: installationinfo.Runtime,
},
Entities: make(entitiesByClass, 0),
Env: envMap,
}
rwmutex.Unlock() rwmutex.Unlock()
} }
func GetAll() *variableBase { func GetEntities() EntitiesByClass {
rwmutex.RLock()
defer rwmutex.RUnlock()
return contents
}
func GetEntities() entitiesByClass {
rwmutex.RLock() rwmutex.RLock()
copiedEntities := make(entitiesByClass, len(contents.Entities)) copiedEntities := make(EntitiesByClass, len(Entities))
for entityName, entityInstances := range contents.Entities { for entityName, entityInstances := range Entities {
copiedInstances := make(entityInstancesByKey, len(entityInstances)) copiedInstances := make(entityInstancesByKey, len(entityInstances))
for key, entity := range entityInstances { for key, entity := range entityInstances {
@ -93,7 +52,7 @@ func GetEntityInstances(entityName string) entityInstancesByKey {
rwmutex.RLock() rwmutex.RLock()
defer rwmutex.RUnlock() defer rwmutex.RUnlock()
if entities, ok := contents.Entities[entityName]; ok { if entities, ok := Entities[entityName]; ok {
copiedInstances := make(entityInstancesByKey, len(entities)) copiedInstances := make(entityInstancesByKey, len(entities))
for key, entity := range entities { for key, entity := range entities {
@ -108,11 +67,11 @@ func GetEntityInstances(entityName string) entityInstancesByKey {
func AddEntity(entityName string, entityKey string, data any) { func AddEntity(entityName string, entityKey string, data any) {
rwmutex.Lock() rwmutex.Lock()
if _, ok := contents.Entities[entityName]; !ok { if _, ok := Entities[entityName]; !ok {
contents.Entities[entityName] = make(entityInstancesByKey, 0) Entities[entityName] = make(entityInstancesByKey, 0)
} }
contents.Entities[entityName][entityKey] = &Entity{ Entities[entityName][entityKey] = &Entity{
Data: data, Data: data,
UniqueKey: entityKey, UniqueKey: entityKey,
Title: findEntityTitle(data), Title: findEntityTitle(data),
@ -144,3 +103,10 @@ func findEntityTitle(data any) string {
return "Untitled Entity" return "Untitled Entity"
} }
func ClearEntitiesOfType(entityType string) {
rwmutex.Lock()
defer rwmutex.Unlock()
delete(Entities, entityType)
}

View File

@ -3,6 +3,7 @@ package executor
import ( import (
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/entities"
"github.com/OliveTin/OliveTin/internal/tpl"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"fmt" "fmt"
@ -75,7 +76,7 @@ func parseSingleExec(a string, values map[string]string, entity *entities.Entity
if err != nil { if err != nil {
return "", err return "", err
} }
return entities.ParseTemplateWithArgs(arg, entity, values), nil return tpl.ParseTemplateWithArgs(arg, entity, values), nil
} }
func validateArguments(values map[string]string, action *config.Action) error { func validateArguments(values map[string]string, action *config.Action) error {
@ -117,7 +118,7 @@ func parseActionArguments(values map[string]string, action *config.Action, entit
}).Debugf("Arg assigned") }).Debugf("Arg assigned")
} }
parsedShellCommand := entities.ParseTemplateWithArgs(rawShellCommand, entity, values) parsedShellCommand := tpl.ParseTemplateWithArgs(rawShellCommand, entity, values)
redactedShellCommand := redactShellCommand(parsedShellCommand, action.Arguments, values) redactedShellCommand := redactShellCommand(parsedShellCommand, action.Arguments, values)
if err != nil { if err != nil {
@ -256,7 +257,7 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
templateChoice := arg.Choices[0].Value templateChoice := arg.Choices[0].Value
for _, ent := range entities.GetEntityInstances(arg.Entity) { for _, ent := range entities.GetEntityInstances(arg.Entity) {
choice := entities.ParseTemplateWith(templateChoice, ent) choice := tpl.ParseTemplateWith(templateChoice, ent)
if value == choice { if value == choice {
return nil return nil

View File

@ -6,6 +6,7 @@ import (
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic" authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/entities"
"github.com/OliveTin/OliveTin/internal/tpl"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -728,7 +729,7 @@ func stepRequestAction(req *ExecutionRequest) bool {
req.logEntry.Binding = req.Binding req.logEntry.Binding = req.Binding
req.logEntry.ActionConfigTitle = req.Binding.Action.Title req.logEntry.ActionConfigTitle = req.Binding.Action.Title
req.logEntry.ActionTitle = entities.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity) req.logEntry.ActionTitle = tpl.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity)
req.logEntry.ActionIcon = req.Binding.Action.Icon req.logEntry.ActionIcon = req.Binding.Action.Icon
req.logEntry.Tags = req.Tags req.logEntry.Tags = req.Tags

View File

@ -1,19 +1,75 @@
package entities package tpl
import ( import (
"fmt" "fmt"
"os"
"regexp" "regexp"
"strings" "strings"
"text/template" "text/template"
"github.com/OliveTin/OliveTin/internal/entities"
"github.com/OliveTin/OliveTin/internal/installationinfo"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var tpl = template.New("tpl") var tpl = template.New("tpl")
type olivetinInfo struct {
Build *installationinfo.BuildInfo
Runtime *installationinfo.RuntimeInfo
}
var legacyArgumentRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+) }}`) var legacyArgumentRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+) }}`)
var legacyEntityPropertiesRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+) }}`) var legacyEntityPropertiesRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+) }}`)
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 = buildEnvMap()
}
func GetNewGeneralTemplateContext() *generalTemplateContext {
return &generalTemplateContext{
OliveTin: cachedOliveTinInfo,
Env: cachedEnvMap,
}
}
func buildEnvMap() map[string]string {
envMap := make(map[string]string)
for _, env := range os.Environ() {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
return envMap
}
func migrateLegacyEntityProperties(rawShellCommand string) string { func migrateLegacyEntityProperties(rawShellCommand string) string {
foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1) foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)
@ -68,7 +124,7 @@ func migrateLegacyArgumentNames(rawShellCommand string) string {
return rawShellCommand return rawShellCommand
} }
func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) string { func ParseTemplateWithArgs(source string, ent *entities.Entity, args map[string]string) string {
source = migrateLegacyArgumentNames(source) source = migrateLegacyArgumentNames(source)
source = migrateLegacyEntityProperties(source) source = migrateLegacyEntityProperties(source)
@ -90,11 +146,12 @@ func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) s
entdata = ent.Data entdata = ent.Data
} }
templateVariables := &variableBase{ templateVariables := &actionTemplateContext{
OliveTin: GetAll().OliveTin, OliveTin: cachedOliveTinInfo,
Env: cachedEnvMap,
Arguments: args, Arguments: args,
CurrentEntity: entdata, CurrentEntity: entdata,
Env: GetAll().Env,
} }
var sb strings.Builder var sb strings.Builder
@ -114,21 +171,14 @@ func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) s
return ret return ret
} }
func ParseTemplateWith(source string, ent *Entity) string { func ParseTemplateWith(source string, ent *entities.Entity) string {
return ParseTemplateWithArgs(source, ent, nil) return ParseTemplateWithArgs(source, ent, nil)
} }
func ParseTemplateBoolWith(source string, ent *Entity) bool { func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
source = strings.TrimSpace(source) source = strings.TrimSpace(source)
tplBool := ParseTemplateWith(source, ent) tplBool := ParseTemplateWith(source, ent)
return tplBool == "true" return tplBool == "true"
} }
func ClearEntities(entityType string) {
rwmutex.Lock()
defer rwmutex.Unlock()
delete(contents.Entities, entityType)
}