mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-23 22:16:45 +00:00
refactor: extract matcher into separate package
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
// Package matcher provides the annotation-based reload decision API. Given a
|
||||
// changed resource and a workload's annotations, Matcher reports whether the
|
||||
// workload should be reloaded. It is workload-type agnostic and safe to import
|
||||
// from outside this module.
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/stakater/Reloader/pkg/config"
|
||||
)
|
||||
|
||||
// MatchResult contains the result of checking if a workload should be reloaded.
|
||||
type MatchResult struct {
|
||||
ShouldReload bool
|
||||
AutoReload bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Matcher determines whether a workload should be reloaded based on annotations.
|
||||
type Matcher struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewMatcher creates a new Matcher with the given configuration.
|
||||
func NewMatcher(cfg *config.Config) *Matcher {
|
||||
return &Matcher{cfg: cfg}
|
||||
}
|
||||
|
||||
// MatchInput contains all the information needed to determine if a reload should occur.
|
||||
type MatchInput struct {
|
||||
ResourceName string
|
||||
ResourceNamespace string
|
||||
ResourceType ResourceType
|
||||
ResourceAnnotations map[string]string
|
||||
WorkloadAnnotations map[string]string
|
||||
PodAnnotations map[string]string
|
||||
}
|
||||
|
||||
// ShouldReload determines if a workload should be reloaded based on its annotations.
|
||||
func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
|
||||
if m.isResourceIgnored(input.ResourceAnnotations) {
|
||||
return MatchResult{
|
||||
ShouldReload: false,
|
||||
Reason: "resource has ignore annotation",
|
||||
}
|
||||
}
|
||||
|
||||
annotations := m.selectAnnotations(input)
|
||||
|
||||
if m.isResourceExcluded(input.ResourceName, input.ResourceType, annotations) {
|
||||
return MatchResult{
|
||||
ShouldReload: false,
|
||||
Reason: "resource is in exclude list",
|
||||
}
|
||||
}
|
||||
|
||||
if m.matchesExplicitAnnotation(input.ResourceName, input.ResourceType, annotations) {
|
||||
return MatchResult{
|
||||
ShouldReload: true,
|
||||
AutoReload: false,
|
||||
Reason: "matches explicit reload annotation",
|
||||
}
|
||||
}
|
||||
|
||||
if m.matchesSearchPattern(input.ResourceAnnotations, annotations) {
|
||||
return MatchResult{
|
||||
ShouldReload: true,
|
||||
AutoReload: true,
|
||||
Reason: "matches search/match pattern",
|
||||
}
|
||||
}
|
||||
|
||||
if m.matchesAutoAnnotation(input.ResourceType, annotations) {
|
||||
return MatchResult{
|
||||
ShouldReload: true,
|
||||
AutoReload: true,
|
||||
Reason: "auto annotation enabled",
|
||||
}
|
||||
}
|
||||
|
||||
if m.matchesAutoReloadAll(input.ResourceType, annotations) {
|
||||
return MatchResult{
|
||||
ShouldReload: true,
|
||||
AutoReload: true,
|
||||
Reason: "auto-reload-all enabled",
|
||||
}
|
||||
}
|
||||
|
||||
return MatchResult{
|
||||
ShouldReload: false,
|
||||
Reason: "no matching annotations",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matcher) isResourceIgnored(resourceAnnotations map[string]string) bool {
|
||||
if resourceAnnotations == nil {
|
||||
return false
|
||||
}
|
||||
return resourceAnnotations[m.cfg.Annotations.Ignore] == "true"
|
||||
}
|
||||
|
||||
func (m *Matcher) selectAnnotations(input MatchInput) map[string]string {
|
||||
if m.hasRelevantAnnotations(input.WorkloadAnnotations, input.ResourceType) {
|
||||
return input.WorkloadAnnotations
|
||||
}
|
||||
if m.hasRelevantAnnotations(input.PodAnnotations, input.ResourceType) {
|
||||
return input.PodAnnotations
|
||||
}
|
||||
return input.WorkloadAnnotations
|
||||
}
|
||||
|
||||
func (m *Matcher) hasRelevantAnnotations(annotations map[string]string, resourceType ResourceType) bool {
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
explicitAnn := m.getExplicitAnnotation(resourceType)
|
||||
if _, ok := annotations[explicitAnn]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := annotations[m.cfg.Annotations.Search]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := annotations[m.cfg.Annotations.Auto]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
typedAutoAnn := m.getTypedAutoAnnotation(resourceType)
|
||||
if _, ok := annotations[typedAutoAnn]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Matcher) isResourceExcluded(resourceName string, resourceType ResourceType, annotations map[string]string) bool {
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var excludeAnn string
|
||||
switch resourceType {
|
||||
case ResourceTypeConfigMap:
|
||||
excludeAnn = m.cfg.Annotations.ConfigmapExclude
|
||||
case ResourceTypeSecret:
|
||||
excludeAnn = m.cfg.Annotations.SecretExclude
|
||||
case ResourceTypeSecretProviderClass:
|
||||
excludeAnn = m.cfg.Annotations.SecretProviderClassExclude
|
||||
}
|
||||
|
||||
excludeList, ok := annotations[excludeAnn]
|
||||
if !ok || excludeList == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, excluded := range strings.Split(excludeList, ",") {
|
||||
if strings.TrimSpace(excluded) == resourceName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Matcher) matchesExplicitAnnotation(resourceName string, resourceType ResourceType, annotations map[string]string) bool {
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
explicitAnn := m.getExplicitAnnotation(resourceType)
|
||||
annotationValue, ok := annotations[explicitAnn]
|
||||
if !ok || annotationValue == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, value := range strings.Split(annotationValue, ",") {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
re, err := regexp.Compile("^" + value + "$")
|
||||
if err != nil {
|
||||
if value == resourceName {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if re.MatchString(resourceName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Matcher) matchesSearchPattern(resourceAnnotations, workloadAnnotations map[string]string) bool {
|
||||
if workloadAnnotations == nil || resourceAnnotations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
searchValue, ok := workloadAnnotations[m.cfg.Annotations.Search]
|
||||
if !ok || searchValue != "true" {
|
||||
return false
|
||||
}
|
||||
|
||||
matchValue, ok := resourceAnnotations[m.cfg.Annotations.Match]
|
||||
return ok && matchValue == "true"
|
||||
}
|
||||
|
||||
func (m *Matcher) matchesAutoAnnotation(resourceType ResourceType, annotations map[string]string) bool {
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if annotations[m.cfg.Annotations.Auto] == "true" {
|
||||
return true
|
||||
}
|
||||
|
||||
typedAutoAnn := m.getTypedAutoAnnotation(resourceType)
|
||||
return annotations[typedAutoAnn] == "true"
|
||||
}
|
||||
|
||||
func (m *Matcher) matchesAutoReloadAll(resourceType ResourceType, annotations map[string]string) bool {
|
||||
if !m.cfg.AutoReloadAll {
|
||||
return false
|
||||
}
|
||||
|
||||
if annotations != nil {
|
||||
if annotations[m.cfg.Annotations.Auto] == "false" {
|
||||
return false
|
||||
}
|
||||
typedAutoAnn := m.getTypedAutoAnnotation(resourceType)
|
||||
if annotations[typedAutoAnn] == "false" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Matcher) getExplicitAnnotation(resourceType ResourceType) string {
|
||||
switch resourceType {
|
||||
case ResourceTypeConfigMap:
|
||||
return m.cfg.Annotations.ConfigmapReload
|
||||
case ResourceTypeSecret:
|
||||
return m.cfg.Annotations.SecretReload
|
||||
case ResourceTypeSecretProviderClass:
|
||||
return m.cfg.Annotations.SecretProviderClassReload
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matcher) getTypedAutoAnnotation(resourceType ResourceType) string {
|
||||
switch resourceType {
|
||||
case ResourceTypeConfigMap:
|
||||
return m.cfg.Annotations.ConfigmapAuto
|
||||
case ResourceTypeSecret:
|
||||
return m.cfg.Annotations.SecretAuto
|
||||
case ResourceTypeSecretProviderClass:
|
||||
return m.cfg.Annotations.SecretProviderClassAuto
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
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")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package matcher
|
||||
|
||||
// ResourceType represents the type of Kubernetes resource.
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
// ResourceTypeConfigMap represents a ConfigMap resource.
|
||||
ResourceTypeConfigMap ResourceType = "configmap"
|
||||
// ResourceTypeSecret represents a Secret resource.
|
||||
ResourceTypeSecret ResourceType = "secret"
|
||||
// ResourceTypeSecretProviderClass represents a CSI SecretProviderClass resource.
|
||||
ResourceTypeSecretProviderClass ResourceType = "secretproviderclass"
|
||||
)
|
||||
|
||||
// Kind returns the capitalized Kubernetes Kind (e.g., "ConfigMap", "Secret").
|
||||
func (r ResourceType) Kind() string {
|
||||
switch r {
|
||||
case ResourceTypeConfigMap:
|
||||
return "ConfigMap"
|
||||
case ResourceTypeSecret:
|
||||
return "Secret"
|
||||
case ResourceTypeSecretProviderClass:
|
||||
return "SecretProviderClass"
|
||||
default:
|
||||
return string(r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceType_Kind(t *testing.T) {
|
||||
tests := []struct {
|
||||
resourceType ResourceType
|
||||
want string
|
||||
}{
|
||||
{ResourceTypeConfigMap, "ConfigMap"},
|
||||
{ResourceTypeSecret, "Secret"},
|
||||
{ResourceType("unknown"), "unknown"},
|
||||
{ResourceType("custom"), "custom"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
string(tt.resourceType), func(t *testing.T) {
|
||||
got := tt.resourceType.Kind()
|
||||
if got != tt.want {
|
||||
t.Errorf("ResourceType(%q).Kind() = %v, want %v", tt.resourceType, got, tt.want)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceTypeSecretProviderClassKind(t *testing.T) {
|
||||
if got := ResourceTypeSecretProviderClass.Kind(); got != "SecretProviderClass" {
|
||||
t.Fatalf("Kind() = %q, want SecretProviderClass", got)
|
||||
}
|
||||
if string(ResourceTypeSecretProviderClass) != "secretproviderclass" {
|
||||
t.Fatalf("value = %q, want secretproviderclass", ResourceTypeSecretProviderClass)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user