// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package template import ( "fmt" "io" "regexp" "slices" "strings" "github.com/valyala/fasttemplate" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" ) var AllowedNamespaceMetadataTemplates = sets.New[string]( "tenant.name", "namespace", ) func AllowedNamespaceMetadataTemplatesString() string { templates := AllowedNamespaceMetadataTemplates.UnsortedList() slices.Sort(templates) for i, tpl := range templates { templates[i] = fmt.Sprintf("{{%s}}", tpl) } return strings.Join(templates, ", ") } var FastTemplateExpression = regexp.MustCompile(`{{\s*([^{}]+)\s*}}`) func ValidateKubernetesStringOrAllowedTemplates( fieldPath string, value string, validate func(string) []string, ) []string { checkValue, errs := validateAllowedTemplatesAndReplace(fieldPath, value) if len(errs) > 0 { return errs } return prefixValidationErrors(fieldPath, validate(checkValue)) } func ValidateAllowedTemplatesOnly( fieldPath string, value string, ) []string { _, errs := validateAllowedTemplatesAndReplace(fieldPath, value) return errs } func validateAllowedTemplatesAndReplace( fieldPath string, value string, ) (string, []string) { if !ContainsFastTemplateSyntax(value) { return value, nil } matches := FastTemplateExpression.FindAllStringSubmatch(value, -1) if len(matches) == 0 { return value, []string{ fmt.Sprintf("%s: malformed template %q", fieldPath, value), } } checkValue := value for _, match := range matches { raw := match[0] name := FastTemplateNormalize(match[1]) if !AllowedNamespaceMetadataTemplates.Has(name) { return value, []string{ fmt.Sprintf( "%s: unsupported template %q in %q, allowed templates are %s", fieldPath, name, value, AllowedNamespaceMetadataTemplatesString(), ), } } checkValue = strings.ReplaceAll(checkValue, raw, "template") } if strings.Contains(checkValue, "{{") || strings.Contains(checkValue, "}}") { return value, []string{ fmt.Sprintf("%s: malformed template %q", fieldPath, value), } } return checkValue, nil } func prefixValidationErrors(fieldPath string, messages []string) []string { if len(messages) == 0 { return nil } errs := make([]string, 0, len(messages)) for _, msg := range messages { errs = append(errs, fmt.Sprintf("%s: %s", fieldPath, msg)) } return errs } func ContainsFastTemplateSyntax(value string) bool { return strings.Contains(value, "{{") || strings.Contains(value, "}}") } // RequiresFastTemplate evaluates if given string requires templating. func RequiresFastTemplate(value string) bool { return strings.Contains(value, "{{") && strings.Contains(value, "}}") } // FastTemplate applies templating to the provided string. func FastTemplate( template string, templateContext map[string]string, ) string { if !RequiresFastTemplate(template) { return template } t := fasttemplate.New(template, "{{", "}}") return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { key := FastTemplateNormalize(tag) if v, ok := templateContext[key]; ok { return w.Write([]byte(v)) } return 0, nil }) } func FastTemplateNormalize(key string) string { return strings.TrimSpace(key) } // FastTemplateMap applies templating to all values in the provided map in place. func FastTemplateMap( m map[string]string, templateContext map[string]string, ) map[string]string { if len(m) == 0 { return map[string]string{} } out := make(map[string]string, len(m)) for k, v := range m { out[FastTemplate(k, templateContext)] = FastTemplate(v, templateContext) } return out } func SelectorRequiresTemplating(sel *metav1.LabelSelector) bool { if sel == nil { return false } for k, v := range sel.MatchLabels { if RequiresFastTemplate(k) || RequiresFastTemplate(v) { return true } } for _, expr := range sel.MatchExpressions { if RequiresFastTemplate(expr.Key) { return true } if slices.ContainsFunc(expr.Values, RequiresFastTemplate) { return true } } return false } func FastTemplateLabelSelector( in *metav1.LabelSelector, templateContext map[string]string, ) (*metav1.LabelSelector, error) { if in == nil { return nil, nil } out := in.DeepCopy() out.MatchLabels = FastTemplateMap(in.MatchLabels, templateContext) for i := range out.MatchExpressions { out.MatchExpressions[i].Key = FastTemplate(out.MatchExpressions[i].Key, templateContext) for j := range out.MatchExpressions[i].Values { out.MatchExpressions[i].Values[j] = FastTemplate(out.MatchExpressions[i].Values[j], templateContext) } } if _, err := metav1.LabelSelectorAsSelector(out); err != nil { return nil, fmt.Errorf("templated label selector is invalid: %w", err) } return out, nil }