@@ -32,8 +32,10 @@ import (
3232
3333 infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
3434 "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
35+ ec2service "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/ec2"
3536 "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger"
3637 "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/record"
38+ clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3739 "sigs.k8s.io/cluster-api/util"
3840 "sigs.k8s.io/cluster-api/util/predicates"
3941)
@@ -53,6 +55,7 @@ type AWSMachineTemplateReconciler struct {
5355// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=awsmachinetemplates/status,verbs=get;update;patch
5456// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=awsclusters,verbs=get;list;watch
5557// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch
58+ // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments,verbs=get;list;watch
5659// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch
5760
5861// Reconcile populates capacity information for AWSMachineTemplate.
@@ -99,18 +102,32 @@ func (r *AWSMachineTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R
99102 return ctrl.Result {}, nil
100103 }
101104
102- // Query instance type capacity and node info
103- capacity , nodeInfo , err := r .getInstanceTypeInfo (ctx , globalScope , awsMachineTemplate , instanceType )
105+ // Create EC2 client from global scope
106+ ec2Client := ec2 .NewFromConfig (globalScope .Session ())
107+
108+ // Query instance type capacity
109+ capacity , err := r .getInstanceTypeCapacity (ctx , ec2Client , instanceType )
104110 if err != nil {
105111 record .Warnf (awsMachineTemplate , "CapacityQueryFailed" , "Failed to query capacity for instance type %q: %v" , instanceType , err )
106112 return ctrl.Result {}, nil
107113 }
108114
109- // Update status with capacity and nodeInfo
110- awsMachineTemplate .Status .Capacity = capacity
111- awsMachineTemplate .Status .NodeInfo = nodeInfo
115+ // Query node info (architecture and OS)
116+ nodeInfo , err := r .getNodeInfo (ctx , ec2Client , awsMachineTemplate , instanceType )
117+ if err != nil {
118+ record .Warnf (awsMachineTemplate , "NodeInfoQueryFailed" , "Failed to query node info for instance type %q: %v" , instanceType , err )
119+ return ctrl.Result {}, nil
120+ }
112121
113- if err := r .Status ().Update (ctx , awsMachineTemplate ); err != nil {
122+ // Save original before modifying, then update all status fields at once
123+ original := awsMachineTemplate .DeepCopy ()
124+ if len (capacity ) > 0 {
125+ awsMachineTemplate .Status .Capacity = capacity
126+ }
127+ if nodeInfo != nil && (nodeInfo .Architecture != "" || nodeInfo .OperatingSystem != "" ) {
128+ awsMachineTemplate .Status .NodeInfo = nodeInfo
129+ }
130+ if err := r .Status ().Patch (ctx , awsMachineTemplate , client .MergeFrom (original )); err != nil {
114131 return ctrl.Result {}, errors .Wrap (err , "failed to update AWSMachineTemplate status" )
115132 }
116133
@@ -147,23 +164,21 @@ func (r *AWSMachineTemplateReconciler) getRegion(ctx context.Context, template *
147164 return "" , nil
148165}
149166
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 ) {
152- // Create EC2 client from global scope
153- ec2Client := ec2 .NewFromConfig (globalScope .Session ())
154-
167+ // getInstanceTypeCapacity queries AWS EC2 API for instance type capacity information.
168+ // Returns the resource list (CPU, Memory).
169+ func (r * AWSMachineTemplateReconciler ) getInstanceTypeCapacity (ctx context.Context , ec2Client * ec2.Client , instanceType string ) (corev1.ResourceList , error ) {
155170 // Query instance type information
156171 input := & ec2.DescribeInstanceTypesInput {
157172 InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
158173 }
159174
160175 result , err := ec2Client .DescribeInstanceTypes (ctx , input )
161176 if err != nil {
162- return nil , nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
177+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
163178 }
164179
165180 if len (result .InstanceTypes ) == 0 {
166- return nil , nil , errors .Errorf ("no information found for instance type %q" , instanceType )
181+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
167182 }
168183
169184 // Extract capacity information
@@ -181,10 +196,16 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
181196 resourceList [corev1 .ResourceMemory ] = * resource .NewQuantity (memoryBytes , resource .BinarySI )
182197 }
183198
184- // Extract node info from AMI if available
199+ return resourceList , nil
200+ }
201+
202+ // getNodeInfo queries node information (architecture and OS) for the AWSMachineTemplate.
203+ // It uses AMI ID if specified, otherwise attempts AMI lookup or falls back to instance type info.
204+ func (r * AWSMachineTemplateReconciler ) getNodeInfo (ctx context.Context , ec2Client * ec2.Client , template * infrav1.AWSMachineTemplate , instanceType string ) (* infrav1.NodeInfo , error ) {
185205 nodeInfo := & infrav1.NodeInfo {}
186206 amiID := template .Spec .Template .Spec .AMI .ID
187207 if amiID != nil && * amiID != "" {
208+ // AMI ID is specified, query it directly
188209 arch , os , err := r .getNodeInfoFromAMI (ctx , ec2Client , * amiID )
189210 if err == nil {
190211 if arch != "" {
@@ -194,9 +215,67 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
194215 nodeInfo .OperatingSystem = os
195216 }
196217 }
218+ } else {
219+ // AMI ID is not specified, query instance type to get architecture
220+ input := & ec2.DescribeInstanceTypesInput {
221+ InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
222+ }
223+
224+ result , err := ec2Client .DescribeInstanceTypes (ctx , input )
225+ if err != nil {
226+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
227+ }
228+
229+ if len (result .InstanceTypes ) == 0 {
230+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
231+ }
232+
233+ instanceTypeInfo := result .InstanceTypes [0 ]
234+
235+ // Infer architecture from instance type
236+ var architecture string
237+ if instanceTypeInfo .ProcessorInfo != nil && len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ) == 1 {
238+ // Use the supported architecture
239+ switch instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ] {
240+ case ec2types .ArchitectureTypeX8664 :
241+ architecture = ec2service .Amd64ArchitectureTag
242+ nodeInfo .Architecture = infrav1 .ArchitectureAmd64
243+ case ec2types .ArchitectureTypeArm64 :
244+ architecture = ec2service .Arm64ArchitectureTag
245+ nodeInfo .Architecture = infrav1 .ArchitectureArm64
246+ }
247+ } else {
248+ return nil , errors .Errorf ("instance type must support exactly one architecture, got %d" , len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ))
249+ }
250+
251+ // Attempt to get Kubernetes version from MachineDeployment
252+ kubernetesVersion , versionErr := r .getKubernetesVersion (ctx , template )
253+ if versionErr == nil && kubernetesVersion != "" {
254+ // Try to look up AMI using the version
255+ image , err := ec2service .DefaultAMILookup (
256+ ec2Client ,
257+ template .Spec .Template .Spec .ImageLookupOrg ,
258+ template .Spec .Template .Spec .ImageLookupBaseOS ,
259+ kubernetesVersion ,
260+ architecture ,
261+ template .Spec .Template .Spec .ImageLookupFormat ,
262+ )
263+ if err == nil && image != nil {
264+ // Successfully found AMI, extract accurate nodeInfo from it
265+ arch , os , _ := r .getNodeInfoFromAMI (ctx , ec2Client , * image .ImageId )
266+ if arch != "" {
267+ nodeInfo .Architecture = arch
268+ }
269+ if os != "" {
270+ nodeInfo .OperatingSystem = os
271+ }
272+ return nodeInfo , nil
273+ }
274+ // AMI lookup failed, fall through to defaults
275+ }
197276 }
198277
199- return resourceList , nodeInfo , nil
278+ return nodeInfo , nil
200279}
201280
202281// getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
@@ -225,28 +304,47 @@ func (r *AWSMachineTemplateReconciler) getNodeInfoFromAMI(ctx context.Context, e
225304 arch = infrav1 .ArchitectureArm64
226305 }
227306
228- // Determine OS - check Platform field first (specifically for Windows identification)
229- var os string
307+ // Determine OS - default to Linux, change to Windows if detected
308+ // Most AMIs are Linux-based, so we initialize with Linux as the default
309+ os := infrav1 .OperatingSystemLinux
230310
231311 // 1. Check Platform field (most reliable for Windows detection)
232312 if image .Platform == ec2types .PlatformValuesWindows {
233- os = "windows"
313+ os = infrav1 . OperatingSystemWindows
234314 }
235315
236- // 2. Check PlatformDetails field (provides more detailed information)
237- if os == "" && image .PlatformDetails != nil {
316+ // 2. Check PlatformDetails field for Windows indication
317+ if os != infrav1 . OperatingSystemWindows && image .PlatformDetails != nil {
238318 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"
319+ if strings .Contains (platformDetails , infrav1 .OperatingSystemWindows ) {
320+ os = infrav1 .OperatingSystemWindows
244321 }
245322 }
246323
247324 return arch , os , nil
248325}
249326
327+ // getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
328+ // that reference this AWSMachineTemplate.
329+ func (r * AWSMachineTemplateReconciler ) getKubernetesVersion (ctx context.Context , template * infrav1.AWSMachineTemplate ) (string , error ) {
330+ // List all MachineDeployments in the same namespace
331+ machineDeploymentList := & clusterv1.MachineDeploymentList {}
332+ if err := r .List (ctx , machineDeploymentList , client .InNamespace (template .Namespace )); err != nil {
333+ return "" , errors .Wrap (err , "failed to list MachineDeployments" )
334+ }
335+
336+ // Find MachineDeployments that reference this AWSMachineTemplate
337+ for _ , md := range machineDeploymentList .Items {
338+ if md .Spec .Template .Spec .InfrastructureRef .Kind == "AWSMachineTemplate" &&
339+ md .Spec .Template .Spec .InfrastructureRef .Name == template .Name &&
340+ md .Spec .Template .Spec .Version != nil {
341+ return * md .Spec .Template .Spec .Version , nil
342+ }
343+ }
344+
345+ return "" , errors .New ("no MachineDeployment found referencing this AWSMachineTemplate with a version" )
346+ }
347+
250348// SetupWithManager sets up the controller with the Manager.
251349func (r * AWSMachineTemplateReconciler ) SetupWithManager (ctx context.Context , mgr ctrl.Manager , options controller.Options ) error {
252350 log := logger .FromContext (ctx )
0 commit comments