controller add CozystackResourceDefinition reconciler (#1313)

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
- add cozystackresource reconciler
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced automated rolling restarts for the "cozystack-api"
deployment in the "cozy-system" namespace when changes are detected in
related custom resources. This ensures updates are applied smoothly
without manual intervention.
* Added debounce logic to optimize restart frequency, preventing
multiple rapid restarts by consolidating events within a configurable
time window.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
klinch0
2025-08-08 17:21:46 +03:00
committed by GitHub
2 changed files with 125 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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)
}