@@ -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,30 @@ 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 }
108-
109- // Update status with capacity and nodeInfo
114+ // Update status with capacity
110115 awsMachineTemplate .Status .Capacity = capacity
111- awsMachineTemplate .Status .NodeInfo = nodeInfo
116+ if err := r .Status ().Patch (ctx , awsMachineTemplate , client .MergeFrom (& infrav1.AWSMachineTemplate {})); err != nil {
117+ return ctrl.Result {}, errors .Wrap (err , "failed to update AWSMachineTemplate status" )
118+ }
112119
113- if err := r .Status ().Update (ctx , awsMachineTemplate ); err != nil {
120+ // Query node info (architecture and OS)
121+ nodeInfo , err := r .getNodeInfo (ctx , ec2Client , awsMachineTemplate , instanceType )
122+ if err != nil {
123+ record .Warnf (awsMachineTemplate , "NodeInfoQueryFailed" , "Failed to query node info for instance type %q: %v" , instanceType , err )
124+ return ctrl.Result {}, nil
125+ }
126+ // Update status with nodeInfo
127+ awsMachineTemplate .Status .NodeInfo = nodeInfo
128+ if err := r .Status ().Patch (ctx , awsMachineTemplate , client .MergeFrom (& infrav1.AWSMachineTemplate {})); err != nil {
114129 return ctrl.Result {}, errors .Wrap (err , "failed to update AWSMachineTemplate status" )
115130 }
116131
@@ -147,23 +162,21 @@ func (r *AWSMachineTemplateReconciler) getRegion(ctx context.Context, template *
147162 return "" , nil
148163}
149164
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-
165+ // getInstanceTypeCapacity queries AWS EC2 API for instance type capacity information.
166+ // Returns the resource list (CPU, Memory).
167+ func (r * AWSMachineTemplateReconciler ) getInstanceTypeCapacity (ctx context.Context , ec2Client * ec2.Client , instanceType string ) (corev1.ResourceList , error ) {
155168 // Query instance type information
156169 input := & ec2.DescribeInstanceTypesInput {
157170 InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
158171 }
159172
160173 result , err := ec2Client .DescribeInstanceTypes (ctx , input )
161174 if err != nil {
162- return nil , nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
175+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
163176 }
164177
165178 if len (result .InstanceTypes ) == 0 {
166- return nil , nil , errors .Errorf ("no information found for instance type %q" , instanceType )
179+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
167180 }
168181
169182 // Extract capacity information
@@ -181,10 +194,16 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
181194 resourceList [corev1 .ResourceMemory ] = * resource .NewQuantity (memoryBytes , resource .BinarySI )
182195 }
183196
184- // Extract node info from AMI if available
197+ return resourceList , nil
198+ }
199+
200+ // getNodeInfo queries node information (architecture and OS) for the AWSMachineTemplate.
201+ // It uses AMI ID if specified, otherwise attempts AMI lookup or falls back to instance type info.
202+ func (r * AWSMachineTemplateReconciler ) getNodeInfo (ctx context.Context , ec2Client * ec2.Client , template * infrav1.AWSMachineTemplate , instanceType string ) (* infrav1.NodeInfo , error ) {
185203 nodeInfo := & infrav1.NodeInfo {}
186204 amiID := template .Spec .Template .Spec .AMI .ID
187205 if amiID != nil && * amiID != "" {
206+ // AMI ID is specified, query it directly
188207 arch , os , err := r .getNodeInfoFromAMI (ctx , ec2Client , * amiID )
189208 if err == nil {
190209 if arch != "" {
@@ -194,9 +213,65 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
194213 nodeInfo .OperatingSystem = os
195214 }
196215 }
216+ } else {
217+ // AMI ID is not specified, query instance type to get architecture
218+ input := & ec2.DescribeInstanceTypesInput {
219+ InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
220+ }
221+
222+ result , err := ec2Client .DescribeInstanceTypes (ctx , input )
223+ if err != nil {
224+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
225+ }
226+
227+ if len (result .InstanceTypes ) == 0 {
228+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
229+ }
230+
231+ instanceTypeInfo := result .InstanceTypes [0 ]
232+
233+ // Infer architecture from instance type
234+ var architecture string
235+ if instanceTypeInfo .ProcessorInfo != nil && len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ) == 1 {
236+ // Use the supported architecture
237+ switch instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ] {
238+ case ec2types .ArchitectureTypeX8664 :
239+ architecture = ec2service .Amd64ArchitectureTag
240+ case ec2types .ArchitectureTypeArm64 :
241+ architecture = ec2service .Arm64ArchitectureTag
242+ }
243+ } else {
244+ return nil , errors .Errorf ("instance type must support exactly one architecture, got %d" , len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ))
245+ }
246+
247+ // Attempt to get Kubernetes version from MachineDeployment
248+ kubernetesVersion , versionErr := r .getKubernetesVersion (ctx , template )
249+ if versionErr == nil && kubernetesVersion != "" {
250+ // Try to look up AMI using the version
251+ image , err := ec2service .DefaultAMILookup (
252+ ec2Client ,
253+ template .Spec .Template .Spec .ImageLookupOrg ,
254+ template .Spec .Template .Spec .ImageLookupBaseOS ,
255+ kubernetesVersion ,
256+ architecture ,
257+ template .Spec .Template .Spec .ImageLookupFormat ,
258+ )
259+ if err == nil && image != nil {
260+ // Successfully found AMI, extract accurate nodeInfo from it
261+ arch , os , _ := r .getNodeInfoFromAMI (ctx , ec2Client , * image .ImageId )
262+ if arch != "" {
263+ nodeInfo .Architecture = arch
264+ }
265+ if os != "" {
266+ nodeInfo .OperatingSystem = os
267+ }
268+ return nodeInfo , nil
269+ }
270+ // AMI lookup failed, fall through to defaults
271+ }
197272 }
198273
199- return resourceList , nodeInfo , nil
274+ return nodeInfo , nil
200275}
201276
202277// getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
@@ -225,28 +300,47 @@ func (r *AWSMachineTemplateReconciler) getNodeInfoFromAMI(ctx context.Context, e
225300 arch = infrav1 .ArchitectureArm64
226301 }
227302
228- // Determine OS - check Platform field first (specifically for Windows identification)
229- var os string
303+ // Determine OS - default to Linux, change to Windows if detected
304+ // Most AMIs are Linux-based, so we initialize with Linux as the default
305+ os := infrav1 .OperatingSystemLinux
230306
231307 // 1. Check Platform field (most reliable for Windows detection)
232308 if image .Platform == ec2types .PlatformValuesWindows {
233- os = "windows"
309+ os = infrav1 . OperatingSystemWindows
234310 }
235311
236- // 2. Check PlatformDetails field (provides more detailed information)
237- if os == "" && image .PlatformDetails != nil {
312+ // 2. Check PlatformDetails field for Windows indication
313+ if os != infrav1 . OperatingSystemWindows && image .PlatformDetails != nil {
238314 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"
315+ if strings .Contains (platformDetails , infrav1 .OperatingSystemWindows ) {
316+ os = infrav1 .OperatingSystemWindows
244317 }
245318 }
246319
247320 return arch , os , nil
248321}
249322
323+ // getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
324+ // that reference this AWSMachineTemplate.
325+ func (r * AWSMachineTemplateReconciler ) getKubernetesVersion (ctx context.Context , template * infrav1.AWSMachineTemplate ) (string , error ) {
326+ // List all MachineDeployments in the same namespace
327+ machineDeploymentList := & clusterv1.MachineDeploymentList {}
328+ if err := r .List (ctx , machineDeploymentList , client .InNamespace (template .Namespace )); err != nil {
329+ return "" , errors .Wrap (err , "failed to list MachineDeployments" )
330+ }
331+
332+ // Find MachineDeployments that reference this AWSMachineTemplate
333+ for _ , md := range machineDeploymentList .Items {
334+ if md .Spec .Template .Spec .InfrastructureRef .Kind == "AWSMachineTemplate" &&
335+ md .Spec .Template .Spec .InfrastructureRef .Name == template .Name &&
336+ md .Spec .Template .Spec .Version != nil {
337+ return * md .Spec .Template .Spec .Version , nil
338+ }
339+ }
340+
341+ return "" , errors .New ("no MachineDeployment found referencing this AWSMachineTemplate with a version" )
342+ }
343+
250344// SetupWithManager sets up the controller with the Manager.
251345func (r * AWSMachineTemplateReconciler ) SetupWithManager (ctx context.Context , mgr ctrl.Manager , options controller.Options ) error {
252346 log := logger .FromContext (ctx )
0 commit comments