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

1475 lines
36 KiB
Go

package reload
import (
"context"
"testing"
"github.com/go-logr/logr/testr"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/internal/pkg/workload"
"github.com/stakater/Reloader/pkg/config"
"github.com/stakater/Reloader/pkg/matcher"
)
func TestService_ProcessConfigMap_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Create a deployment with auto annotation that uses the configmap
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test-cm",
},
},
},
},
}
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
Data: map[string]string{
"key": "value",
},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 {
t.Fatalf("Expected 1 decision, got %d", len(decisions))
}
if !decisions[0].ShouldReload {
t.Error("Expected ShouldReload to be true")
}
if !decisions[0].AutoReload {
t.Error("Expected AutoReload to be true")
}
if decisions[0].Hash == "" {
t.Error("Expected Hash to be non-empty")
}
}
func TestService_ProcessConfigMap_ExplicitAnnotation(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
"configmap.reloader.stakater.com/reload": "test-cm",
},
)
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
Data: map[string]string{
"key": "value",
},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 {
t.Fatalf("Expected 1 decision, got %d", len(decisions))
}
if !decisions[0].ShouldReload {
t.Error("Expected ShouldReload to be true for explicit annotation")
}
if decisions[0].AutoReload {
t.Error("Expected AutoReload to be false for explicit annotation")
}
}
func TestService_ProcessConfigMap_IgnoredResource(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Create a deployment with auto annotation
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test-cm",
},
},
},
},
}
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
// ConfigMap with ignore annotation
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
Annotations: map[string]string{
"reloader.stakater.com/ignore": "true",
},
},
Data: map[string]string{
"key": "value",
},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
// Should still get a decision, but ShouldReload should be false
for _, d := range decisions {
if d.ShouldReload {
t.Error("Expected ShouldReload to be false for ignored resource")
}
}
}
func TestService_ProcessSecret_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Create a deployment with auto annotation that uses the secret
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "secret-vol",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "test-secret",
},
},
},
}
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
},
Data: map[string][]byte{
"key": []byte("value"),
},
}
change := SecretChange{
Secret: secret,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 {
t.Fatalf("Expected 1 decision, got %d", len(decisions))
}
if !decisions[0].ShouldReload {
t.Error("Expected ShouldReload to be true")
}
if !decisions[0].AutoReload {
t.Error("Expected AutoReload to be true")
}
}
func TestService_ProcessConfigMap_DeleteEvent(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnDelete = true
svc := NewService(cfg, testr.New(t))
// Create a deployment with explicit configmap annotation
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
"configmap.reloader.stakater.com/reload": "test-cm",
},
)
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeDelete,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 {
t.Fatalf("Expected 1 decision, got %d", len(decisions))
}
if !decisions[0].ShouldReload {
t.Error("Expected ShouldReload to be true for delete event")
}
// Hash should be empty for delete events
if decisions[0].Hash != "" {
t.Errorf("Expected empty hash for delete event, got %s", decisions[0].Hash)
}
}
func TestService_ProcessConfigMap_DeleteEventDisabled(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnDelete = false // Disabled by default
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
"configmap.reloader.stakater.com/reload": "test-cm",
},
)
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeDelete,
}
decisions := svc.Process(change, workloads)
// Should return nil when delete events are disabled
if decisions != nil {
t.Error("Expected nil decisions when delete events are disabled")
}
}
func TestService_ApplyReload_EnvVarStrategy(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
accessor := workload.NewDeploymentWorkload(deploy)
ctx := context.Background()
updated, err := svc.ApplyReload(ctx, accessor, "test-cm", matcher.ResourceTypeConfigMap, "default", "abc123hash", false)
if err != nil {
t.Fatalf("ApplyReload failed: %v", err)
}
if !updated {
t.Error("Expected updated to be true")
}
// Verify env var was added
containers := accessor.GetContainers()
if len(containers) == 0 {
t.Fatal("No containers found")
}
found := false
for _, env := range containers[0].Env {
if env.Name == "STAKATER_TEST_CM_CONFIGMAP" && env.Value == "abc123hash" {
found = true
break
}
}
if !found {
t.Error("Expected env var STAKATER_TEST_CM_CONFIGMAP to be set")
}
// Verify attribution annotation was set
annotations := accessor.GetPodTemplateAnnotations()
if annotations["reloader.stakater.com/last-reloaded-from"] == "" {
t.Error("Expected last-reloaded-from annotation to be set")
}
}
func TestService_ApplyReload_AnnotationStrategy(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyAnnotations
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
accessor := workload.NewDeploymentWorkload(deploy)
ctx := context.Background()
updated, err := svc.ApplyReload(ctx, accessor, "test-cm", matcher.ResourceTypeConfigMap, "default", "abc123hash", false)
if err != nil {
t.Fatalf("ApplyReload failed: %v", err)
}
if !updated {
t.Error("Expected updated to be true")
}
// Verify annotation was added
annotations := accessor.GetPodTemplateAnnotations()
if annotations["reloader.stakater.com/last-reloaded-from"] == "" {
t.Error("Expected last-reloaded-from annotation to be set")
}
}
func TestService_ApplyReload_EnvVarDeletion(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
// Pre-add an env var
deploy.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{
{Name: "STAKATER_TEST_CM_CONFIGMAP", Value: "oldhash"},
{Name: "OTHER_VAR", Value: "keep"},
}
accessor := workload.NewDeploymentWorkload(deploy)
ctx := context.Background()
// Empty hash signals deletion
updated, err := svc.ApplyReload(ctx, accessor, "test-cm", matcher.ResourceTypeConfigMap, "default", "", false)
if err != nil {
t.Fatalf("ApplyReload failed: %v", err)
}
if !updated {
t.Error("Expected updated to be true for env var removal")
}
// Verify env var was removed
containers := accessor.GetContainers()
for _, env := range containers[0].Env {
if env.Name == "STAKATER_TEST_CM_CONFIGMAP" {
t.Error("Expected env var STAKATER_TEST_CM_CONFIGMAP to be removed")
}
}
// Verify other env var was kept
found := false
for _, env := range containers[0].Env {
if env.Name == "OTHER_VAR" {
found = true
break
}
}
if !found {
t.Error("Expected OTHER_VAR to be kept")
}
}
func TestService_ApplyReload_NoChangeIfSameHash(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
// Pre-add env var with same hash
deploy.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{
{Name: "STAKATER_TEST_CM_CONFIGMAP", Value: "abc123hash"},
}
accessor := workload.NewDeploymentWorkload(deploy)
ctx := context.Background()
updated, err := svc.ApplyReload(ctx, accessor, "test-cm", matcher.ResourceTypeConfigMap, "default", "abc123hash", false)
if err != nil {
t.Fatalf("ApplyReload failed: %v", err)
}
if updated {
t.Error("Expected updated to be false when hash is unchanged")
}
}
func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Create multiple workloads
deploy1 := testutil.NewDeployment(
"deploy1", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy1.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "shared-cm",
},
},
},
},
}
deploy2 := testutil.NewDeployment(
"deploy2", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy2.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "shared-cm",
},
},
},
},
}
// Deploy3 doesn't use the configmap
deploy3 := testutil.NewDeployment(
"deploy3", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy1),
workload.NewDeploymentWorkload(deploy2),
workload.NewDeploymentWorkload(deploy3),
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "shared-cm",
Namespace: "default",
},
Data: map[string]string{"key": "value"},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 3 {
t.Fatalf("Expected 3 decisions, got %d", len(decisions))
}
// Count how many should reload
reloadCount := 0
for _, d := range decisions {
if d.ShouldReload {
reloadCount++
}
}
// Only deploy1 and deploy2 should reload (they use the configmap)
if reloadCount != 2 {
t.Errorf("Expected 2 workloads to reload, got %d", reloadCount)
}
}
func TestService_ProcessConfigMap_DifferentNamespaces(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Create deployments in different namespaces
deploy1 := testutil.NewDeployment(
"deploy1", "namespace-a", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy1.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test-cm",
},
},
},
},
}
deploy2 := testutil.NewDeployment(
"deploy2", "namespace-b", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
deploy2.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test-cm",
},
},
},
},
}
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy1),
workload.NewDeploymentWorkload(deploy2),
}
// ConfigMap in namespace-a
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "namespace-a",
},
Data: map[string]string{"key": "value"},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
// Should only affect deploy1 (same namespace)
reloadCount := 0
for _, d := range decisions {
if d.ShouldReload {
reloadCount++
}
}
if reloadCount != 1 {
t.Errorf("Expected 1 workload to reload (same namespace), got %d", reloadCount)
}
}
func TestService_Hasher(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
hasher := svc.Hasher()
if hasher == nil {
t.Fatal("Expected Hasher to return non-nil hasher")
}
// Verify it's functional
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string]string{"key": "value"},
}
hash := hasher.HashConfigMap(cm)
if hash == "" {
t.Error("Expected hasher to produce non-empty hash")
}
}
func TestService_shouldProcessEvent(t *testing.T) {
tests := []struct {
name string
reloadOnCreate bool
reloadOnDelete bool
eventType EventType
expected bool
}{
{"create enabled", true, false, EventTypeCreate, true},
{"create disabled", false, false, EventTypeCreate, false},
{"delete enabled", false, true, EventTypeDelete, true},
{"delete disabled", false, false, EventTypeDelete, false},
{"update always true", false, false, EventTypeUpdate, true},
{"unknown event", false, false, EventType("unknown"), false},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnCreate = tt.reloadOnCreate
cfg.ReloadOnDelete = tt.reloadOnDelete
svc := NewService(cfg, testr.New(t))
result := svc.shouldProcessEvent(tt.eventType)
if result != tt.expected {
t.Errorf("shouldProcessEvent(%s) = %v, want %v", tt.eventType, result, tt.expected)
}
},
)
}
}
func TestService_findVolumeUsingResource_ConfigMap(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
volumes []corev1.Volume
resourceName string
resourceType matcher.ResourceType
wantVolume string
}{
{
name: "direct configmap volume",
volumes: []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
},
},
},
},
resourceName: "my-cm",
resourceType: matcher.ResourceTypeConfigMap,
wantVolume: "config-vol",
},
{
name: "projected configmap volume",
volumes: []corev1.Volume{
{
Name: "projected-vol",
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: "projected-cm"},
},
},
},
},
},
},
},
resourceName: "projected-cm",
resourceType: matcher.ResourceTypeConfigMap,
wantVolume: "projected-vol",
},
{
name: "no match",
volumes: []corev1.Volume{
{
Name: "other-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "other-cm"},
},
},
},
},
resourceName: "my-cm",
resourceType: matcher.ResourceTypeConfigMap,
wantVolume: "",
},
{
name: "empty volumes",
volumes: []corev1.Volume{},
resourceName: "my-cm",
resourceType: matcher.ResourceTypeConfigMap,
wantVolume: "",
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
got := svc.findVolumeUsingResource(tt.volumes, tt.resourceName, tt.resourceType)
if got != tt.wantVolume {
t.Errorf("findVolumeUsingResource() = %q, want %q", got, tt.wantVolume)
}
},
)
}
}
func TestService_findVolumeUsingResource_Secret(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
volumes []corev1.Volume
resourceName string
wantVolume string
}{
{
name: "direct secret volume",
volumes: []corev1.Volume{
{
Name: "secret-vol",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "my-secret",
},
},
},
},
resourceName: "my-secret",
wantVolume: "secret-vol",
},
{
name: "projected secret volume",
volumes: []corev1.Volume{
{
Name: "projected-vol",
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: "projected-secret"},
},
},
},
},
},
},
},
resourceName: "projected-secret",
wantVolume: "projected-vol",
},
{
name: "no match",
volumes: []corev1.Volume{
{
Name: "other-vol",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "other-secret",
},
},
},
},
resourceName: "my-secret",
wantVolume: "",
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
got := svc.findVolumeUsingResource(tt.volumes, tt.resourceName, matcher.ResourceTypeSecret)
if got != tt.wantVolume {
t.Errorf("findVolumeUsingResource() = %q, want %q", got, tt.wantVolume)
}
},
)
}
}
func TestService_findContainerWithVolumeMount(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
containers []corev1.Container
volumeName string
wantName string
shouldMatch bool
}{
{
name: "container with matching volume mount",
containers: []corev1.Container{
{
Name: "container1",
VolumeMounts: []corev1.VolumeMount{
{Name: "config-vol", MountPath: "/config"},
},
},
},
volumeName: "config-vol",
wantName: "container1",
shouldMatch: true,
},
{
name: "second container with matching mount",
containers: []corev1.Container{
{
Name: "container1",
VolumeMounts: []corev1.VolumeMount{},
},
{
Name: "container2",
VolumeMounts: []corev1.VolumeMount{
{Name: "config-vol", MountPath: "/config"},
},
},
},
volumeName: "config-vol",
wantName: "container2",
shouldMatch: true,
},
{
name: "no matching mount",
containers: []corev1.Container{
{
Name: "container1",
VolumeMounts: []corev1.VolumeMount{
{Name: "other-vol", MountPath: "/other"},
},
},
},
volumeName: "config-vol",
shouldMatch: false,
},
{
name: "empty containers",
containers: []corev1.Container{},
volumeName: "config-vol",
shouldMatch: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
got := svc.findContainerWithVolumeMount(tt.containers, tt.volumeName)
if tt.shouldMatch {
if got == nil {
t.Error("Expected to find a container, got nil")
} else if got.Name != tt.wantName {
t.Errorf("findContainerWithVolumeMount() container name = %q, want %q", got.Name, tt.wantName)
}
} else {
if got != nil {
t.Errorf("Expected nil, got container %q", got.Name)
}
}
},
)
}
}
func TestService_findContainerWithEnvRef_ConfigMap(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
containers []corev1.Container
resourceName string
wantName string
shouldMatch bool
}{
{
name: "container with ConfigMapKeyRef",
containers: []corev1.Container{
{
Name: "app",
Env: []corev1.EnvVar{
{
Name: "CONFIG_VALUE",
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
Key: "key",
},
},
},
},
},
},
resourceName: "my-cm",
wantName: "app",
shouldMatch: true,
},
{
name: "container with ConfigMapRef in EnvFrom",
containers: []corev1.Container{
{
Name: "app",
EnvFrom: []corev1.EnvFromSource{
{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
},
},
},
},
},
resourceName: "my-cm",
wantName: "app",
shouldMatch: true,
},
{
name: "no matching env ref",
containers: []corev1.Container{
{
Name: "app",
Env: []corev1.EnvVar{
{
Name: "SIMPLE_VAR",
Value: "value",
},
},
},
},
resourceName: "my-cm",
shouldMatch: false,
},
{
name: "env without ValueFrom",
containers: []corev1.Container{
{
Name: "app",
Env: []corev1.EnvVar{
{Name: "VAR1", Value: "val"},
},
},
},
resourceName: "my-cm",
shouldMatch: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
got := svc.findContainerWithEnvRef(tt.containers, tt.resourceName, matcher.ResourceTypeConfigMap)
if tt.shouldMatch {
if got == nil {
t.Error("Expected to find a container, got nil")
} else if got.Name != tt.wantName {
t.Errorf("findContainerWithEnvRef() container name = %q, want %q", got.Name, tt.wantName)
}
} else {
if got != nil {
t.Errorf("Expected nil, got container %q", got.Name)
}
}
},
)
}
}
func TestService_findContainerWithEnvRef_Secret(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
containers []corev1.Container
resourceName string
wantName string
shouldMatch bool
}{
{
name: "container with SecretKeyRef",
containers: []corev1.Container{
{
Name: "app",
Env: []corev1.EnvVar{
{
Name: "SECRET_VALUE",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"},
Key: "password",
},
},
},
},
},
},
resourceName: "my-secret",
wantName: "app",
shouldMatch: true,
},
{
name: "container with SecretRef in EnvFrom",
containers: []corev1.Container{
{
Name: "app",
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"},
},
},
},
},
},
resourceName: "my-secret",
wantName: "app",
shouldMatch: true,
},
{
name: "no matching env ref",
containers: []corev1.Container{
{
Name: "app",
Env: []corev1.EnvVar{
{
Name: "SIMPLE_VAR",
Value: "value",
},
},
},
},
resourceName: "my-secret",
shouldMatch: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
got := svc.findContainerWithEnvRef(tt.containers, tt.resourceName, matcher.ResourceTypeSecret)
if tt.shouldMatch {
if got == nil {
t.Error("Expected to find a container, got nil")
} else if got.Name != tt.wantName {
t.Errorf("findContainerWithEnvRef() container name = %q, want %q", got.Name, tt.wantName)
}
} else {
if got != nil {
t.Errorf("Expected nil, got container %q", got.Name)
}
}
},
)
}
}
func TestService_findTargetContainer_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true and volume mount
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
},
},
},
}
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{
Name: "app",
Image: "nginx",
VolumeMounts: []corev1.VolumeMount{
{Name: "config-vol", MountPath: "/config"},
},
},
}
accessor := workload.NewDeploymentWorkload(deploy)
container := svc.findTargetContainer(accessor, "my-cm", matcher.ResourceTypeConfigMap, true)
if container == nil {
t.Fatal("Expected to find a container")
}
if container.Name != "app" {
t.Errorf("Expected container 'app', got %q", container.Name)
}
}
func TestService_findTargetContainer_AutoReload_EnvRef(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true and env ref (no volume)
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{
Name: "sidecar",
Image: "busybox",
},
{
Name: "app",
Image: "nginx",
Env: []corev1.EnvVar{
{
Name: "CONFIG_VAL",
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
Key: "key",
},
},
},
},
},
}
accessor := workload.NewDeploymentWorkload(deploy)
container := svc.findTargetContainer(accessor, "my-cm", matcher.ResourceTypeConfigMap, true)
if container == nil {
t.Fatal("Expected to find a container")
}
if container.Name != "app" {
t.Errorf("Expected container 'app', got %q", container.Name)
}
}
func TestService_findTargetContainer_AutoReload_InitContainer(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true where init container uses the volume
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "config-vol",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
},
},
},
}
deploy.Spec.Template.Spec.InitContainers = []corev1.Container{
{
Name: "init",
Image: "busybox",
VolumeMounts: []corev1.VolumeMount{
{Name: "config-vol", MountPath: "/config"},
},
},
}
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{
Name: "app",
Image: "nginx",
},
}
accessor := workload.NewDeploymentWorkload(deploy)
container := svc.findTargetContainer(accessor, "my-cm", matcher.ResourceTypeConfigMap, true)
if container == nil {
t.Fatal("Expected to find a container")
}
// Should return first main container when init container uses the volume
if container.Name != "app" {
t.Errorf("Expected container 'app', got %q", container.Name)
}
}
func TestService_findTargetContainer_AutoReload_InitContainerEnvRef(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true where init container has env ref
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.InitContainers = []corev1.Container{
{
Name: "init",
Image: "busybox",
Env: []corev1.EnvVar{
{
Name: "CONFIG_VAL",
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
Key: "key",
},
},
},
},
},
}
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{
Name: "app",
Image: "nginx",
},
}
accessor := workload.NewDeploymentWorkload(deploy)
container := svc.findTargetContainer(accessor, "my-cm", matcher.ResourceTypeConfigMap, true)
if container == nil {
t.Fatal("Expected to find a container")
}
// Should return first main container when init container has the env ref
if container.Name != "app" {
t.Errorf("Expected container 'app', got %q", container.Name)
}
}
func TestService_findTargetContainer_NoContainers(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Containers = []corev1.Container{}
accessor := workload.NewDeploymentWorkload(deploy)
container := svc.findTargetContainer(accessor, "my-cm", matcher.ResourceTypeConfigMap, false)
if container != nil {
t.Error("Expected nil container for empty container list")
}
}
func TestService_findTargetContainer_NonAutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{Name: "first", Image: "nginx"},
{Name: "second", Image: "busybox"},
}
accessor := workload.NewDeploymentWorkload(deploy)
// Without autoReload, should return first container
container := svc.findTargetContainer(accessor, "my-cm", matcher.ResourceTypeConfigMap, false)
if container == nil {
t.Fatal("Expected to find a container")
}
if container.Name != "first" {
t.Errorf("Expected first container, got %q", container.Name)
}
}
func TestService_findTargetContainer_AutoReload_FallbackToFirst(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// autoReload=true but no matching volume or env ref - should fallback to first container
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{Name: "first", Image: "nginx"},
{Name: "second", Image: "busybox"},
}
accessor := workload.NewDeploymentWorkload(deploy)
container := svc.findTargetContainer(accessor, "non-existent", matcher.ResourceTypeConfigMap, true)
if container == nil {
t.Fatal("Expected to find a container")
}
if container.Name != "first" {
t.Errorf("Expected first container as fallback, got %q", container.Name)
}
}
func TestService_ProcessNilChange(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test", "default", nil)
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
// Test with nil ConfigMap
change := ConfigMapChange{
ConfigMap: nil,
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if decisions != nil {
t.Errorf("Expected nil decisions for nil change, got %v", decisions)
}
}
func TestServiceProcessSecretProviderClassAuto(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", map[string]string{
"secretproviderclass.reloader.stakater.com/auto": "true",
})
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
change := SecretProviderClassChange{
Name: "my-spc",
Namespace: "default",
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "my-spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "1"}},
},
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 || !decisions[0].ShouldReload {
t.Fatalf("expected auto SPC reload, got %+v", decisions)
}
}
func TestService_ProcessCreateEventDisabled(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnCreate = false
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment(
"test", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test-cm", Namespace: "default"},
Data: map[string]string{"key": "value"},
}
change := ConfigMapChange{
ConfigMap: cm,
EventType: EventTypeCreate,
}
decisions := svc.Process(change, workloads)
if decisions != nil {
t.Errorf("Expected nil decisions when create events disabled, got %v", decisions)
}
}
func TestService_ApplyReload_SPC_TargetsMountingContainer(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg, testr.New(t))
// Two containers; only the second mounts the CSI volume that references the SPC.
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "multi", Namespace: "default"},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c0"},
{Name: "c1", VolumeMounts: []corev1.VolumeMount{{Name: "spc-vol", MountPath: "/mnt/secrets-store"}}},
},
Volumes: []corev1.Volume{
{
Name: "spc-vol",
VolumeSource: corev1.VolumeSource{
CSI: &corev1.CSIVolumeSource{
Driver: "secrets-store.csi.k8s.io",
VolumeAttributes: map[string]string{"secretProviderClass": "my-spc"},
},
},
},
},
},
},
},
}
accessor := workload.NewDeploymentWorkload(dep)
// autoReload=true exercises volume-based container targeting.
updated, err := svc.ApplyReload(context.Background(), accessor, "my-spc", matcher.ResourceTypeSecretProviderClass, "default", "spchash", true)
if err != nil {
t.Fatalf("ApplyReload failed: %v", err)
}
if !updated {
t.Fatal("expected updated=true")
}
containers := accessor.GetContainers()
const envName = "STAKATER_MY_SPC_SECRETPROVIDERCLASS"
hasEnv := func(c corev1.Container) bool {
for _, e := range c.Env {
if e.Name == envName {
return true
}
}
return false
}
if hasEnv(containers[0]) {
t.Error("env var must NOT land on container[0] (it does not mount the SPC volume)")
}
if !hasEnv(containers[1]) {
t.Error("env var must land on container[1], which mounts the SPC CSI volume")
}
}
func TestService_ProcessSecretProviderClass_GenericAuto(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
// Generic auto annotation (not the typed SPC one) must also trigger an SPC reload.
deploy := testutil.NewDeployment("test-deploy", "default", map[string]string{
"reloader.stakater.com/auto": "true",
})
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
change := SecretProviderClassChange{
Name: "my-spc",
Namespace: "default",
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "my-spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "1"}},
},
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 || !decisions[0].ShouldReload {
t.Fatalf("expected generic-auto SPC reload, got %+v", decisions)
}
}