fix(controller): template concurrency (#1802)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2025-12-19 08:14:37 +01:00
committed by GitHub
parent 8eea90731c
commit a42d910ba1
6 changed files with 499 additions and 72 deletions

View File

@@ -46,7 +46,7 @@ all: manager
# Run tests # Run tests
.PHONY: test .PHONY: test
test: test-clean generate manifests test-clean test: test-clean generate manifests test-clean
@GO111MODULE=on go test -v $(shell go list ./... | grep -v "e2e") -coverprofile coverage.out @GO111MODULE=on go test -race -v $(shell go list ./... | grep -v "e2e") -coverprofile coverage.out
.PHONY: test-clean .PHONY: test-clean
test-clean: ## Clean tests cache test-clean: ## Clean tests cache

View File

@@ -5,6 +5,7 @@ package template
import ( import (
"io" "io"
"maps"
"strings" "strings"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
@@ -37,8 +38,15 @@ func TemplateForTenantAndNamespace(template string, tnt *capsulev1beta2.Tenant,
} }
// TemplateForTenantAndNamespace applies templating to all values in the provided map in place. // TemplateForTenantAndNamespace applies templating to all values in the provided map in place.
func TemplateForTenantAndNamespaceMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) { func TemplateForTenantAndNamespaceMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) map[string]string {
for k, v := range m { if len(m) == 0 {
m[k] = TemplateForTenantAndNamespace(v, tnt, ns) return map[string]string{}
} }
out := maps.Clone(m)
for k, v := range out {
out[k] = TemplateForTenantAndNamespace(v, tnt, ns)
}
return out
} }

View File

@@ -4,6 +4,7 @@
package template_test package template_test
import ( import (
"sync"
"testing" "testing"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@@ -15,17 +16,13 @@ import (
func newTenant(name string) *capsulev1beta2.Tenant { func newTenant(name string) *capsulev1beta2.Tenant {
return &capsulev1beta2.Tenant{ return &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: name},
Name: name,
},
} }
} }
func newNamespace(name string) *v1.Namespace { func newNamespace(name string) *v1.Namespace {
return &v1.Namespace{ return &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: name},
Name: name,
},
} }
} }
@@ -85,13 +82,15 @@ func TestTemplateForTenantAndNamespace_OnlyNamespace(t *testing.T) {
} }
} }
func TestTemplateForTenantAndNamespace_NoDelimitersReturnsEmpty(t *testing.T) { func TestTemplateForTenantAndNamespace_NoDelimiters_ReturnsInput(t *testing.T) {
tnt := newTenant("tenant-a") tnt := newTenant("tenant-a")
ns := newNamespace("ns-1") ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace("plain-value-without-templates", tnt, ns) in := "plain-value-without-templates"
if got != "plain-value-without-templates" { got := tpl.TemplateForTenantAndNamespace(in, tnt, ns)
t.Fatalf("expected empty string for input without delimiters, got %q", got)
if got != in {
t.Fatalf("expected %q, got %q", in, got)
} }
} }
@@ -111,19 +110,24 @@ func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholders(t *testing.T) {
tnt := newTenant("tenant-a") tnt := newTenant("tenant-a")
ns := newNamespace("ns-1") ns := newNamespace("ns-1")
m := map[string]string{ orig := map[string]string{
"key1": "tenant={{tenant.name}}, ns={{namespace}}", "key1": "tenant={{tenant.name}}, ns={{namespace}}",
"key2": "plain-value", "key2": "plain-value",
} }
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" { // output is templated
if got := out["key1"]; got != "tenant=tenant-a, ns=ns-1" {
t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got)
} }
if got := out["key2"]; got != "plain-value" {
t.Fatalf("key2: expected %q, got %q", "plain-value", got)
}
if got := m["key2"]; got != "plain-value" { // input map must remain unchanged (new behavior)
t.Fatalf("key2: expected %q to remain unchanged, got %q", "plain-value", got) if got := orig["key1"]; got != "tenant={{tenant.name}}, ns={{namespace}}" {
t.Fatalf("input map must not be mutated; key1 got %q", got)
} }
} }
@@ -131,41 +135,46 @@ func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholdersSpaces(t *testing.
tnt := newTenant("tenant-a") tnt := newTenant("tenant-a")
ns := newNamespace("ns-1") ns := newNamespace("ns-1")
m := map[string]string{ orig := map[string]string{
"key1": "tenant={{ tenant.name }}, ns={{ namespace }}", "key1": "tenant={{ tenant.name }}, ns={{ namespace }}",
"key2": "plain-value", "key2": "plain-value",
} }
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" { if got := out["key1"]; got != "tenant=tenant-a, ns=ns-1" {
t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got)
} }
if got := out["key2"]; got != "plain-value" {
t.Fatalf("key2: expected %q, got %q", "plain-value", got)
}
if got := m["key2"]; got != "plain-value" { // input map must remain unchanged
t.Fatalf("key2: expected %q to remain unchanged, got %q", "plain-value", got) if got := orig["key1"]; got != "tenant={{ tenant.name }}, ns={{ namespace }}" {
t.Fatalf("input map must not be mutated; key1 got %q", got)
} }
} }
func TestTemplateForTenantAndNamespaceMap_SkipsValuesWithoutDelimiters(t *testing.T) { func TestTemplateForTenantAndNamespaceMap_TransformsValuesWithDelimiters(t *testing.T) {
tnt := newTenant("tenant-a") tnt := newTenant("tenant-a")
ns := newNamespace("ns-1") ns := newNamespace("ns-1")
// Note: no space after '{{' and before '}}', so the guard should skip it orig := map[string]string{
m := map[string]string{ "t1": "hello {{tenant.name}}",
"noTemplate1": "hello {{tenant.name}}", "t2": "namespace {{namespace}}",
"noTemplate2": "namespace {{namespace}}", "t3": "static",
} }
original2 := m["noTemplate2"] out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) if got := out["t1"]; got != "hello tenant-a" {
t.Fatalf("t1: expected %q, got %q", "hello tenant-a", 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 != "namespace ns-1" { if got := out["t2"]; got != "namespace ns-1" {
t.Fatalf("noTemplate2: expected %q to remain unchanged, got %q", original2, got) t.Fatalf("t2: expected %q, got %q", "namespace ns-1", got)
}
if got := out["t3"]; got != "static" {
t.Fatalf("t3: expected %q, got %q", "static", got)
} }
} }
@@ -173,22 +182,22 @@ func TestTemplateForTenantAndNamespaceMap_MixedKeys(t *testing.T) {
tnt := newTenant("tenant-x") tnt := newTenant("tenant-x")
ns := newNamespace("ns-x") ns := newNamespace("ns-x")
m := map[string]string{ orig := map[string]string{
"onlyTenant": "T={{ tenant.name }}", "onlyTenant": "T={{ tenant.name }}",
"onlyNS": "N={{ namespace }}", "onlyNS": "N={{ namespace }}",
"none": "static", "none": "static",
} }
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
if got := m["onlyTenant"]; got != "T=tenant-x" { if got := out["onlyTenant"]; got != "T=tenant-x" {
t.Fatalf("onlyTenant: expected %q, got %q", "T=tenant-x", got) t.Fatalf("onlyTenant: expected %q, got %q", "T=tenant-x", got)
} }
if got := m["onlyNS"]; got != "N=ns-x" { if got := out["onlyNS"]; got != "N=ns-x" {
t.Fatalf("onlyNS: expected %q, got %q", "N=ns-x", got) t.Fatalf("onlyNS: expected %q, got %q", "N=ns-x", got)
} }
if got := m["none"]; got != "static" { if got := out["none"]; got != "static" {
t.Fatalf("none: expected %q to remain unchanged, got %q", "static", got) t.Fatalf("none: expected %q, got %q", "static", got)
} }
} }
@@ -196,14 +205,86 @@ func TestTemplateForTenantAndNamespaceMap_UnknownKeyBecomesEmpty(t *testing.T) {
tnt := newTenant("tenant-a") tnt := newTenant("tenant-a")
ns := newNamespace("ns-1") ns := newNamespace("ns-1")
m := map[string]string{ orig := map[string]string{
"unknown": "X={{ unknown.key }}", "unknown": "X={{ unknown.key }}",
} }
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns) out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
// fasttemplate with missing key returns an empty string for that placeholder if got := out["unknown"]; got != "X=" {
if got := m["unknown"]; got != "X=" {
t.Fatalf("unknown: expected %q, got %q", "X=", got) t.Fatalf("unknown: expected %q, got %q", "X=", got)
} }
} }
func TestTemplateForTenantAndNamespaceMap_EmptyOrNilInput(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
// nil map
outNil := tpl.TemplateForTenantAndNamespaceMap(nil, tnt, ns)
if outNil == nil {
t.Fatalf("expected non-nil map for nil input")
}
if len(outNil) != 0 {
t.Fatalf("expected empty map for nil input, got %v", outNil)
}
// empty map
outEmpty := tpl.TemplateForTenantAndNamespaceMap(map[string]string{}, tnt, ns)
if outEmpty == nil || len(outEmpty) != 0 {
t.Fatalf("expected empty map, got %v", outEmpty)
}
}
// Concurrency test: should never panic with "concurrent map writes"
// Run with: go test -race ./...
func TestTemplateForTenantAndNamespaceMap_Concurrency(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
// Shared input map across goroutines (this used to be unsafe if the function mutated in-place)
shared := map[string]string{
"k1": "tenant={{tenant.name}}",
"k2": "ns={{namespace}}",
"k3": "static",
}
const goroutines = 50
const iterations = 200
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
out := tpl.TemplateForTenantAndNamespaceMap(shared, tnt, ns)
// sanity checks
if out["k1"] != "tenant=tenant-a" {
t.Errorf("unexpected k1: %q", out["k1"])
return
}
if out["k2"] != "ns=ns-1" {
t.Errorf("unexpected k2: %q", out["k2"])
return
}
if out["k3"] != "static" {
t.Errorf("unexpected k3: %q", out["k3"])
return
}
}
}()
}
wg.Wait()
// verify input map was not mutated
if shared["k1"] != "tenant={{tenant.name}}" {
t.Fatalf("input map mutated under concurrency: k1=%q", shared["k1"])
}
if shared["k2"] != "ns={{namespace}}" {
t.Fatalf("input map mutated under concurrency: k2=%q", shared["k2"])
}
}

View File

@@ -0,0 +1,337 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package tenant_test
import (
"sync"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
tenant "github.com/projectcapsule/capsule/pkg/utils/tenant"
)
// Helpers
func ns(name string, uid types.UID) *corev1.Namespace {
n := &corev1.Namespace{}
n.SetName(name)
n.SetUID(uid)
return n
}
func tenantWithName(name string) *capsulev1beta2.Tenant {
t := &capsulev1beta2.Tenant{}
t.SetName(name)
if t.Annotations == nil {
t.Annotations = map[string]string{}
}
return t
}
func mustInstance(t *capsulev1beta2.Tenant, name string, uid types.UID, labels, ann map[string]string) {
// Ensure Status + instance storage exists in your impl;
// if TenantStatus needs initialization in your project, do it here.
item := &capsulev1beta2.TenantStatusNamespaceItem{
Name: name,
UID: uid,
Metadata: &capsulev1beta2.TenantStatusNamespaceMetadata{
Labels: labels,
Annotations: ann,
},
}
t.Status.UpdateInstance(item)
}
// --- Tests
func TestAddNamespaceNameLabels(t *testing.T) {
t.Parallel()
labels := map[string]string{"keep": "me"}
n := ns("myns", "u1")
tenant.AddNamespaceNameLabels(labels, n)
if got := labels["kubernetes.io/metadata.name"]; got != "myns" {
t.Fatalf("expected kubernetes.io/metadata.name to be %q, got %q", "myns", got)
}
if got := labels["keep"]; got != "me" {
t.Fatalf("expected existing key to remain, got %q", got)
}
}
func TestAddTenantNameLabel(t *testing.T) {
t.Parallel()
labels := map[string]string{}
n := ns("myns", "u1")
tt := tenantWithName("mytenant")
tenant.AddTenantNameLabel(labels, n, tt)
if got := labels[meta.TenantLabel]; got != "mytenant" {
t.Fatalf("expected %s to be %q, got %q", meta.TenantLabel, "mytenant", got)
}
}
func TestBuildInstanceMetadataForNamespace_NoInstance(t *testing.T) {
t.Parallel()
n := ns("myns", "u1")
tt := tenantWithName("t1")
labels, annotations := tenant.BuildInstanceMetadataForNamespace(n, tt)
if labels == nil || annotations == nil {
t.Fatalf("expected non-nil maps")
}
if len(labels) != 0 || len(annotations) != 0 {
t.Fatalf("expected empty maps, got labels=%v annotations=%v", labels, annotations)
}
}
func TestBuildInstanceMetadataForNamespace_WithInstance(t *testing.T) {
t.Parallel()
n := ns("myns", "u1")
tt := tenantWithName("t1")
origLabels := map[string]string{"a": "1"}
origAnn := map[string]string{"x": "y"}
mustInstance(tt, n.GetName(), n.GetUID(), origLabels, origAnn)
labels, annotations := tenant.BuildInstanceMetadataForNamespace(n, tt)
// Implementation returns instance.Metadata maps directly (not cloned)
if labels["a"] != "1" || annotations["x"] != "y" {
t.Fatalf("unexpected returned metadata: labels=%v annotations=%v", labels, annotations)
}
}
func TestBuildNamespaceLabelsForTenant(t *testing.T) {
t.Parallel()
t.Run("additional labels copied + cordoned", func(t *testing.T) {
t.Parallel()
tt := tenantWithName("t1")
tt.Spec.Cordoned = true
tt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
//nolint:staticcheck
AdditionalMetadata: &api.AdditionalMetadataSpec{
Labels: map[string]string{
"base": "label",
},
},
}
labels := tenant.BuildNamespaceLabelsForTenant(tt)
if labels["base"] != "label" {
t.Fatalf("expected base label copied, got %v", labels)
}
if labels[meta.CordonedLabel] != "true" {
t.Fatalf("expected cordoned label true, got %v", labels[meta.CordonedLabel])
}
})
}
func TestBuildNamespaceAnnotationsForTenant(t *testing.T) {
t.Parallel()
t.Run("copies additional annotations and forwards forbidden annotations", func(t *testing.T) {
t.Parallel()
tt := tenantWithName("t1")
tt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
//nolint:staticcheck
AdditionalMetadata: &api.AdditionalMetadataSpec{
Annotations: map[string]string{
"a": "b",
},
},
}
tt.Annotations[meta.ForbiddenNamespaceLabelsAnnotation] = "l1,l2"
tt.Annotations[meta.ForbiddenNamespaceAnnotationsAnnotation] = "a1,a2"
ann := tenant.BuildNamespaceAnnotationsForTenant(tt)
if ann["a"] != "b" {
t.Fatalf("expected additional annotation copied, got %v", ann)
}
if ann[meta.ForbiddenNamespaceLabelsAnnotation] != "l1,l2" {
t.Fatalf("expected forbidden labels annotation forwarded, got %v", ann[meta.ForbiddenNamespaceLabelsAnnotation])
}
if ann[meta.ForbiddenNamespaceAnnotationsAnnotation] != "a1,a2" {
t.Fatalf("expected forbidden annotations forwarded, got %v", ann[meta.ForbiddenNamespaceAnnotationsAnnotation])
}
})
t.Run("ingress/storage/registry exact join", func(t *testing.T) {
t.Parallel()
tt := tenantWithName("t1")
tt.Spec.IngressOptions.AllowedClasses = &api.DefaultAllowedListSpec{
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
AllowedListSpec: api.AllowedListSpec{
Exact: []string{"nginx", "traefik"},
},
},
}
tt.Spec.StorageClasses = &api.DefaultAllowedListSpec{
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
AllowedListSpec: api.AllowedListSpec{
Exact: []string{"fast", "slow"},
},
},
}
tt.Spec.ContainerRegistries = &api.AllowedListSpec{
Exact: []string{"docker.io", "ghcr.io"},
}
ann := tenant.BuildNamespaceAnnotationsForTenant(tt)
if ann[meta.AvailableIngressClassesAnnotation] != "nginx,traefik" {
t.Fatalf("unexpected ingress exact annotation: %v", ann[meta.AvailableIngressClassesAnnotation])
}
if ann[meta.AvailableStorageClassesAnnotation] != "fast,slow" {
t.Fatalf("unexpected storage exact annotation: %v", ann[meta.AvailableStorageClassesAnnotation])
}
if ann[meta.AllowedRegistriesAnnotation] != "docker.io,ghcr.io" {
t.Fatalf("unexpected registries exact annotation: %v", ann[meta.AllowedRegistriesAnnotation])
}
})
}
func TestBuildNamespaceMetadataForTenant_AppliesAdditionalMetadataAndDoesNotOverwrite(t *testing.T) {
t.Parallel()
tt := tenantWithName("tenant-x")
// Base AdditionalMetadata (staticcheck path)
tt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
//nolint:staticcheck
AdditionalMetadata: &api.AdditionalMetadataSpec{
Labels: map[string]string{
"base": "keep",
"dup": "base-val",
},
Annotations: map[string]string{
"baseAnn": "keep",
"dupAnn": "base-ann",
},
},
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
{
// Use an “empty selector” that should match everything in your selector logic.
// If your IsNamespaceSelectedBySelector behaves differently, set selector accordingly.
NamespaceSelector: nil,
Labels: map[string]string{
"extra": "{{ tenant.name }}",
"dup": "should-not-overwrite",
"namespaceKey": "{{ namespace }}",
},
Annotations: map[string]string{
"extraAnn": "{{ tenant.name }}",
"dupAnn": "should-not-overwrite",
},
},
},
}
n := ns("ns-1", "u1")
labels, ann, err := tenant.BuildNamespaceMetadataForTenant(n, tt)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
// templating must apply
if labels["extra"] != "tenant-x" {
t.Fatalf("expected templated label extra=tenant-x, got %q", labels["extra"])
}
if labels["namespaceKey"] != "ns-1" {
t.Fatalf("expected templated namespaceKey=ns-1, got %q", labels["namespaceKey"])
}
if ann["extraAnn"] != "tenant-x" {
t.Fatalf("expected templated annotation extraAnn=tenant-x, got %q", ann["extraAnn"])
}
// MapMergeNoOverrite means base wins on duplicates
if labels["dup"] != "base-val" {
t.Fatalf("expected duplicate label to remain base-val, got %q", labels["dup"])
}
if ann["dupAnn"] != "base-ann" {
t.Fatalf("expected duplicate annotation to remain base-ann, got %q", ann["dupAnn"])
}
// base keys remain
if labels["base"] != "keep" || ann["baseAnn"] != "keep" {
t.Fatalf("expected base metadata to remain, labels=%v ann=%v", labels, ann)
}
}
func TestBuildNamespaceMetadataForTenant_Concurrency_NoConcurrentMapWrites(t *testing.T) {
// Dont run this test in parallel with other tests if your package has global shared state.
// Keep it isolated; this is meant to be run with: go test -race ./...
tt := tenantWithName("tenant-race")
n := ns("ns-race", "u-race")
// Critical: reuse the SAME maps inside AdditionalMetadataList across goroutines.
// If TemplateForTenantAndNamespaceMap still mutates in-place, this can panic.
sharedLabels := map[string]string{
"l1": "{{ tenant.name }}",
"l2": "{{ namespace }}",
}
sharedAnn := map[string]string{
"a1": "{{ tenant.name }}",
}
tt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
{
NamespaceSelector: nil,
Labels: sharedLabels,
Annotations: sharedAnn,
},
},
}
const goroutines = 50
const iterations = 200
var wg sync.WaitGroup
wg.Add(goroutines)
errCh := make(chan error, goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
_, _, err := tenant.BuildNamespaceMetadataForTenant(n, tt)
if err != nil {
errCh <- err
return
}
}
}()
}
wg.Wait()
close(errCh)
for err := range errCh {
t.Fatalf("unexpected error under concurrency: %v", err)
}
}

View File

@@ -59,11 +59,11 @@ func BuildNamespaceMetadataForTenant(ns *corev1.Namespace, tnt *capsulev1beta2.T
continue continue
} }
template.TemplateForTenantAndNamespaceMap(md.Labels, tnt, ns) tLabels := template.TemplateForTenantAndNamespaceMap(md.Labels, tnt, ns)
template.TemplateForTenantAndNamespaceMap(md.Annotations, tnt, ns) tAnnotations := template.TemplateForTenantAndNamespaceMap(md.Annotations, tnt, ns)
utils.MapMergeNoOverrite(labels, md.Labels) utils.MapMergeNoOverrite(labels, tLabels)
utils.MapMergeNoOverrite(annotations, md.Annotations) utils.MapMergeNoOverrite(annotations, tAnnotations)
} }
} }

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2025 Project Capsule Authors // Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package tenant package tenant_test
import ( import (
"testing" "testing"
@@ -9,6 +9,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
tenant "github.com/projectcapsule/capsule/pkg/utils/tenant"
) )
func TestIsTenantOwnerReference(t *testing.T) { func TestIsTenantOwnerReference(t *testing.T) {
@@ -23,7 +24,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "valid tenant ownerRef with exact group and version", name: "valid tenant ownerRef with exact group and version",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1beta2", APIVersion: capsuleGroup + "/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: true, want: true,
@@ -32,7 +33,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "valid tenant ownerRef with same group but different version", name: "valid tenant ownerRef with same group but different version",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1", APIVersion: capsuleGroup + "/v1",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: true, // we intentionally only check the group, not the version want: true, // we intentionally only check the group, not the version
@@ -41,7 +42,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "wrong group", name: "wrong group",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "other.group.io/v1beta2", APIVersion: "other.group.io/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: false, want: false,
@@ -59,7 +60,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "empty APIVersion", name: "empty APIVersion",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "", APIVersion: "",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: false, want: false,
@@ -68,7 +69,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "APIVersion without slash (only version)", name: "APIVersion without slash (only version)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "v1beta2", APIVersion: "v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: false, want: false,
@@ -77,7 +78,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "APIVersion with empty group", name: "APIVersion with empty group",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "/v1beta2", APIVersion: "/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: false, want: false,
@@ -86,7 +87,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "APIVersion with empty version", name: "APIVersion with empty version",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "", APIVersion: "",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: false, want: false,
@@ -95,7 +96,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
name: "APIVersion with extra slash in version (still ok as long as group matches)", name: "APIVersion with extra slash in version (still ok as long as group matches)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1beta2/extra", APIVersion: capsuleGroup + "/v1beta2/extra",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
want: false, want: false,
@@ -114,7 +115,7 @@ func TestIsTenantOwnerReference(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tt := tt // capture tt := tt // capture
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := IsTenantOwnerReference(tt.or) got := tenant.IsTenantOwnerReference(tt.or)
if got != tt.want { if got != tt.want {
t.Fatalf("IsTenantOwnerReference(%+v) = %v, want %v", tt.or, got, tt.want) t.Fatalf("IsTenantOwnerReference(%+v) = %v, want %v", tt.or, got, tt.want)
} }
@@ -135,7 +136,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "valid tenant ownerRef with exact group and version (same tenant)", name: "valid tenant ownerRef with exact group and version (same tenant)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1beta2", APIVersion: capsuleGroup + "/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -149,7 +150,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "valid tenant ownerRef with exact group and version (different tenant)", name: "valid tenant ownerRef with exact group and version (different tenant)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1beta2", APIVersion: capsuleGroup + "/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -163,7 +164,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "valid tenant ownerRef with same group but different version (same tenant)", name: "valid tenant ownerRef with same group but different version (same tenant)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1", APIVersion: capsuleGroup + "/v1",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -177,7 +178,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "valid tenant ownerRef with same group but different version (different tenant)", name: "valid tenant ownerRef with same group but different version (different tenant)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1", APIVersion: capsuleGroup + "/v1",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -191,7 +192,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "wrong group", name: "wrong group",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "other.group.io/v1beta2", APIVersion: "other.group.io/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -219,7 +220,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "empty APIVersion", name: "empty APIVersion",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "", APIVersion: "",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -233,7 +234,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "empty tenant", name: "empty tenant",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1", APIVersion: capsuleGroup + "/v1",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: nil, tenant: nil,
@@ -243,7 +244,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "APIVersion without slash (only version)", name: "APIVersion without slash (only version)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "v1beta2", APIVersion: "v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -257,7 +258,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "APIVersion with empty group", name: "APIVersion with empty group",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "/v1beta2", APIVersion: "/v1beta2",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -271,7 +272,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "APIVersion with empty version", name: "APIVersion with empty version",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: "", APIVersion: "",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -285,7 +286,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
name: "APIVersion with extra slash in version (still ok as long as group matches)", name: "APIVersion with extra slash in version (still ok as long as group matches)",
or: metav1.OwnerReference{ or: metav1.OwnerReference{
APIVersion: capsuleGroup + "/v1beta2/extra", APIVersion: capsuleGroup + "/v1beta2/extra",
Kind: ObjectReferenceTenantKind, Kind: tenant.ObjectReferenceTenantKind,
Name: "my-tenant", Name: "my-tenant",
}, },
tenant: &capsulev1beta2.Tenant{ tenant: &capsulev1beta2.Tenant{
@@ -314,7 +315,7 @@ func TestIsTenantOwnerReferenceForTenant(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tt := tt // capture tt := tt // capture
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := IsTenantOwnerReferenceForTenant(tt.or, tt.tenant) got := tenant.IsTenantOwnerReferenceForTenant(tt.or, tt.tenant)
if got != tt.want { if got != tt.want {
t.Fatalf("IsTenantOwnerReference(%+v) = %v, want %v", tt.or, got, tt.want) t.Fatalf("IsTenantOwnerReference(%+v) = %v, want %v", tt.or, got, tt.want)
} }