Files
Reloader/internal/pkg/reload/predicate_test.go
T
2026-06-22 20:34:40 +05:00

987 lines
25 KiB
Go

package reload
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/event"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/config"
)
func TestNamespaceFilterPredicate_Create(t *testing.T) {
tests := []struct {
name string
ignoredNamespaces []string
eventNamespace string
wantAllow bool
}{
{
name: "allow non-ignored namespace",
ignoredNamespaces: []string{"kube-system"},
eventNamespace: "default",
wantAllow: true,
},
{
name: "block ignored namespace",
ignoredNamespaces: []string{"kube-system"},
eventNamespace: "kube-system",
wantAllow: false,
},
{
name: "allow when no namespaces ignored",
ignoredNamespaces: []string{},
eventNamespace: "kube-system",
wantAllow: true,
},
{
name: "block multiple ignored namespaces",
ignoredNamespaces: []string{"kube-system", "kube-public", "test-ns"},
eventNamespace: "test-ns",
wantAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = tt.ignoredNamespaces
predicate := NamespaceFilterPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: tt.eventNamespace,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
func TestNamespaceFilterPredicate_Update(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
predicate := NamespaceFilterPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
}
e := event.UpdateEvent{ObjectNew: cm}
if !predicate.Update(e) {
t.Error("Update() should allow non-ignored namespace")
}
cm.Namespace = "kube-system"
e = event.UpdateEvent{ObjectNew: cm}
if predicate.Update(e) {
t.Error("Update() should block ignored namespace")
}
}
func TestNamespaceFilterPredicate_Delete(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
predicate := NamespaceFilterPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
}
e := event.DeleteEvent{Object: cm}
if !predicate.Delete(e) {
t.Error("Delete() should allow non-ignored namespace")
}
}
func TestNamespaceFilterPredicate_Generic(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
predicate := NamespaceFilterPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
}
e := event.GenericEvent{Object: cm}
if !predicate.Generic(e) {
t.Error("Generic() should allow non-ignored namespace")
}
}
func TestLabelSelectorPredicate_Create(t *testing.T) {
tests := []struct {
name string
selector string
objectLabels map[string]string
wantAllow bool
}{
{
name: "match single label",
selector: "app=reloader",
objectLabels: map[string]string{"app": "reloader"},
wantAllow: true,
},
{
name: "no match single label",
selector: "app=reloader",
objectLabels: map[string]string{"app": "other"},
wantAllow: false,
},
{
name: "match multiple labels",
selector: "app=reloader,env=prod",
objectLabels: map[string]string{"app": "reloader", "env": "prod", "extra": "value"},
wantAllow: true,
},
{
name: "partial match fails",
selector: "app=reloader,env=prod",
objectLabels: map[string]string{"app": "reloader"},
wantAllow: false,
},
{
name: "empty labels no match",
selector: "app=reloader",
objectLabels: map[string]string{},
wantAllow: false,
},
{
name: "nil labels no match",
selector: "app=reloader",
objectLabels: nil,
wantAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cfg := config.NewDefault()
selector, err := labels.Parse(tt.selector)
if err != nil {
t.Fatalf("Failed to parse selector: %v", err)
}
cfg.ResourceSelectors = []labels.Selector{selector}
predicate := LabelSelectorPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: tt.objectLabels,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
func TestLabelSelectorPredicate_NoSelectors(t *testing.T) {
cfg := config.NewDefault()
predicate := LabelSelectorPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: map[string]string{"any": "label"},
},
}
e := event.CreateEvent{Object: cm}
if !predicate.Create(e) {
t.Error("Create() should allow all when no selectors configured")
}
}
func TestLabelSelectorPredicate_MultipleSelectors(t *testing.T) {
cfg := config.NewDefault()
selector1, _ := labels.Parse("app=reloader")
selector2, _ := labels.Parse("type=config")
cfg.ResourceSelectors = []labels.Selector{selector1, selector2}
predicate := LabelSelectorPredicate(cfg)
tests := []struct {
name string
labels map[string]string
wantAllow bool
}{
{
name: "matches first selector",
labels: map[string]string{"app": "reloader"},
wantAllow: true,
},
{
name: "matches second selector",
labels: map[string]string{"type": "config"},
wantAllow: true,
},
{
name: "matches both selectors",
labels: map[string]string{"app": "reloader", "type": "config"},
wantAllow: true,
},
{
name: "matches neither selector",
labels: map[string]string{"other": "value"},
wantAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: tt.labels,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
func TestLabelSelectorPredicate_Update(t *testing.T) {
cfg := config.NewDefault()
selector, _ := labels.Parse("app=reloader")
cfg.ResourceSelectors = []labels.Selector{selector}
predicate := LabelSelectorPredicate(cfg)
cmMatching := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: map[string]string{"app": "reloader"},
},
}
cmNotMatching := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: map[string]string{"app": "other"},
},
}
e := event.UpdateEvent{ObjectNew: cmMatching}
if !predicate.Update(e) {
t.Error("Update() should allow matching labels")
}
e = event.UpdateEvent{ObjectNew: cmNotMatching}
if predicate.Update(e) {
t.Error("Update() should block non-matching labels")
}
}
func TestLabelSelectorPredicate_Delete(t *testing.T) {
cfg := config.NewDefault()
selector, _ := labels.Parse("app=reloader")
cfg.ResourceSelectors = []labels.Selector{selector}
predicate := LabelSelectorPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: map[string]string{"app": "reloader"},
},
}
e := event.DeleteEvent{Object: cm}
if !predicate.Delete(e) {
t.Error("Delete() should allow matching labels")
}
}
func TestLabelSelectorPredicate_Generic(t *testing.T) {
cfg := config.NewDefault()
selector, _ := labels.Parse("app=reloader")
cfg.ResourceSelectors = []labels.Selector{selector}
predicate := LabelSelectorPredicate(cfg)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: map[string]string{"app": "reloader"},
},
}
e := event.GenericEvent{Object: cm}
if !predicate.Generic(e) {
t.Error("Generic() should allow matching labels")
}
}
func TestCombinedFiltering(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
selector, _ := labels.Parse("managed=true")
cfg.ResourceSelectors = []labels.Selector{selector}
nsPredicate := NamespaceFilterPredicate(cfg)
labelPredicate := LabelSelectorPredicate(cfg)
tests := []struct {
name string
namespace string
labels map[string]string
wantNSAllow bool
wantLabelAllow bool
}{
{
name: "allowed namespace and matching labels",
namespace: "default",
labels: map[string]string{"managed": "true"},
wantNSAllow: true,
wantLabelAllow: true,
},
{
name: "allowed namespace but non-matching labels",
namespace: "default",
labels: map[string]string{"managed": "false"},
wantNSAllow: true,
wantLabelAllow: false,
},
{
name: "ignored namespace with matching labels",
namespace: "kube-system",
labels: map[string]string{"managed": "true"},
wantNSAllow: false,
wantLabelAllow: true,
},
{
name: "ignored namespace and non-matching labels",
namespace: "kube-system",
labels: map[string]string{"managed": "false"},
wantNSAllow: false,
wantLabelAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: tt.namespace,
Labels: tt.labels,
},
}
e := event.CreateEvent{Object: cm}
gotNS := nsPredicate.Create(e)
if gotNS != tt.wantNSAllow {
t.Errorf("Namespace predicate Create() = %v, want %v", gotNS, tt.wantNSAllow)
}
gotLabel := labelPredicate.Create(e)
if gotLabel != tt.wantLabelAllow {
t.Errorf("Label predicate Create() = %v, want %v", gotLabel, tt.wantLabelAllow)
}
combinedAllow := gotNS && gotLabel
expectedCombined := tt.wantNSAllow && tt.wantLabelAllow
if combinedAllow != expectedCombined {
t.Errorf("Combined allow = %v, want %v", combinedAllow, expectedCombined)
}
},
)
}
}
func TestFilteringWithSecrets(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
nsPredicate := NamespaceFilterPredicate(cfg)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
},
}
e := event.CreateEvent{Object: secret}
if !nsPredicate.Create(e) {
t.Error("Should allow secret in non-ignored namespace")
}
secret.Namespace = "kube-system"
e = event.CreateEvent{Object: secret}
if nsPredicate.Create(e) {
t.Error("Should block secret in ignored namespace")
}
}
func TestExistsLabelSelector(t *testing.T) {
cfg := config.NewDefault()
selector, _ := labels.Parse("managed")
cfg.ResourceSelectors = []labels.Selector{selector}
predicate := LabelSelectorPredicate(cfg)
tests := []struct {
name string
labels map[string]string
wantAllow bool
}{
{
name: "label exists with value true",
labels: map[string]string{"managed": "true"},
wantAllow: true,
},
{
name: "label exists with value false",
labels: map[string]string{"managed": "false"},
wantAllow: true,
},
{
name: "label exists with empty value",
labels: map[string]string{"managed": ""},
wantAllow: true,
},
{
name: "label does not exist",
labels: map[string]string{"other": "value"},
wantAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Labels: tt.labels,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
// mockNamespaceChecker implements NamespaceChecker for testing.
type mockNamespaceChecker struct {
allowed map[string]bool
}
func (m *mockNamespaceChecker) Contains(name string) bool {
return m.allowed[name]
}
func TestNamespaceFilterPredicateWithCache(t *testing.T) {
tests := []struct {
name string
ignoredNamespaces []string
cacheAllowed map[string]bool
eventNamespace string
wantAllow bool
}{
{
name: "allowed by cache and not ignored",
ignoredNamespaces: []string{"kube-system"},
cacheAllowed: map[string]bool{"production": true},
eventNamespace: "production",
wantAllow: true,
},
{
name: "blocked by cache",
ignoredNamespaces: []string{},
cacheAllowed: map[string]bool{"production": true},
eventNamespace: "staging",
wantAllow: false,
},
{
name: "blocked by ignore list even if in cache",
ignoredNamespaces: []string{"kube-system"},
cacheAllowed: map[string]bool{"kube-system": true},
eventNamespace: "kube-system",
wantAllow: false,
},
{
name: "ignore list checked before cache",
ignoredNamespaces: []string{"blocked-ns"},
cacheAllowed: map[string]bool{"blocked-ns": true},
eventNamespace: "blocked-ns",
wantAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = tt.ignoredNamespaces
cache := &mockNamespaceChecker{allowed: tt.cacheAllowed}
predicate := NamespaceFilterPredicateWithCache(cfg, cache)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: tt.eventNamespace,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
func TestNamespaceFilterPredicateWithCache_NilCache(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
predicate := NamespaceFilterPredicateWithCache(cfg, nil)
tests := []struct {
namespace string
wantAllow bool
}{
{"default", true},
{"production", true},
{"kube-system", false}, // Should still respect ignore list
}
for _, tt := range tests {
t.Run(
tt.namespace, func(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: tt.namespace,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v for namespace %s", got, tt.wantAllow, tt.namespace)
}
},
)
}
}
func TestIgnoreAnnotationPredicate_Create(t *testing.T) {
cfg := config.NewDefault()
predicate := IgnoreAnnotationPredicate(cfg)
tests := []struct {
name string
annotations map[string]string
wantAllow bool
}{
{
name: "no annotations",
annotations: nil,
wantAllow: true,
},
{
name: "empty annotations",
annotations: map[string]string{},
wantAllow: true,
},
{
name: "other annotations only",
annotations: map[string]string{"other": "value"},
wantAllow: true,
},
{
name: "ignore annotation true",
annotations: map[string]string{cfg.Annotations.Ignore: "true"},
wantAllow: false,
},
{
name: "ignore annotation false",
annotations: map[string]string{cfg.Annotations.Ignore: "false"},
wantAllow: true,
},
{
name: "ignore annotation with other value",
annotations: map[string]string{cfg.Annotations.Ignore: "yes"},
wantAllow: true,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Annotations: tt.annotations,
},
}
e := event.CreateEvent{Object: cm}
got := predicate.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
func TestIgnoreAnnotationPredicate_AllEventTypes(t *testing.T) {
cfg := config.NewDefault()
predicate := IgnoreAnnotationPredicate(cfg)
ignoredCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "ignored-cm",
Namespace: "default",
Annotations: map[string]string{cfg.Annotations.Ignore: "true"},
},
}
allowedCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-cm",
Namespace: "default",
},
}
if predicate.Update(event.UpdateEvent{ObjectNew: ignoredCM}) {
t.Error("Update() should block ignored resource")
}
if !predicate.Update(event.UpdateEvent{ObjectNew: allowedCM}) {
t.Error("Update() should allow non-ignored resource")
}
if predicate.Delete(event.DeleteEvent{Object: ignoredCM}) {
t.Error("Delete() should block ignored resource")
}
if !predicate.Delete(event.DeleteEvent{Object: allowedCM}) {
t.Error("Delete() should allow non-ignored resource")
}
if predicate.Generic(event.GenericEvent{Object: ignoredCM}) {
t.Error("Generic() should block ignored resource")
}
if !predicate.Generic(event.GenericEvent{Object: allowedCM}) {
t.Error("Generic() should allow non-ignored resource")
}
}
func TestCombinedPredicates(t *testing.T) {
cfg := config.NewDefault()
cfg.IgnoredNamespaces = []string{"kube-system"}
nsPredicate := NamespaceFilterPredicate(cfg)
ignorePredicate := IgnoreAnnotationPredicate(cfg)
combined := CombinedPredicates(nsPredicate, ignorePredicate)
tests := []struct {
name string
namespace string
annotations map[string]string
wantAllow bool
}{
{
name: "both predicates pass",
namespace: "default",
annotations: nil,
wantAllow: true,
},
{
name: "namespace predicate fails",
namespace: "kube-system",
annotations: nil,
wantAllow: false,
},
{
name: "ignore predicate fails",
namespace: "default",
annotations: map[string]string{cfg.Annotations.Ignore: "true"},
wantAllow: false,
},
{
name: "both predicates fail",
namespace: "kube-system",
annotations: map[string]string{cfg.Annotations.Ignore: "true"},
wantAllow: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: tt.namespace,
Annotations: tt.annotations,
},
}
e := event.CreateEvent{Object: cm}
got := combined.Create(e)
if got != tt.wantAllow {
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
}
},
)
}
}
func TestConfigMapPredicates_Update(t *testing.T) {
cfg := config.NewDefault()
hasher := NewHasher()
predicate := ConfigMapPredicates(cfg, hasher)
oldCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Data: map[string]string{"key": "value1"},
}
newCMSameContent := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Data: map[string]string{"key": "value1"},
}
newCMDifferentContent := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Data: map[string]string{"key": "value2"},
}
e := event.UpdateEvent{ObjectOld: oldCM, ObjectNew: newCMSameContent}
if predicate.Update(e) {
t.Error("Update() should return false when content is the same")
}
e = event.UpdateEvent{ObjectOld: oldCM, ObjectNew: newCMDifferentContent}
if !predicate.Update(e) {
t.Error("Update() should return true when content changed")
}
}
func TestConfigMapPredicates_InvalidTypes(t *testing.T) {
cfg := config.NewDefault()
hasher := NewHasher()
predicate := ConfigMapPredicates(cfg, hasher)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
}
e := event.UpdateEvent{ObjectOld: secret, ObjectNew: cm}
if predicate.Update(e) {
t.Error("Update() should return false for mismatched types")
}
e = event.UpdateEvent{ObjectOld: secret, ObjectNew: secret}
if predicate.Update(e) {
t.Error("Update() should return false for non-ConfigMap types")
}
}
func TestConfigMapPredicates_CreateDeleteGeneric(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnCreate = true
cfg.ReloadOnDelete = true
hasher := NewHasher()
predicate := ConfigMapPredicates(cfg, hasher)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
}
if !predicate.Create(event.CreateEvent{Object: cm}) {
t.Error("Create() should return true when ReloadOnCreate is true")
}
if !predicate.Delete(event.DeleteEvent{Object: cm}) {
t.Error("Delete() should return true when ReloadOnDelete is true")
}
if predicate.Generic(event.GenericEvent{Object: cm}) {
t.Error("Generic() should always return false")
}
}
func TestSecretPredicates_Update(t *testing.T) {
cfg := config.NewDefault()
hasher := NewHasher()
predicate := SecretPredicates(cfg, hasher)
oldSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Data: map[string][]byte{"key": []byte("value1")},
}
newSecretSameContent := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Data: map[string][]byte{"key": []byte("value1")},
}
newSecretDifferentContent := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Data: map[string][]byte{"key": []byte("value2")},
}
e := event.UpdateEvent{ObjectOld: oldSecret, ObjectNew: newSecretSameContent}
if predicate.Update(e) {
t.Error("Update() should return false when content is the same")
}
e = event.UpdateEvent{ObjectOld: oldSecret, ObjectNew: newSecretDifferentContent}
if !predicate.Update(e) {
t.Error("Update() should return true when content changed")
}
}
func TestSecretPredicates_InvalidTypes(t *testing.T) {
cfg := config.NewDefault()
hasher := NewHasher()
predicate := SecretPredicates(cfg, hasher)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
}
e := event.UpdateEvent{ObjectOld: cm, ObjectNew: secret}
if predicate.Update(e) {
t.Error("Update() should return false for mismatched types")
}
e = event.UpdateEvent{ObjectOld: cm, ObjectNew: cm}
if predicate.Update(e) {
t.Error("Update() should return false for non-Secret types")
}
}
func TestLabelsSet(t *testing.T) {
ls := LabelsSet{"app": "test", "env": "prod"}
if !ls.Has("app") {
t.Error("Has(app) should return true")
}
if ls.Has("nonexistent") {
t.Error("Has(nonexistent) should return false")
}
if ls.Get("app") != "test" {
t.Errorf("Get(app) = %v, want test", ls.Get("app"))
}
if ls.Get("env") != "prod" {
t.Errorf("Get(env) = %v, want prod", ls.Get("env"))
}
if ls.Get("nonexistent") != "" {
t.Errorf("Get(nonexistent) = %v, want empty string", ls.Get("nonexistent"))
}
}
func TestSecretProviderClassPodStatusPredicates(t *testing.T) {
cfg := config.NewDefault()
p := SecretProviderClassPodStatusPredicates(cfg, NewHasher())
oldObj := &csiv1.SecretProviderClassPodStatus{
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "1"}},
},
}
newObjChanged := &csiv1.SecretProviderClassPodStatus{
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "2"}},
},
}
newObjSame := oldObj.DeepCopy()
// Create and Delete are always ignored for SPCPS.
if p.Create(event.CreateEvent{Object: oldObj}) {
t.Fatal("CreateFunc should return false")
}
if p.Delete(event.DeleteEvent{Object: oldObj}) {
t.Fatal("DeleteFunc should return false")
}
// Update only when the status hash changes.
if !p.Update(event.UpdateEvent{ObjectOld: oldObj, ObjectNew: newObjChanged}) {
t.Fatal("UpdateFunc should return true on changed status")
}
if p.Update(event.UpdateEvent{ObjectOld: oldObj, ObjectNew: newObjSame}) {
t.Fatal("UpdateFunc should return false on unchanged status")
}
// A metadata/label-only change (same Status) must NOT trigger a reload,
// since the predicate hashes only the status. (Master tested this via
// UpdateSecretProviderClassPodStatusLabels.)
labelOnly := oldObj.DeepCopy()
labelOnly.Labels = map[string]string{"unrelated": "changed"}
labelOnly.Annotations = map[string]string{"note": "touched"}
if p.Update(event.UpdateEvent{ObjectOld: oldObj, ObjectNew: labelOnly}) {
t.Fatal("UpdateFunc should return false on a label/metadata-only change")
}
// Type-assertion failure (wrong object type) must be rejected, not panic.
if p.Update(event.UpdateEvent{ObjectOld: &corev1.ConfigMap{}, ObjectNew: &corev1.ConfigMap{}}) {
t.Fatal("UpdateFunc should return false when objects are not SPCPS")
}
}