package matcher import ( "testing" "github.com/stakater/Reloader/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 }{ { 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", }, { 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", }, { 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", }, { 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", }, { 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", }, { 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", }, { 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_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_AutoDoesNotIgnoreExplicit(t *testing.T) { cfg := config.NewDefault() matcher := NewMatcher(cfg) 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") } if result.AutoReload { t.Errorf("Expected AutoReload=false for explicit match, got true") } t.Log("✓ Explicit reload annotation works even when auto is enabled") } func TestMatcherSecretProviderClassExplicit(t *testing.T) { cfg := config.NewDefault() m := NewMatcher(cfg) res := m.ShouldReload(MatchInput{ ResourceName: "my-spc", ResourceType: ResourceTypeSecretProviderClass, WorkloadAnnotations: map[string]string{ "secretproviderclass.reloader.stakater.com/reload": "my-spc", }, }) if !res.ShouldReload || res.AutoReload { t.Fatalf("explicit SPC reload: got %+v", res) } } func TestMatcherSecretProviderClassAuto(t *testing.T) { cfg := config.NewDefault() m := NewMatcher(cfg) res := m.ShouldReload(MatchInput{ ResourceName: "my-spc", ResourceType: ResourceTypeSecretProviderClass, WorkloadAnnotations: map[string]string{ "secretproviderclass.reloader.stakater.com/auto": "true", }, }) if !res.ShouldReload || !res.AutoReload { t.Fatalf("auto SPC reload: got %+v", res) } } func TestMatcherSecretProviderClassExcluded(t *testing.T) { cfg := config.NewDefault() m := NewMatcher(cfg) res := m.ShouldReload(MatchInput{ ResourceName: "my-spc", ResourceType: ResourceTypeSecretProviderClass, WorkloadAnnotations: map[string]string{ "secretproviderclass.reloader.stakater.com/auto": "true", "secretproviderclasses.exclude.reloader.stakater.com/reload": "my-spc", }, }) if res.ShouldReload { t.Fatalf("excluded SPC should not reload: got %+v", res) } } // 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") } }, ) }