refactor: Move all common reconcile logic to lister and reload handler

This commit is contained in:
TheiLLeniumStudios
2025-12-28 08:47:55 +01:00
parent 9d588cadb2
commit 3defc8bb29
9 changed files with 752 additions and 1091 deletions
+33 -341
View File
@@ -3,7 +3,6 @@ package controller
import (
"context"
"sync"
"time"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/alerting"
@@ -13,14 +12,10 @@ import (
"github.com/stakater/Reloader/internal/pkg/reload"
"github.com/stakater/Reloader/internal/pkg/webhook"
"github.com/stakater/Reloader/internal/pkg/workload"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
@@ -36,6 +31,7 @@ type ConfigMapReconciler struct {
WebhookClient *webhook.Client
Alerter alerting.Alerter
handler *ReloadHandler
initialized bool
initOnce sync.Once
}
@@ -44,17 +40,14 @@ type ConfigMapReconciler struct {
func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("configmap", req.NamespacedName)
// Mark as initialized after first reconcile (caches are synced at this point)
r.initOnce.Do(func() {
r.initialized = true
log.Info("ConfigMap controller initialized")
})
// Fetch the ConfigMap
var cm corev1.ConfigMap
if err := r.Get(ctx, req.NamespacedName, &cm); err != nil {
if errors.IsNotFound(err) {
// ConfigMap was deleted - handle if ReloadOnDelete is enabled
if r.Config.ReloadOnDelete {
return r.handleDelete(ctx, req, log)
}
@@ -64,364 +57,63 @@ func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, err
}
// Check if namespace should be ignored
if r.Config.IsNamespaceIgnored(cm.Namespace) {
log.V(1).Info("skipping ConfigMap in ignored namespace")
return ctrl.Result{}, nil
}
// Get all workloads in the same namespace
workloads, err := r.listWorkloads(ctx, cm.Namespace)
if err != nil {
log.Error(err, "failed to list workloads")
return ctrl.Result{}, err
}
// Evaluate which workloads should be reloaded
change := reload.ConfigMapChange{
ConfigMap: &cm,
EventType: reload.EventTypeUpdate,
}
decisions := r.ReloadService.ProcessConfigMap(change, workloads)
// Collect workloads that should be reloaded
var workloadsToReload []reload.ReloadDecision
for _, decision := range decisions {
if decision.ShouldReload {
workloadsToReload = append(workloadsToReload, decision)
}
}
// If webhook is configured, send notification instead of modifying workloads
if r.WebhookClient.IsConfigured() && len(workloadsToReload) > 0 {
return r.sendWebhookNotification(ctx, cm.Name, cm.Namespace, reload.ResourceTypeConfigMap, workloadsToReload, log)
}
// Apply reloads with conflict retry
for _, decision := range workloadsToReload {
log.Info("reloading workload",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
"reason", decision.Reason,
)
updated, err := UpdateWorkloadWithRetry(
ctx,
r.Client,
r.ReloadService,
decision.Workload,
cm.Name,
reload.ResourceTypeConfigMap,
cm.Namespace,
decision.Hash,
decision.AutoReload,
)
if err != nil {
log.Error(err, "failed to update workload",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
r.EventRecorder.ReloadFailed(decision.Workload.GetObject(), "ConfigMap", cm.Name, err)
r.recordMetrics(false, cm.Namespace)
continue
}
if updated {
r.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), "ConfigMap", cm.Name)
r.recordMetrics(true, cm.Namespace)
log.Info("workload reloaded successfully",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "ConfigMap",
ResourceName: cm.Name,
ResourceNamespace: cm.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
return ctrl.Result{}, nil
return r.reloadHandler().Process(ctx, cm.Namespace, cm.Name, "ConfigMap", reload.ResourceTypeConfigMap,
func(workloads []workload.WorkloadAccessor) []reload.ReloadDecision {
return r.ReloadService.ProcessConfigMap(reload.ConfigMapChange{
ConfigMap: &cm,
EventType: reload.EventTypeUpdate,
}, workloads)
}, log)
}
// FieldManager is the field manager name used for server-side apply.
const FieldManager = "reloader"
// handleDelete handles ConfigMap deletion events.
func (r *ConfigMapReconciler) handleDelete(ctx context.Context, req ctrl.Request, log logr.Logger) (ctrl.Result, error) {
log.Info("handling ConfigMap deletion")
// Get all workloads in the namespace
workloads, err := r.listWorkloads(ctx, req.Namespace)
if err != nil {
log.Error(err, "failed to list workloads")
return ctrl.Result{}, err
}
cm := &corev1.ConfigMap{}
cm.Name = req.Name
cm.Namespace = req.Namespace
// For delete events, we create a change with empty ConfigMap
change := reload.ConfigMapChange{
ConfigMap: &corev1.ConfigMap{},
EventType: reload.EventTypeDelete,
}
change.ConfigMap.Name = req.Name
change.ConfigMap.Namespace = req.Namespace
return r.reloadHandler().Process(ctx, req.Namespace, req.Name, "ConfigMap", reload.ResourceTypeConfigMap,
func(workloads []workload.WorkloadAccessor) []reload.ReloadDecision {
return r.ReloadService.ProcessConfigMap(reload.ConfigMapChange{
ConfigMap: cm,
EventType: reload.EventTypeDelete,
}, workloads)
}, log)
}
decisions := r.ReloadService.ProcessConfigMap(change, workloads)
// Collect workloads that should be reloaded
var workloadsToReload []reload.ReloadDecision
for _, decision := range decisions {
if decision.ShouldReload {
workloadsToReload = append(workloadsToReload, decision)
func (r *ConfigMapReconciler) reloadHandler() *ReloadHandler {
if r.handler == nil {
r.handler = &ReloadHandler{
Client: r.Client,
Lister: workload.NewLister(r.Client, r.Registry, r.Config),
ReloadService: r.ReloadService,
WebhookClient: r.WebhookClient,
Collectors: r.Collectors,
EventRecorder: r.EventRecorder,
Alerter: r.Alerter,
}
}
// If webhook is configured, send notification instead of modifying workloads
if r.WebhookClient.IsConfigured() && len(workloadsToReload) > 0 {
return r.sendWebhookNotification(ctx, req.Name, req.Namespace, reload.ResourceTypeConfigMap, workloadsToReload, log)
}
// Apply reloads for delete with conflict retry
for _, decision := range workloadsToReload {
log.Info("reloading workload due to ConfigMap deletion",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
updated, err := UpdateWorkloadWithRetry(
ctx,
r.Client,
r.ReloadService,
decision.Workload,
req.Name,
reload.ResourceTypeConfigMap,
req.Namespace,
decision.Hash,
decision.AutoReload,
)
if err != nil {
log.Error(err, "failed to update workload")
r.EventRecorder.ReloadFailed(decision.Workload.GetObject(), "ConfigMap", req.Name, err)
r.recordMetrics(false, req.Namespace)
continue
}
if updated {
r.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), "ConfigMap", req.Name)
r.recordMetrics(true, req.Namespace)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "ConfigMap",
ResourceName: req.Name,
ResourceNamespace: req.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
return ctrl.Result{}, nil
}
// listWorkloads returns all workloads in the given namespace.
func (r *ConfigMapReconciler) listWorkloads(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var result []workload.WorkloadAccessor
for _, kind := range r.Registry.SupportedKinds() {
// Skip ignored workload types
if r.Config.IsWorkloadIgnored(string(kind)) {
continue
}
workloads, err := r.listWorkloadsByKind(ctx, namespace, kind)
if err != nil {
return nil, err
}
result = append(result, workloads...)
}
return result, nil
}
// listWorkloadsByKind lists workloads of a specific kind in the namespace.
func (r *ConfigMapReconciler) listWorkloadsByKind(ctx context.Context, namespace string, kind workload.Kind) ([]workload.WorkloadAccessor, error) {
switch kind {
case workload.KindDeployment:
return r.listDeployments(ctx, namespace)
case workload.KindDaemonSet:
return r.listDaemonSets(ctx, namespace)
case workload.KindStatefulSet:
return r.listStatefulSets(ctx, namespace)
case workload.KindJob:
return r.listJobs(ctx, namespace)
case workload.KindCronJob:
return r.listCronJobs(ctx, namespace)
default:
return nil, nil
}
}
func (r *ConfigMapReconciler) listDeployments(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list appsv1.DeploymentList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewDeploymentWorkload(&list.Items[i])
}
return result, nil
}
func (r *ConfigMapReconciler) listDaemonSets(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list appsv1.DaemonSetList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewDaemonSetWorkload(&list.Items[i])
}
return result, nil
}
func (r *ConfigMapReconciler) listStatefulSets(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list appsv1.StatefulSetList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewStatefulSetWorkload(&list.Items[i])
}
return result, nil
}
func (r *ConfigMapReconciler) listJobs(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list batchv1.JobList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewJobWorkload(&list.Items[i])
}
return result, nil
}
func (r *ConfigMapReconciler) listCronJobs(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list batchv1.CronJobList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewCronJobWorkload(&list.Items[i])
}
return result, nil
}
// recordMetrics records reload metrics.
func (r *ConfigMapReconciler) recordMetrics(success bool, namespace string) {
r.Collectors.RecordReload(success, namespace)
}
// sendWebhookNotification sends a webhook notification instead of modifying workloads.
func (r *ConfigMapReconciler) sendWebhookNotification(
ctx context.Context,
resourceName, namespace string,
resourceType reload.ResourceType,
decisions []reload.ReloadDecision,
log logr.Logger,
) (ctrl.Result, error) {
var workloads []webhook.WorkloadInfo
var hash string
for _, d := range decisions {
workloads = append(workloads, webhook.WorkloadInfo{
Kind: string(d.Workload.Kind()),
Name: d.Workload.GetName(),
Namespace: d.Workload.GetNamespace(),
})
if hash == "" {
hash = d.Hash
}
}
payload := webhook.Payload{
Kind: string(resourceType),
Namespace: namespace,
ResourceName: resourceName,
ResourceType: string(resourceType),
Hash: hash,
Timestamp: time.Now().UTC(),
Workloads: workloads,
}
if err := r.WebhookClient.Send(ctx, payload); err != nil {
log.Error(err, "failed to send webhook notification")
r.recordMetrics(false, namespace)
return ctrl.Result{}, err
}
log.Info("webhook notification sent",
"resource", resourceName,
"workloadCount", len(workloads),
)
r.recordMetrics(true, namespace)
return ctrl.Result{}, nil
return r.handler
}
// SetupWithManager sets up the controller with the Manager.
func (r *ConfigMapReconciler) SetupWithManager(mgr ctrl.Manager) error {
hasher := r.ReloadService.Hasher()
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.ConfigMap{}).
WithEventFilter(predicate.And(
reload.ConfigMapPredicates(r.Config, hasher),
reload.NamespaceFilterPredicate(r.Config),
reload.LabelSelectorPredicate(r.Config),
reload.IgnoreAnnotationPredicate(r.Config),
r.createEventFilter(),
WithEventFilter(BuildEventFilter(
reload.ConfigMapPredicates(r.Config, r.ReloadService.Hasher()),
r.Config, &r.initialized,
)).
Complete(r)
}
// createEventFilter filters create events based on initialization state.
func (r *ConfigMapReconciler) createEventFilter() predicate.Predicate {
return predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
// During startup, skip create events unless SyncAfterRestart is enabled
if !r.initialized && !r.Config.SyncAfterRestart {
return false
}
// After initialization, only process creates if ReloadOnCreate is enabled
return r.Config.ReloadOnCreate
},
UpdateFunc: func(e event.UpdateEvent) bool {
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
return r.Config.ReloadOnDelete
},
GenericFunc: func(e event.GenericEvent) bool {
return false
},
}
}
// Ensure ConfigMapReconciler implements reconcile.Reconciler
var _ reconcile.Reconciler = &ConfigMapReconciler{}
+187
View File
@@ -0,0 +1,187 @@
package controller
import (
"context"
"time"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/alerting"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/events"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/reload"
"github.com/stakater/Reloader/internal/pkg/webhook"
"github.com/stakater/Reloader/internal/pkg/workload"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
// ReloadHandler handles the common reload workflow.
type ReloadHandler struct {
Client client.Client
Lister *workload.Lister
ReloadService *reload.Service
WebhookClient *webhook.Client
Collectors *metrics.Collectors
EventRecorder *events.Recorder
Alerter alerting.Alerter
}
// Process handles the reload workflow: list workloads, get decisions, webhook or apply.
func (h *ReloadHandler) Process(
ctx context.Context,
namespace, resourceName, resourceKind string,
resourceType reload.ResourceType,
getDecisions func([]workload.WorkloadAccessor) []reload.ReloadDecision,
log logr.Logger,
) (ctrl.Result, error) {
workloads, err := h.Lister.List(ctx, namespace)
if err != nil {
log.Error(err, "failed to list workloads")
return ctrl.Result{}, err
}
decisions := reload.FilterDecisions(getDecisions(workloads))
if h.WebhookClient.IsConfigured() && len(decisions) > 0 {
return h.sendWebhook(ctx, resourceName, namespace, resourceType, decisions, log)
}
h.applyReloads(ctx, resourceName, namespace, resourceKind, resourceType, decisions, log)
return ctrl.Result{}, nil
}
func (h *ReloadHandler) sendWebhook(
ctx context.Context,
resourceName, namespace string,
resourceType reload.ResourceType,
decisions []reload.ReloadDecision,
log logr.Logger,
) (ctrl.Result, error) {
var workloads []webhook.WorkloadInfo
var hash string
for _, d := range decisions {
workloads = append(workloads, webhook.WorkloadInfo{
Kind: string(d.Workload.Kind()),
Name: d.Workload.GetName(),
Namespace: d.Workload.GetNamespace(),
})
if hash == "" {
hash = d.Hash
}
}
payload := webhook.Payload{
Kind: string(resourceType),
Namespace: namespace,
ResourceName: resourceName,
ResourceType: string(resourceType),
Hash: hash,
Timestamp: time.Now().UTC(),
Workloads: workloads,
}
if err := h.WebhookClient.Send(ctx, payload); err != nil {
log.Error(err, "failed to send webhook notification")
h.Collectors.RecordReload(false, namespace)
return ctrl.Result{}, err
}
log.Info("webhook notification sent",
"resource", resourceName,
"workloadCount", len(workloads),
)
h.Collectors.RecordReload(true, namespace)
return ctrl.Result{}, nil
}
func (h *ReloadHandler) applyReloads(
ctx context.Context,
resourceName, resourceNamespace, resourceKind string,
resourceType reload.ResourceType,
decisions []reload.ReloadDecision,
log logr.Logger,
) {
for _, decision := range decisions {
log.Info("reloading workload",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
"reason", decision.Reason,
)
updated, err := UpdateWorkloadWithRetry(
ctx,
h.Client,
h.ReloadService,
decision.Workload,
resourceName,
resourceType,
resourceNamespace,
decision.Hash,
decision.AutoReload,
)
if err != nil {
log.Error(err, "failed to update workload",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
h.EventRecorder.ReloadFailed(decision.Workload.GetObject(), resourceKind, resourceName, err)
h.Collectors.RecordReload(false, resourceNamespace)
continue
}
if updated {
h.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), resourceKind, resourceName)
h.Collectors.RecordReload(true, resourceNamespace)
log.Info("workload reloaded successfully",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
if err := h.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: resourceKind,
ResourceName: resourceName,
ResourceNamespace: resourceNamespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
}
// BuildEventFilter combines a resource-specific predicate with common filters.
func BuildEventFilter(resourcePredicate predicate.Predicate, cfg *config.Config, initialized *bool) predicate.Predicate {
return predicate.And(
resourcePredicate,
reload.NamespaceFilterPredicate(cfg),
reload.LabelSelectorPredicate(cfg),
reload.IgnoreAnnotationPredicate(cfg),
createEventPredicate(cfg, initialized),
)
}
func createEventPredicate(cfg *config.Config, initialized *bool) predicate.Predicate {
return predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
if !*initialized && !cfg.SyncAfterRestart {
return false
}
return cfg.ReloadOnCreate
},
UpdateFunc: func(e event.UpdateEvent) bool {
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
return cfg.ReloadOnDelete
},
GenericFunc: func(e event.GenericEvent) bool {
return false
},
}
}
+33 -341
View File
@@ -3,7 +3,6 @@ package controller
import (
"context"
"sync"
"time"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/alerting"
@@ -13,14 +12,10 @@ import (
"github.com/stakater/Reloader/internal/pkg/reload"
"github.com/stakater/Reloader/internal/pkg/webhook"
"github.com/stakater/Reloader/internal/pkg/workload"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
@@ -36,6 +31,7 @@ type SecretReconciler struct {
WebhookClient *webhook.Client
Alerter alerting.Alerter
handler *ReloadHandler
initialized bool
initOnce sync.Once
}
@@ -44,17 +40,14 @@ type SecretReconciler struct {
func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("secret", req.NamespacedName)
// Mark as initialized after first reconcile (caches are synced at this point)
r.initOnce.Do(func() {
r.initialized = true
log.Info("Secret controller initialized")
})
// Fetch the Secret
var secret corev1.Secret
if err := r.Get(ctx, req.NamespacedName, &secret); err != nil {
if errors.IsNotFound(err) {
// Secret was deleted - handle if ReloadOnDelete is enabled
if r.Config.ReloadOnDelete {
return r.handleDelete(ctx, req, log)
}
@@ -64,361 +57,60 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, err
}
// Check if namespace should be ignored
if r.Config.IsNamespaceIgnored(secret.Namespace) {
log.V(1).Info("skipping Secret in ignored namespace")
return ctrl.Result{}, nil
}
// Get all workloads in the same namespace
workloads, err := r.listWorkloads(ctx, secret.Namespace)
if err != nil {
log.Error(err, "failed to list workloads")
return ctrl.Result{}, err
}
// Evaluate which workloads should be reloaded
change := reload.SecretChange{
Secret: &secret,
EventType: reload.EventTypeUpdate,
}
decisions := r.ReloadService.ProcessSecret(change, workloads)
// Collect workloads that should be reloaded
var workloadsToReload []reload.ReloadDecision
for _, decision := range decisions {
if decision.ShouldReload {
workloadsToReload = append(workloadsToReload, decision)
}
}
// If webhook is configured, send notification instead of modifying workloads
if r.WebhookClient.IsConfigured() && len(workloadsToReload) > 0 {
return r.sendWebhookNotification(ctx, secret.Name, secret.Namespace, reload.ResourceTypeSecret, workloadsToReload, log)
}
// Apply reloads with conflict retry
for _, decision := range workloadsToReload {
log.Info("reloading workload",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
"reason", decision.Reason,
)
updated, err := UpdateWorkloadWithRetry(
ctx,
r.Client,
r.ReloadService,
decision.Workload,
secret.Name,
reload.ResourceTypeSecret,
secret.Namespace,
decision.Hash,
decision.AutoReload,
)
if err != nil {
log.Error(err, "failed to update workload",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
r.EventRecorder.ReloadFailed(decision.Workload.GetObject(), "Secret", secret.Name, err)
r.recordMetrics(false, secret.Namespace)
continue
}
if updated {
r.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), "Secret", secret.Name)
r.recordMetrics(true, secret.Namespace)
log.Info("workload reloaded successfully",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "Secret",
ResourceName: secret.Name,
ResourceNamespace: secret.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
return ctrl.Result{}, nil
return r.reloadHandler().Process(ctx, secret.Namespace, secret.Name, "Secret", reload.ResourceTypeSecret,
func(workloads []workload.WorkloadAccessor) []reload.ReloadDecision {
return r.ReloadService.ProcessSecret(reload.SecretChange{
Secret: &secret,
EventType: reload.EventTypeUpdate,
}, workloads)
}, log)
}
// handleDelete handles Secret deletion events.
func (r *SecretReconciler) handleDelete(ctx context.Context, req ctrl.Request, log logr.Logger) (ctrl.Result, error) {
log.Info("handling Secret deletion")
// Get all workloads in the namespace
workloads, err := r.listWorkloads(ctx, req.Namespace)
if err != nil {
log.Error(err, "failed to list workloads")
return ctrl.Result{}, err
}
secret := &corev1.Secret{}
secret.Name = req.Name
secret.Namespace = req.Namespace
// For delete events, we create a change with empty Secret
change := reload.SecretChange{
Secret: &corev1.Secret{},
EventType: reload.EventTypeDelete,
}
change.Secret.Name = req.Name
change.Secret.Namespace = req.Namespace
return r.reloadHandler().Process(ctx, req.Namespace, req.Name, "Secret", reload.ResourceTypeSecret,
func(workloads []workload.WorkloadAccessor) []reload.ReloadDecision {
return r.ReloadService.ProcessSecret(reload.SecretChange{
Secret: secret,
EventType: reload.EventTypeDelete,
}, workloads)
}, log)
}
decisions := r.ReloadService.ProcessSecret(change, workloads)
// Collect workloads that should be reloaded
var workloadsToReload []reload.ReloadDecision
for _, decision := range decisions {
if decision.ShouldReload {
workloadsToReload = append(workloadsToReload, decision)
func (r *SecretReconciler) reloadHandler() *ReloadHandler {
if r.handler == nil {
r.handler = &ReloadHandler{
Client: r.Client,
Lister: workload.NewLister(r.Client, r.Registry, r.Config),
ReloadService: r.ReloadService,
WebhookClient: r.WebhookClient,
Collectors: r.Collectors,
EventRecorder: r.EventRecorder,
Alerter: r.Alerter,
}
}
// If webhook is configured, send notification instead of modifying workloads
if r.WebhookClient.IsConfigured() && len(workloadsToReload) > 0 {
return r.sendWebhookNotification(ctx, req.Name, req.Namespace, reload.ResourceTypeSecret, workloadsToReload, log)
}
// Apply reloads for delete with conflict retry
for _, decision := range workloadsToReload {
log.Info("reloading workload due to Secret deletion",
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
updated, err := UpdateWorkloadWithRetry(
ctx,
r.Client,
r.ReloadService,
decision.Workload,
req.Name,
reload.ResourceTypeSecret,
req.Namespace,
decision.Hash,
decision.AutoReload,
)
if err != nil {
log.Error(err, "failed to update workload")
r.EventRecorder.ReloadFailed(decision.Workload.GetObject(), "Secret", req.Name, err)
r.recordMetrics(false, req.Namespace)
continue
}
if updated {
r.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), "Secret", req.Name)
r.recordMetrics(true, req.Namespace)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "Secret",
ResourceName: req.Name,
ResourceNamespace: req.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
return ctrl.Result{}, nil
}
// listWorkloads returns all workloads in the given namespace.
func (r *SecretReconciler) listWorkloads(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var result []workload.WorkloadAccessor
for _, kind := range r.Registry.SupportedKinds() {
// Skip ignored workload types
if r.Config.IsWorkloadIgnored(string(kind)) {
continue
}
workloads, err := r.listWorkloadsByKind(ctx, namespace, kind)
if err != nil {
return nil, err
}
result = append(result, workloads...)
}
return result, nil
}
// listWorkloadsByKind lists workloads of a specific kind in the namespace.
func (r *SecretReconciler) listWorkloadsByKind(ctx context.Context, namespace string, kind workload.Kind) ([]workload.WorkloadAccessor, error) {
switch kind {
case workload.KindDeployment:
return r.listDeployments(ctx, namespace)
case workload.KindDaemonSet:
return r.listDaemonSets(ctx, namespace)
case workload.KindStatefulSet:
return r.listStatefulSets(ctx, namespace)
case workload.KindJob:
return r.listJobs(ctx, namespace)
case workload.KindCronJob:
return r.listCronJobs(ctx, namespace)
default:
return nil, nil
}
}
func (r *SecretReconciler) listDeployments(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list appsv1.DeploymentList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewDeploymentWorkload(&list.Items[i])
}
return result, nil
}
func (r *SecretReconciler) listDaemonSets(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list appsv1.DaemonSetList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewDaemonSetWorkload(&list.Items[i])
}
return result, nil
}
func (r *SecretReconciler) listStatefulSets(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list appsv1.StatefulSetList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewStatefulSetWorkload(&list.Items[i])
}
return result, nil
}
func (r *SecretReconciler) listJobs(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list batchv1.JobList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewJobWorkload(&list.Items[i])
}
return result, nil
}
func (r *SecretReconciler) listCronJobs(ctx context.Context, namespace string) ([]workload.WorkloadAccessor, error) {
var list batchv1.CronJobList
if err := r.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, err
}
result := make([]workload.WorkloadAccessor, len(list.Items))
for i := range list.Items {
result[i] = workload.NewCronJobWorkload(&list.Items[i])
}
return result, nil
}
// recordMetrics records reload metrics.
func (r *SecretReconciler) recordMetrics(success bool, namespace string) {
r.Collectors.RecordReload(success, namespace)
}
// sendWebhookNotification sends a webhook notification instead of modifying workloads.
func (r *SecretReconciler) sendWebhookNotification(
ctx context.Context,
resourceName, namespace string,
resourceType reload.ResourceType,
decisions []reload.ReloadDecision,
log logr.Logger,
) (ctrl.Result, error) {
var workloads []webhook.WorkloadInfo
var hash string
for _, d := range decisions {
workloads = append(workloads, webhook.WorkloadInfo{
Kind: string(d.Workload.Kind()),
Name: d.Workload.GetName(),
Namespace: d.Workload.GetNamespace(),
})
if hash == "" {
hash = d.Hash
}
}
payload := webhook.Payload{
Kind: string(resourceType),
Namespace: namespace,
ResourceName: resourceName,
ResourceType: string(resourceType),
Hash: hash,
Timestamp: time.Now().UTC(),
Workloads: workloads,
}
if err := r.WebhookClient.Send(ctx, payload); err != nil {
log.Error(err, "failed to send webhook notification")
r.recordMetrics(false, namespace)
return ctrl.Result{}, err
}
log.Info("webhook notification sent",
"resource", resourceName,
"workloadCount", len(workloads),
)
r.recordMetrics(true, namespace)
return ctrl.Result{}, nil
return r.handler
}
// SetupWithManager sets up the controller with the Manager.
func (r *SecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
hasher := r.ReloadService.Hasher()
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}).
WithEventFilter(predicate.And(
reload.SecretPredicates(r.Config, hasher),
reload.NamespaceFilterPredicate(r.Config),
reload.LabelSelectorPredicate(r.Config),
reload.IgnoreAnnotationPredicate(r.Config),
r.createEventFilter(),
WithEventFilter(BuildEventFilter(
reload.SecretPredicates(r.Config, r.ReloadService.Hasher()),
r.Config, &r.initialized,
)).
Complete(r)
}
// createEventFilter filters create events based on initialization state.
func (r *SecretReconciler) createEventFilter() predicate.Predicate {
return predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
// During startup, skip create events unless SyncAfterRestart is enabled
if !r.initialized && !r.Config.SyncAfterRestart {
return false
}
// After initialization, only process creates if ReloadOnCreate is enabled
return r.Config.ReloadOnCreate
},
UpdateFunc: func(e event.UpdateEvent) bool {
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
return r.Config.ReloadOnDelete
},
GenericFunc: func(e event.GenericEvent) bool {
return false
},
}
}
// Ensure SecretReconciler implements reconcile.Reconciler
var _ reconcile.Reconciler = &SecretReconciler{}