feat: Add controller-runtime reconcilers and unit tests for reload packag

This commit is contained in:
TheiLLeniumStudios
2025-12-28 08:47:52 +01:00
parent 94e3fcd702
commit 0a2aa122f5
7 changed files with 1702 additions and 1 deletions
+231
View File
@@ -0,0 +1,231 @@
package reload
import (
"testing"
corev1 "k8s.io/api/core/v1"
)
func TestHasher_HashConfigMap(t *testing.T) {
hasher := NewHasher()
tests := []struct {
name string
cm *corev1.ConfigMap
wantHash string
}{
{
name: "empty configmap",
cm: &corev1.ConfigMap{
Data: nil,
BinaryData: nil,
},
wantHash: hasher.EmptyHash(),
},
{
name: "configmap with data",
cm: &corev1.ConfigMap{
Data: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
// Hash should be deterministic
wantHash: hasher.HashConfigMap(&corev1.ConfigMap{
Data: map[string]string{
"key1": "value1",
"key2": "value2",
},
}),
},
{
name: "configmap with binary data",
cm: &corev1.ConfigMap{
BinaryData: map[string][]byte{
"binary1": []byte("binaryvalue1"),
},
},
wantHash: hasher.HashConfigMap(&corev1.ConfigMap{
BinaryData: map[string][]byte{
"binary1": []byte("binaryvalue1"),
},
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := hasher.HashConfigMap(tt.cm)
if got != tt.wantHash {
t.Errorf("HashConfigMap() = %v, want %v", got, tt.wantHash)
}
})
}
}
func TestHasher_HashConfigMap_Deterministic(t *testing.T) {
hasher := NewHasher()
cm := &corev1.ConfigMap{
Data: map[string]string{
"z-key": "value-z",
"a-key": "value-a",
"m-key": "value-m",
},
}
// Hash should be the same regardless of iteration order
hash1 := hasher.HashConfigMap(cm)
hash2 := hasher.HashConfigMap(cm)
hash3 := hasher.HashConfigMap(cm)
if hash1 != hash2 || hash2 != hash3 {
t.Errorf("Hash is not deterministic: %s, %s, %s", hash1, hash2, hash3)
}
}
func TestHasher_HashConfigMap_DifferentValues(t *testing.T) {
hasher := NewHasher()
cm1 := &corev1.ConfigMap{
Data: map[string]string{
"key": "value1",
},
}
cm2 := &corev1.ConfigMap{
Data: map[string]string{
"key": "value2",
},
}
hash1 := hasher.HashConfigMap(cm1)
hash2 := hasher.HashConfigMap(cm2)
if hash1 == hash2 {
t.Errorf("Different values should produce different hashes")
}
}
func TestHasher_HashSecret(t *testing.T) {
hasher := NewHasher()
tests := []struct {
name string
secret *corev1.Secret
wantHash string
}{
{
name: "empty secret",
secret: &corev1.Secret{
Data: nil,
},
wantHash: hasher.EmptyHash(),
},
{
name: "secret with data",
secret: &corev1.Secret{
Data: map[string][]byte{
"key1": []byte("value1"),
"key2": []byte("value2"),
},
},
wantHash: hasher.HashSecret(&corev1.Secret{
Data: map[string][]byte{
"key1": []byte("value1"),
"key2": []byte("value2"),
},
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := hasher.HashSecret(tt.secret)
if got != tt.wantHash {
t.Errorf("HashSecret() = %v, want %v", got, tt.wantHash)
}
})
}
}
func TestHasher_HashSecret_Deterministic(t *testing.T) {
hasher := NewHasher()
secret := &corev1.Secret{
Data: map[string][]byte{
"z-key": []byte("value-z"),
"a-key": []byte("value-a"),
"m-key": []byte("value-m"),
},
}
// Hash should be the same regardless of iteration order
hash1 := hasher.HashSecret(secret)
hash2 := hasher.HashSecret(secret)
hash3 := hasher.HashSecret(secret)
if hash1 != hash2 || hash2 != hash3 {
t.Errorf("Hash is not deterministic: %s, %s, %s", hash1, hash2, hash3)
}
}
func TestHasher_HashSecret_DifferentValues(t *testing.T) {
hasher := NewHasher()
secret1 := &corev1.Secret{
Data: map[string][]byte{
"key": []byte("value1"),
},
}
secret2 := &corev1.Secret{
Data: map[string][]byte{
"key": []byte("value2"),
},
}
hash1 := hasher.HashSecret(secret1)
hash2 := hasher.HashSecret(secret2)
if hash1 == hash2 {
t.Errorf("Different values should produce different hashes")
}
}
func TestHasher_EmptyHash(t *testing.T) {
hasher := NewHasher()
emptyHash := hasher.EmptyHash()
if emptyHash == "" {
t.Error("EmptyHash should not be empty string")
}
// Empty ConfigMap should match EmptyHash
cm := &corev1.ConfigMap{}
if hasher.HashConfigMap(cm) != emptyHash {
t.Error("Empty ConfigMap hash should equal EmptyHash")
}
// Empty Secret should match EmptyHash
secret := &corev1.Secret{}
if hasher.HashSecret(secret) != emptyHash {
t.Error("Empty Secret hash should equal EmptyHash")
}
}
func TestHasher_NilInput(t *testing.T) {
hasher := NewHasher()
// Test nil ConfigMap
cmHash := hasher.HashConfigMap(nil)
if cmHash != hasher.EmptyHash() {
t.Errorf("nil ConfigMap should return EmptyHash, got %s", cmHash)
}
// Test nil Secret
secretHash := hasher.HashSecret(nil)
if secretHash != hasher.EmptyHash() {
t.Errorf("nil Secret should return EmptyHash, got %s", secretHash)
}
}
+488
View File
@@ -0,0 +1,488 @@
package reload
import (
"testing"
"github.com/stakater/Reloader/internal/pkg/config"
)
func TestMatcher_ShouldReload(t *testing.T) {
defaultCfg := config.NewDefault()
matcher := NewMatcher(defaultCfg)
tests := []struct {
name string
input MatchInput
wantReload bool
wantAutoReload bool
description string
}{
// Ignore annotation tests
{
name: "ignore annotation on resource skips reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: map[string]string{"reloader.stakater.com/ignore": "true"},
WorkloadAnnotations: map[string]string{"reloader.stakater.com/auto": "true"},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Resources with ignore annotation should never trigger reload",
},
{
name: "ignore annotation false allows reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: map[string]string{"reloader.stakater.com/ignore": "false"},
WorkloadAnnotations: map[string]string{"reloader.stakater.com/auto": "true"},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: true,
description: "Resources with ignore=false should allow reload",
},
// Exclude annotation tests
{
name: "exclude annotation skips reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"configmaps.exclude.reloader.stakater.com/reload": "my-config",
},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Excluded ConfigMaps should not trigger reload",
},
{
name: "exclude annotation with multiple values",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"configmaps.exclude.reloader.stakater.com/reload": "other-config,my-config,another-config",
},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "ConfigMaps in comma-separated exclude list should not trigger reload",
},
// BUG FIX: Explicit annotation checked BEFORE auto
{
name: "explicit reload annotation with auto enabled - should reload",
input: MatchInput{
ResourceName: "external-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"configmap.reloader.stakater.com/reload": "external-config",
},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: false, // Explicit, not auto
description: "BUG FIX: Explicit reload annotation should work even when auto is enabled",
},
{
name: "explicit reload annotation matches pattern - should reload",
input: MatchInput{
ResourceName: "app-config-v2",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"configmap.reloader.stakater.com/reload": "app-config-.*",
},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: false,
description: "Regex pattern in reload annotation should match",
},
{
name: "explicit reload annotation does not match - should not reload",
input: MatchInput{
ResourceName: "other-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"configmap.reloader.stakater.com/reload": "app-config",
},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "ConfigMaps not in reload list should not trigger reload",
},
// Auto annotation tests
{
name: "auto annotation on workload triggers reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{"reloader.stakater.com/auto": "true"},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: true,
description: "Auto annotation on workload should trigger reload",
},
{
name: "auto annotation on pod template triggers reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: nil,
PodAnnotations: map[string]string{"reloader.stakater.com/auto": "true"},
},
wantReload: true,
wantAutoReload: true,
description: "Auto annotation on pod template should trigger reload",
},
{
name: "configmap-specific auto annotation",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{"configmap.reloader.stakater.com/auto": "true"},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: true,
description: "ConfigMap-specific auto annotation should trigger reload",
},
{
name: "secret-specific auto annotation for secret",
input: MatchInput{
ResourceName: "my-secret",
ResourceNamespace: "default",
ResourceType: ResourceTypeSecret,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{"secret.reloader.stakater.com/auto": "true"},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: true,
description: "Secret-specific auto annotation should trigger reload for secrets",
},
{
name: "configmap-specific auto annotation does not match secret",
input: MatchInput{
ResourceName: "my-secret",
ResourceNamespace: "default",
ResourceType: ResourceTypeSecret,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{"configmap.reloader.stakater.com/auto": "true"},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "ConfigMap-specific auto annotation should not match secrets",
},
// Search/Match annotation tests
{
name: "search annotation with matching resource",
input: MatchInput{
ResourceName: "app-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: map[string]string{"reloader.stakater.com/match": "true"},
WorkloadAnnotations: map[string]string{"reloader.stakater.com/search": "true"},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: true, // Search mode is an auto-discovery mechanism
description: "Search annotation with matching resource should trigger reload",
},
{
name: "search annotation without matching resource",
input: MatchInput{
ResourceName: "app-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{"reloader.stakater.com/search": "true"},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Search annotation without matching resource should not trigger reload",
},
// No annotations - should not reload
{
name: "no annotations does not trigger reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: nil,
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Without any annotations, should not trigger reload",
},
// Secret tests
{
name: "secret reload annotation",
input: MatchInput{
ResourceName: "my-secret",
ResourceNamespace: "default",
ResourceType: ResourceTypeSecret,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"secret.reloader.stakater.com/reload": "my-secret",
},
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: false,
description: "Secret reload annotation should trigger reload",
},
{
name: "secret exclude annotation",
input: MatchInput{
ResourceName: "my-secret",
ResourceNamespace: "default",
ResourceType: ResourceTypeSecret,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"secrets.exclude.reloader.stakater.com/reload": "my-secret",
},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Secret exclude annotation should prevent reload",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := matcher.ShouldReload(tt.input)
if result.ShouldReload != tt.wantReload {
t.Errorf("ShouldReload = %v, want %v (%s)", result.ShouldReload, tt.wantReload, tt.description)
}
if result.AutoReload != tt.wantAutoReload {
t.Errorf("AutoReload = %v, want %v (%s)", result.AutoReload, tt.wantAutoReload, tt.description)
}
t.Logf("✓ %s", tt.description)
})
}
}
func TestMatcher_ShouldReload_AutoReloadAll(t *testing.T) {
cfg := config.NewDefault()
cfg.AutoReloadAll = true
matcher := NewMatcher(cfg)
tests := []struct {
name string
input MatchInput
wantReload bool
wantAutoReload bool
description string
}{
{
name: "auto-reload-all triggers reload",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: nil,
PodAnnotations: nil,
},
wantReload: true,
wantAutoReload: true,
description: "With auto-reload-all enabled, all ConfigMaps should trigger reload",
},
{
name: "auto-reload-all respects ignore annotation",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: map[string]string{"reloader.stakater.com/ignore": "true"},
WorkloadAnnotations: nil,
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Even with auto-reload-all, ignore annotation should be respected",
},
{
name: "auto-reload-all respects exclude annotation",
input: MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"configmaps.exclude.reloader.stakater.com/reload": "my-config",
},
PodAnnotations: nil,
},
wantReload: false,
wantAutoReload: false,
description: "Even with auto-reload-all, exclude annotation should be respected",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := matcher.ShouldReload(tt.input)
if result.ShouldReload != tt.wantReload {
t.Errorf("ShouldReload = %v, want %v (%s)", result.ShouldReload, tt.wantReload, tt.description)
}
if result.AutoReload != tt.wantAutoReload {
t.Errorf("AutoReload = %v, want %v (%s)", result.AutoReload, tt.wantAutoReload, tt.description)
}
t.Logf("✓ %s", tt.description)
})
}
}
// TestMatcher_BugFix_AutoDoesNotIgnoreExplicit tests the fix for the bug where
// having reloader.stakater.com/auto: "true" would cause explicit reload annotations
// to be ignored due to an early return.
func TestMatcher_BugFix_AutoDoesNotIgnoreExplicit(t *testing.T) {
cfg := config.NewDefault()
matcher := NewMatcher(cfg)
// This is the exact scenario from the bug report:
// Workload has:
// reloader.stakater.com/auto: "true" (watches all referenced CMs)
// configmap.reloader.stakater.com/reload: "external-config" (ALSO watches this one)
// Container references: app-config
//
// When "external-config" changes:
// - Expected: Reload (explicitly listed)
// - Bug behavior: No reload (auto annotation causes early return)
input := MatchInput{
ResourceName: "external-config", // Not referenced by workload
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true", // Enables auto-reload
"configmap.reloader.stakater.com/reload": "external-config", // Explicit list
},
PodAnnotations: nil,
}
result := matcher.ShouldReload(input)
if !result.ShouldReload {
t.Errorf("BUG: Explicit reload annotation ignored when auto is enabled")
t.Errorf("Expected ShouldReload=true for explicitly listed ConfigMap, got false")
}
// Should be marked as non-auto since it matched the explicit list
if result.AutoReload {
t.Errorf("Expected AutoReload=false for explicit match, got true")
}
t.Log("✓ Bug fixed: Explicit reload annotation works even when auto is enabled")
}
// TestMatcher_PrecedenceOrder verifies the correct order of precedence:
// 1. Ignore annotation → skip
// 2. Exclude annotation → skip
// 3. Explicit reload annotation → reload (BUG FIX: before auto!)
// 4. Search/Match → reload
// 5. Auto annotation → reload
// 6. Auto-reload-all → reload
func TestMatcher_PrecedenceOrder(t *testing.T) {
cfg := config.NewDefault()
matcher := NewMatcher(cfg)
t.Run("explicit takes precedence over auto", func(t *testing.T) {
input := MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"configmap.reloader.stakater.com/reload": "my-config",
},
}
result := matcher.ShouldReload(input)
if result.AutoReload {
t.Error("Expected explicit match (AutoReload=false), got auto match")
}
if !result.ShouldReload {
t.Error("Expected ShouldReload=true")
}
})
t.Run("ignore takes precedence over explicit", func(t *testing.T) {
input := MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: map[string]string{"reloader.stakater.com/ignore": "true"},
WorkloadAnnotations: map[string]string{
"configmap.reloader.stakater.com/reload": "my-config",
},
}
result := matcher.ShouldReload(input)
if result.ShouldReload {
t.Error("Expected ignore to take precedence, but got ShouldReload=true")
}
})
t.Run("exclude takes precedence over explicit", func(t *testing.T) {
input := MatchInput{
ResourceName: "my-config",
ResourceNamespace: "default",
ResourceType: ResourceTypeConfigMap,
WorkloadAnnotations: map[string]string{
"configmap.reloader.stakater.com/reload": "my-config",
"configmaps.exclude.reloader.stakater.com/reload": "my-config",
},
}
result := matcher.ShouldReload(input)
if result.ShouldReload {
t.Error("Expected exclude to take precedence, but got ShouldReload=true")
}
})
}
+292
View File
@@ -0,0 +1,292 @@
package reload
import (
"encoding/json"
"testing"
"github.com/stakater/Reloader/internal/pkg/config"
corev1 "k8s.io/api/core/v1"
)
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: 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: 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: 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: 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: 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 ResourceType
expected string
}{
{"my-config", ResourceTypeConfigMap, "STAKATER_MY_CONFIG_CONFIGMAP"},
{"my-secret", ResourceTypeSecret, "STAKATER_MY_SECRET_SECRET"},
{"app-config-v2", ResourceTypeConfigMap, "STAKATER_APP_CONFIG_V2_CONFIGMAP"},
{"my.dotted.config", ResourceTypeConfigMap, "STAKATER_MY_DOTTED_CONFIG_CONFIGMAP"},
{"MyMixedCase", ResourceTypeConfigMap, "STAKATER_MYMIXEDCASE_CONFIGMAP"},
{"config-with-123-numbers", 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: 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(ResourceTypeConfigMap) {
t.Errorf("expected kind=%s, got %s", 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("error when annotations map is nil", func(t *testing.T) {
input := StrategyInput{
ResourceName: "my-config",
ResourceType: 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())
}
})
}