mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-21 04:56:36 +00:00
621 lines
15 KiB
Go
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",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|