Files
Reloader/internal/pkg/reload/predicate_test.go
T

610 lines
15 KiB
Go

package reload
import (
"testing"
"github.com/stakater/Reloader/internal/pkg/config"
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"
)
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()
// No selectors configured
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)
}
// Both must be true for the event to pass through
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 that checks if label exists (any value)
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"}
// Nil cache should allow all namespaces (only check ignore list)
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)
}
})
}
}