Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions api/v1alpha1/sandbox_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,41 @@ type ConditionType string

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

type TTLPolicyType string

func (c TTLPolicyType) String() string { return string(c) }

const (
// SandboxConditionReady indicates readiness for Sandbox
SandboxConditionReady ConditionType = "Ready"

// TTL policy
TTLPolicyOnCreate TTLPolicyType = "onCreate"
TTLPolicyOnReady TTLPolicyType = "onReady"
TTLPolicyOnEnable TTLPolicyType = "onEnable"
TTLPolicyNever TTLPolicyType = "never"
)

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

type TTLConfig struct {
// Seconds sets after how many seconds should the sandbox be deleted
Seconds int32 `json:"seconds,omitempty"`

// StartPolicy indicated when the count down for shutdown should start
// 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
// +kubebuilder:validation:Enum=onCreate;onReady;onEnable;disable
StartPolicy TTLPolicyType `json:"startPolicy,omitempty"`

//ShutdownAt - Absolute time when the sandbox is deleted.
// setting this would override StartPolicy and Seconds
ShutdownAt string `json:"shutdownAt,omitempty"`
}

// SandboxSpec defines the desired state of Sandbox
type SandboxSpec struct {
// The following markers will use OpenAPI v3 schema to validate the value
Expand All @@ -40,6 +67,9 @@ type SandboxSpec struct {
// PodTemplate describes the pod spec that will be used to create an agent sandbox.
// +kubebuilder:validation:Required
PodTemplate corev1.PodTemplateSpec `json:"podTemplate" protobuf:"bytes,3,opt,name=podTemplate"`

// +optional
TTL *TTLConfig `json:"ttl,omitempty"`
}

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

// status conditions array
Conditions []metav1.Condition `json:"conditions,omitempty"`

// FirstReadyTime - when did the sandbox become ready first
FirstReadyTime *metav1.Time `json:"firstReadyTime,omitempty"`

// ShutdownAt - when will the sandbox be deleted
ShutdownAt *metav1.Time `json:"ttlShutdownAt,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
28 changes: 28 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 78 additions & 5 deletions controllers/sandbox_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"hash/fnv"
"reflect"
"time"

corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -33,6 +34,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1"
)
Expand Down Expand Up @@ -95,21 +97,24 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
allErrors = errors.Join(allErrors, err)

// compute and set overall Ready condition
readyCondition := r.computeReadyCondition(sandbox.Generation, allErrors, svc, pod)
readyCondition := r.computeReadyCondition(sandbox, allErrors, svc, pod)
meta.SetStatusCondition(&sandbox.Status.Conditions, readyCondition)

result, err := r.ReconcileSandboxTTL(ctx, sandbox)
allErrors = errors.Join(allErrors, err)

// Update status
err = r.updateStatus(ctx, oldStatus, sandbox)
allErrors = errors.Join(allErrors, err)

// return errors seen
return ctrl.Result{}, allErrors
return result, allErrors
}

func (r *SandboxReconciler) computeReadyCondition(generation int64, err error, svc *corev1.Service, pod *corev1.Pod) metav1.Condition {
func (r *SandboxReconciler) computeReadyCondition(sandbox *sandboxv1alpha1.Sandbox, err error, svc *corev1.Service, pod *corev1.Pod) metav1.Condition {
readyCondition := metav1.Condition{
Type: string(sandboxv1alpha1.SandboxConditionReady),
ObservedGeneration: generation,
ObservedGeneration: sandbox.Generation,
Message: "",
Status: metav1.ConditionFalse,
Reason: "DependenciesNotReady",
Expand Down Expand Up @@ -150,7 +155,10 @@ func (r *SandboxReconciler) computeReadyCondition(generation int64, err error, s
if podReady && svcReady {
readyCondition.Status = metav1.ConditionTrue
readyCondition.Reason = "DependenciesReady"
return readyCondition
if sandbox.Status.FirstReadyTime == nil {
now := metav1.Now()
sandbox.Status.FirstReadyTime = &now
}
}

return readyCondition
Expand Down Expand Up @@ -270,6 +278,71 @@ func (r *SandboxReconciler) reconcilePod(ctx context.Context, sandbox *sandboxv1
return pod, nil
}

// ReconcileSandboxTTL will check if a sandbox has expired, and if so, delete it.
// If the sandbox has not expired, it will requeue the request for the remaining time.

func (r *SandboxReconciler) ReconcileSandboxTTL(ctx context.Context, sandbox *sandboxv1alpha1.Sandbox) (ctrl.Result, error) {
log := log.FromContext(ctx)
sandbox.Status.ShutdownAt = nil
if sandbox.Spec.TTL == nil || (sandbox.Spec.TTL.Seconds == 0 && sandbox.Spec.TTL.ShutdownAt == "") {
log.Info("Sandbox TTL is not set, skipping TTL check.")
return reconcile.Result{}, nil
}

var expiryTime time.Time
if sandbox.Spec.TTL.ShutdownAt != "" {
// Try parsing the endtime as a RFC 3339 string
fromTime, err := time.Parse(time.RFC3339, sandbox.Spec.TTL.ShutdownAt)
if err != nil {
log.Error(err, "Failed to parse TTLFromTime as RFC3339 string", "ShutdownAt", sandbox.Spec.TTL.ShutdownAt)
return reconcile.Result{}, err
}
expiryTime = fromTime.Add(time.Duration(sandbox.Spec.TTL.Seconds) * time.Second)
} else {
switch sandbox.Spec.TTL.StartPolicy {
case sandboxv1alpha1.TTLPolicyOnCreate:
// Look at the sandbox create time and calculate the endtime
expiryTime = sandbox.CreationTimestamp.Add(time.Duration(sandbox.Spec.TTL.Seconds) * time.Second)
case sandboxv1alpha1.TTLPolicyOnReady:
if sandbox.Status.FirstReadyTime != nil {
expiryTime = sandbox.Status.FirstReadyTime.Add(time.Duration(sandbox.Spec.TTL.Seconds) * time.Second)
} else {
log.Info("Sandbox not yet ready, cannot calculate TTLFromReady")
return reconcile.Result{RequeueAfter: 5 * time.Second}, nil
}
case sandboxv1alpha1.TTLPolicyNever:
log.Info("TTL is disabled for this sandbox")
return reconcile.Result{}, nil
case sandboxv1alpha1.TTLPolicyOnEnable:
if sandbox.Status.ShutdownAt == nil {
expiryTime = time.Now().Add(time.Duration(sandbox.Spec.TTL.Seconds) * time.Second)
}
default:
// should not happen
log.Info("TTL policy unknown: %s", sandbox.Spec.TTL.StartPolicy)
return reconcile.Result{}, nil
}
}

// Set the .status.ttlExpiryTime
sandbox.Status.ShutdownAt = &metav1.Time{Time: expiryTime}

// Calculate remaining time
remainingTime := time.Until(expiryTime)
if remainingTime <= 0 {
log.Info("Sandbox has expired, deleting")
if err := r.Delete(ctx, sandbox); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to delete sandbox: %w", err)
}
return ctrl.Result{}, nil
}

requeueAfter := max(remainingTime/2, 2*time.Second) // Requeue at most every 2 seconds
log.Info("Requeuing sandbox for TTL", "remaining time", remainingTime, "requeue after", requeueAfter,
"expiry time", expiryTime)
return reconcile.Result{RequeueAfter: requeueAfter}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *SandboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
labelSelectorPredicate, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
Expand Down
5 changes: 4 additions & 1 deletion controllers/sandbox_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ func TestComputeReadyCondition(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
condition := r.computeReadyCondition(tc.generation, tc.err, tc.svc, tc.pod)
sandbox := &sandboxv1alpha1.Sandbox{
ObjectMeta: metav1.ObjectMeta{Generation: tc.generation},
}
condition := r.computeReadyCondition(sandbox, tc.err, tc.svc, tc.pod)
g.Expect(condition.Type).To(Equal(string(sandboxv1alpha1.SandboxConditionReady)))
g.Expect(condition.ObservedGeneration).To(Equal(tc.generation))
g.Expect(condition.Status).To(Equal(tc.expectedStatus))
Expand Down
3 changes: 3 additions & 0 deletions examples/sandbox.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ kind: Sandbox
metadata:
name: sandbox-example
spec:
ttl:
seconds: 200
startPolicy: onCreate
podTemplate:
spec:
containers:
Expand Down
21 changes: 21 additions & 0 deletions k8s/crds/agents.x-k8s.io_sandboxes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3803,6 +3803,21 @@ spec:
- containers
type: object
type: object
ttl:
properties:
seconds:
format: int32
type: integer
shutdownAt:
type: string
startPolicy:
enum:
- onCreate
- onReady
- onEnable
- disable
type: string
type: object
required:
- podTemplate
type: object
Expand Down Expand Up @@ -3844,10 +3859,16 @@ spec:
- type
type: object
type: array
firstReadyTime:
format: date-time
type: string
service:
type: string
serviceFQDN:
type: string
ttlShutdownAt:
format: date-time
type: string
type: object
required:
- spec
Expand Down