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

621 lines
15 KiB
Go

package reload
import (
"context"
"testing"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/workload"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestService_ProcessConfigMap_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
// Create a deployment with auto annotation that uses the configmap
deploy := createTestDeployment("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.WorkloadAccessor{
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)
// Create a deployment with explicit configmap annotation
deploy := createTestDeployment("test-deploy", "default", map[string]string{
"configmap.reloader.stakater.com/reload": "test-cm",
})
workloads := []workload.WorkloadAccessor{
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)
// Create a deployment with auto annotation
deploy := createTestDeployment("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.WorkloadAccessor{
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)
// Create a deployment with auto annotation that uses the secret
deploy := createTestDeployment("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.WorkloadAccessor{
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)
// Create a deployment with explicit configmap annotation
deploy := createTestDeployment("test-deploy", "default", map[string]string{
"configmap.reloader.stakater.com/reload": "test-cm",
})
workloads := []workload.WorkloadAccessor{
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)
deploy := createTestDeployment("test-deploy", "default", map[string]string{
"configmap.reloader.stakater.com/reload": "test-cm",
})
workloads := []workload.WorkloadAccessor{
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)
deploy := createTestDeployment("test-deploy", "default", nil)
accessor := workload.NewDeploymentWorkload(deploy)
ctx := context.Background()
updated, err := svc.ApplyReload(ctx, accessor, "test-cm", 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)
deploy := createTestDeployment("test-deploy", "default", nil)
accessor := workload.NewDeploymentWorkload(deploy)
ctx := context.Background()
updated, err := svc.ApplyReload(ctx, accessor, "test-cm", 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)
deploy := createTestDeployment("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", 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)
deploy := createTestDeployment("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", 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)
// Create multiple workloads
deploy1 := createTestDeployment("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 := createTestDeployment("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 := createTestDeployment("deploy3", "default", map[string]string{
"reloader.stakater.com/auto": "true",
})
workloads := []workload.WorkloadAccessor{
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)
// Create deployments in different namespaces
deploy1 := createTestDeployment("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 := createTestDeployment("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.WorkloadAccessor{
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)
}
}
// Helper function to create a test deployment
func createTestDeployment(name, namespace string, annotations map[string]string) *appsv1.Deployment {
replicas := int32(1)
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: annotations,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": name},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": name},
Annotations: map[string]string{},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Image: "nginx:latest",
},
},
},
},
},
}
}