package template import ( "bytes" "encoding/json" tmpl "text/template" sprig "github.com/go-task/slim-sprig/v3" ) func Template[T any](obj *T, templateContext map[string]any) (*T, error) { b, err := json.Marshal(obj) if err != nil { return nil, err } var unstructured any err = json.Unmarshal(b, &unstructured) if err != nil { return nil, err } templateFunc := func(in string) string { out, err := template(in, templateContext) if err != nil { return in } return out } unstructured = mapAtStrings(unstructured, templateFunc) b, err = json.Marshal(unstructured) if err != nil { return nil, err } var out T err = json.Unmarshal(b, &out) if err != nil { return nil, err } return &out, nil } // String renders a single template string against the context and surfaces any // parse or execute error. Template walks a struct's string leaves and, by // design, leaves a leaf unchanged when its template fails; a caller that gates // on the rendered value — a strategy precondition, an artifact URI — needs the // error instead, so it can fail loud rather than act on a half-rendered string. func String(in string, templateContext map[string]any) (string, error) { return template(in, templateContext) } func mapAtStrings(v any, f func(string) string) any { switch x := v.(type) { case map[string]any: for k, val := range x { x[k] = mapAtStrings(val, f) } return x case []any: for i, val := range x { x[i] = mapAtStrings(val, f) } return x case string: return f(x) default: return v } } func template(in string, templateContext map[string]any) (string, error) { // FuncMap from slim-sprig is text-template-only (no HTML escaping, which // is what we want for Pod spec strings) and excludes network/OS/crypto // helpers - the subset matches what strategy authors actually reach for // (default, required, quote, printf wrappers, dict/list/index helpers). tpl, err := tmpl.New("this").Funcs(sprig.FuncMap()).Parse(in) if err != nil { return "", err } var buf bytes.Buffer if err := tpl.Execute(&buf, templateContext); err != nil { return "", err } return buf.String(), nil }