@@ -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,145 @@ 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+ // Strategy 1: Extract nodeInfo from the AMI if an ID is set.
242+ if amiID := ptr .Deref (template .Spec .Template .Spec .AMI .ID , "" ); amiID != "" {
243+ result , err := ec2Client .DescribeImages (ctx , & ec2.DescribeImagesInput {
244+ ImageIds : []string {amiID },
245+ })
255246 if err != nil {
256- return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
247+ return nil , errors .Wrapf (err , "failed to describe AMI %q" , amiID )
257248 }
258-
259- if len (result .InstanceTypes ) == 0 {
260- return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
249+ if len (result .Images ) == 0 {
250+ return nil , errors .Errorf ("no information found for AMI %q" , amiID )
261251 }
252+ // Extract nodeInfo directly from the image object (no additional API call needed)
253+ return r .extractNodeInfoFromImage (result .Images [0 ]), nil
254+ }
262255
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- }
256+ // No explicit AMI ID specified, query instance type to determine architecture
257+ // This architecture will be used to lookup default AMI (Strategy 2) or as fallback (Strategy 3)
258+ result , err := ec2Client .DescribeInstanceTypes (ctx , & ec2.DescribeInstanceTypesInput {
259+ InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
260+ })
261+ if err != nil {
262+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
263+ }
280264
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- }
265+ if len (result .InstanceTypes ) == 0 {
266+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
308267 }
309268
310- return nodeInfo , nil
311- }
269+ instanceTypeInfo := result .InstanceTypes [0 ]
312270
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 },
271+ // Instance type must support exactly one architecture
272+ if instanceTypeInfo .ProcessorInfo == nil || len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ) != 1 {
273+ return nil , errors .Errorf ("instance type must support exactly one architecture, got %d" , len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ))
317274 }
318275
319- result , err := ec2Client .DescribeImages (ctx , input )
320- if err != nil {
321- return "" , "" , errors .Wrapf (err , "failed to describe AMI %q" , amiID )
276+ // Map EC2 architecture type to architecture tag for AMI lookup
277+ var architecture string
278+ var nodeInfoArch infrav1.Architecture
279+ switch instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ] {
280+ case ec2types .ArchitectureTypeX8664 :
281+ architecture = ec2service .Amd64ArchitectureTag
282+ nodeInfoArch = infrav1 .ArchitectureAmd64
283+ case ec2types .ArchitectureTypeArm64 :
284+ architecture = ec2service .Arm64ArchitectureTag
285+ nodeInfoArch = infrav1 .ArchitectureArm64
286+ default :
287+ return nil , errors .Errorf ("unsupported architecture: %v" , instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ])
322288 }
323289
324- if len (result .Images ) == 0 {
325- return "" , "" , errors .Errorf ("no information found for AMI %q" , amiID )
290+ // Strategy 2: Try to get Kubernetes version and lookup default AMI
291+ kubernetesVersion , err := r .getKubernetesVersion (ctx , template )
292+ if err == nil && kubernetesVersion != "" {
293+ // Attempt AMI lookup with the version
294+ image , err := ec2service .DefaultAMILookup (
295+ ec2Client ,
296+ template .Spec .Template .Spec .ImageLookupOrg ,
297+ template .Spec .Template .Spec .ImageLookupBaseOS ,
298+ kubernetesVersion ,
299+ architecture ,
300+ template .Spec .Template .Spec .ImageLookupFormat ,
301+ )
302+ if err == nil && image != nil {
303+ // Successfully found AMI, extract accurate nodeInfo from it
304+ return r .extractNodeInfoFromImage (* image ), nil
305+ }
306+ // AMI lookup failed, fall through to Strategy 3
326307 }
327308
328- image := result .Images [0 ]
309+ // Strategy 3: Fallback to instance type architecture only
310+ // Note: OS cannot be determined from instance type alone, only architecture
311+ return & infrav1.NodeInfo {
312+ Architecture : nodeInfoArch ,
313+ }, nil
314+ }
329315
330- // Get architecture from AMI
331- var arch infrav1.Architecture
316+ // extractNodeInfoFromImage extracts nodeInfo (architecture and OS) from an EC2 image.
317+ // This is a pure function with no AWS API calls.
318+ func (r * AWSMachineTemplateReconciler ) extractNodeInfoFromImage (image ec2types.Image ) * infrav1.NodeInfo {
319+ nodeInfo := & infrav1.NodeInfo {}
320+
321+ // Extract architecture from AMI
332322 switch image .Architecture {
333323 case ec2types .ArchitectureValuesX8664 :
334- arch = infrav1 .ArchitectureAmd64
324+ nodeInfo . Architecture = infrav1 .ArchitectureAmd64
335325 case ec2types .ArchitectureValuesArm64 :
336- arch = infrav1 .ArchitectureArm64
326+ nodeInfo . Architecture = infrav1 .ArchitectureArm64
337327 }
338328
339329 // Determine OS - default to Linux, change to Windows if detected
340330 // Most AMIs are Linux-based, so we initialize with Linux as the default
341- os : = infrav1 .OperatingSystemLinux
331+ nodeInfo . OperatingSystem = infrav1 .OperatingSystemLinux
342332
343- // 1. Check Platform field (most reliable for Windows detection)
333+ // Check Platform field (most reliable for Windows detection)
344334 if image .Platform == ec2types .PlatformValuesWindows {
345- os = infrav1 .OperatingSystemWindows
335+ nodeInfo .OperatingSystem = infrav1 .OperatingSystemWindows
336+ return nodeInfo
346337 }
347338
348- // 2. Check PlatformDetails field for Windows indication
349- if os != infrav1 . OperatingSystemWindows && image .PlatformDetails != nil {
339+ // Check PlatformDetails field for Windows indication
340+ if image .PlatformDetails != nil {
350341 platformDetails := strings .ToLower (* image .PlatformDetails )
351342 if strings .Contains (platformDetails , string (infrav1 .OperatingSystemWindows )) {
352- os = infrav1 .OperatingSystemWindows
343+ nodeInfo . OperatingSystem = infrav1 .OperatingSystemWindows
353344 }
354345 }
355346
356- return arch , os , nil
347+ return nodeInfo
357348}
358349
359350// getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
360351// or KubeadmControlPlanes that reference this AWSMachineTemplate.
361352func (r * AWSMachineTemplateReconciler ) getKubernetesVersion (ctx context.Context , template * infrav1.AWSMachineTemplate ) (string , error ) {
353+ listOpts , err := getParentListOptions (template .ObjectMeta )
354+ if err != nil {
355+ return "" , errors .Wrap (err , "failed to get parent list options" )
356+ }
357+
362358 // Try to find version from MachineDeployment first
363359 machineDeploymentList := & clusterv1.MachineDeploymentList {}
364- if err := r .List (ctx , machineDeploymentList , client . InNamespace ( template . Namespace ) ); err != nil {
360+ if err := r .List (ctx , machineDeploymentList , listOpts ... ); err != nil {
365361 return "" , errors .Wrap (err , "failed to list MachineDeployments" )
366362 }
367363
368364 // Find MachineDeployments that reference this AWSMachineTemplate
369365 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
366+ if version := ptr .Deref (md .Spec .Template .Spec .Version , "" ); md .Spec .Template .Spec .InfrastructureRef .Kind == "AWSMachineTemplate" &&
367+ md .Spec .Template .Spec .InfrastructureRef .Name == template .Name && version != "" {
368+ return version , nil
374369 }
375370 }
376371
377372 // If not found in MachineDeployment, try KubeadmControlPlane
378373 kcpList := & controlplanev1.KubeadmControlPlaneList {}
379- if err := r .List (ctx , kcpList , client . InNamespace ( template . Namespace ) ); err != nil {
374+ if err := r .List (ctx , kcpList , listOpts ... ); err != nil {
380375 return "" , errors .Wrap (err , "failed to list KubeadmControlPlanes" )
381376 }
382377
@@ -391,3 +386,26 @@ func (r *AWSMachineTemplateReconciler) getKubernetesVersion(ctx context.Context,
391386
392387 return "" , errors .New ("no MachineDeployment or KubeadmControlPlane found referencing this AWSMachineTemplate with a version" )
393388}
389+
390+ func getParentListOptions (obj metav1.ObjectMeta ) ([]client.ListOption , error ) {
391+ listOpts := []client.ListOption {
392+ client .InNamespace (obj .Namespace ),
393+ }
394+
395+ for _ , ref := range obj .GetOwnerReferences () {
396+ if ref .Kind != "Cluster" {
397+ continue
398+ }
399+ gv , err := schema .ParseGroupVersion (ref .APIVersion )
400+ if err != nil {
401+ return nil , errors .WithStack (err )
402+ }
403+ if gv .Group == clusterv1 .GroupVersion .Group {
404+ listOpts = append (listOpts , client.MatchingLabels {
405+ clusterv1 .ClusterNameLabel : ref .Name ,
406+ })
407+ break
408+ }
409+ }
410+ return listOpts , nil
411+ }
0 commit comments