@@ -2,7 +2,12 @@ package controller
22
33import (
44 "context"
5+ "sort"
6+ "time"
57
8+ "github.com/robfig/cron/v3"
9+ corev1 "k8s.io/api/core/v1"
10+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
611 "k8s.io/apimachinery/pkg/runtime"
712 ctrl "sigs.k8s.io/controller-runtime"
813 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -21,6 +26,8 @@ type ImpVMSnapshotReconciler struct {
2126// +kubebuilder:rbac:groups=imp.dev,resources=impvmsnapshots/status,verbs=get;update;patch
2227// +kubebuilder:rbac:groups=imp.dev,resources=impvmsnapshots/finalizers,verbs=update
2328
29+ var cronParser = cron .NewParser (cron .Minute | cron .Hour | cron .Dom | cron .Month | cron .Dow )
30+
2431func (r * ImpVMSnapshotReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
2532 log := logf .FromContext (ctx )
2633
@@ -29,7 +36,12 @@ func (r *ImpVMSnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Reques
2936 return ctrl.Result {}, client .IgnoreNotFound (err )
3037 }
3138
32- // Set initial phase
39+ // Design rule 1: only reconcile parent objects (no LabelSnapshotParent label).
40+ if _ , isChild := snap .Labels [impv1alpha1 .LabelSnapshotParent ]; isChild {
41+ return ctrl.Result {}, nil
42+ }
43+
44+ // Set initial phase to Pending if not yet set.
3345 if snap .Status .Phase == "" {
3446 base := snap .DeepCopy ()
3547 snap .Status .Phase = "Pending"
@@ -39,9 +51,152 @@ func (r *ImpVMSnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Reques
3951 log .Info ("ImpVMSnapshot created, set to Pending" , "name" , snap .Name )
4052 }
4153
42- // TODO: trigger agent snapshot, handle cron scheduling, OCI push.
54+ // List all child executions.
55+ childList := & impv1alpha1.ImpVMSnapshotList {}
56+ if err := r .List (ctx , childList ,
57+ client .InNamespace (snap .Namespace ),
58+ client.MatchingLabels {impv1alpha1 .LabelSnapshotParent : snap .Name },
59+ ); err != nil {
60+ return ctrl.Result {}, err
61+ }
62+ children := childList .Items
63+
64+ // Design rule 2: serialization gate — if any child has TerminatedAt == nil, requeue.
65+ for i := range children {
66+ if children [i ].Status .TerminatedAt == nil {
67+ log .Info ("active child exists, requeueing" , "parent" , snap .Name )
68+ return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
69+ }
70+ }
71+
72+ // Design rule 5: retention pruning — sort by creationTimestamp ascending, prune oldest beyond retention.
73+ retention := int (snap .Spec .Retention )
74+ if retention == 0 {
75+ retention = 3 // default
76+ }
77+ if len (children ) > retention {
78+ sort .Slice (children , func (i , j int ) bool {
79+ return children [i ].CreationTimestamp .Before (& children [j ].CreationTimestamp )
80+ })
81+ toDelete := children [:len (children )- retention ]
82+ for i := range toDelete {
83+ // Design rule 5: never delete the baseSnapshot child.
84+ if snap .Spec .BaseSnapshot != "" && toDelete [i ].Name == snap .Spec .BaseSnapshot {
85+ continue
86+ }
87+ if err := r .Delete (ctx , & toDelete [i ]); client .IgnoreNotFound (err ) != nil {
88+ return ctrl.Result {}, err
89+ }
90+ log .Info ("pruned old child" , "parent" , snap .Name , "child" , toDelete [i ].Name )
91+ }
92+ // Refresh children slice after pruning (rebuild from remaining).
93+ remaining := children [len (children )- retention :]
94+ // If baseSnapshot was skipped in toDelete, add it back if it was in toDelete range.
95+ children = remaining
96+ }
97+
98+ // Design rule 6: BaseSnapshot validation.
99+ if snap .Spec .BaseSnapshot != "" {
100+ for i := range children {
101+ if children [i ].Name == snap .Spec .BaseSnapshot &&
102+ children [i ].Status .Phase == "Succeeded" &&
103+ children [i ].Status .TerminatedAt != nil {
104+ if snap .Status .BaseSnapshot != snap .Spec .BaseSnapshot {
105+ base := snap .DeepCopy ()
106+ snap .Status .BaseSnapshot = snap .Spec .BaseSnapshot
107+ if err := r .Status ().Patch (ctx , snap , client .MergeFrom (base )); err != nil {
108+ return ctrl.Result {}, err
109+ }
110+ log .Info ("set status.baseSnapshot" , "parent" , snap .Name , "child" , snap .Spec .BaseSnapshot )
111+ }
112+ break
113+ }
114+ }
115+ }
116+
117+ // Design rule 3 & 4: decide whether to create a new child.
118+ if snap .Spec .Schedule == "" {
119+ // One-shot: create child only if no children exist at all.
120+ if len (childList .Items ) == 0 {
121+ if err := r .createChild (ctx , snap ); err != nil {
122+ return ctrl.Result {}, err
123+ }
124+ }
125+ // If children exist and all terminated, do nothing.
126+ return ctrl.Result {}, nil
127+ }
128+
129+ // Scheduled: parse cron and decide whether to create.
130+ sched , err := cronParser .Parse (snap .Spec .Schedule )
131+ if err != nil {
132+ log .Error (err , "invalid cron schedule" , "schedule" , snap .Spec .Schedule )
133+ return ctrl.Result {}, nil // non-retriable config error
134+ }
135+ now := time .Now ()
136+ next := sched .Next (now )
137+ untilNext := time .Until (next )
138+
139+ // If next tick is within 5s of now, create a child.
140+ if untilNext <= 5 * time .Second {
141+ if err := r .createChild (ctx , snap ); err != nil {
142+ return ctrl.Result {}, err
143+ }
144+ // Requeue after the next scheduled slot.
145+ next = sched .Next (time .Now ())
146+ untilNext = time .Until (next )
147+ }
148+
149+ // Update nextScheduledAt in status.
150+ nextTime := metav1 .NewTime (next )
151+ if snap .Status .NextScheduledAt == nil || ! snap .Status .NextScheduledAt .Equal (& nextTime ) {
152+ base := snap .DeepCopy ()
153+ snap .Status .NextScheduledAt = & nextTime
154+ if err := r .Status ().Patch (ctx , snap , client .MergeFrom (base )); err != nil {
155+ return ctrl.Result {}, err
156+ }
157+ }
158+
159+ return ctrl.Result {RequeueAfter : untilNext }, nil
160+ }
161+
162+ // createChild creates a new child execution ImpVMSnapshot for the given parent.
163+ func (r * ImpVMSnapshotReconciler ) createChild (ctx context.Context , parent * impv1alpha1.ImpVMSnapshot ) error {
164+ log := logf .FromContext (ctx )
165+
166+ childName := parent .Name + "-" + time .Now ().UTC ().Format ("20060102-1504" )
167+
168+ childSpec := parent .Spec .DeepCopy ()
169+ childSpec .Schedule = "" // clear schedule on child
170+
171+ child := & impv1alpha1.ImpVMSnapshot {
172+ ObjectMeta : metav1.ObjectMeta {
173+ Name : childName ,
174+ Namespace : parent .Namespace ,
175+ Labels : map [string ]string {
176+ impv1alpha1 .LabelSnapshotParent : parent .Name ,
177+ },
178+ },
179+ Spec : * childSpec ,
180+ }
181+
182+ if err := ctrl .SetControllerReference (parent , child , r .Scheme ); err != nil {
183+ return err
184+ }
185+
186+ if err := r .Create (ctx , child ); err != nil {
187+ return err
188+ }
189+
190+ log .Info ("created child execution" , "parent" , parent .Name , "child" , childName )
191+
192+ // Update lastExecutionRef on parent.
193+ base := parent .DeepCopy ()
194+ parent .Status .LastExecutionRef = & corev1.LocalObjectReference {Name : childName }
195+ if err := r .Status ().Patch (ctx , parent , client .MergeFrom (base )); err != nil {
196+ return err
197+ }
43198
44- return ctrl. Result {}, nil
199+ return nil
45200}
46201
47202func (r * ImpVMSnapshotReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
0 commit comments