Skip to content

Commit 8371031

Browse files
test
1 parent b1e1e45 commit 8371031

File tree

4 files changed

+161
-26
lines changed

4 files changed

+161
-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.

config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ rules:
101101
- controlplane.cluster.x-k8s.io
102102
resources:
103103
- '*'
104+
- kubeadmcontrolplanes
104105
verbs:
105106
- get
106107
- list

controllers/awsmachinetemplate_controller.go

Lines changed: 142 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,69 @@ 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+
} else {
278+
return nil, errors.Errorf("failed to get Kubernetes version: %v", versionErr)
279+
}
197280
}
198281

199-
return resourceList, nodeInfo, nil
282+
return nodeInfo, nil
200283
}
201284

202285
// getNodeInfoFromAMI queries the AMI to determine architecture and operating system.
@@ -225,28 +308,62 @@ func (r *AWSMachineTemplateReconciler) getNodeInfoFromAMI(ctx context.Context, e
225308
arch = infrav1.ArchitectureArm64
226309
}
227310

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

231315
// 1. Check Platform field (most reliable for Windows detection)
232316
if image.Platform == ec2types.PlatformValuesWindows {
233-
os = "windows"
317+
os = infrav1.OperatingSystemWindows
234318
}
235319

236-
// 2. Check PlatformDetails field (provides more detailed information)
237-
if os == "" && image.PlatformDetails != nil {
320+
// 2. Check PlatformDetails field for Windows indication
321+
if os != infrav1.OperatingSystemWindows && image.PlatformDetails != nil {
238322
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"
323+
if strings.Contains(platformDetails, infrav1.OperatingSystemWindows) {
324+
os = infrav1.OperatingSystemWindows
244325
}
245326
}
246327

247328
return arch, os, nil
248329
}
249330

331+
// getKubernetesVersion attempts to find the Kubernetes version by querying MachineDeployments
332+
// or KubeadmControlPlanes that reference this AWSMachineTemplate.
333+
func (r *AWSMachineTemplateReconciler) getKubernetesVersion(ctx context.Context, template *infrav1.AWSMachineTemplate) (string, error) {
334+
// Try to find version from MachineDeployment first
335+
machineDeploymentList := &clusterv1.MachineDeploymentList{}
336+
if err := r.List(ctx, machineDeploymentList, client.InNamespace(template.Namespace)); err != nil {
337+
return "", errors.Wrap(err, "failed to list MachineDeployments")
338+
}
339+
340+
// Find MachineDeployments that reference this AWSMachineTemplate
341+
for _, md := range machineDeploymentList.Items {
342+
if md.Spec.Template.Spec.InfrastructureRef.Kind == "AWSMachineTemplate" &&
343+
md.Spec.Template.Spec.InfrastructureRef.Name == template.Name &&
344+
md.Spec.Template.Spec.Version != nil {
345+
return *md.Spec.Template.Spec.Version, nil
346+
}
347+
}
348+
349+
// If not found in MachineDeployment, try KubeadmControlPlane
350+
kcpList := &controlplanev1.KubeadmControlPlaneList{}
351+
if err := r.List(ctx, kcpList, client.InNamespace(template.Namespace)); err != nil {
352+
return "", errors.Wrap(err, "failed to list KubeadmControlPlanes")
353+
}
354+
355+
// Find KubeadmControlPlanes that reference this AWSMachineTemplate
356+
for _, kcp := range kcpList.Items {
357+
if kcp.Spec.MachineTemplate.InfrastructureRef.Kind == "AWSMachineTemplate" &&
358+
kcp.Spec.MachineTemplate.InfrastructureRef.Name == template.Name &&
359+
kcp.Spec.Version != "" {
360+
return kcp.Spec.Version, nil
361+
}
362+
}
363+
364+
return "", errors.New("no MachineDeployment or KubeadmControlPlane found referencing this AWSMachineTemplate with a version")
365+
}
366+
250367
// SetupWithManager sets up the controller with the Manager.
251368
func (r *AWSMachineTemplateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
252369
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)