Skip to content

Commit 8d0a866

Browse files
committed
feat(controller): resolveClassSpec — follows ClassRef or TemplateRef chain
1 parent 88f09ba commit 8d0a866

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

internal/controller/capacity.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ limitations under the License.
1717
package controller
1818

1919
import (
20+
"context"
21+
"fmt"
2022
"strconv"
23+
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
26+
impdevv1alpha1 "github.com/syscode-labs/imp/api/v1alpha1"
2127
)
2228

2329
// parseFraction parses a fraction string (e.g. "0.9") into a float64 in [0,1].
@@ -59,3 +65,28 @@ func effectiveMaxVMs(allocCPUMillis, allocMemBytes int64, vcpu, memMiB int32, fr
5965
}
6066
return int32(result) //nolint:gosec
6167
}
68+
69+
// resolveClassSpec returns the ImpVMClassSpec for vm by following either
70+
// vm.Spec.ClassRef (direct) or vm.Spec.TemplateRef → template.Spec.ClassRef.
71+
// Returns an error if neither ref is set or if the referenced objects are missing.
72+
func resolveClassSpec(ctx context.Context, c client.Client, vm *impdevv1alpha1.ImpVM) (*impdevv1alpha1.ImpVMClassSpec, error) {
73+
if vm.Spec.ClassRef != nil {
74+
var class impdevv1alpha1.ImpVMClass
75+
if err := c.Get(ctx, client.ObjectKey{Name: vm.Spec.ClassRef.Name}, &class); err != nil {
76+
return nil, fmt.Errorf("get class %q: %w", vm.Spec.ClassRef.Name, err)
77+
}
78+
return &class.Spec, nil
79+
}
80+
if vm.Spec.TemplateRef != nil {
81+
var tmpl impdevv1alpha1.ImpVMTemplate
82+
if err := c.Get(ctx, client.ObjectKey{Namespace: vm.Namespace, Name: vm.Spec.TemplateRef.Name}, &tmpl); err != nil {
83+
return nil, fmt.Errorf("get template %q: %w", vm.Spec.TemplateRef.Name, err)
84+
}
85+
var class impdevv1alpha1.ImpVMClass
86+
if err := c.Get(ctx, client.ObjectKey{Name: tmpl.Spec.ClassRef.Name}, &class); err != nil {
87+
return nil, fmt.Errorf("get class %q (via template %q): %w", tmpl.Spec.ClassRef.Name, tmpl.Name, err)
88+
}
89+
return &class.Spec, nil
90+
}
91+
return nil, fmt.Errorf("vm %s/%s has neither classRef nor templateRef", vm.Namespace, vm.Name)
92+
}

internal/controller/capacity_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ limitations under the License.
1717
package controller
1818

1919
import (
20+
"context"
2021
"testing"
22+
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
25+
26+
impdevv1alpha1 "github.com/syscode-labs/imp/api/v1alpha1"
2127
)
2228

2329
func TestParseFraction(t *testing.T) {
@@ -103,3 +109,100 @@ func TestEffectiveMaxVMs(t *testing.T) {
103109
})
104110
}
105111
}
112+
113+
func TestResolveClassSpec_DirectClassRef(t *testing.T) {
114+
scheme := runtime.NewScheme()
115+
if err := impdevv1alpha1.AddToScheme(scheme); err != nil {
116+
t.Fatalf("scheme: %v", err)
117+
}
118+
119+
class := &impdevv1alpha1.ImpVMClass{}
120+
class.Name = "small"
121+
class.Spec.VCPU = 2
122+
class.Spec.MemoryMiB = 512
123+
124+
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(class).Build()
125+
126+
vm := &impdevv1alpha1.ImpVM{}
127+
vm.Namespace = "default"
128+
vm.Name = "test-vm"
129+
vm.Spec.ClassRef = &impdevv1alpha1.ClusterObjectRef{Name: "small"}
130+
131+
spec, err := resolveClassSpec(context.Background(), c, vm)
132+
if err != nil {
133+
t.Fatalf("unexpected error: %v", err)
134+
}
135+
if spec.VCPU != 2 {
136+
t.Errorf("VCPU = %d, want 2", spec.VCPU)
137+
}
138+
if spec.MemoryMiB != 512 {
139+
t.Errorf("MemoryMiB = %d, want 512", spec.MemoryMiB)
140+
}
141+
}
142+
143+
func TestResolveClassSpec_ViaTemplateRef(t *testing.T) {
144+
scheme := runtime.NewScheme()
145+
if err := impdevv1alpha1.AddToScheme(scheme); err != nil {
146+
t.Fatalf("scheme: %v", err)
147+
}
148+
149+
class := &impdevv1alpha1.ImpVMClass{}
150+
class.Name = "large"
151+
class.Spec.VCPU = 4
152+
class.Spec.MemoryMiB = 2048
153+
154+
tmpl := &impdevv1alpha1.ImpVMTemplate{}
155+
tmpl.Namespace = "default"
156+
tmpl.Name = "ubuntu-tmpl"
157+
tmpl.Spec.ClassRef = impdevv1alpha1.ClusterObjectRef{Name: "large"}
158+
159+
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(class, tmpl).Build()
160+
161+
vm := &impdevv1alpha1.ImpVM{}
162+
vm.Namespace = "default"
163+
vm.Name = "tmpl-vm"
164+
vm.Spec.TemplateRef = &impdevv1alpha1.LocalObjectRef{Name: "ubuntu-tmpl"}
165+
166+
spec, err := resolveClassSpec(context.Background(), c, vm)
167+
if err != nil {
168+
t.Fatalf("unexpected error: %v", err)
169+
}
170+
if spec.VCPU != 4 {
171+
t.Errorf("VCPU = %d, want 4", spec.VCPU)
172+
}
173+
}
174+
175+
func TestResolveClassSpec_NoRef(t *testing.T) {
176+
scheme := runtime.NewScheme()
177+
if err := impdevv1alpha1.AddToScheme(scheme); err != nil {
178+
t.Fatalf("scheme: %v", err)
179+
}
180+
c := fake.NewClientBuilder().WithScheme(scheme).Build()
181+
182+
vm := &impdevv1alpha1.ImpVM{}
183+
vm.Namespace = "default"
184+
vm.Name = "no-ref"
185+
186+
_, err := resolveClassSpec(context.Background(), c, vm)
187+
if err == nil {
188+
t.Fatal("expected error for VM with no classRef or templateRef")
189+
}
190+
}
191+
192+
func TestResolveClassSpec_ClassNotFound(t *testing.T) {
193+
scheme := runtime.NewScheme()
194+
if err := impdevv1alpha1.AddToScheme(scheme); err != nil {
195+
t.Fatalf("scheme: %v", err)
196+
}
197+
c := fake.NewClientBuilder().WithScheme(scheme).Build()
198+
199+
vm := &impdevv1alpha1.ImpVM{}
200+
vm.Namespace = "default"
201+
vm.Name = "missing-class"
202+
vm.Spec.ClassRef = &impdevv1alpha1.ClusterObjectRef{Name: "nonexistent"}
203+
204+
_, err := resolveClassSpec(context.Background(), c, vm)
205+
if err == nil {
206+
t.Fatal("expected error when class does not exist")
207+
}
208+
}

0 commit comments

Comments
 (0)