feat: Introduce a Generic ResourceReconciler and a generic BaseWorkload to de-duplicate a lot of code

This commit is contained in:
TheiLLeniumStudios
2026-01-05 00:59:57 +01:00
parent c785067a44
commit 5548ce559a
31 changed files with 1491 additions and 1356 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ import (
// ReloadDecision contains the result of evaluating whether to reload a workload.
type ReloadDecision struct {
// Workload is the workload accessor.
Workload workload.WorkloadAccessor
Workload workload.Workload
// ShouldReload indicates whether the workload should be reloaded.
ShouldReload bool
// AutoReload indicates if this is an auto-reload.
+3 -3
View File
@@ -20,7 +20,7 @@ func NewPauseHandler(cfg *config.Config) *PauseHandler {
}
// ShouldPause checks if a deployment should be paused after reload.
func (h *PauseHandler) ShouldPause(wl workload.WorkloadAccessor) bool {
func (h *PauseHandler) ShouldPause(wl workload.Workload) bool {
if wl.Kind() != workload.KindDeployment {
return false
}
@@ -35,7 +35,7 @@ func (h *PauseHandler) ShouldPause(wl workload.WorkloadAccessor) bool {
}
// GetPausePeriod returns the configured pause period for a workload.
func (h *PauseHandler) GetPausePeriod(wl workload.WorkloadAccessor) (time.Duration, error) {
func (h *PauseHandler) GetPausePeriod(wl workload.Workload) (time.Duration, error) {
annotations := wl.GetAnnotations()
if annotations == nil {
return 0, fmt.Errorf("no annotations on workload")
@@ -50,7 +50,7 @@ func (h *PauseHandler) GetPausePeriod(wl workload.WorkloadAccessor) (time.Durati
}
// ApplyPause pauses a deployment and sets the paused-at annotation.
func (h *PauseHandler) ApplyPause(wl workload.WorkloadAccessor) error {
func (h *PauseHandler) ApplyPause(wl workload.Workload) error {
deployWl, ok := wl.(*workload.DeploymentWorkload)
if !ok {
return fmt.Errorf("workload is not a deployment")
+2 -2
View File
@@ -16,7 +16,7 @@ func TestPauseHandler_ShouldPause(t *testing.T) {
tests := []struct {
name string
workload workload.WorkloadAccessor
workload workload.Workload
want bool
}{
{
@@ -66,7 +66,7 @@ func TestPauseHandler_GetPausePeriod(t *testing.T) {
tests := []struct {
name string
workload workload.WorkloadAccessor
workload workload.Workload
wantPeriod time.Duration
wantErr bool
}{
+26 -16
View File
@@ -3,8 +3,10 @@ package reload
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/workload"
corev1 "k8s.io/api/core/v1"
@@ -13,15 +15,17 @@ import (
// Service orchestrates the reload logic for ConfigMaps and Secrets.
type Service struct {
cfg *config.Config
log logr.Logger
hasher *Hasher
matcher *Matcher
strategy Strategy
}
// NewService creates a new reload Service with the given configuration.
func NewService(cfg *config.Config) *Service {
func NewService(cfg *config.Config, log logr.Logger) *Service {
return &Service{
cfg: cfg,
log: log,
hasher: NewHasher(),
matcher: NewMatcher(cfg),
strategy: NewStrategy(cfg),
@@ -29,7 +33,7 @@ func NewService(cfg *config.Config) *Service {
}
// Process evaluates all workloads to determine which should be reloaded.
func (s *Service) Process(change ResourceChange, workloads []workload.WorkloadAccessor) []ReloadDecision {
func (s *Service) Process(change ResourceChange, workloads []workload.Workload) []ReloadDecision {
if change.IsNil() {
return nil
}
@@ -59,7 +63,7 @@ func (s *Service) processResource(
resourceAnnotations map[string]string,
resourceType ResourceType,
hash string,
workloads []workload.WorkloadAccessor,
workloads []workload.Workload,
) []ReloadDecision {
var decisions []ReloadDecision
@@ -96,13 +100,15 @@ func (s *Service) processResource(
shouldReload = false
}
decisions = append(decisions, ReloadDecision{
Workload: wl,
ShouldReload: shouldReload,
AutoReload: matchResult.AutoReload,
Reason: matchResult.Reason,
Hash: hash,
})
decisions = append(
decisions, ReloadDecision{
Workload: wl,
ShouldReload: shouldReload,
AutoReload: matchResult.AutoReload,
Reason: matchResult.Reason,
Hash: hash,
},
)
}
return decisions
@@ -124,7 +130,7 @@ func (s *Service) shouldProcessEvent(eventType EventType) bool {
// ApplyReload applies the reload strategy to a workload.
func (s *Service) ApplyReload(
ctx context.Context,
wl workload.WorkloadAccessor,
wl workload.Workload,
resourceName string,
resourceType ResourceType,
namespace string,
@@ -149,20 +155,23 @@ func (s *Service) ApplyReload(
}
if updated {
s.setAttributionAnnotation(wl, resourceName, resourceType, namespace, hash, container)
// Attribution annotation is informational; log errors but don't fail reloads
if err := s.setAttributionAnnotation(wl, resourceName, resourceType, namespace, hash, container); err != nil {
s.log.V(1).Info("failed to set attribution annotation", "error", err, "workload", wl.GetName())
}
}
return updated, nil
}
func (s *Service) setAttributionAnnotation(
wl workload.WorkloadAccessor,
wl workload.Workload,
resourceName string,
resourceType ResourceType,
namespace string,
hash string,
container *corev1.Container,
) {
) error {
containerName := ""
if container != nil {
containerName = container.Name
@@ -179,14 +188,15 @@ func (s *Service) setAttributionAnnotation(
sourceJSON, err := json.Marshal(source)
if err != nil {
return
return fmt.Errorf("failed to marshal reload source: %w", err)
}
wl.SetPodTemplateAnnotation(s.cfg.Annotations.LastReloadedFrom, string(sourceJSON))
return nil
}
func (s *Service) findTargetContainer(
wl workload.WorkloadAccessor,
wl workload.Workload,
resourceName string,
resourceType ResourceType,
autoReload bool,
+39 -38
View File
@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/go-logr/logr/testr"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/internal/pkg/workload"
@@ -13,7 +14,7 @@ import (
func TestService_ProcessConfigMap_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Create a deployment with auto annotation that uses the configmap
deploy := testutil.NewDeployment(
@@ -34,7 +35,7 @@ func TestService_ProcessConfigMap_AutoReload(t *testing.T) {
},
}
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
@@ -74,7 +75,7 @@ func TestService_ProcessConfigMap_AutoReload(t *testing.T) {
func TestService_ProcessConfigMap_ExplicitAnnotation(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
@@ -82,7 +83,7 @@ func TestService_ProcessConfigMap_ExplicitAnnotation(t *testing.T) {
},
)
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
@@ -118,7 +119,7 @@ func TestService_ProcessConfigMap_ExplicitAnnotation(t *testing.T) {
func TestService_ProcessConfigMap_IgnoredResource(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Create a deployment with auto annotation
deploy := testutil.NewDeployment(
@@ -139,7 +140,7 @@ func TestService_ProcessConfigMap_IgnoredResource(t *testing.T) {
},
}
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
@@ -174,7 +175,7 @@ func TestService_ProcessConfigMap_IgnoredResource(t *testing.T) {
func TestService_ProcessSecret_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Create a deployment with auto annotation that uses the secret
deploy := testutil.NewDeployment(
@@ -193,7 +194,7 @@ func TestService_ProcessSecret_AutoReload(t *testing.T) {
},
}
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
@@ -230,7 +231,7 @@ func TestService_ProcessSecret_AutoReload(t *testing.T) {
func TestService_ProcessConfigMap_DeleteEvent(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnDelete = true
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Create a deployment with explicit configmap annotation
deploy := testutil.NewDeployment(
@@ -239,7 +240,7 @@ func TestService_ProcessConfigMap_DeleteEvent(t *testing.T) {
},
)
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
@@ -274,7 +275,7 @@ func TestService_ProcessConfigMap_DeleteEvent(t *testing.T) {
func TestService_ProcessConfigMap_DeleteEventDisabled(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnDelete = false // Disabled by default
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment(
"test-deploy", "default", map[string]string{
@@ -282,7 +283,7 @@ func TestService_ProcessConfigMap_DeleteEventDisabled(t *testing.T) {
},
)
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy),
}
@@ -309,7 +310,7 @@ func TestService_ProcessConfigMap_DeleteEventDisabled(t *testing.T) {
func TestService_ApplyReload_EnvVarStrategy(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
accessor := workload.NewDeploymentWorkload(deploy)
@@ -353,7 +354,7 @@ func TestService_ApplyReload_EnvVarStrategy(t *testing.T) {
func TestService_ApplyReload_AnnotationStrategy(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyAnnotations
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
accessor := workload.NewDeploymentWorkload(deploy)
@@ -379,7 +380,7 @@ func TestService_ApplyReload_AnnotationStrategy(t *testing.T) {
func TestService_ApplyReload_EnvVarDeletion(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
// Pre-add an env var
@@ -425,7 +426,7 @@ func TestService_ApplyReload_EnvVarDeletion(t *testing.T) {
func TestService_ApplyReload_NoChangeIfSameHash(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadStrategy = config.ReloadStrategyEnvVars
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", nil)
// Pre-add env var with same hash
@@ -448,7 +449,7 @@ func TestService_ApplyReload_NoChangeIfSameHash(t *testing.T) {
func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Create multiple workloads
deploy1 := testutil.NewDeployment(
@@ -494,7 +495,7 @@ func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
},
)
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy1),
workload.NewDeploymentWorkload(deploy2),
workload.NewDeploymentWorkload(deploy3),
@@ -535,7 +536,7 @@ func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
func TestService_ProcessConfigMap_DifferentNamespaces(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Create deployments in different namespaces
deploy1 := testutil.NewDeployment(
@@ -574,7 +575,7 @@ func TestService_ProcessConfigMap_DifferentNamespaces(t *testing.T) {
},
}
workloads := []workload.WorkloadAccessor{
workloads := []workload.Workload{
workload.NewDeploymentWorkload(deploy1),
workload.NewDeploymentWorkload(deploy2),
}
@@ -610,7 +611,7 @@ func TestService_ProcessConfigMap_DifferentNamespaces(t *testing.T) {
func TestService_Hasher(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
hasher := svc.Hasher()
if hasher == nil {
@@ -650,7 +651,7 @@ func TestService_shouldProcessEvent(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnCreate = tt.reloadOnCreate
cfg.ReloadOnDelete = tt.reloadOnDelete
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
result := svc.shouldProcessEvent(tt.eventType)
if result != tt.expected {
@@ -663,7 +664,7 @@ func TestService_shouldProcessEvent(t *testing.T) {
func TestService_findVolumeUsingResource_ConfigMap(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
@@ -749,7 +750,7 @@ func TestService_findVolumeUsingResource_ConfigMap(t *testing.T) {
func TestService_findVolumeUsingResource_Secret(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
@@ -824,7 +825,7 @@ func TestService_findVolumeUsingResource_Secret(t *testing.T) {
func TestService_findContainerWithVolumeMount(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
@@ -908,7 +909,7 @@ func TestService_findContainerWithVolumeMount(t *testing.T) {
func TestService_findContainerWithEnvRef_ConfigMap(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
@@ -1010,7 +1011,7 @@ func TestService_findContainerWithEnvRef_ConfigMap(t *testing.T) {
func TestService_findContainerWithEnvRef_Secret(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
tests := []struct {
name string
@@ -1099,7 +1100,7 @@ func TestService_findContainerWithEnvRef_Secret(t *testing.T) {
func TestService_findTargetContainer_AutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true and volume mount
deploy := testutil.NewDeployment("test", "default", nil)
@@ -1135,7 +1136,7 @@ func TestService_findTargetContainer_AutoReload(t *testing.T) {
func TestService_findTargetContainer_AutoReload_EnvRef(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true and env ref (no volume)
deploy := testutil.NewDeployment("test", "default", nil)
@@ -1173,7 +1174,7 @@ func TestService_findTargetContainer_AutoReload_EnvRef(t *testing.T) {
func TestService_findTargetContainer_AutoReload_InitContainer(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true where init container uses the volume
deploy := testutil.NewDeployment("test", "default", nil)
@@ -1216,7 +1217,7 @@ func TestService_findTargetContainer_AutoReload_InitContainer(t *testing.T) {
func TestService_findTargetContainer_AutoReload_InitContainerEnvRef(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// Test with autoReload=true where init container has env ref
deploy := testutil.NewDeployment("test", "default", nil)
@@ -1257,7 +1258,7 @@ func TestService_findTargetContainer_AutoReload_InitContainerEnvRef(t *testing.T
func TestService_findTargetContainer_NoContainers(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Containers = []corev1.Container{}
@@ -1271,7 +1272,7 @@ func TestService_findTargetContainer_NoContainers(t *testing.T) {
func TestService_findTargetContainer_NonAutoReload(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test", "default", nil)
deploy.Spec.Template.Spec.Containers = []corev1.Container{
@@ -1292,7 +1293,7 @@ func TestService_findTargetContainer_NonAutoReload(t *testing.T) {
func TestService_findTargetContainer_AutoReload_FallbackToFirst(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
// autoReload=true but no matching volume or env ref - should fallback to first container
deploy := testutil.NewDeployment("test", "default", nil)
@@ -1313,10 +1314,10 @@ func TestService_findTargetContainer_AutoReload_FallbackToFirst(t *testing.T) {
func TestService_ProcessNilChange(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test", "default", nil)
workloads := []workload.WorkloadAccessor{workload.NewDeploymentWorkload(deploy)}
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
// Test with nil ConfigMap
change := ConfigMapChange{
@@ -1333,14 +1334,14 @@ func TestService_ProcessNilChange(t *testing.T) {
func TestService_ProcessCreateEventDisabled(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnCreate = false
svc := NewService(cfg)
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment(
"test", "default", map[string]string{
"reloader.stakater.com/auto": "true",
},
)
workloads := []workload.WorkloadAccessor{workload.NewDeploymentWorkload(deploy)}
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test-cm", Namespace: "default"},