package reload import ( "encoding/json" "testing" corev1 "k8s.io/api/core/v1" "github.com/stakater/Reloader/pkg/config" "github.com/stakater/Reloader/pkg/matcher" ) func TestEnvVarStrategy_Apply(t *testing.T) { strategy := NewEnvVarStrategy() t.Run("adds new env var", func(t *testing.T) { container := &corev1.Container{ Name: "test-container", Env: []corev1.EnvVar{}, } input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "abc123", Container: container, } changed, err := strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if !changed { t.Error("expected changed=true for new env var") } // Verify env var was added found := false for _, env := range container.Env { if env.Name == "STAKATER_MY_CONFIG_CONFIGMAP" && env.Value == "abc123" { found = true break } } if !found { t.Errorf("expected env var STAKATER_MY_CONFIG_CONFIGMAP=abc123, got %+v", container.Env) } }) t.Run("updates existing env var", func(t *testing.T) { container := &corev1.Container{ Name: "test-container", Env: []corev1.EnvVar{ {Name: "STAKATER_MY_CONFIG_CONFIGMAP", Value: "old-hash"}, }, } input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "new-hash", Container: container, } changed, err := strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if !changed { t.Error("expected changed=true for updated env var") } // Verify env var was updated if container.Env[0].Value != "new-hash" { t.Errorf("expected env var value=new-hash, got %s", container.Env[0].Value) } }) t.Run("no change when hash is same", func(t *testing.T) { container := &corev1.Container{ Name: "test-container", Env: []corev1.EnvVar{ {Name: "STAKATER_MY_CONFIG_CONFIGMAP", Value: "same-hash"}, }, } input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "same-hash", Container: container, } changed, err := strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if changed { t.Error("expected changed=false when hash is unchanged") } }) t.Run("error when container is nil", func(t *testing.T) { input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "abc123", Container: nil, } _, err := strategy.Apply(input) if err == nil { t.Error("expected error for nil container") } }) t.Run("secret env var has correct postfix", func(t *testing.T) { container := &corev1.Container{ Name: "test-container", Env: []corev1.EnvVar{}, } input := StrategyInput{ ResourceName: "my-secret", ResourceType: matcher.ResourceTypeSecret, Namespace: "default", Hash: "abc123", Container: container, } changed, err := strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if !changed { t.Error("expected changed=true") } // Verify env var name has SECRET postfix found := false for _, env := range container.Env { if env.Name == "STAKATER_MY_SECRET_SECRET" && env.Value == "abc123" { found = true break } } if !found { t.Errorf("expected env var STAKATER_MY_SECRET_SECRET=abc123, got %+v", container.Env) } }) } func TestEnvVarStrategy_EnvVarName(t *testing.T) { strategy := NewEnvVarStrategy() tests := []struct { resourceName string resourceType matcher.ResourceType expected string }{ {"my-config", matcher.ResourceTypeConfigMap, "STAKATER_MY_CONFIG_CONFIGMAP"}, {"my-secret", matcher.ResourceTypeSecret, "STAKATER_MY_SECRET_SECRET"}, {"app-config-v2", matcher.ResourceTypeConfigMap, "STAKATER_APP_CONFIG_V2_CONFIGMAP"}, {"my.dotted.config", matcher.ResourceTypeConfigMap, "STAKATER_MY_DOTTED_CONFIG_CONFIGMAP"}, {"MyMixedCase", matcher.ResourceTypeConfigMap, "STAKATER_MYMIXEDCASE_CONFIGMAP"}, {"config-with-123-numbers", matcher.ResourceTypeConfigMap, "STAKATER_CONFIG_WITH_123_NUMBERS_CONFIGMAP"}, } for _, tt := range tests { t.Run(tt.resourceName, func(t *testing.T) { got := strategy.envVarName(tt.resourceName, tt.resourceType) if got != tt.expected { t.Errorf("envVarName(%q, %q) = %q, want %q", tt.resourceName, tt.resourceType, got, tt.expected) } }) } } func TestConvertToEnvVarName(t *testing.T) { tests := []struct { input string expected string }{ {"my-config", "MY_CONFIG"}, {"my.config", "MY_CONFIG"}, {"my_config", "MY_CONFIG"}, {"MY-CONFIG", "MY_CONFIG"}, {"config123", "CONFIG123"}, {"123config", "123CONFIG"}, {"my--config", "MY_CONFIG"}, {"my..config", "MY_CONFIG"}, {"", ""}, {"-leading-dash", "LEADING_DASH"}, {"trailing-dash-", "TRAILING_DASH_"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := convertToEnvVarName(tt.input) if got != tt.expected { t.Errorf("convertToEnvVarName(%q) = %q, want %q", tt.input, got, tt.expected) } }) } } func TestAnnotationStrategy_Apply(t *testing.T) { cfg := config.NewDefault() strategy := NewAnnotationStrategy(cfg) t.Run("adds new annotation", func(t *testing.T) { annotations := make(map[string]string) container := &corev1.Container{Name: "test-container"} input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "abc123", Container: container, PodAnnotations: annotations, } changed, err := strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if !changed { t.Error("expected changed=true for new annotation") } // Verify annotation was added annotationValue := annotations[cfg.Annotations.LastReloadedFrom] if annotationValue == "" { t.Error("expected annotation to be set") } // Verify annotation content var source ReloadSource if err := json.Unmarshal([]byte(annotationValue), &source); err != nil { t.Fatalf("failed to unmarshal annotation: %v", err) } if source.Kind != string(matcher.ResourceTypeConfigMap) { t.Errorf("expected kind=%s, got %s", matcher.ResourceTypeConfigMap, source.Kind) } if source.Name != "my-config" { t.Errorf("expected name=my-config, got %s", source.Name) } if source.Hash != "abc123" { t.Errorf("expected hash=abc123, got %s", source.Hash) } }) t.Run("idempotent for same resource and hash (ignores timestamp)", func(t *testing.T) { annotations := make(map[string]string) input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "abc123", Container: &corev1.Container{Name: "c"}, PodAnnotations: annotations, } changed, err := strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if !changed { t.Fatal("expected changed=true on first apply") } firstValue := annotations[cfg.Annotations.LastReloadedFrom] // Re-applying the identical change must NOT report a change, even though // a fresh ReloadedAt timestamp would make the serialized value differ. changed, err = strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if changed { t.Error("expected changed=false when re-applying the same resource+hash") } if annotations[cfg.Annotations.LastReloadedFrom] != firstValue { t.Error("annotation value must not change on an idempotent re-apply") } // A different hash (real content change) must trigger an update. input.Hash = "def456" changed, err = strategy.Apply(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if !changed { t.Error("expected changed=true when the hash changes") } }) t.Run("error when annotations map is nil", func(t *testing.T) { input := StrategyInput{ ResourceName: "my-config", ResourceType: matcher.ResourceTypeConfigMap, Namespace: "default", Hash: "abc123", PodAnnotations: nil, } _, err := strategy.Apply(input) if err == nil { t.Error("expected error for nil annotations map") } }) } func TestNewStrategy(t *testing.T) { t.Run("default strategy is env-vars", func(t *testing.T) { cfg := config.NewDefault() strategy := NewStrategy(cfg) if strategy.Name() != string(config.ReloadStrategyEnvVars) { t.Errorf("expected env-vars strategy, got %s", strategy.Name()) } }) t.Run("annotations strategy when configured", func(t *testing.T) { cfg := config.NewDefault() cfg.ReloadStrategy = config.ReloadStrategyAnnotations strategy := NewStrategy(cfg) if strategy.Name() != string(config.ReloadStrategyAnnotations) { t.Errorf("expected annotations strategy, got %s", strategy.Name()) } }) } func TestEnvVarNameSecretProviderClass(t *testing.T) { s := NewEnvVarStrategy() got := s.envVarName("my-vault-spc", matcher.ResourceTypeSecretProviderClass) want := "STAKATER_MY_VAULT_SPC_SECRETPROVIDERCLASS" if got != want { t.Fatalf("envVarName = %q, want %q", got, want) } }