feat: Implement NamespaceReconciler for namespace label selector filtering

This commit is contained in:
TheiLLeniumStudios
2025-12-28 08:47:53 +01:00
parent b5df945fff
commit 3cb45e8dc7
6 changed files with 1270 additions and 3 deletions
@@ -1,3 +1,6 @@
//go:build integration
// +build integration
package controller
import (
@@ -0,0 +1,155 @@
package controller
import (
"context"
"sync"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/config"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// NamespaceCache provides thread-safe access to the set of namespaces
// that match the configured namespace label selector.
type NamespaceCache struct {
mu sync.RWMutex
namespaces map[string]struct{}
enabled bool
}
// NewNamespaceCache creates a new NamespaceCache.
// If enabled is false, all namespace checks return true (allow all).
func NewNamespaceCache(enabled bool) *NamespaceCache {
return &NamespaceCache{
namespaces: make(map[string]struct{}),
enabled: enabled,
}
}
// Add adds a namespace to the cache.
func (c *NamespaceCache) Add(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.namespaces[name] = struct{}{}
}
// Remove removes a namespace from the cache.
func (c *NamespaceCache) Remove(name string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.namespaces, name)
}
// Contains checks if a namespace is in the cache.
// If namespace selectors are not enabled, always returns true.
func (c *NamespaceCache) Contains(name string) bool {
if !c.enabled {
return true
}
c.mu.RLock()
defer c.mu.RUnlock()
_, ok := c.namespaces[name]
return ok
}
// List returns a copy of all namespace names in the cache.
func (c *NamespaceCache) List() []string {
c.mu.RLock()
defer c.mu.RUnlock()
result := make([]string, 0, len(c.namespaces))
for name := range c.namespaces {
result = append(result, name)
}
return result
}
// IsEnabled returns whether namespace selector filtering is enabled.
func (c *NamespaceCache) IsEnabled() bool {
return c.enabled
}
// NamespaceReconciler watches Namespace objects and maintains a cache
// of namespaces that match the configured label selector.
type NamespaceReconciler struct {
client.Client
Log logr.Logger
Config *config.Config
Cache *NamespaceCache
}
// Reconcile handles Namespace events and updates the namespace cache.
func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("namespace", req.Name)
var ns corev1.Namespace
if err := r.Get(ctx, req.NamespacedName, &ns); err != nil {
if errors.IsNotFound(err) {
// Namespace was deleted - remove from cache
r.Cache.Remove(req.Name)
log.V(1).Info("removed namespace from cache (deleted)")
return ctrl.Result{}, nil
}
log.Error(err, "failed to get Namespace")
return ctrl.Result{}, err
}
// Check if namespace matches any of the configured selectors
if r.matchesSelectors(&ns) {
r.Cache.Add(ns.Name)
log.V(1).Info("added namespace to cache")
} else {
// Labels might have changed, remove from cache if no longer matches
r.Cache.Remove(ns.Name)
log.V(1).Info("removed namespace from cache (labels no longer match)")
}
return ctrl.Result{}, nil
}
// matchesSelectors checks if the namespace matches any configured label selector.
func (r *NamespaceReconciler) matchesSelectors(ns *corev1.Namespace) bool {
if len(r.Config.NamespaceSelectors) == 0 {
// No selectors configured - should not happen since reconciler is only
// set up when selectors are configured, but handle gracefully
return true
}
nsLabels := ns.GetLabels()
if nsLabels == nil {
nsLabels = make(map[string]string)
}
for _, selector := range r.Config.NamespaceSelectors {
if selector.Matches(nsLabelsSet(nsLabels)) {
return true
}
}
return false
}
// nsLabelsSet implements labels.Labels interface for a map.
type nsLabelsSet map[string]string
func (ls nsLabelsSet) Has(key string) bool {
_, ok := ls[key]
return ok
}
func (ls nsLabelsSet) Get(key string) string {
return ls[key]
}
// SetupWithManager sets up the controller with the Manager.
func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Namespace{}).
Complete(r)
}
// Ensure NamespaceReconciler implements reconcile.Reconciler
var _ reconcile.Reconciler = &NamespaceReconciler{}
@@ -0,0 +1,295 @@
package controller_test
import (
"context"
"testing"
"github.com/go-logr/logr/testr"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/controller"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestNamespaceCache_Basic(t *testing.T) {
cache := controller.NewNamespaceCache(true)
// Test Add and Contains
cache.Add("namespace-1")
if !cache.Contains("namespace-1") {
t.Error("Cache should contain namespace-1")
}
if cache.Contains("namespace-2") {
t.Error("Cache should not contain namespace-2")
}
// Test Remove
cache.Remove("namespace-1")
if cache.Contains("namespace-1") {
t.Error("Cache should not contain namespace-1 after removal")
}
}
func TestNamespaceCache_Disabled(t *testing.T) {
cache := controller.NewNamespaceCache(false)
// When disabled, Contains should always return true
if !cache.Contains("any-namespace") {
t.Error("Disabled cache should return true for any namespace")
}
if !cache.Contains("other-namespace") {
t.Error("Disabled cache should return true for any namespace")
}
}
func TestNamespaceCache_List(t *testing.T) {
cache := controller.NewNamespaceCache(true)
cache.Add("ns-1")
cache.Add("ns-2")
cache.Add("ns-3")
list := cache.List()
if len(list) != 3 {
t.Errorf("Expected 3 namespaces, got %d", len(list))
}
// Check all namespaces are in the list
found := make(map[string]bool)
for _, ns := range list {
found[ns] = true
}
for _, expected := range []string{"ns-1", "ns-2", "ns-3"} {
if !found[expected] {
t.Errorf("Expected %s in list", expected)
}
}
}
func TestNamespaceCache_IsEnabled(t *testing.T) {
enabledCache := controller.NewNamespaceCache(true)
disabledCache := controller.NewNamespaceCache(false)
if !enabledCache.IsEnabled() {
t.Error("EnabledCache.IsEnabled() should return true")
}
if disabledCache.IsEnabled() {
t.Error("DisabledCache.IsEnabled() should return false")
}
}
func TestNamespaceReconciler_Add(t *testing.T) {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ns",
Labels: map[string]string{"env": "production"},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ns).
Build()
cfg := config.NewDefault()
selector, _ := labels.Parse("env=production")
cfg.NamespaceSelectors = []labels.Selector{selector}
cache := controller.NewNamespaceCache(true)
reconciler := &controller.NamespaceReconciler{
Client: fakeClient,
Log: testr.New(t),
Config: cfg,
Cache: cache,
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{Name: "test-ns"},
}
_, err := reconciler.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile failed: %v", err)
}
if !cache.Contains("test-ns") {
t.Error("Cache should contain test-ns after reconcile")
}
}
func TestNamespaceReconciler_Remove_LabelChange(t *testing.T) {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
// Namespace with non-matching labels
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ns",
Labels: map[string]string{"env": "staging"},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ns).
Build()
cfg := config.NewDefault()
selector, _ := labels.Parse("env=production")
cfg.NamespaceSelectors = []labels.Selector{selector}
cache := controller.NewNamespaceCache(true)
// Pre-populate cache
cache.Add("test-ns")
reconciler := &controller.NamespaceReconciler{
Client: fakeClient,
Log: testr.New(t),
Config: cfg,
Cache: cache,
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{Name: "test-ns"},
}
_, err := reconciler.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile failed: %v", err)
}
if cache.Contains("test-ns") {
t.Error("Cache should not contain test-ns after reconcile (labels no longer match)")
}
}
func TestNamespaceReconciler_Remove_Delete(t *testing.T) {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
// No namespace in cluster (simulates delete)
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
Build()
cfg := config.NewDefault()
selector, _ := labels.Parse("env=production")
cfg.NamespaceSelectors = []labels.Selector{selector}
cache := controller.NewNamespaceCache(true)
// Pre-populate cache
cache.Add("deleted-ns")
reconciler := &controller.NamespaceReconciler{
Client: fakeClient,
Log: testr.New(t),
Config: cfg,
Cache: cache,
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{Name: "deleted-ns"},
}
_, err := reconciler.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile failed: %v", err)
}
if cache.Contains("deleted-ns") {
t.Error("Cache should not contain deleted-ns after reconcile")
}
}
func TestNamespaceReconciler_MultipleSelectors(t *testing.T) {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ns",
Labels: map[string]string{"team": "platform"},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ns).
Build()
cfg := config.NewDefault()
selector1, _ := labels.Parse("env=production")
selector2, _ := labels.Parse("team=platform")
cfg.NamespaceSelectors = []labels.Selector{selector1, selector2}
cache := controller.NewNamespaceCache(true)
reconciler := &controller.NamespaceReconciler{
Client: fakeClient,
Log: testr.New(t),
Config: cfg,
Cache: cache,
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{Name: "test-ns"},
}
_, err := reconciler.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile failed: %v", err)
}
// Should be added because it matches second selector (team=platform)
if !cache.Contains("test-ns") {
t.Error("Cache should contain test-ns (matches second selector)")
}
}
func TestNamespaceReconciler_NoLabels(t *testing.T) {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
// Namespace with no labels
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ns",
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ns).
Build()
cfg := config.NewDefault()
selector, _ := labels.Parse("env=production")
cfg.NamespaceSelectors = []labels.Selector{selector}
cache := controller.NewNamespaceCache(true)
reconciler := &controller.NamespaceReconciler{
Client: fakeClient,
Log: testr.New(t),
Config: cfg,
Cache: cache,
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{Name: "test-ns"},
}
_, err := reconciler.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile failed: %v", err)
}
if cache.Contains("test-ns") {
t.Error("Cache should not contain test-ns (no labels)")
}
}
+16 -3
View File
@@ -72,8 +72,19 @@ func SecretPredicates(cfg *config.Config, hasher *Hasher) predicate.Predicate {
}
}
// NamespaceChecker defines the interface for checking if a namespace is allowed.
type NamespaceChecker interface {
Contains(name string) bool
}
// NamespaceFilterPredicate returns a predicate that filters resources by namespace.
func NamespaceFilterPredicate(cfg *config.Config) predicate.Predicate {
return NamespaceFilterPredicateWithCache(cfg, nil)
}
// NamespaceFilterPredicateWithCache returns a predicate that filters resources by namespace,
// using the provided NamespaceChecker for namespace selector filtering.
func NamespaceFilterPredicateWithCache(cfg *config.Config, nsCache NamespaceChecker) predicate.Predicate {
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
namespace := obj.GetNamespace()
@@ -82,9 +93,11 @@ func NamespaceFilterPredicate(cfg *config.Config) predicate.Predicate {
return false
}
// Check namespace selectors
// Note: For now, we pass through and let the controller handle selector matching
// A more efficient implementation would check labels here
// Check namespace selector cache if provided
if nsCache != nil && !nsCache.Contains(namespace) {
return false
}
return true
})
}
+107
View File
@@ -500,3 +500,110 @@ func TestExistsLabelSelector(t *testing.T) {
})
}
}
// 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)
}
})
}
}
+694
View File
@@ -0,0 +1,694 @@
package workload
import (
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestDeploymentWorkload_BasicGetters(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deploy",
Namespace: "test-ns",
Annotations: map[string]string{
"key": "value",
},
},
}
w := NewDeploymentWorkload(deploy)
if w.Kind() != KindDeployment {
t.Errorf("Kind() = %v, want %v", w.Kind(), KindDeployment)
}
if w.GetName() != "test-deploy" {
t.Errorf("GetName() = %v, want test-deploy", w.GetName())
}
if w.GetNamespace() != "test-ns" {
t.Errorf("GetNamespace() = %v, want test-ns", w.GetNamespace())
}
if w.GetAnnotations()["key"] != "value" {
t.Errorf("GetAnnotations()[key] = %v, want value", w.GetAnnotations()["key"])
}
if w.GetObject() != deploy {
t.Error("GetObject() should return the underlying deployment")
}
}
func TestDeploymentWorkload_PodTemplateAnnotations(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"existing": "annotation",
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
// Test get
annotations := w.GetPodTemplateAnnotations()
if annotations["existing"] != "annotation" {
t.Errorf("GetPodTemplateAnnotations()[existing] = %v, want annotation", annotations["existing"])
}
// Test set
w.SetPodTemplateAnnotation("new-key", "new-value")
if w.GetPodTemplateAnnotations()["new-key"] != "new-value" {
t.Error("SetPodTemplateAnnotation should add new annotation")
}
}
func TestDeploymentWorkload_PodTemplateAnnotations_NilInit(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
// No annotations set
},
},
}
w := NewDeploymentWorkload(deploy)
// Should initialize nil map
annotations := w.GetPodTemplateAnnotations()
if annotations == nil {
t.Error("GetPodTemplateAnnotations should initialize nil map")
}
// Should work with nil initial map
w.SetPodTemplateAnnotation("key", "value")
if w.GetPodTemplateAnnotations()["key"] != "value" {
t.Error("SetPodTemplateAnnotation should work with nil initial map")
}
}
func TestDeploymentWorkload_Containers(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Image: "nginx"},
},
InitContainers: []corev1.Container{
{Name: "init", Image: "busybox"},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
// Test get containers
containers := w.GetContainers()
if len(containers) != 1 || containers[0].Name != "main" {
t.Errorf("GetContainers() = %v, want [main]", containers)
}
// Test get init containers
initContainers := w.GetInitContainers()
if len(initContainers) != 1 || initContainers[0].Name != "init" {
t.Errorf("GetInitContainers() = %v, want [init]", initContainers)
}
// Test set containers
newContainers := []corev1.Container{{Name: "new-main", Image: "alpine"}}
w.SetContainers(newContainers)
if w.GetContainers()[0].Name != "new-main" {
t.Error("SetContainers should update containers")
}
// Test set init containers
newInitContainers := []corev1.Container{{Name: "new-init", Image: "alpine"}}
w.SetInitContainers(newInitContainers)
if w.GetInitContainers()[0].Name != "new-init" {
t.Error("SetInitContainers should update init containers")
}
}
func TestDeploymentWorkload_Volumes(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{Name: "config-vol"},
{Name: "secret-vol"},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
volumes := w.GetVolumes()
if len(volumes) != 2 {
t.Errorf("GetVolumes() length = %d, want 2", len(volumes))
}
}
func TestDeploymentWorkload_UsesConfigMap_Volume(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "my-config",
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesConfigMap("my-config") {
t.Error("UsesConfigMap should return true for ConfigMap volume")
}
if w.UsesConfigMap("other-config") {
t.Error("UsesConfigMap should return false for non-existent ConfigMap")
}
}
func TestDeploymentWorkload_UsesConfigMap_ProjectedVolume(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "projected-vol",
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: "projected-config",
},
},
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesConfigMap("projected-config") {
t.Error("UsesConfigMap should return true for projected ConfigMap volume")
}
}
func TestDeploymentWorkload_UsesConfigMap_EnvFrom(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
EnvFrom: []corev1.EnvFromSource{
{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "env-config",
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesConfigMap("env-config") {
t.Error("UsesConfigMap should return true for envFrom ConfigMap")
}
}
func TestDeploymentWorkload_UsesConfigMap_EnvVar(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Env: []corev1.EnvVar{
{
Name: "CONFIG_VALUE",
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "var-config",
},
Key: "some-key",
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesConfigMap("var-config") {
t.Error("UsesConfigMap should return true for env var ConfigMapKeyRef")
}
}
func TestDeploymentWorkload_UsesConfigMap_InitContainer(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init",
EnvFrom: []corev1.EnvFromSource{
{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "init-config",
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesConfigMap("init-config") {
t.Error("UsesConfigMap should return true for init container ConfigMap")
}
}
func TestDeploymentWorkload_UsesSecret_Volume(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "secret-vol",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "my-secret",
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesSecret("my-secret") {
t.Error("UsesSecret should return true for Secret volume")
}
if w.UsesSecret("other-secret") {
t.Error("UsesSecret should return false for non-existent Secret")
}
}
func TestDeploymentWorkload_UsesSecret_ProjectedVolume(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "projected-vol",
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: "projected-secret",
},
},
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesSecret("projected-secret") {
t.Error("UsesSecret should return true for projected Secret volume")
}
}
func TestDeploymentWorkload_UsesSecret_EnvFrom(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "env-secret",
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesSecret("env-secret") {
t.Error("UsesSecret should return true for envFrom Secret")
}
}
func TestDeploymentWorkload_UsesSecret_EnvVar(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Env: []corev1.EnvVar{
{
Name: "SECRET_VALUE",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "var-secret",
},
Key: "some-key",
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesSecret("var-secret") {
t.Error("UsesSecret should return true for env var SecretKeyRef")
}
}
func TestDeploymentWorkload_UsesSecret_InitContainer(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init",
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "init-secret",
},
},
},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
if !w.UsesSecret("init-secret") {
t.Error("UsesSecret should return true for init container Secret")
}
}
func TestDeploymentWorkload_GetEnvFromSources(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
EnvFrom: []corev1.EnvFromSource{
{ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}},
},
},
{
Name: "sidecar",
EnvFrom: []corev1.EnvFromSource{
{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}},
},
},
},
InitContainers: []corev1.Container{
{
Name: "init",
EnvFrom: []corev1.EnvFromSource{
{ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "init-cm"}}},
},
},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
sources := w.GetEnvFromSources()
if len(sources) != 3 {
t.Errorf("GetEnvFromSources() returned %d sources, want 3", len(sources))
}
}
func TestDeploymentWorkload_DeepCopy(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Image: "nginx"},
},
},
},
},
}
w := NewDeploymentWorkload(deploy)
copy := w.DeepCopy()
// Modify original
w.SetPodTemplateAnnotation("modified", "true")
// Copy should not be affected
copyAnnotations := copy.GetPodTemplateAnnotations()
if copyAnnotations["modified"] == "true" {
t.Error("DeepCopy should create independent copy")
}
}
func TestDeploymentWorkload_GetOwnerReferences(t *testing.T) {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "test-rs",
},
},
},
}
w := NewDeploymentWorkload(deploy)
refs := w.GetOwnerReferences()
if len(refs) != 1 || refs[0].Name != "test-rs" {
t.Errorf("GetOwnerReferences() = %v, want owner ref to test-rs", refs)
}
}
// DaemonSet tests
func TestDaemonSetWorkload_BasicGetters(t *testing.T) {
ds := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ds",
Namespace: "test-ns",
},
}
w := NewDaemonSetWorkload(ds)
if w.Kind() != KindDaemonSet {
t.Errorf("Kind() = %v, want %v", w.Kind(), KindDaemonSet)
}
if w.GetName() != "test-ds" {
t.Errorf("GetName() = %v, want test-ds", w.GetName())
}
}
func TestDaemonSetWorkload_UsesConfigMap(t *testing.T) {
ds := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.DaemonSetSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "ds-config",
},
},
},
},
},
},
},
},
}
w := NewDaemonSetWorkload(ds)
if !w.UsesConfigMap("ds-config") {
t.Error("DaemonSet UsesConfigMap should return true for ConfigMap volume")
}
}
// StatefulSet tests
func TestStatefulSetWorkload_BasicGetters(t *testing.T) {
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sts",
Namespace: "test-ns",
},
}
w := NewStatefulSetWorkload(sts)
if w.Kind() != KindStatefulSet {
t.Errorf("Kind() = %v, want %v", w.Kind(), KindStatefulSet)
}
if w.GetName() != "test-sts" {
t.Errorf("GetName() = %v, want test-sts", w.GetName())
}
}
func TestStatefulSetWorkload_UsesSecret(t *testing.T) {
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: appsv1.StatefulSetSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "secret-vol",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "sts-secret",
},
},
},
},
},
},
},
}
w := NewStatefulSetWorkload(sts)
if !w.UsesSecret("sts-secret") {
t.Error("StatefulSet UsesSecret should return true for Secret volume")
}
}
// Test that workloads implement the interface
func TestWorkloadInterface(t *testing.T) {
var _ WorkloadAccessor = (*DeploymentWorkload)(nil)
var _ WorkloadAccessor = (*DaemonSetWorkload)(nil)
var _ WorkloadAccessor = (*StatefulSetWorkload)(nil)
}