Skip to content

Commit 45e4a39

Browse files
authored
Add Sandbox Warm Pool Controller basic logic (#84)
* add sandboxwarmpool CRD * add sandboxwarmpool CRD * Warmpool Controller * Warmpool Controller
1 parent 0de00de commit 45e4a39

File tree

5 files changed

+627
-6
lines changed

5 files changed

+627
-6
lines changed

cmd/agent-sandbox-controller/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,15 @@ func main() {
8888
setupLog.Error(err, "unable to create controller", "controller", "SandboxClaim")
8989
os.Exit(1)
9090
}
91+
92+
if err = (&extensionscontrollers.SandboxWarmPoolReconciler{
93+
Client: mgr.GetClient(),
94+
}).SetupWithManager(mgr); err != nil {
95+
setupLog.Error(err, "unable to create controller", "controller", "SandboxWarmPool")
96+
os.Exit(1)
97+
}
9198
}
99+
92100
//+kubebuilder:scaffold:builder
93101

94102
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright 2025 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controllers
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/equality"
24+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/labels"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
30+
"sigs.k8s.io/controller-runtime/pkg/log"
31+
32+
extensionsv1alpha1 "sigs.k8s.io/agent-sandbox/extensions/api/v1alpha1"
33+
)
34+
35+
const (
36+
poolLabel = "agents.x-k8s.io/pool"
37+
)
38+
39+
// SandboxWarmPoolReconciler reconciles a SandboxWarmPool object
40+
type SandboxWarmPoolReconciler struct {
41+
client.Client
42+
}
43+
44+
//+kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxwarmpools,verbs=get;list;watch;create;update;patch;delete
45+
//+kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxwarmpools/status,verbs=get;update;patch
46+
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
47+
48+
// Reconcile implements the reconciliation loop for SandboxWarmPool
49+
func (r *SandboxWarmPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
50+
log := log.FromContext(ctx)
51+
52+
// Fetch the SandboxWarmPool instance
53+
warmPool := &extensionsv1alpha1.SandboxWarmPool{}
54+
if err := r.Get(ctx, req.NamespacedName, warmPool); err != nil {
55+
if k8serrors.IsNotFound(err) {
56+
log.Info("SandboxWarmPool resource not found. Ignoring since object must be deleted")
57+
return ctrl.Result{}, nil
58+
}
59+
log.Error(err, "Failed to get SandboxWarmPool")
60+
return ctrl.Result{}, err
61+
}
62+
63+
// Handle deletion
64+
if !warmPool.ObjectMeta.DeletionTimestamp.IsZero() {
65+
log.Info("SandboxWarmPool is being deleted")
66+
return ctrl.Result{}, nil
67+
}
68+
69+
// Save old status for comparison
70+
oldStatus := warmPool.Status.DeepCopy()
71+
72+
// Reconcile the pool (create or delete Pods as needed)
73+
if err := r.reconcilePool(ctx, warmPool); err != nil {
74+
return ctrl.Result{}, err
75+
}
76+
77+
// Update status if it has changed
78+
if err := r.updateStatus(ctx, oldStatus, warmPool); err != nil {
79+
log.Error(err, "Failed to update SandboxWarmPool status")
80+
return ctrl.Result{}, err
81+
}
82+
83+
return ctrl.Result{}, nil
84+
}
85+
86+
// reconcilePool ensures the correct number of pods exist in the pool
87+
func (r *SandboxWarmPoolReconciler) reconcilePool(ctx context.Context, warmPool *extensionsv1alpha1.SandboxWarmPool) error {
88+
log := log.FromContext(ctx)
89+
90+
// TODO: Use a hash value for the poolLabelValue
91+
poolLabelValue := warmPool.Name
92+
93+
// List all pods with the pool label
94+
podList := &corev1.PodList{}
95+
labelSelector := labels.SelectorFromSet(labels.Set{
96+
poolLabel: poolLabelValue,
97+
})
98+
99+
if err := r.List(ctx, podList, &client.ListOptions{
100+
LabelSelector: labelSelector,
101+
Namespace: warmPool.Namespace,
102+
}); err != nil {
103+
log.Error(err, "Failed to list pods")
104+
return err
105+
}
106+
107+
// Filter pods by ownership and adopt orphans
108+
var activePods []corev1.Pod
109+
var allErrors error
110+
111+
for _, pod := range podList.Items {
112+
// Skip pods that are being deleted
113+
if !pod.ObjectMeta.DeletionTimestamp.IsZero() {
114+
continue
115+
}
116+
117+
// Get the controller owner reference
118+
controllerRef := metav1.GetControllerOf(&pod)
119+
120+
if controllerRef == nil {
121+
// Pod has no controller - adopt it
122+
log.Info("Adopting orphaned pod", "pod", pod.Name)
123+
if err := r.adoptPod(ctx, warmPool, &pod); err != nil {
124+
log.Error(err, "Failed to adopt pod", "pod", pod.Name)
125+
allErrors = errors.Join(allErrors, err)
126+
continue
127+
}
128+
activePods = append(activePods, pod)
129+
} else if controllerRef.UID == warmPool.UID {
130+
// Pod belongs to this warmpool - include it
131+
activePods = append(activePods, pod)
132+
} else {
133+
// Pod has a different controller - ignore it
134+
log.Info("Ignoring pod with different controller",
135+
"pod", pod.Name,
136+
"controller", controllerRef.Name,
137+
"controllerKind", controllerRef.Kind)
138+
}
139+
}
140+
141+
desiredReplicas := warmPool.Spec.Replicas
142+
currentReplicas := int32(len(activePods))
143+
144+
log.Info("Pool status",
145+
"desired", desiredReplicas,
146+
"current", currentReplicas,
147+
"poolLabel", poolLabelValue)
148+
149+
// Update status replicas
150+
warmPool.Status.Replicas = currentReplicas
151+
152+
// Create new pods if we need more
153+
if currentReplicas < desiredReplicas {
154+
podsToCreate := desiredReplicas - currentReplicas
155+
log.Info("Creating new pods", "count", podsToCreate)
156+
157+
for i := int32(0); i < podsToCreate; i++ {
158+
if err := r.createPoolPod(ctx, warmPool, poolLabelValue); err != nil {
159+
log.Error(err, "Failed to create pod")
160+
allErrors = errors.Join(allErrors, err)
161+
}
162+
}
163+
}
164+
165+
// Delete excess pods if we have too many
166+
if currentReplicas > desiredReplicas {
167+
podsToDelete := currentReplicas - desiredReplicas
168+
log.Info("Deleting excess pods", "count", podsToDelete)
169+
170+
// Delete the first N active pods from the list
171+
for i := int32(0); i < podsToDelete && i < int32(len(activePods)); i++ {
172+
pod := &activePods[i]
173+
174+
if err := r.Delete(ctx, pod); err != nil {
175+
log.Error(err, "Failed to delete pod", "pod", pod.Name)
176+
allErrors = errors.Join(allErrors, err)
177+
}
178+
}
179+
}
180+
181+
return allErrors
182+
}
183+
184+
// adoptPod sets this warmpool as the owner of an orphaned pod
185+
func (r *SandboxWarmPoolReconciler) adoptPod(ctx context.Context, warmPool *extensionsv1alpha1.SandboxWarmPool, pod *corev1.Pod) error {
186+
if err := controllerutil.SetControllerReference(warmPool, pod, r.Scheme()); err != nil {
187+
return err
188+
}
189+
return r.Update(ctx, pod)
190+
}
191+
192+
// createPoolPod creates a new pod for the warm pool
193+
func (r *SandboxWarmPoolReconciler) createPoolPod(ctx context.Context, warmPool *extensionsv1alpha1.SandboxWarmPool, poolLabelValue string) error {
194+
log := log.FromContext(ctx)
195+
196+
// Create labels for the pod
197+
podLabels := make(map[string]string)
198+
podLabels[poolLabel] = poolLabelValue
199+
200+
// Copy labels from pod template
201+
for k, v := range warmPool.Spec.PodTemplate.ObjectMeta.Labels {
202+
podLabels[k] = v
203+
}
204+
205+
// Create annotations for the pod
206+
podAnnotations := make(map[string]string)
207+
for k, v := range warmPool.Spec.PodTemplate.ObjectMeta.Annotations {
208+
podAnnotations[k] = v
209+
}
210+
211+
// Create the pod
212+
pod := &corev1.Pod{
213+
ObjectMeta: metav1.ObjectMeta{
214+
GenerateName: fmt.Sprintf("%s-", warmPool.Name),
215+
Namespace: warmPool.Namespace,
216+
Labels: podLabels,
217+
Annotations: podAnnotations,
218+
},
219+
Spec: warmPool.Spec.PodTemplate.Spec,
220+
}
221+
222+
// Set controller reference so the Pod is owned by the SandboxWarmPool
223+
if err := ctrl.SetControllerReference(warmPool, pod, r.Client.Scheme()); err != nil {
224+
return fmt.Errorf("SetControllerReference for Pod failed: %w", err)
225+
}
226+
227+
// Create the Pod
228+
if err := r.Create(ctx, pod); err != nil {
229+
log.Error(err, "Failed to create pod")
230+
return err
231+
}
232+
233+
log.Info("Created new pool pod", "pod", pod.Name, "pool", poolLabelValue)
234+
return nil
235+
}
236+
237+
// updateStatus updates the status of the SandboxWarmPool if it has changed
238+
func (r *SandboxWarmPoolReconciler) updateStatus(ctx context.Context, oldStatus *extensionsv1alpha1.SandboxWarmPoolStatus, warmPool *extensionsv1alpha1.SandboxWarmPool) error {
239+
log := log.FromContext(ctx)
240+
241+
// Check if status has changed
242+
if equality.Semantic.DeepEqual(oldStatus, &warmPool.Status) {
243+
return nil
244+
}
245+
246+
if err := r.Status().Update(ctx, warmPool); err != nil {
247+
log.Error(err, "Failed to update SandboxWarmPool status")
248+
return err
249+
}
250+
251+
log.Info("Updated SandboxWarmPool status", "replicas", warmPool.Status.Replicas)
252+
return nil
253+
}
254+
255+
// SetupWithManager sets up the controller with the Manager
256+
func (r *SandboxWarmPoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
257+
return ctrl.NewControllerManagedBy(mgr).
258+
For(&extensionsv1alpha1.SandboxWarmPool{}).
259+
Owns(&corev1.Pod{}).
260+
Complete(r)
261+
}

0 commit comments

Comments
 (0)