Skip to content

Commit 182346c

Browse files
committed
Add support for shutdownTime in sandbox.spec
* Add .spec.shutdownTime string that parses to a time * The child resources are deleted once the current time >= shutdownAt time * Sandbox is not deleted
1 parent 5ee237e commit 182346c

File tree

4 files changed

+88
-6
lines changed

4 files changed

+88
-6
lines changed

api/v1alpha1/sandbox_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ type SandboxSpec struct {
6666
// PodTemplate describes the pod spec that will be used to create an agent sandbox.
6767
// +kubebuilder:validation:Required
6868
PodTemplate PodTemplate `json:"podTemplate" protobuf:"bytes,3,opt,name=podTemplate"`
69+
70+
// ShutdownTime - Absolute time when the sandbox is deleted.
71+
// If a time in the past is provided, the sandbox will be deleted immediately.
72+
// +kubebuilder:validation:Format="date-time"
73+
ShutdownTime *metav1.Time `json:"shutdownTime,omitempty"`
6974
}
7075

7176
// SandboxStatus defines the observed state of Sandbox.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 4 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: 76 additions & 6 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"
@@ -80,7 +81,26 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
8081
}
8182

8283
oldStatus := sandbox.Status.DeepCopy()
84+
var err error
8385

86+
expired, requeueAfter := r.processSandboxExpiry(sandbox)
87+
88+
// Check if sandbox has expired
89+
if expired {
90+
log.Info("Sandbox has expired, deleting pod and service")
91+
err = r.deleteChildResources(ctx, sandbox)
92+
} else {
93+
err = r.reconcileChildResources(ctx, sandbox)
94+
}
95+
96+
// Update status
97+
err = errors.Join(err, r.updateStatus(ctx, oldStatus, sandbox))
98+
99+
// return errors seen
100+
return ctrl.Result{RequeueAfter: requeueAfter}, err
101+
}
102+
103+
func (r *SandboxReconciler) reconcileChildResources(ctx context.Context, sandbox *sandboxv1alpha1.Sandbox) error {
84104
// Create a hash from the sandbox.Name and use it as label value
85105
nameHash := NameHash(sandbox.Name)
86106

@@ -98,12 +118,7 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
98118
readyCondition := r.computeReadyCondition(sandbox.Generation, allErrors, svc, pod)
99119
meta.SetStatusCondition(&sandbox.Status.Conditions, readyCondition)
100120

101-
// Update status
102-
err = r.updateStatus(ctx, oldStatus, sandbox)
103-
allErrors = errors.Join(allErrors, err)
104-
105-
// return errors seen
106-
return ctrl.Result{}, allErrors
121+
return allErrors
107122
}
108123

109124
func (r *SandboxReconciler) computeReadyCondition(generation int64, err error, svc *corev1.Service, pod *corev1.Pod) metav1.Condition {
@@ -280,6 +295,61 @@ func (r *SandboxReconciler) reconcilePod(ctx context.Context, sandbox *sandboxv1
280295
return pod, nil
281296
}
282297

298+
func (r *SandboxReconciler) deleteChildResources(ctx context.Context, sandbox *sandboxv1alpha1.Sandbox) error {
299+
var allErrors error
300+
pod := &corev1.Pod{
301+
ObjectMeta: metav1.ObjectMeta{
302+
Name: sandbox.Name,
303+
Namespace: sandbox.Namespace,
304+
},
305+
}
306+
if err := r.Delete(ctx, pod); err != nil && !k8serrors.IsNotFound(err) {
307+
allErrors = errors.Join(allErrors, fmt.Errorf("failed to delete pod: %w", err))
308+
}
309+
310+
service := &corev1.Service{
311+
ObjectMeta: metav1.ObjectMeta{
312+
Name: sandbox.Name,
313+
Namespace: sandbox.Namespace,
314+
},
315+
}
316+
if err := r.Delete(ctx, service); err != nil && !k8serrors.IsNotFound(err) {
317+
allErrors = errors.Join(allErrors, fmt.Errorf("failed to delete service: %w", err))
318+
}
319+
320+
// Update status to remove Ready condition
321+
meta.SetStatusCondition(&sandbox.Status.Conditions, metav1.Condition{
322+
Type: string(sandboxv1alpha1.SandboxConditionReady),
323+
Status: metav1.ConditionFalse,
324+
ObservedGeneration: sandbox.Generation,
325+
Reason: "SandboxExpired",
326+
Message: "Sandbox has expired",
327+
})
328+
329+
return allErrors
330+
}
331+
332+
func (r *SandboxReconciler) processSandboxExpiry(sandbox *sandboxv1alpha1.Sandbox) (bool, time.Duration) {
333+
if sandbox.Spec.ShutdownTime == nil {
334+
return false, 0
335+
}
336+
337+
expiryTime := sandbox.Spec.ShutdownTime.Time
338+
if time.Now().After(expiryTime) {
339+
return true, 0
340+
}
341+
342+
// Calculate remaining time
343+
remainingTime := time.Until(expiryTime)
344+
345+
// TODO(barney-s): Do we need a inverse exponential backoff here ?
346+
//requeueAfter := max(remainingTime/2, 2*time.Second)
347+
348+
// Requeue at expiry time or in 2 seconds whichever is later
349+
requeueAfter := max(remainingTime, 2*time.Second)
350+
return false, requeueAfter
351+
}
352+
283353
// SetupWithManager sets up the controller with the Manager.
284354
func (r *SandboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
285355
labelSelectorPredicate, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3814,6 +3814,9 @@ spec:
38143814
required:
38153815
- spec
38163816
type: object
3817+
shutdownTime:
3818+
format: date-time
3819+
type: string
38173820
required:
38183821
- podTemplate
38193822
type: object

0 commit comments

Comments
 (0)