Skip to content

Commit ae396bf

Browse files
add AWSMachineTemplate NodeInfo
1 parent 5abc575 commit ae396bf

File tree

9 files changed

+224
-19
lines changed

9 files changed

+224
-19
lines changed

api/v1beta1/awsmachine_conversion.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
129129
}
130130
}
131131

132+
// Restore Status fields that don't exist in v1beta1.
133+
dst.Status.NodeInfo = restored.Status.NodeInfo
134+
132135
return nil
133136
}
134137

api/v1beta1/conversion.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,10 @@ func Convert_v1beta2_S3Bucket_To_v1beta1_S3Bucket(in *v1beta2.S3Bucket, out *S3B
103103
func Convert_v1beta2_Ignition_To_v1beta1_Ignition(in *v1beta2.Ignition, out *Ignition, s conversion.Scope) error {
104104
return autoConvert_v1beta2_Ignition_To_v1beta1_Ignition(in, out, s)
105105
}
106+
107+
// Convert_v1beta2_AWSMachineTemplateStatus_To_v1beta1_AWSMachineTemplateStatus converts v1beta2 AWSMachineTemplateStatus to v1beta1.
108+
// The NodeInfo field is dropped during conversion as it doesn't exist in v1beta1.
109+
func Convert_v1beta2_AWSMachineTemplateStatus_To_v1beta1_AWSMachineTemplateStatus(in *v1beta2.AWSMachineTemplateStatus, out *AWSMachineTemplateStatus, s conversion.Scope) error {
110+
// NodeInfo field is ignored (dropped) as it doesn't exist in v1beta1
111+
return autoConvert_v1beta2_AWSMachineTemplateStatus_To_v1beta1_AWSMachineTemplateStatus(in, out, s)
112+
}

api/v1beta1/zz_generated.conversion.go

Lines changed: 1 addition & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta2/awsmachinetemplate_types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,43 @@ import (
2323
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2424
)
2525

26+
// Architecture represents the CPU architecture of the node.
27+
// Its underlying type is a string and its value can be any of amd64, arm64.
28+
// +kubebuilder:validation:Enum=amd64;arm64
29+
// +enum
30+
type Architecture string
31+
32+
// Architecture constants.
33+
const (
34+
ArchitectureAmd64 Architecture = "amd64"
35+
ArchitectureArm64 Architecture = "arm64"
36+
)
37+
38+
// NodeInfo contains information about the node's architecture and operating system.
39+
type NodeInfo struct {
40+
// Architecture is the CPU architecture of the node.
41+
// Its underlying type is a string and its value can be any of amd64, arm64.
42+
// +optional
43+
Architecture Architecture `json:"architecture,omitempty"`
44+
// OperatingSystem is a string representing the operating system of the node.
45+
// This may be a string like 'linux' or 'windows'.
46+
// +optional
47+
OperatingSystem string `json:"operatingSystem,omitempty"`
48+
}
49+
2650
// AWSMachineTemplateStatus defines a status for an AWSMachineTemplate.
2751
type AWSMachineTemplateStatus struct {
2852
// Capacity defines the resource capacity for this machine.
2953
// This value is used for autoscaling from zero operations as defined in:
3054
// https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md
3155
// +optional
3256
Capacity corev1.ResourceList `json:"capacity,omitempty"`
57+
58+
// NodeInfo contains information about the node's architecture and operating system.
59+
// This value is used for autoscaling from zero operations as defined in:
60+
// https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md
61+
// +optional
62+
NodeInfo *NodeInfo `json:"nodeInfo,omitempty"`
3363
}
3464

3565
// AWSMachineTemplateSpec defines the desired state of AWSMachineTemplate.

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,26 @@ spec:
11341134
This value is used for autoscaling from zero operations as defined in:
11351135
https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md
11361136
type: object
1137+
nodeInfo:
1138+
description: |-
1139+
NodeInfo contains information about the node's architecture and operating system.
1140+
This value is used for autoscaling from zero operations as defined in:
1141+
https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md
1142+
properties:
1143+
architecture:
1144+
description: |-
1145+
Architecture is the CPU architecture of the node.
1146+
Its underlying type is a string and its value can be any of amd64, arm64.
1147+
enum:
1148+
- amd64
1149+
- arm64
1150+
type: string
1151+
operatingSystem:
1152+
description: |-
1153+
OperatingSystem is a string representing the operating system of the node.
1154+
This may be a string like 'linux' or 'windows'.
1155+
type: string
1156+
type: object
11371157
type: object
11381158
type: object
11391159
served: true

controllers/awsmachinetemplate_controller.go

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers
1818

1919
import (
2020
"context"
21+
"strings"
2122

2223
"github.com/aws/aws-sdk-go-v2/service/ec2"
2324
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
@@ -67,8 +68,8 @@ func (r *AWSMachineTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R
6768
return ctrl.Result{}, err
6869
}
6970

70-
// Skip if capacity is already set
71-
if len(awsMachineTemplate.Status.Capacity) > 0 {
71+
// Skip if capacity and nodeInfo are already set
72+
if len(awsMachineTemplate.Status.Capacity) > 0 && awsMachineTemplate.Status.NodeInfo != nil {
7273
return ctrl.Result{}, nil
7374
}
7475

@@ -98,21 +99,22 @@ func (r *AWSMachineTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R
9899
return ctrl.Result{}, nil
99100
}
100101

101-
// Query instance type capacity
102-
capacity, err := r.getInstanceTypeCapacity(ctx, globalScope, instanceType)
102+
// Query instance type capacity and node info
103+
capacity, nodeInfo, err := r.getInstanceTypeInfo(ctx, globalScope, awsMachineTemplate, instanceType)
103104
if err != nil {
104105
record.Warnf(awsMachineTemplate, "CapacityQueryFailed", "Failed to query capacity for instance type %q: %v", instanceType, err)
105106
return ctrl.Result{}, nil
106107
}
107108

108-
// Update status with capacity
109+
// Update status with capacity and nodeInfo
109110
awsMachineTemplate.Status.Capacity = capacity
111+
awsMachineTemplate.Status.NodeInfo = nodeInfo
110112

111113
if err := r.Status().Update(ctx, awsMachineTemplate); err != nil {
112114
return ctrl.Result{}, errors.Wrap(err, "failed to update AWSMachineTemplate status")
113115
}
114116

115-
log.Info("Successfully populated capacity information", "instanceType", instanceType, "region", region, "capacity", capacity)
117+
log.Info("Successfully populated capacity and nodeInfo", "instanceType", instanceType, "region", region, "capacity", capacity, "nodeInfo", nodeInfo)
116118
return ctrl.Result{}, nil
117119
}
118120

@@ -145,8 +147,8 @@ func (r *AWSMachineTemplateReconciler) getRegion(ctx context.Context, template *
145147
return "", nil
146148
}
147149

148-
// getInstanceTypeCapacity queries AWS EC2 API for instance type capacity.
149-
func (r *AWSMachineTemplateReconciler) getInstanceTypeCapacity(ctx context.Context, globalScope *scope.GlobalScope, instanceType string) (corev1.ResourceList, error) {
150+
// getInstanceTypeInfo queries AWS EC2 API for instance type capacity and node info.
151+
func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context, globalScope *scope.GlobalScope, template *infrav1.AWSMachineTemplate, instanceType string) (corev1.ResourceList, *infrav1.NodeInfo, error) {
150152
// Create EC2 client from global scope
151153
ec2Client := ec2.NewFromConfig(globalScope.Session())
152154

@@ -157,11 +159,11 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeCapacity(ctx context.Conte
157159

158160
result, err := ec2Client.DescribeInstanceTypes(ctx, input)
159161
if err != nil {
160-
return nil, errors.Wrapf(err, "failed to describe instance type %q", instanceType)
162+
return nil, nil, errors.Wrapf(err, "failed to describe instance type %q", instanceType)
161163
}
162164

163165
if len(result.InstanceTypes) == 0 {
164-
return nil, errors.Errorf("no information found for instance type %q", instanceType)
166+
return nil, nil, errors.Errorf("no information found for instance type %q", instanceType)
165167
}
166168

167169
// Extract capacity information
@@ -178,7 +180,71 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeCapacity(ctx context.Conte
178180
memoryBytes := *info.MemoryInfo.SizeInMiB * 1024 * 1024
179181
resourceList[corev1.ResourceMemory] = *resource.NewQuantity(memoryBytes, resource.BinarySI)
180182
}
181-
return resourceList, nil
183+
184+
// Extract node info from AMI if available
185+
nodeInfo := &infrav1.NodeInfo{}
186+
amiID := template.Spec.Template.Spec.AMI.ID
187+
if amiID != nil && *amiID != "" {
188+
arch, os, err := r.getNodeInfoFromAMI(ctx, ec2Client, *amiID)
189+
if err == nil {
190+
if arch != "" {
191+
nodeInfo.Architecture = arch
192+
}
193+
if os != "" {
194+
nodeInfo.OperatingSystem = os
195+
}
196+
}
197+
}
198+
199+
return resourceList, nodeInfo, nil
200+
}
201+
202+
// getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
203+
func (r *AWSMachineTemplateReconciler) getNodeInfoFromAMI(ctx context.Context, ec2Client *ec2.Client, amiID string) (infrav1.Architecture, string, error) {
204+
input := &ec2.DescribeImagesInput{
205+
ImageIds: []string{amiID},
206+
}
207+
208+
result, err := ec2Client.DescribeImages(ctx, input)
209+
if err != nil {
210+
return "", "", errors.Wrapf(err, "failed to describe AMI %q", amiID)
211+
}
212+
213+
if len(result.Images) == 0 {
214+
return "", "", errors.Errorf("no information found for AMI %q", amiID)
215+
}
216+
217+
image := result.Images[0]
218+
219+
// Get architecture from AMI
220+
var arch infrav1.Architecture
221+
switch image.Architecture {
222+
case ec2types.ArchitectureValuesX8664:
223+
arch = infrav1.ArchitectureAmd64
224+
case ec2types.ArchitectureValuesArm64:
225+
arch = infrav1.ArchitectureArm64
226+
}
227+
228+
// Determine OS - check Platform field first (specifically for Windows identification)
229+
var os string
230+
231+
// 1. Check Platform field (most reliable for Windows detection)
232+
if image.Platform == ec2types.PlatformValuesWindows {
233+
os = "windows"
234+
}
235+
236+
// 2. Check PlatformDetails field (provides more detailed information)
237+
if os == "" && image.PlatformDetails != nil {
238+
platformDetails := strings.ToLower(*image.PlatformDetails)
239+
switch {
240+
case strings.Contains(platformDetails, "windows"):
241+
os = "windows"
242+
case strings.Contains(platformDetails, "linux"), strings.Contains(platformDetails, "unix"):
243+
os = "linux"
244+
}
245+
}
246+
247+
return arch, os, nil
182248
}
183249

184250
// SetupWithManager sets up the controller with the Manager.

controllers/awsmachinetemplate_controller_unit_test.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,20 @@ func TestAWSMachineTemplateReconciler(t *testing.T) {
187187
})
188188
})
189189

190-
// Note: getInstanceTypeCapacity tests are skipped as they require EC2 client injection
190+
// Note: getInstanceTypeInfo tests are skipped as they require EC2 client injection
191191
// which would need significant refactoring. The function is tested indirectly through
192192
// integration tests.
193193

194194
t.Run("Reconcile", func(t *testing.T) {
195-
t.Run("should skip when capacity already set", func(t *testing.T) {
195+
t.Run("should skip when capacity and nodeInfo already set", func(t *testing.T) {
196196
g := NewWithT(t)
197197
template := newAWSMachineTemplate("test-template")
198198
template.Status.Capacity = corev1.ResourceList{
199199
corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI),
200200
}
201+
template.Status.NodeInfo = &infrav1.NodeInfo{
202+
Architecture: infrav1.ArchitectureAmd64,
203+
}
201204

202205
reconciler := &AWSMachineTemplateReconciler{
203206
Client: newFakeClient(template),
@@ -211,6 +214,55 @@ func TestAWSMachineTemplateReconciler(t *testing.T) {
211214
g.Expect(result.Requeue).To(BeFalse())
212215
})
213216

217+
t.Run("should reconcile when capacity set but nodeInfo is not", func(t *testing.T) {
218+
g := NewWithT(t)
219+
template := newAWSMachineTemplate("test-template")
220+
template.Status.Capacity = corev1.ResourceList{
221+
corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI),
222+
}
223+
template.OwnerReferences = []metav1.OwnerReference{
224+
{
225+
APIVersion: clusterv1.GroupVersion.String(),
226+
Kind: "Cluster",
227+
Name: "test-cluster",
228+
},
229+
}
230+
cluster := &clusterv1.Cluster{
231+
ObjectMeta: metav1.ObjectMeta{
232+
Name: "test-cluster",
233+
Namespace: "default",
234+
},
235+
Spec: clusterv1.ClusterSpec{
236+
InfrastructureRef: &corev1.ObjectReference{
237+
Kind: "AWSCluster",
238+
Name: "test-aws-cluster",
239+
Namespace: "default",
240+
},
241+
},
242+
}
243+
awsCluster := &infrav1.AWSCluster{
244+
ObjectMeta: metav1.ObjectMeta{
245+
Name: "test-aws-cluster",
246+
Namespace: "default",
247+
},
248+
Spec: infrav1.AWSClusterSpec{
249+
Region: "us-west-2",
250+
},
251+
}
252+
253+
reconciler := &AWSMachineTemplateReconciler{
254+
Client: newFakeClient(template, cluster, awsCluster),
255+
}
256+
257+
// This will fail at AWS API call, but demonstrates that reconcile proceeds
258+
result, err := reconciler.Reconcile(context.Background(), ctrl.Request{
259+
NamespacedName: client.ObjectKeyFromObject(template),
260+
})
261+
262+
g.Expect(err).To(BeNil())
263+
g.Expect(result.Requeue).To(BeFalse())
264+
})
265+
214266
t.Run("should skip when instance type is empty", func(t *testing.T) {
215267
g := NewWithT(t)
216268
template := newAWSMachineTemplate("test-template")

0 commit comments

Comments
 (0)