Skip to content

Commit 3c89048

Browse files
test
1 parent 15d3232 commit 3c89048

File tree

3 files changed

+141
-26
lines changed

3 files changed

+141
-26
lines changed

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: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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,32 @@ 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
}
108114

109-
// Update status with capacity and nodeInfo
110-
awsMachineTemplate.Status.Capacity = capacity
111-
awsMachineTemplate.Status.NodeInfo = nodeInfo
115+
// Query node info (architecture and OS)
116+
nodeInfo, err := r.getNodeInfo(ctx, ec2Client, awsMachineTemplate, instanceType)
117+
if err != nil {
118+
record.Warnf(awsMachineTemplate, "NodeInfoQueryFailed", "Failed to query node info for instance type %q: %v", instanceType, err)
119+
return ctrl.Result{}, nil
120+
}
112121

113-
if err := r.Status().Update(ctx, awsMachineTemplate); err != nil {
122+
// Save original before modifying, then update all status fields at once
123+
original := awsMachineTemplate.DeepCopy()
124+
if len(capacity) > 0 {
125+
awsMachineTemplate.Status.Capacity = capacity
126+
}
127+
if nodeInfo != nil && (nodeInfo.Architecture != "" || nodeInfo.OperatingSystem != "") {
128+
awsMachineTemplate.Status.NodeInfo = nodeInfo
129+
}
130+
if err := r.Status().Patch(ctx, awsMachineTemplate, client.MergeFrom(original)); err != nil {
114131
return ctrl.Result{}, errors.Wrap(err, "failed to update AWSMachineTemplate status")
115132
}
116133

@@ -147,23 +164,21 @@ func (r *AWSMachineTemplateReconciler) getRegion(ctx context.Context, template *
147164
return "", nil
148165
}
149166

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-
167+
// getInstanceTypeCapacity queries AWS EC2 API for instance type capacity information.
168+
// Returns the resource list (CPU, Memory).
169+
func (r *AWSMachineTemplateReconciler) getInstanceTypeCapacity(ctx context.Context, ec2Client *ec2.Client, instanceType string) (corev1.ResourceList, error) {
155170
// Query instance type information
156171
input := &ec2.DescribeInstanceTypesInput{
157172
InstanceTypes: []ec2types.InstanceType{ec2types.InstanceType(instanceType)},
158173
}
159174

160175
result, err := ec2Client.DescribeInstanceTypes(ctx, input)
161176
if err != nil {
162-
return nil, nil, errors.Wrapf(err, "failed to describe instance type %q", instanceType)
177+
return nil, errors.Wrapf(err, "failed to describe instance type %q", instanceType)
163178
}
164179

165180
if len(result.InstanceTypes) == 0 {
166-
return nil, nil, errors.Errorf("no information found for instance type %q", instanceType)
181+
return nil, errors.Errorf("no information found for instance type %q", instanceType)
167182
}
168183

169184
// Extract capacity information
@@ -181,10 +196,16 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
181196
resourceList[corev1.ResourceMemory] = *resource.NewQuantity(memoryBytes, resource.BinarySI)
182197
}
183198

184-
// Extract node info from AMI if available
199+
return resourceList, nil
200+
}
201+
202+
// getNodeInfo queries node information (architecture and OS) for the AWSMachineTemplate.
203+
// It uses AMI ID if specified, otherwise attempts AMI lookup or falls back to instance type info.
204+
func (r *AWSMachineTemplateReconciler) getNodeInfo(ctx context.Context, ec2Client *ec2.Client, template *infrav1.AWSMachineTemplate, instanceType string) (*infrav1.NodeInfo, error) {
185205
nodeInfo := &infrav1.NodeInfo{}
186206
amiID := template.Spec.Template.Spec.AMI.ID
187207
if amiID != nil && *amiID != "" {
208+
// AMI ID is specified, query it directly
188209
arch, os, err := r.getNodeInfoFromAMI(ctx, ec2Client, *amiID)
189210
if err == nil {
190211
if arch != "" {
@@ -194,9 +215,67 @@ func (r *AWSMachineTemplateReconciler) getInstanceTypeInfo(ctx context.Context,
194215
nodeInfo.OperatingSystem = os
195216
}
196217
}
218+
} else {
219+
// AMI ID is not specified, query instance type to get architecture
220+
input := &ec2.DescribeInstanceTypesInput{
221+
InstanceTypes: []ec2types.InstanceType{ec2types.InstanceType(instanceType)},
222+
}
223+
224+
result, err := ec2Client.DescribeInstanceTypes(ctx, input)
225+
if err != nil {
226+
return nil, errors.Wrapf(err, "failed to describe instance type %q", instanceType)
227+
}
228+
229+
if len(result.InstanceTypes) == 0 {
230+
return nil, errors.Errorf("no information found for instance type %q", instanceType)
231+
}
232+
233+
instanceTypeInfo := result.InstanceTypes[0]
234+
235+
// Infer architecture from instance type
236+
var architecture string
237+
if instanceTypeInfo.ProcessorInfo != nil && len(instanceTypeInfo.ProcessorInfo.SupportedArchitectures) == 1 {
238+
// Use the supported architecture
239+
switch instanceTypeInfo.ProcessorInfo.SupportedArchitectures[0] {
240+
case ec2types.ArchitectureTypeX8664:
241+
architecture = ec2service.Amd64ArchitectureTag
242+
nodeInfo.Architecture = infrav1.ArchitectureAmd64
243+
case ec2types.ArchitectureTypeArm64:
244+
architecture = ec2service.Arm64ArchitectureTag
245+
nodeInfo.Architecture = infrav1.ArchitectureArm64
246+
}
247+
} else {
248+
return nil, errors.Errorf("instance type must support exactly one architecture, got %d", len(instanceTypeInfo.ProcessorInfo.SupportedArchitectures))
249+
}
250+
251+
// Attempt to get Kubernetes version from MachineDeployment
252+
kubernetesVersion, versionErr := r.getKubernetesVersion(ctx, template)
253+
if versionErr == nil && kubernetesVersion != "" {
254+
// Try to look up AMI using the version
255+
image, err := ec2service.DefaultAMILookup(
256+
ec2Client,
257+
template.Spec.Template.Spec.ImageLookupOrg,
258+
template.Spec.Template.Spec.ImageLookupBaseOS,
259+
kubernetesVersion,
260+
architecture,
261+
template.Spec.Template.Spec.ImageLookupFormat,
262+
)
263+
if err == nil && image != nil {
264+
// Successfully found AMI, extract accurate nodeInfo from it
265+
arch, os, _ := r.getNodeInfoFromAMI(ctx, ec2Client, *image.ImageId)
266+
if arch != "" {
267+
nodeInfo.Architecture = arch
268+
}
269+
if os != "" {
270+
nodeInfo.OperatingSystem = os
271+
}
272+
return nodeInfo, nil
273+
}
274+
// AMI lookup failed, fall through to defaults
275+
}
197276
}
198277

199-
return resourceList, nodeInfo, nil
278+
return nodeInfo, nil
200279
}
201280

202281
// getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
@@ -225,28 +304,47 @@ func (r *AWSMachineTemplateReconciler) getNodeInfoFromAMI(ctx context.Context, e
225304
arch = infrav1.ArchitectureArm64
226305
}
227306

228-
// Determine OS - check Platform field first (specifically for Windows identification)
229-
var os string
307+
// Determine OS - default to Linux, change to Windows if detected
308+
// Most AMIs are Linux-based, so we initialize with Linux as the default
309+
os := infrav1.OperatingSystemLinux
230310

231311
// 1. Check Platform field (most reliable for Windows detection)
232312
if image.Platform == ec2types.PlatformValuesWindows {
233-
os = "windows"
313+
os = infrav1.OperatingSystemWindows
234314
}
235315

236-
// 2. Check PlatformDetails field (provides more detailed information)
237-
if os == "" && image.PlatformDetails != nil {
316+
// 2. Check PlatformDetails field for Windows indication
317+
if os != infrav1.OperatingSystemWindows && image.PlatformDetails != nil {
238318
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"
319+
if strings.Contains(platformDetails, infrav1.OperatingSystemWindows) {
320+
os = infrav1.OperatingSystemWindows
244321
}
245322
}
246323

247324
return arch, os, nil
248325
}
249326

327+
// getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
328+
// that reference this AWSMachineTemplate.
329+
func (r *AWSMachineTemplateReconciler) getKubernetesVersion(ctx context.Context, template *infrav1.AWSMachineTemplate) (string, error) {
330+
// List all MachineDeployments in the same namespace
331+
machineDeploymentList := &clusterv1.MachineDeploymentList{}
332+
if err := r.List(ctx, machineDeploymentList, client.InNamespace(template.Namespace)); err != nil {
333+
return "", errors.Wrap(err, "failed to list MachineDeployments")
334+
}
335+
336+
// Find MachineDeployments that reference this AWSMachineTemplate
337+
for _, md := range machineDeploymentList.Items {
338+
if md.Spec.Template.Spec.InfrastructureRef.Kind == "AWSMachineTemplate" &&
339+
md.Spec.Template.Spec.InfrastructureRef.Name == template.Name &&
340+
md.Spec.Template.Spec.Version != nil {
341+
return *md.Spec.Template.Spec.Version, nil
342+
}
343+
}
344+
345+
return "", errors.New("no MachineDeployment found referencing this AWSMachineTemplate with a version")
346+
}
347+
250348
// SetupWithManager sets up the controller with the Manager.
251349
func (r *AWSMachineTemplateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
252350
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)