@@ -27,6 +27,9 @@ import (
2727 corev1 "k8s.io/api/core/v1"
2828 apierrors "k8s.io/apimachinery/pkg/api/errors"
2929 "k8s.io/apimachinery/pkg/api/resource"
30+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+ "k8s.io/apimachinery/pkg/runtime/schema"
32+ "k8s.io/utils/ptr"
3033 ctrl "sigs.k8s.io/controller-runtime"
3134 "sigs.k8s.io/controller-runtime/pkg/client"
3235 "sigs.k8s.io/controller-runtime/pkg/controller"
@@ -230,153 +233,144 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeCapacity(ctx context.Conte
230233}
231234
232235// getNodeInfo queries node information (architecture and OS) for the AWSMachineTemplate.
233- // It uses AMI ID if specified, otherwise attempts AMI lookup or falls back to instance type info.
236+ // It attempts to resolve nodeInfo using three strategies in order of priority:
237+ // 1. Directly from explicitly specified AMI ID
238+ // 2. From default AMI lookup (requires Kubernetes version from owner MachineDeployment/KubeadmControlPlane)
239+ // 3. From instance type architecture (OS cannot be determined, only architecture)
234240func (r * AWSMachineTemplateReconciler ) getNodeInfo (ctx context.Context , ec2Client * ec2.Client , template * infrav1.AWSMachineTemplate , instanceType string ) (* infrav1.NodeInfo , error ) {
235- nodeInfo := & infrav1.NodeInfo {}
236- amiID := template .Spec .Template .Spec .AMI .ID
237- if amiID != nil && * amiID != "" {
238- // AMI ID is specified, query it directly
239- arch , os , err := r .getNodeInfoFromAMI (ctx , ec2Client , * amiID )
240- if err == nil {
241- if arch != "" {
242- nodeInfo .Architecture = arch
243- }
244- if os != "" {
245- nodeInfo .OperatingSystem = os
246- }
247- }
248- } else {
249- // AMI ID is not specified, query instance type to get architecture
250- input := & ec2.DescribeInstanceTypesInput {
251- InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
252- }
253-
254- result , err := ec2Client .DescribeInstanceTypes (ctx , input )
241+ if amiID := ptr .Deref (template .Spec .Template .Spec .AMI .ID , "" ); amiID != "" {
242+ result , err := ec2Client .DescribeImages (ctx , & ec2.DescribeImagesInput {
243+ ImageIds : []string {amiID },
244+ })
255245 if err != nil {
256- return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
246+ return nil , errors .Wrapf (err , "failed to describe AMI %q" , amiID )
257247 }
258-
259- if len (result .InstanceTypes ) == 0 {
260- return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
248+ if len (result .Images ) == 0 {
249+ return nil , errors .Errorf ("no information found for AMI %q" , amiID )
261250 }
251+ // Extract nodeInfo directly from the image object (no additional API call needed)
252+ return r .extractNodeInfoFromImage (result .Images [0 ]), nil
253+ }
262254
263- instanceTypeInfo := result .InstanceTypes [0 ]
264-
265- // Infer architecture from instance type
266- var architecture string
267- if instanceTypeInfo .ProcessorInfo != nil && len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ) == 1 {
268- // Use the supported architecture
269- switch instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ] {
270- case ec2types .ArchitectureTypeX8664 :
271- architecture = ec2service .Amd64ArchitectureTag
272- nodeInfo .Architecture = infrav1 .ArchitectureAmd64
273- case ec2types .ArchitectureTypeArm64 :
274- architecture = ec2service .Arm64ArchitectureTag
275- nodeInfo .Architecture = infrav1 .ArchitectureArm64
276- }
277- } else {
278- return nil , errors .Errorf ("instance type must support exactly one architecture, got %d" , len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ))
279- }
255+ // No explicit AMI ID specified, query instance type to determine architecture
256+ // This architecture will be used to lookup default AMI (Strategy 2) or as fallback (Strategy 3)
257+ result , err := ec2Client .DescribeInstanceTypes (ctx , & ec2.DescribeInstanceTypesInput {
258+ InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
259+ })
260+ if err != nil {
261+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
262+ }
280263
281- // Attempt to get Kubernetes version from MachineDeployment
282- kubernetesVersion , versionErr := r .getKubernetesVersion (ctx , template )
283- if versionErr == nil && kubernetesVersion != "" {
284- // Try to look up AMI using the version
285- image , err := ec2service .DefaultAMILookup (
286- ec2Client ,
287- template .Spec .Template .Spec .ImageLookupOrg ,
288- template .Spec .Template .Spec .ImageLookupBaseOS ,
289- kubernetesVersion ,
290- architecture ,
291- template .Spec .Template .Spec .ImageLookupFormat ,
292- )
293- if err == nil && image != nil {
294- // Successfully found AMI, extract accurate nodeInfo from it
295- arch , os , _ := r .getNodeInfoFromAMI (ctx , ec2Client , * image .ImageId )
296- if arch != "" {
297- nodeInfo .Architecture = arch
298- }
299- if os != "" {
300- nodeInfo .OperatingSystem = os
301- }
302- return nodeInfo , nil
303- }
304- // AMI lookup failed, fall through to defaults
305- } else {
306- return nil , errors .Errorf ("failed to get Kubernetes version: %v" , versionErr )
307- }
264+ if len (result .InstanceTypes ) == 0 {
265+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
308266 }
309267
310- return nodeInfo , nil
311- }
268+ instanceTypeInfo := result .InstanceTypes [0 ]
312269
313- // getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
314- func (r * AWSMachineTemplateReconciler ) getNodeInfoFromAMI (ctx context.Context , ec2Client * ec2.Client , amiID string ) (infrav1.Architecture , infrav1.OperatingSystem , error ) {
315- input := & ec2.DescribeImagesInput {
316- ImageIds : []string {amiID },
270+ // Instance type must support exactly one architecture
271+ if instanceTypeInfo .ProcessorInfo == nil || len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ) != 1 {
272+ return nil , errors .Errorf ("instance type must support exactly one architecture, got %d" , len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ))
317273 }
318274
319- result , err := ec2Client .DescribeImages (ctx , input )
320- if err != nil {
321- return "" , "" , errors .Wrapf (err , "failed to describe AMI %q" , amiID )
275+ // Map EC2 architecture type to architecture tag for AMI lookup
276+ var architecture string
277+ var nodeInfoArch infrav1.Architecture
278+ switch instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ] {
279+ case ec2types .ArchitectureTypeX8664 :
280+ architecture = ec2service .Amd64ArchitectureTag
281+ nodeInfoArch = infrav1 .ArchitectureAmd64
282+ case ec2types .ArchitectureTypeArm64 :
283+ architecture = ec2service .Arm64ArchitectureTag
284+ nodeInfoArch = infrav1 .ArchitectureArm64
285+ default :
286+ return nil , errors .Errorf ("unsupported architecture: %v" , instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ])
322287 }
323288
324- if len (result .Images ) == 0 {
325- return "" , "" , errors .Errorf ("no information found for AMI %q" , amiID )
289+ // Strategy 2: Try to get Kubernetes version and lookup default AMI
290+ kubernetesVersion , err := r .getKubernetesVersion (ctx , template )
291+ if err == nil && kubernetesVersion != "" {
292+ // Attempt AMI lookup with the version
293+ image , err := ec2service .DefaultAMILookup (
294+ ec2Client ,
295+ template .Spec .Template .Spec .ImageLookupOrg ,
296+ template .Spec .Template .Spec .ImageLookupBaseOS ,
297+ kubernetesVersion ,
298+ architecture ,
299+ template .Spec .Template .Spec .ImageLookupFormat ,
300+ )
301+ if err == nil && image != nil {
302+ // Successfully found AMI, extract accurate nodeInfo from it
303+ return r .extractNodeInfoFromImage (* image ), nil
304+ }
305+ // AMI lookup failed, fall through to Strategy 3
326306 }
327307
328- image := result .Images [0 ]
308+ // Strategy 3: Fallback to instance type architecture only
309+ // Note: OS cannot be determined from instance type alone, only architecture
310+ return & infrav1.NodeInfo {
311+ Architecture : nodeInfoArch ,
312+ }, nil
313+ }
329314
330- // Get architecture from AMI
331- var arch infrav1.Architecture
315+ // extractNodeInfoFromImage extracts nodeInfo (architecture and OS) from an EC2 image.
316+ // This is a pure function with no AWS API calls.
317+ func (r * AWSMachineTemplateReconciler ) extractNodeInfoFromImage (image ec2types.Image ) * infrav1.NodeInfo {
318+ nodeInfo := & infrav1.NodeInfo {}
319+
320+ // Extract architecture from AMI
332321 switch image .Architecture {
333322 case ec2types .ArchitectureValuesX8664 :
334- arch = infrav1 .ArchitectureAmd64
323+ nodeInfo . Architecture = infrav1 .ArchitectureAmd64
335324 case ec2types .ArchitectureValuesArm64 :
336- arch = infrav1 .ArchitectureArm64
325+ nodeInfo . Architecture = infrav1 .ArchitectureArm64
337326 }
338327
339328 // Determine OS - default to Linux, change to Windows if detected
340329 // Most AMIs are Linux-based, so we initialize with Linux as the default
341- os : = infrav1 .OperatingSystemLinux
330+ nodeInfo . OperatingSystem = infrav1 .OperatingSystemLinux
342331
343- // 1. Check Platform field (most reliable for Windows detection)
332+ // Check Platform field (most reliable for Windows detection)
344333 if image .Platform == ec2types .PlatformValuesWindows {
345- os = infrav1 .OperatingSystemWindows
334+ nodeInfo .OperatingSystem = infrav1 .OperatingSystemWindows
335+ return nodeInfo
346336 }
347337
348- // 2. Check PlatformDetails field for Windows indication
349- if os != infrav1 . OperatingSystemWindows && image .PlatformDetails != nil {
338+ // Check PlatformDetails field for Windows indication
339+ if image .PlatformDetails != nil {
350340 platformDetails := strings .ToLower (* image .PlatformDetails )
351341 if strings .Contains (platformDetails , string (infrav1 .OperatingSystemWindows )) {
352- os = infrav1 .OperatingSystemWindows
342+ nodeInfo . OperatingSystem = infrav1 .OperatingSystemWindows
353343 }
354344 }
355345
356- return arch , os , nil
346+ return nodeInfo
357347}
358348
359349// getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
360350// or KubeadmControlPlanes that reference this AWSMachineTemplate.
361351func (r * AWSMachineTemplateReconciler ) getKubernetesVersion (ctx context.Context , template * infrav1.AWSMachineTemplate ) (string , error ) {
352+ listOpts , err := getParentListOptions (template .ObjectMeta )
353+ if err != nil {
354+ return "" , errors .Wrap (err , "failed to get parent list options" )
355+ }
356+
362357 // Try to find version from MachineDeployment first
363358 machineDeploymentList := & clusterv1.MachineDeploymentList {}
364- if err := r .List (ctx , machineDeploymentList , client . InNamespace ( template . Namespace ) ); err != nil {
359+ if err := r .List (ctx , machineDeploymentList , listOpts ... ); err != nil {
365360 return "" , errors .Wrap (err , "failed to list MachineDeployments" )
366361 }
367362
368363 // Find MachineDeployments that reference this AWSMachineTemplate
369364 for _ , md := range machineDeploymentList .Items {
370- if md .Spec .Template .Spec .InfrastructureRef .Kind == "AWSMachineTemplate" &&
371- md .Spec .Template .Spec .InfrastructureRef .Name == template .Name &&
372- md .Spec .Template .Spec .Version != nil {
373- return * md .Spec .Template .Spec .Version , nil
365+ if version := ptr .Deref (md .Spec .Template .Spec .Version , "" ); md .Spec .Template .Spec .InfrastructureRef .Kind == "AWSMachineTemplate" &&
366+ md .Spec .Template .Spec .InfrastructureRef .Name == template .Name && version != "" {
367+ return version , nil
374368 }
375369 }
376370
377371 // If not found in MachineDeployment, try KubeadmControlPlane
378372 kcpList := & controlplanev1.KubeadmControlPlaneList {}
379- if err := r .List (ctx , kcpList , client . InNamespace ( template . Namespace ) ); err != nil {
373+ if err := r .List (ctx , kcpList , listOpts ... ); err != nil {
380374 return "" , errors .Wrap (err , "failed to list KubeadmControlPlanes" )
381375 }
382376
@@ -391,3 +385,26 @@ func (r *AWSMachineTemplateReconciler) getKubernetesVersion(ctx context.Context,
391385
392386 return "" , errors .New ("no MachineDeployment or KubeadmControlPlane found referencing this AWSMachineTemplate with a version" )
393387}
388+
389+ func getParentListOptions (obj metav1.ObjectMeta ) ([]client.ListOption , error ) {
390+ listOpts := []client.ListOption {
391+ client .InNamespace (obj .Namespace ),
392+ }
393+
394+ for _ , ref := range obj .GetOwnerReferences () {
395+ if ref .Kind != "Cluster" {
396+ continue
397+ }
398+ gv , err := schema .ParseGroupVersion (ref .APIVersion )
399+ if err != nil {
400+ return nil , errors .WithStack (err )
401+ }
402+ if gv .Group == clusterv1 .GroupVersion .Group {
403+ listOpts = append (listOpts , client.MatchingLabels {
404+ clusterv1 .ClusterNameLabel : ref .Name ,
405+ })
406+ break
407+ }
408+ }
409+ return listOpts , nil
410+ }
0 commit comments