@@ -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