Skip to content

Commit ecc2bd0

Browse files
committed
Add support for TTL in sandbox
* Add TTL info in status * .status.firstReadyTime - When the first time sandbox became ready * .status.shutdownAt - When is the sandbox scheduled for deletion * Add TTL config to Sandbox * .spec.tt.seconds - How many seconds does the sandbox live * .spec.ttl.startPolicy - When do we start the counting the ttl * onCreate - TTL starts from sandbox creation * onReady - TTL starts from sandbox ready * onEnable - When this is set and .status.shutdownAt is nil * never - TTL is disabled * User can modify the .ttl.seconds and .ttl.startPolicy * If the .status.shutdownAt is computed to a past-time, the sandbox is immediately deleted
1 parent 03f7d51 commit ecc2bd0

File tree

6 files changed

+170
-6
lines changed

6 files changed

+170
-6
lines changed

api/v1alpha1/sandbox_types.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,41 @@ type ConditionType string
2424

2525
func (c ConditionType) String() string { return string(c) }
2626

27+
type TTLPolicyType string
28+
29+
func (c TTLPolicyType) String() string { return string(c) }
30+
2731
const (
2832
// SandboxConditionReady indicates readiness for Sandbox
2933
SandboxConditionReady ConditionType = "Ready"
34+
35+
// TTL policy
36+
TTLPolicyOnCreate TTLPolicyType = "onCreate"
37+
TTLPolicyOnReady TTLPolicyType = "onReady"
38+
TTLPolicyOnEnable TTLPolicyType = "onEnable"
39+
TTLPolicyNever TTLPolicyType = "never"
3040
)
3141

3242
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
3343
// Important: Run "make" to regenerate code after modifying this file
3444

45+
type TTLConfig struct {
46+
// Seconds sets after how many seconds should the sandbox be deleted
47+
Seconds int32 `json:"seconds,omitempty"`
48+
49+
// StartPolicy indicated when the count down for shutdown should start
50+
// onCreate - TTL starts from sandbox creation
51+
// onReady - TTL starts from sandbox ready
52+
// onEnable - When this is set and .status.shutdownAt is nil
53+
// never - TTL is disabled
54+
// +kubebuilder:validation:Enum=onCreate;onReady;onEnable;disable
55+
StartPolicy TTLPolicyType `json:"startPolicy,omitempty"`
56+
57+
//ShutdownAt - Absolute time when the sandbox is deleted.
58+
// setting this would override StartPolicy and Seconds
59+
ShutdownAt string `json:"shutdownAt,omitempty"`
60+
}
61+
3562
// SandboxSpec defines the desired state of Sandbox
3663
type SandboxSpec struct {
3764
// The following markers will use OpenAPI v3 schema to validate the value
@@ -40,6 +67,9 @@ type SandboxSpec struct {
4067
// PodTemplate describes the pod spec that will be used to create an agent sandbox.
4168
// +kubebuilder:validation:Required
4269
PodTemplate corev1.PodTemplateSpec `json:"podTemplate" protobuf:"bytes,3,opt,name=podTemplate"`
70+
71+
// +optional
72+
TTL *TTLConfig `json:"ttl,omitempty"`
4373
}
4474

4575
// SandboxStatus defines the observed state of Sandbox.
@@ -54,6 +84,12 @@ type SandboxStatus struct {
5484

5585
// status conditions array
5686
Conditions []metav1.Condition `json:"conditions,omitempty"`
87+
88+
// FirstReadyTime - when did the sandbox become ready first
89+
FirstReadyTime *metav1.Time `json:"firstReadyTime,omitempty"`
90+
91+
// ShutdownAt - when will the sandbox be deleted
92+
ShutdownAt *metav1.Time `json:"ttlShutdownAt,omitempty"`
5793
}
5894

5995
// +kubebuilder:object:root=true

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controllers/sandbox_controller.go

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
274347
func (r *SandboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
275348
labelSelectorPredicate, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{

controllers/sandbox_controller_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ func TestComputeReadyCondition(t *testing.T) {
142142

143143
for _, tc := range testCases {
144144
t.Run(tc.name, func(t *testing.T) {
145-
condition := r.computeReadyCondition(tc.generation, tc.err, tc.svc, tc.pod)
145+
sandbox := &sandboxv1alpha1.Sandbox{
146+
ObjectMeta: metav1.ObjectMeta{Generation: tc.generation},
147+
}
148+
condition := r.computeReadyCondition(sandbox, tc.err, tc.svc, tc.pod)
146149
g.Expect(condition.Type).To(Equal(string(sandboxv1alpha1.SandboxConditionReady)))
147150
g.Expect(condition.ObservedGeneration).To(Equal(tc.generation))
148151
g.Expect(condition.Status).To(Equal(tc.expectedStatus))

examples/sandbox.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ kind: Sandbox
33
metadata:
44
name: sandbox-example
55
spec:
6+
ttl:
7+
seconds: 200
8+
startPolicy: onCreate
69
podTemplate:
710
spec:
811
containers:

k8s/crds/agents.x-k8s.io_sandboxes.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3803,6 +3803,21 @@ spec:
38033803
- containers
38043804
type: object
38053805
type: object
3806+
ttl:
3807+
properties:
3808+
seconds:
3809+
format: int32
3810+
type: integer
3811+
shutdownAt:
3812+
type: string
3813+
startPolicy:
3814+
enum:
3815+
- onCreate
3816+
- onReady
3817+
- onEnable
3818+
- disable
3819+
type: string
3820+
type: object
38063821
required:
38073822
- podTemplate
38083823
type: object
@@ -3844,10 +3859,16 @@ spec:
38443859
- type
38453860
type: object
38463861
type: array
3862+
firstReadyTime:
3863+
format: date-time
3864+
type: string
38473865
service:
38483866
type: string
38493867
serviceFQDN:
38503868
type: string
3869+
ttlShutdownAt:
3870+
format: date-time
3871+
type: string
38513872
type: object
38523873
required:
38533874
- spec

0 commit comments

Comments
 (0)