From 8b1e55dec2de72e152dfcfd4bc99d871bbd1f05a Mon Sep 17 00:00:00 2001 From: kklinch0 Date: Fri, 8 Aug 2025 16:57:31 +0300 Subject: [PATCH] controller add CozystackResourceDefinition reconciler Signed-off-by: kklinch0 --- cmd/cozystack-controller/main.go | 8 ++ .../cozystackresource_controller.go | 117 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 internal/controller/cozystackresource_controller.go diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index 3ffc3027..40220b85 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -206,6 +206,14 @@ func main() { os.Exit(1) } + if err = (&controller.CozystackResourceDefinitionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/internal/controller/cozystackresource_controller.go b/internal/controller/cozystackresource_controller.go new file mode 100644 index 00000000..c9880b40 --- /dev/null +++ b/internal/controller/cozystackresource_controller.go @@ -0,0 +1,117 @@ +package controller + +import ( + "context" + "sync" + "time" + + cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type CozystackResourceDefinitionReconciler struct { + client.Client + Scheme *runtime.Scheme + + // Configurable debounce duration + Debounce time.Duration + + // Internal state for debouncing + mu sync.Mutex + lastEvent time.Time // Time of last CRUD event on CozystackResourceDefinition + lastHandled time.Time // Last time the Deployment was actually restarted +} + +// Reconcile handles the logic to restart the target Deployment only once, +// even if multiple events occur close together +func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // Only respond to our target deployment + if req.Namespace != "cozy-system" || req.Name != "cozystack-api" { + return ctrl.Result{}, nil + } + + r.mu.Lock() + le := r.lastEvent + lh := r.lastHandled + debounce := r.Debounce + r.mu.Unlock() + + if debounce <= 0 { + debounce = 5 * time.Second + } + + // No events received yet — nothing to do + if le.IsZero() { + return ctrl.Result{}, nil + } + + // Wait until the debounce duration has passed since the last event + if d := time.Since(le); d < debounce { + return ctrl.Result{RequeueAfter: debounce - d}, nil + } + + // Already handled this event — skip restart + if !lh.Before(le) { + return ctrl.Result{}, nil + } + + // Perform the restart by patching the deployment annotation + deploy := &appsv1.Deployment{} + if err := r.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}, deploy); err != nil { + log.Error(err, "Failed to get Deployment cozy-system/cozystack-api") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + patch := client.MergeFrom(deploy.DeepCopy()) + if deploy.Spec.Template.Annotations == nil { + deploy.Spec.Template.Annotations = make(map[string]string) + } + deploy.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + + if err := r.Patch(ctx, deploy, patch); err != nil { + log.Error(err, "Failed to patch Deployment annotation") + return ctrl.Result{}, err + } + + // Mark this event as handled + r.mu.Lock() + r.lastHandled = le + r.mu.Unlock() + + log.Info("Deployment cozy-system/cozystack-api successfully restarted") + return ctrl.Result{}, nil +} + +// SetupWithManager configures how the controller listens to events +func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error { + if r.Debounce == 0 { + r.Debounce = 5 * time.Second + } + + return ctrl.NewControllerManagedBy(mgr). + Named("cozystack-restart-controller"). + Watches( + &cozyv1alpha1.CozystackResourceDefinition{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + r.mu.Lock() + r.lastEvent = time.Now() + r.mu.Unlock() + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{ + Namespace: "cozy-system", + Name: "cozystack-api", + }, + }} + }), + ). + Complete(r) +}