Skip to content

Commit b7803ad

Browse files
committed
feat(controller): ImpWarmPool — maintain pool of snapshot-booted VMs
1 parent aacf6f1 commit b7803ad

3 files changed

Lines changed: 492 additions & 0 deletions

File tree

cmd/operator/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ func main() {
178178
os.Exit(1)
179179
}
180180

181+
if err = (&controller.ImpWarmPoolReconciler{
182+
Client: mgr.GetClient(),
183+
Scheme: mgr.GetScheme(),
184+
}).SetupWithManager(mgr); err != nil {
185+
setupLog.Error(err, "unable to create controller", "controller", "ImpWarmPool")
186+
os.Exit(1)
187+
}
188+
181189
if err = builder.WebhookManagedBy(mgr, &impv1alpha1.ImpVM{}).
182190
WithDefaulter(&webhookv1alpha1.ImpVMWebhook{}).
183191
WithValidator(&webhookv1alpha1.ImpVMWebhook{}).
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
logf "sigs.k8s.io/controller-runtime/pkg/log"
30+
31+
impv1alpha1 "github.com/syscode-labs/imp/api/v1alpha1"
32+
)
33+
34+
// ImpWarmPoolReconciler reconciles ImpWarmPool objects.
35+
type ImpWarmPoolReconciler struct {
36+
client.Client
37+
Scheme *runtime.Scheme
38+
}
39+
40+
// +kubebuilder:rbac:groups=imp.dev,resources=impwarmpools,verbs=get;list;watch;create;update;patch;delete
41+
// +kubebuilder:rbac:groups=imp.dev,resources=impwarmpools/status,verbs=get;update;patch
42+
// +kubebuilder:rbac:groups=imp.dev,resources=impwarmpools/finalizers,verbs=update
43+
// +kubebuilder:rbac:groups=imp.dev,resources=impvmsnapshots,verbs=get;list;watch
44+
// +kubebuilder:rbac:groups=imp.dev,resources=impvmtemplates,verbs=get;list;watch
45+
// +kubebuilder:rbac:groups=imp.dev,resources=impvms,verbs=get;list;watch;create
46+
47+
func (r *ImpWarmPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
48+
log := logf.FromContext(ctx)
49+
50+
pool := &impv1alpha1.ImpWarmPool{}
51+
if err := r.Get(ctx, req.NamespacedName, pool); err != nil {
52+
return ctrl.Result{}, client.IgnoreNotFound(err)
53+
}
54+
55+
// Fetch the ImpVMSnapshot named by spec.snapshotRef.
56+
snap := &impv1alpha1.ImpVMSnapshot{}
57+
if err := r.Get(ctx, client.ObjectKey{
58+
Namespace: pool.Namespace,
59+
Name: pool.Spec.SnapshotRef,
60+
}, snap); err != nil {
61+
if apierrors.IsNotFound(err) {
62+
log.Info("ImpVMSnapshot not found, requeueing", "snapshotRef", pool.Spec.SnapshotRef)
63+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
64+
}
65+
return ctrl.Result{}, err
66+
}
67+
68+
// Pool stays idle until a base snapshot has been elected.
69+
baseSnapshot := snap.Status.BaseSnapshot
70+
if baseSnapshot == "" {
71+
log.Info("no base snapshot elected yet, pool idle", "pool", pool.Name, "snapshotRef", pool.Spec.SnapshotRef)
72+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
73+
}
74+
75+
// Fetch the ImpVMTemplate to build VM specs.
76+
tpl := &impv1alpha1.ImpVMTemplate{}
77+
if err := r.Get(ctx, client.ObjectKey{
78+
Namespace: pool.Namespace,
79+
Name: pool.Spec.TemplateName,
80+
}, tpl); err != nil {
81+
if apierrors.IsNotFound(err) {
82+
log.Info("ImpVMTemplate not found, requeueing", "templateName", pool.Spec.TemplateName)
83+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
84+
}
85+
return ctrl.Result{}, err
86+
}
87+
88+
// List pool members: ImpVMs labeled with this pool name.
89+
vmList := &impv1alpha1.ImpVMList{}
90+
if err := r.List(ctx, vmList,
91+
client.InNamespace(pool.Namespace),
92+
client.MatchingLabels{impv1alpha1.LabelWarmPool: pool.Name},
93+
); err != nil {
94+
return ctrl.Result{}, err
95+
}
96+
97+
// Count ready (Running) and active (not Failed/RetryExhausted) members.
98+
var readyCount, activeCount int32
99+
for i := range vmList.Items {
100+
phase := vmList.Items[i].Status.Phase
101+
switch phase {
102+
case impv1alpha1.VMPhaseRunning:
103+
readyCount++
104+
activeCount++
105+
case impv1alpha1.VMPhaseFailed, impv1alpha1.VMPhaseRetryExhausted, impv1alpha1.VMPhaseSucceeded:
106+
// terminal — do not count as active
107+
default:
108+
// Pending, Scheduled, Starting, Terminating — count as active
109+
activeCount++
110+
}
111+
}
112+
113+
// Create missing VMs to reach spec.size.
114+
toCreate := pool.Spec.Size - activeCount
115+
for i := int32(0); i < toCreate; i++ {
116+
if err := r.createPoolMember(ctx, pool, tpl, baseSnapshot); err != nil {
117+
return ctrl.Result{}, err
118+
}
119+
}
120+
if toCreate > 0 {
121+
log.Info("created pool members", "pool", pool.Name, "count", toCreate, "baseSnapshot", baseSnapshot)
122+
}
123+
124+
// Patch status.readyCount.
125+
base := pool.DeepCopy()
126+
pool.Status.ReadyCount = readyCount
127+
if err := r.Status().Patch(ctx, pool, client.MergeFrom(base)); err != nil {
128+
return ctrl.Result{}, err
129+
}
130+
131+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
132+
}
133+
134+
// createPoolMember creates a single ImpVM pool member.
135+
func (r *ImpWarmPoolReconciler) createPoolMember(
136+
ctx context.Context,
137+
pool *impv1alpha1.ImpWarmPool,
138+
tpl *impv1alpha1.ImpVMTemplate,
139+
baseSnapshot string,
140+
) error {
141+
vmName := fmt.Sprintf("%s-%s", pool.Name, time.Now().UTC().Format("20060102-150405-000000"))
142+
143+
vm := &impv1alpha1.ImpVM{
144+
ObjectMeta: metav1.ObjectMeta{
145+
GenerateName: pool.Name + "-",
146+
Namespace: pool.Namespace,
147+
Labels: map[string]string{
148+
impv1alpha1.LabelWarmPool: pool.Name,
149+
},
150+
},
151+
Spec: impv1alpha1.ImpVMSpec{
152+
ClassRef: &tpl.Spec.ClassRef,
153+
Image: tpl.Spec.Image,
154+
SnapshotRef: baseSnapshot,
155+
},
156+
}
157+
_ = vmName // GenerateName is used instead; vmName would conflict on fast loops
158+
159+
if tpl.Spec.NetworkRef != nil {
160+
vm.Spec.NetworkRef = tpl.Spec.NetworkRef
161+
}
162+
if tpl.Spec.RestartPolicy != nil {
163+
vm.Spec.RestartPolicy = tpl.Spec.RestartPolicy
164+
}
165+
if tpl.Spec.Probes != nil {
166+
vm.Spec.Probes = tpl.Spec.Probes
167+
}
168+
if tpl.Spec.GuestAgent != nil {
169+
vm.Spec.GuestAgent = tpl.Spec.GuestAgent
170+
}
171+
if tpl.Spec.NetworkGroup != "" {
172+
vm.Spec.NetworkGroup = tpl.Spec.NetworkGroup
173+
}
174+
175+
if err := ctrl.SetControllerReference(pool, vm, r.Scheme); err != nil {
176+
return err
177+
}
178+
179+
return r.Create(ctx, vm)
180+
}
181+
182+
func (r *ImpWarmPoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
183+
return ctrl.NewControllerManagedBy(mgr).
184+
For(&impv1alpha1.ImpWarmPool{}).
185+
Owns(&impv1alpha1.ImpVM{}).
186+
Complete(r)
187+
}

0 commit comments

Comments
 (0)