@@ -20,6 +20,7 @@ import (
2020 "fmt"
2121 "hash/fnv"
2222 "reflect"
23+ "time"
2324
2425 corev1 "k8s.io/api/core/v1"
2526 k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -33,6 +34,7 @@ import (
3334 "sigs.k8s.io/controller-runtime/pkg/handler"
3435 "sigs.k8s.io/controller-runtime/pkg/log"
3536 "sigs.k8s.io/controller-runtime/pkg/predicate"
37+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
3638
3739 sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1"
3840)
@@ -95,21 +97,24 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
9597 allErrors = errors .Join (allErrors , err )
9698
9799 // compute and set overall Ready condition
98- readyCondition := r .computeReadyCondition (sandbox . Generation , allErrors , svc , pod )
100+ readyCondition := r .computeReadyCondition (sandbox , allErrors , svc , pod )
99101 meta .SetStatusCondition (& sandbox .Status .Conditions , readyCondition )
100102
103+ result , err := r .ReconcileSandboxTTL (ctx , sandbox )
104+ allErrors = errors .Join (allErrors , err )
105+
101106 // Update status
102107 err = r .updateStatus (ctx , oldStatus , sandbox )
103108 allErrors = errors .Join (allErrors , err )
104109
105110 // return errors seen
106- return ctrl. Result {} , allErrors
111+ return result , allErrors
107112}
108113
109- func (r * SandboxReconciler ) computeReadyCondition (generation int64 , err error , svc * corev1.Service , pod * corev1.Pod ) metav1.Condition {
114+ func (r * SandboxReconciler ) computeReadyCondition (sandbox * sandboxv1alpha1. Sandbox , err error , svc * corev1.Service , pod * corev1.Pod ) metav1.Condition {
110115 readyCondition := metav1.Condition {
111116 Type : string (sandboxv1alpha1 .SandboxConditionReady ),
112- ObservedGeneration : generation ,
117+ ObservedGeneration : sandbox . Generation ,
113118 Message : "" ,
114119 Status : metav1 .ConditionFalse ,
115120 Reason : "DependenciesNotReady" ,
@@ -150,7 +155,10 @@ func (r *SandboxReconciler) computeReadyCondition(generation int64, err error, s
150155 if podReady && svcReady {
151156 readyCondition .Status = metav1 .ConditionTrue
152157 readyCondition .Reason = "DependenciesReady"
153- return readyCondition
158+ if sandbox .Status .FirstReadyTime == nil {
159+ now := metav1 .Now ()
160+ sandbox .Status .FirstReadyTime = & now
161+ }
154162 }
155163
156164 return readyCondition
@@ -270,6 +278,71 @@ func (r *SandboxReconciler) reconcilePod(ctx context.Context, sandbox *sandboxv1
270278 return pod , nil
271279}
272280
281+ // ReconcileSandboxTTL will check if a sandbox has expired, and if so, delete it.
282+ // If the sandbox has not expired, it will requeue the request for the remaining time.
283+
284+ func (r * SandboxReconciler ) ReconcileSandboxTTL (ctx context.Context , sandbox * sandboxv1alpha1.Sandbox ) (ctrl.Result , error ) {
285+ log := log .FromContext (ctx )
286+ sandbox .Status .ShutdownAt = nil
287+ if sandbox .Spec .TTL == nil || (sandbox .Spec .TTL .Seconds == 0 && sandbox .Spec .TTL .ShutdownAt == "" ) {
288+ log .Info ("Sandbox TTL is not set, skipping TTL check." )
289+ return reconcile.Result {}, nil
290+ }
291+
292+ var expiryTime time.Time
293+ if sandbox .Spec .TTL .ShutdownAt != "" {
294+ // Try parsing the endtime as a RFC 3339 string
295+ fromTime , err := time .Parse (time .RFC3339 , sandbox .Spec .TTL .ShutdownAt )
296+ if err != nil {
297+ log .Error (err , "Failed to parse TTLFromTime as RFC3339 string" , "ShutdownAt" , sandbox .Spec .TTL .ShutdownAt )
298+ return reconcile.Result {}, err
299+ }
300+ expiryTime = fromTime .Add (time .Duration (sandbox .Spec .TTL .Seconds ) * time .Second )
301+ } else {
302+ switch sandbox .Spec .TTL .StartPolicy {
303+ case sandboxv1alpha1 .TTLPolicyOnCreate :
304+ // Look at the sandbox create time and calculate the endtime
305+ expiryTime = sandbox .CreationTimestamp .Add (time .Duration (sandbox .Spec .TTL .Seconds ) * time .Second )
306+ case sandboxv1alpha1 .TTLPolicyOnReady :
307+ if sandbox .Status .FirstReadyTime != nil {
308+ expiryTime = sandbox .Status .FirstReadyTime .Add (time .Duration (sandbox .Spec .TTL .Seconds ) * time .Second )
309+ } else {
310+ log .Info ("Sandbox not yet ready, cannot calculate TTLFromReady" )
311+ return reconcile.Result {RequeueAfter : 5 * time .Second }, nil
312+ }
313+ case sandboxv1alpha1 .TTLPolicyNever :
314+ log .Info ("TTL is disabled for this sandbox" )
315+ return reconcile.Result {}, nil
316+ case sandboxv1alpha1 .TTLPolicyOnEnable :
317+ if sandbox .Status .ShutdownAt == nil {
318+ expiryTime = time .Now ().Add (time .Duration (sandbox .Spec .TTL .Seconds ) * time .Second )
319+ }
320+ default :
321+ // should not happen
322+ log .Info ("TTL policy unknown: %s" , sandbox .Spec .TTL .StartPolicy )
323+ return reconcile.Result {}, nil
324+ }
325+ }
326+
327+ // Set the .status.ttlExpiryTime
328+ sandbox .Status .ShutdownAt = & metav1.Time {Time : expiryTime }
329+
330+ // Calculate remaining time
331+ remainingTime := time .Until (expiryTime )
332+ if remainingTime <= 0 {
333+ log .Info ("Sandbox has expired, deleting" )
334+ if err := r .Delete (ctx , sandbox ); err != nil {
335+ return ctrl.Result {}, fmt .Errorf ("failed to delete sandbox: %w" , err )
336+ }
337+ return ctrl.Result {}, nil
338+ }
339+
340+ requeueAfter := max (remainingTime / 2 , 2 * time .Second ) // Requeue at most every 2 seconds
341+ log .Info ("Requeuing sandbox for TTL" , "remaining time" , remainingTime , "requeue after" , requeueAfter ,
342+ "expiry time" , expiryTime )
343+ return reconcile.Result {RequeueAfter : requeueAfter }, nil
344+ }
345+
273346// SetupWithManager sets up the controller with the Manager.
274347func (r * SandboxReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
275348 labelSelectorPredicate , err := predicate .LabelSelectorPredicate (metav1.LabelSelector {
0 commit comments