From e19575bcbde335df497eaddda403fdecc7008a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20B=C3=A4hler?= Date: Thu, 11 Dec 2025 17:03:52 +0100 Subject: [PATCH] fix(controller): allow no spaces in template references (#1789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler * chore: modernize golang Signed-off-by: Oliver Bähler * chore: modernize golang Signed-off-by: Oliver Bähler * chore: modernize golang Signed-off-by: Oliver Bähler * fix(controller): allow no spaces in template references Signed-off-by: Oliver Bähler * fix(controller): allow no spaces in template references Signed-off-by: Oliver Bähler --------- Signed-off-by: Oliver Bähler --- internal/controllers/resources/processor.go | 9 +- pkg/template/fast.go | 36 ++++-- pkg/template/fast_test.go | 127 +++++++++++++++++--- pkg/utils/tenant/metdata.go | 4 +- 4 files changed, 142 insertions(+), 34 deletions(-) diff --git a/internal/controllers/resources/processor.go b/internal/controllers/resources/processor.go index ab804fde..79286d67 100644 --- a/internal/controllers/resources/processor.go +++ b/internal/controllers/resources/processor.go @@ -10,7 +10,6 @@ import ( "maps" "sync" - "github.com/valyala/fasttemplate" corev1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,6 +24,7 @@ import ( ctrllog "sigs.k8s.io/controller-runtime/pkg/log" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + tpl "github.com/projectcapsule/capsule/pkg/template" ) const ( @@ -243,12 +243,7 @@ func (r *Processor) HandleSection(ctx context.Context, tnt capsulev1beta2.Tenant for rawIndex, item := range spec.RawItems { template := string(item.Raw) - t := fasttemplate.New(template, "{{ ", " }}") - - tmplString := t.ExecuteString(map[string]any{ - "tenant.name": tnt.Name, - "namespace": ns.Name, - }) + tmplString := tpl.TemplateForTenantAndNamespace(template, &tnt, &ns) obj, keysAndValues := unstructured.Unstructured{}, []any{"index", rawIndex} diff --git a/pkg/template/fast.go b/pkg/template/fast.go index ee6b8e97..146ba16a 100644 --- a/pkg/template/fast.go +++ b/pkg/template/fast.go @@ -4,6 +4,7 @@ package template import ( + "io" "strings" "github.com/valyala/fasttemplate" @@ -12,19 +13,32 @@ import ( capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" ) -// TemplateForTenantAndNamespace applies templating to all values in the provided map in place. -func TemplateForTenantAndNamespace(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) { - for k, v := range m { - if !strings.Contains(v, "{{ ") && !strings.Contains(v, " }}") { - continue +// TemplateForTenantAndNamespace applies templatingto the provided string. +func TemplateForTenantAndNamespace(template string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) string { + if !strings.Contains(template, "{{") && !strings.Contains(template, "}}") { + return template + } + + t := fasttemplate.New(template, "{{", "}}") + + values := map[string]string{ + "tenant.name": tnt.Name, + "namespace": ns.Name, + } + + return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { + key := strings.TrimSpace(tag) + if v, ok := values[key]; ok { + return w.Write([]byte(v)) } - t := fasttemplate.New(v, "{{ ", " }}") - tmplString := t.ExecuteString(map[string]any{ - "tenant.name": tnt.Name, - "namespace": ns.Name, - }) + return 0, nil + }) +} - m[k] = tmplString +// TemplateForTenantAndNamespace applies templating to all values in the provided map in place. +func TemplateForTenantAndNamespaceMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) { + for k, v := range m { + m[k] = TemplateForTenantAndNamespace(v, tnt, ns) } } diff --git a/pkg/template/fast_test.go b/pkg/template/fast_test.go index 85b133d2..fa1bcd74 100644 --- a/pkg/template/fast_test.go +++ b/pkg/template/fast_test.go @@ -1,14 +1,16 @@ // Copyright 2020-2025 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 -package template +package template_test import ( "testing" - capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + tpl "github.com/projectcapsule/capsule/pkg/template" ) func newTenant(name string) *capsulev1beta2.Tenant { @@ -31,12 +33,90 @@ func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) { tnt := newTenant("tenant-a") ns := newNamespace("ns-1") + got := tpl.TemplateForTenantAndNamespace( + "tenant={{tenant.name}}, ns={{namespace}}", + tnt, + ns, + ) + + want := "tenant=tenant-a, ns=ns-1" + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} + +func TestTemplateForTenantAndNamespace_ReplacesPlaceholdersSpaces(t *testing.T) { + tnt := newTenant("tenant-a") + ns := newNamespace("ns-1") + + got := tpl.TemplateForTenantAndNamespace( + "tenant={{ tenant.name }}, ns={{ namespace }}", + tnt, + ns, + ) + + want := "tenant=tenant-a, ns=ns-1" + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} + +func TestTemplateForTenantAndNamespace_OnlyTenant(t *testing.T) { + tnt := newTenant("tenant-x") + ns := newNamespace("ns-y") + + got := tpl.TemplateForTenantAndNamespace("T={{tenant.name}}", tnt, ns) + want := "T=tenant-x" + + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} + +func TestTemplateForTenantAndNamespace_OnlyNamespace(t *testing.T) { + tnt := newTenant("tenant-x") + ns := newNamespace("ns-y") + + got := tpl.TemplateForTenantAndNamespace("N={{namespace}}", tnt, ns) + want := "N=ns-y" + + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} + +func TestTemplateForTenantAndNamespace_NoDelimitersReturnsEmpty(t *testing.T) { + tnt := newTenant("tenant-a") + ns := newNamespace("ns-1") + + got := tpl.TemplateForTenantAndNamespace("plain-value-without-templates", tnt, ns) + if got != "plain-value-without-templates" { + t.Fatalf("expected empty string for input without delimiters, got %q", got) + } +} + +func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) { + tnt := newTenant("tenant-a") + ns := newNamespace("ns-1") + + got := tpl.TemplateForTenantAndNamespace("X={{unknown.key}}", tnt, ns) + want := "X=" + + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} + +func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholders(t *testing.T) { + tnt := newTenant("tenant-a") + ns := newNamespace("ns-1") + m := map[string]string{ - "key1": "tenant={{ tenant.name }}, ns={{ namespace }}", + "key1": "tenant={{tenant.name}}, ns={{namespace}}", "key2": "plain-value", } - TemplateForTenantAndNamespace(m, tnt, ns) + tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" { t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) @@ -47,7 +127,27 @@ func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) { } } -func TestTemplateForTenantAndNamespace_SkipsValuesWithoutDelimiters(t *testing.T) { +func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholdersSpaces(t *testing.T) { + tnt := newTenant("tenant-a") + ns := newNamespace("ns-1") + + m := map[string]string{ + "key1": "tenant={{ tenant.name }}, ns={{ namespace }}", + "key2": "plain-value", + } + + tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) + + if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" { + t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) + } + + if got := m["key2"]; got != "plain-value" { + t.Fatalf("key2: expected %q to remain unchanged, got %q", "plain-value", got) + } +} + +func TestTemplateForTenantAndNamespaceMap_SkipsValuesWithoutDelimiters(t *testing.T) { tnt := newTenant("tenant-a") ns := newNamespace("ns-1") @@ -57,20 +157,19 @@ func TestTemplateForTenantAndNamespace_SkipsValuesWithoutDelimiters(t *testing.T "noTemplate2": "namespace {{namespace}}", } - original1 := m["noTemplate1"] original2 := m["noTemplate2"] - TemplateForTenantAndNamespace(m, tnt, ns) + tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) - if got := m["noTemplate1"]; got != original1 { - t.Fatalf("noTemplate1: expected %q to remain unchanged, got %q", original1, got) + if got := m["noTemplate1"]; got != "hello tenant-a" { + t.Fatalf("noTemplate1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) } - if got := m["noTemplate2"]; got != original2 { + if got := m["noTemplate2"]; got != "namespace ns-1" { t.Fatalf("noTemplate2: expected %q to remain unchanged, got %q", original2, got) } } -func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) { +func TestTemplateForTenantAndNamespaceMap_MixedKeys(t *testing.T) { tnt := newTenant("tenant-x") ns := newNamespace("ns-x") @@ -80,7 +179,7 @@ func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) { "none": "static", } - TemplateForTenantAndNamespace(m, tnt, ns) + tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) if got := m["onlyTenant"]; got != "T=tenant-x" { t.Fatalf("onlyTenant: expected %q, got %q", "T=tenant-x", got) @@ -93,7 +192,7 @@ func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) { } } -func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) { +func TestTemplateForTenantAndNamespaceMap_UnknownKeyBecomesEmpty(t *testing.T) { tnt := newTenant("tenant-a") ns := newNamespace("ns-1") @@ -101,7 +200,7 @@ func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) { "unknown": "X={{ unknown.key }}", } - TemplateForTenantAndNamespace(m, tnt, ns) + tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) // fasttemplate with missing key returns an empty string for that placeholder if got := m["unknown"]; got != "X=" { diff --git a/pkg/utils/tenant/metdata.go b/pkg/utils/tenant/metdata.go index 25dac3e3..8298ee9b 100644 --- a/pkg/utils/tenant/metdata.go +++ b/pkg/utils/tenant/metdata.go @@ -59,8 +59,8 @@ func BuildNamespaceMetadataForTenant(ns *corev1.Namespace, tnt *capsulev1beta2.T continue } - template.TemplateForTenantAndNamespace(md.Labels, tnt, ns) - template.TemplateForTenantAndNamespace(md.Annotations, tnt, ns) + template.TemplateForTenantAndNamespaceMap(md.Labels, tnt, ns) + template.TemplateForTenantAndNamespaceMap(md.Annotations, tnt, ns) utils.MapMergeNoOverrite(labels, md.Labels) utils.MapMergeNoOverrite(annotations, md.Annotations)