Skip to content

Commit b0eae44

Browse files
test
1 parent 15d3232 commit b0eae44

File tree

4 files changed

+165
-26
lines changed

4 files changed

+165
-26
lines changed

api/v1beta1/conversion.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ func Convert_v1beta2_Ignition_To_v1beta1_Ignition(in *v1beta2.Ignition, out *Ign
104104
return autoConvert_v1beta2_Ignition_To_v1beta1_Ignition(in, out, s)
105105
}
106106

107+
// Convert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus converts v1beta2 AWSMachineStatus to v1beta1.
108+
// The DedicatedHostID field is dropped during conversion as it doesn't exist in v1beta1.
109+
func Convert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus(in *v1beta2.AWSMachineStatus, out *AWSMachineStatus, s conversion.Scope) error {
110+
// Note: DedicatedHostID is not present in v1beta1, so it will be dropped during conversion
111+
return autoConvert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus(in, out, s)
112+
}
113+
107114
// Convert_v1beta2_AWSMachineTemplateStatus_To_v1beta1_AWSMachineTemplateStatus converts v1beta2 AWSMachineTemplateStatus to v1beta1.
108115
// The NodeInfo field is dropped during conversion as it doesn't exist in v1beta1.
109116
func Convert_v1beta2_AWSMachineTemplateStatus_To_v1beta1_AWSMachineTemplateStatus(in *v1beta2.AWSMachineTemplateStatus, out *AWSMachineTemplateStatus, s conversion.Scope) error {

api/v1beta2/awsmachinetemplate_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ const (
3535
ArchitectureArm64 Architecture = "arm64"
3636
)
3737

38+
// Operating system constants.
39+
const (
40+
// OperatingSystemLinux represents the Linux operating system.
41+
OperatingSystemLinux = "linux"
42+
// OperatingSystemWindows represents the Windows operating system.
43+
OperatingSystemWindows = "windows"
44+
)
45+
3846
// NodeInfo contains information about the node's architecture and operating system.
3947
type NodeInfo struct {
4048
// Architecture is the CPU architecture of the node.

controllers/awsmachinetemplate_controller.go

Lines changed: 140 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
251366
func (r *AWSMachineTemplateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
252367
log := logger.FromContext(ctx)

test/e2e/suites/unmanaged/unmanaged_functional_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() {
337337
configCluster.ControlPlaneMachineCount = ptr.To[int64](1)
338338
configCluster.WorkerMachineCount = ptr.To[int64](1)
339339
configCluster.Flavor = shared.SSMFlavor
340-
_, md, _ := createCluster(ctx, configCluster, result)
340+
cluster, md, _ := createCluster(ctx, configCluster, result)
341341

342342
workerMachines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{
343343
Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(),
@@ -359,6 +359,15 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() {
359359
Expect(err).To(BeNil())
360360
Expect(len(awsMachineTemplateList.Items)).To(BeNumerically(">", 0), "Expected at least one AWSMachineTemplate")
361361

362+
ginkgo.By(fmt.Sprintf("Found %d AWSMachineTemplates", len(awsMachineTemplateList.Items)))
363+
ginkgo.By(fmt.Sprintf("Cluster: name=%s, namespace=%s, infrastructureRef=%v",
364+
cluster.Name, cluster.Namespace, cluster.Spec.InfrastructureRef))
365+
366+
// Print each AWSMachineTemplate for debugging
367+
for i, template := range awsMachineTemplateList.Items {
368+
ginkgo.By(fmt.Sprintf("AWSMachineTemplate[%d]: %+v", i, template))
369+
}
370+
362371
foundTemplateWithCapacity := false
363372
foundTemplateWithNodeInfo := false
364373
for _, template := range awsMachineTemplateList.Items {

0 commit comments

Comments
 (0)