@@ -32,8 +32,11 @@ 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"
39+ controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
3740 "sigs.k8s.io/cluster-api/util"
3841 "sigs.k8s.io/cluster-api/util/predicates"
3942)
@@ -53,6 +56,8 @@ type AWSMachineTemplateReconciler struct {
5356// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=awsmachinetemplates/status,verbs=get;update;patch
5457// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=awsclusters,verbs=get;list;watch
5558// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch
59+ // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments,verbs=get;list;watch
60+ // +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=kubeadmcontrolplanes,verbs=get;list;watch
5661// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch
5762
5863// Reconcile populates capacity information for AWSMachineTemplate.
@@ -99,18 +104,32 @@ func (r *AWSMachineTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R
99104 return ctrl.Result {}, nil
100105 }
101106
102- // Query instance type capacity and node info
103- capacity , nodeInfo , err := r .getInstanceTypeInfo (ctx , globalScope , awsMachineTemplate , instanceType )
107+ // Create EC2 client from global scope
108+ ec2Client := ec2 .NewFromConfig (globalScope .Session ())
109+
110+ // Query instance type capacity
111+ capacity , err := r .getInstanceTypeCapacity (ctx , ec2Client , instanceType )
104112 if err != nil {
105113 record .Warnf (awsMachineTemplate , "CapacityQueryFailed" , "Failed to query capacity for instance type %q: %v" , instanceType , err )
106114 return ctrl.Result {}, nil
107115 }
108116
109- // Update status with capacity and nodeInfo
110- awsMachineTemplate .Status .Capacity = capacity
111- awsMachineTemplate .Status .NodeInfo = nodeInfo
117+ // Query node info (architecture and OS)
118+ nodeInfo , err := r .getNodeInfo (ctx , ec2Client , awsMachineTemplate , instanceType )
119+ if err != nil {
120+ record .Warnf (awsMachineTemplate , "NodeInfoQueryFailed" , "Failed to query node info for instance type %q: %v" , instanceType , err )
121+ return ctrl.Result {}, nil
122+ }
112123
113- if err := r .Status ().Update (ctx , awsMachineTemplate ); err != nil {
124+ // Save original before modifying, then update all status fields at once
125+ original := awsMachineTemplate .DeepCopy ()
126+ if len (capacity ) > 0 {
127+ awsMachineTemplate .Status .Capacity = capacity
128+ }
129+ if nodeInfo != nil && (nodeInfo .Architecture != "" || nodeInfo .OperatingSystem != "" ) {
130+ awsMachineTemplate .Status .NodeInfo = nodeInfo
131+ }
132+ if err := r .Status ().Patch (ctx , awsMachineTemplate , client .MergeFrom (original )); err != nil {
114133 return ctrl.Result {}, errors .Wrap (err , "failed to update AWSMachineTemplate status" )
115134 }
116135
@@ -147,23 +166,21 @@ func (r *AWSMachineTemplateReconciler) getRegion(ctx context.Context, template *
147166 return "" , nil
148167}
149168
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-
169+ // getInstanceTypeCapacity queries AWS EC2 API for instance type capacity information.
170+ // Returns the resource list (CPU, Memory).
171+ func (r * AWSMachineTemplateReconciler ) getInstanceTypeCapacity (ctx context.Context , ec2Client * ec2.Client , instanceType string ) (corev1.ResourceList , error ) {
155172 // Query instance type information
156173 input := & ec2.DescribeInstanceTypesInput {
157174 InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
158175 }
159176
160177 result , err := ec2Client .DescribeInstanceTypes (ctx , input )
161178 if err != nil {
162- return nil , nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
179+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
163180 }
164181
165182 if len (result .InstanceTypes ) == 0 {
166- return nil , nil , errors .Errorf ("no information found for instance type %q" , instanceType )
183+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
167184 }
168185
169186 // Extract capacity information
@@ -181,10 +198,16 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
181198 resourceList [corev1 .ResourceMemory ] = * resource .NewQuantity (memoryBytes , resource .BinarySI )
182199 }
183200
184- // Extract node info from AMI if available
201+ return resourceList , nil
202+ }
203+
204+ // getNodeInfo queries node information (architecture and OS) for the AWSMachineTemplate.
205+ // It uses AMI ID if specified, otherwise attempts AMI lookup or falls back to instance type info.
206+ func (r * AWSMachineTemplateReconciler ) getNodeInfo (ctx context.Context , ec2Client * ec2.Client , template * infrav1.AWSMachineTemplate , instanceType string ) (* infrav1.NodeInfo , error ) {
185207 nodeInfo := & infrav1.NodeInfo {}
186208 amiID := template .Spec .Template .Spec .AMI .ID
187209 if amiID != nil && * amiID != "" {
210+ // AMI ID is specified, query it directly
188211 arch , os , err := r .getNodeInfoFromAMI (ctx , ec2Client , * amiID )
189212 if err == nil {
190213 if arch != "" {
@@ -194,9 +217,67 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
194217 nodeInfo .OperatingSystem = os
195218 }
196219 }
220+ } else {
221+ // AMI ID is not specified, query instance type to get architecture
222+ input := & ec2.DescribeInstanceTypesInput {
223+ InstanceTypes : []ec2types.InstanceType {ec2types .InstanceType (instanceType )},
224+ }
225+
226+ result , err := ec2Client .DescribeInstanceTypes (ctx , input )
227+ if err != nil {
228+ return nil , errors .Wrapf (err , "failed to describe instance type %q" , instanceType )
229+ }
230+
231+ if len (result .InstanceTypes ) == 0 {
232+ return nil , errors .Errorf ("no information found for instance type %q" , instanceType )
233+ }
234+
235+ instanceTypeInfo := result .InstanceTypes [0 ]
236+
237+ // Infer architecture from instance type
238+ var architecture string
239+ if instanceTypeInfo .ProcessorInfo != nil && len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ) == 1 {
240+ // Use the supported architecture
241+ switch instanceTypeInfo .ProcessorInfo .SupportedArchitectures [0 ] {
242+ case ec2types .ArchitectureTypeX8664 :
243+ architecture = ec2service .Amd64ArchitectureTag
244+ nodeInfo .Architecture = infrav1 .ArchitectureAmd64
245+ case ec2types .ArchitectureTypeArm64 :
246+ architecture = ec2service .Arm64ArchitectureTag
247+ nodeInfo .Architecture = infrav1 .ArchitectureArm64
248+ }
249+ } else {
250+ return nil , errors .Errorf ("instance type must support exactly one architecture, got %d" , len (instanceTypeInfo .ProcessorInfo .SupportedArchitectures ))
251+ }
252+
253+ // Attempt to get Kubernetes version from MachineDeployment
254+ kubernetesVersion , versionErr := r .getKubernetesVersion (ctx , template )
255+ if versionErr == nil && kubernetesVersion != "" {
256+ // Try to look up AMI using the version
257+ image , err := ec2service .DefaultAMILookup (
258+ ec2Client ,
259+ template .Spec .Template .Spec .ImageLookupOrg ,
260+ template .Spec .Template .Spec .ImageLookupBaseOS ,
261+ kubernetesVersion ,
262+ architecture ,
263+ template .Spec .Template .Spec .ImageLookupFormat ,
264+ )
265+ if err == nil && image != nil {
266+ // Successfully found AMI, extract accurate nodeInfo from it
267+ arch , os , _ := r .getNodeInfoFromAMI (ctx , ec2Client , * image .ImageId )
268+ if arch != "" {
269+ nodeInfo .Architecture = arch
270+ }
271+ if os != "" {
272+ nodeInfo .OperatingSystem = os
273+ }
274+ return nodeInfo , nil
275+ }
276+ // AMI lookup failed, fall through to defaults
277+ }
197278 }
198279
199- return resourceList , nodeInfo , nil
280+ return nodeInfo , nil
200281}
201282
202283// getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
@@ -225,28 +306,62 @@ func (r *AWSMachineTemplateReconciler) getNodeInfoFromAMI(ctx context.Context, e
225306 arch = infrav1 .ArchitectureArm64
226307 }
227308
228- // Determine OS - check Platform field first (specifically for Windows identification)
229- var os string
309+ // Determine OS - default to Linux, change to Windows if detected
310+ // Most AMIs are Linux-based, so we initialize with Linux as the default
311+ os := infrav1 .OperatingSystemLinux
230312
231313 // 1. Check Platform field (most reliable for Windows detection)
232314 if image .Platform == ec2types .PlatformValuesWindows {
233- os = "windows"
315+ os = infrav1 . OperatingSystemWindows
234316 }
235317
236- // 2. Check PlatformDetails field (provides more detailed information)
237- if os == "" && image .PlatformDetails != nil {
318+ // 2. Check PlatformDetails field for Windows indication
319+ if os != infrav1 . OperatingSystemWindows && image .PlatformDetails != nil {
238320 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"
321+ if strings .Contains (platformDetails , infrav1 .OperatingSystemWindows ) {
322+ os = infrav1 .OperatingSystemWindows
244323 }
245324 }
246325
247326 return arch , os , nil
248327}
249328
329+ // getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
330+ // or KubeadmControlPlanes that reference this AWSMachineTemplate.
331+ func (r * AWSMachineTemplateReconciler ) getKubernetesVersion (ctx context.Context , template * infrav1.AWSMachineTemplate ) (string , error ) {
332+ // Try to find version from MachineDeployment first
333+ machineDeploymentList := & clusterv1.MachineDeploymentList {}
334+ if err := r .List (ctx , machineDeploymentList , client .InNamespace (template .Namespace )); err != nil {
335+ return "" , errors .Wrap (err , "failed to list MachineDeployments" )
336+ }
337+
338+ // Find MachineDeployments that reference this AWSMachineTemplate
339+ for _ , md := range machineDeploymentList .Items {
340+ if md .Spec .Template .Spec .InfrastructureRef .Kind == "AWSMachineTemplate" &&
341+ md .Spec .Template .Spec .InfrastructureRef .Name == template .Name &&
342+ md .Spec .Template .Spec .Version != nil {
343+ return * md .Spec .Template .Spec .Version , nil
344+ }
345+ }
346+
347+ // If not found in MachineDeployment, try KubeadmControlPlane
348+ kcpList := & controlplanev1.KubeadmControlPlaneList {}
349+ if err := r .List (ctx , kcpList , client .InNamespace (template .Namespace )); err != nil {
350+ return "" , errors .Wrap (err , "failed to list KubeadmControlPlanes" )
351+ }
352+
353+ // Find KubeadmControlPlanes that reference this AWSMachineTemplate
354+ for _ , kcp := range kcpList .Items {
355+ if kcp .Spec .MachineTemplate .InfrastructureRef .Kind == "AWSMachineTemplate" &&
356+ kcp .Spec .MachineTemplate .InfrastructureRef .Name == template .Name &&
357+ kcp .Spec .Version != "" {
358+ return kcp .Spec .Version , nil
359+ }
360+ }
361+
362+ return "" , errors .New ("no MachineDeployment or KubeadmControlPlane found referencing this AWSMachineTemplate with a version" )
363+ }
364+
250365// SetupWithManager sets up the controller with the Manager.
251366func (r * AWSMachineTemplateReconciler ) SetupWithManager (ctx context.Context , mgr ctrl.Manager , options controller.Options ) error {
252367 log := logger .FromContext (ctx )
0 commit comments