Skip to content

Commit f0d813d

Browse files
authored
Merge pull request #51 from barney-s/sandbox-expiry
Add support for shutdownTime in sandbox.spec
2 parents dc749e6 + a914d61 commit f0d813d

File tree

4 files changed

+91
-6
lines changed

4 files changed

+91
-6
lines changed

api/v1alpha1/sandbox_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ type SandboxSpec struct {
8383
// +optional
8484
// +kubebuilder:validation:Optional
8585
VolumeClaimTemplates []PersistentVolumeClaimTemplate `json:"volumeClaimTemplates,omitempty" protobuf:"bytes,4,rep,name=volumeClaimTemplates"`
86+
87+
// ShutdownTime - Absolute time when the sandbox is deleted.
88+
// If a time in the past is provided, the sandbox will be deleted immediately.
89+
// +kubebuilder:validation:Format="date-time"
90+
ShutdownTime *metav1.Time `json:"shutdownTime,omitempty"`
8691
}
8792

8893
// 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: 79 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"
@@ -93,7 +94,26 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
9394
}
9495

9596
oldStatus := sandbox.Status.DeepCopy()
97+
var err error
9698

99+
expired, requeueAfter := r.processSandboxExpiry(sandbox)
100+
101+
// Check if sandbox has expired
102+
if expired {
103+
log.Info("Sandbox has expired, deleting pod and service")
104+
err = r.deleteChildResources(ctx, sandbox)
105+
} else {
106+
err = r.reconcileChildResources(ctx, sandbox)
107+
}
108+
109+
// Update status
110+
err = errors.Join(err, r.updateStatus(ctx, oldStatus, sandbox))
111+
112+
// return errors seen
113+
return ctrl.Result{RequeueAfter: requeueAfter}, err
114+
}
115+
116+
func (r *SandboxReconciler) reconcileChildResources(ctx context.Context, sandbox *sandboxv1alpha1.Sandbox) error {
97117
// Create a hash from the sandbox.Name and use it as label value
98118
nameHash := NameHash(sandbox.Name)
99119

@@ -115,12 +135,7 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
115135
readyCondition := r.computeReadyCondition(sandbox.Generation, allErrors, svc, pod)
116136
meta.SetStatusCondition(&sandbox.Status.Conditions, readyCondition)
117137

118-
// Update status
119-
err = r.updateStatus(ctx, oldStatus, sandbox)
120-
allErrors = errors.Join(allErrors, err)
121-
122-
// return errors seen
123-
return ctrl.Result{}, allErrors
138+
return allErrors
124139
}
125140

126141
func (r *SandboxReconciler) computeReadyCondition(generation int64, err error, svc *corev1.Service, pod *corev1.Pod) metav1.Condition {
@@ -342,6 +357,64 @@ func (r *SandboxReconciler) reconcilePVCs(ctx context.Context, sandbox *sandboxv
342357
return nil
343358
}
344359

360+
func (r *SandboxReconciler) deleteChildResources(ctx context.Context, sandbox *sandboxv1alpha1.Sandbox) error {
361+
var allErrors error
362+
pod := &corev1.Pod{
363+
ObjectMeta: metav1.ObjectMeta{
364+
Name: sandbox.Name,
365+
Namespace: sandbox.Namespace,
366+
},
367+
}
368+
if err := r.Delete(ctx, pod); err != nil && !k8serrors.IsNotFound(err) {
369+
allErrors = errors.Join(allErrors, fmt.Errorf("failed to delete pod: %w", err))
370+
}
371+
372+
service := &corev1.Service{
373+
ObjectMeta: metav1.ObjectMeta{
374+
Name: sandbox.Name,
375+
Namespace: sandbox.Namespace,
376+
},
377+
}
378+
if err := r.Delete(ctx, service); err != nil && !k8serrors.IsNotFound(err) {
379+
allErrors = errors.Join(allErrors, fmt.Errorf("failed to delete service: %w", err))
380+
}
381+
382+
// Update status to remove Ready condition
383+
meta.SetStatusCondition(&sandbox.Status.Conditions, metav1.Condition{
384+
Type: string(sandboxv1alpha1.SandboxConditionReady),
385+
Status: metav1.ConditionFalse,
386+
ObservedGeneration: sandbox.Generation,
387+
Reason: "SandboxExpired",
388+
Message: "Sandbox has expired",
389+
})
390+
391+
return allErrors
392+
}
393+
394+
// checks if the sandbox has expired
395+
// returns true if expired, false otherwise
396+
// if not expired, also returns the duration to requeue after
397+
func (r *SandboxReconciler) processSandboxExpiry(sandbox *sandboxv1alpha1.Sandbox) (bool, time.Duration) {
398+
if sandbox.Spec.ShutdownTime == nil {
399+
return false, 0
400+
}
401+
402+
expiryTime := sandbox.Spec.ShutdownTime.Time
403+
if time.Now().After(expiryTime) {
404+
return true, 0
405+
}
406+
407+
// Calculate remaining time
408+
remainingTime := time.Until(expiryTime)
409+
410+
// TODO(barney-s): Do we need a inverse exponential backoff here ?
411+
//requeueAfter := max(remainingTime/2, 2*time.Second)
412+
413+
// Requeue at expiry time or in 2 seconds whichever is later
414+
requeueAfter := max(remainingTime, 2*time.Second)
415+
return false, requeueAfter
416+
}
417+
345418
// SetupWithManager sets up the controller with the Manager.
346419
func (r *SandboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
347420
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
volumeClaimTemplates:
38183821
items:
38193822
properties:

0 commit comments

Comments
 (0)