diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 95ab00ceb7..b3a99a0b2d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -81,6 +81,7 @@ rules: - clusters/status - machinedeployments - machines/status + - machinesets verbs: - get - list diff --git a/controllers/awsmachinetemplate_controller.go b/controllers/awsmachinetemplate_controller.go index f692a3f386..ad54b55f29 100644 --- a/controllers/awsmachinetemplate_controller.go +++ b/controllers/awsmachinetemplate_controller.go @@ -31,9 +31,12 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2" @@ -75,12 +78,23 @@ func (r *AWSMachineTemplateReconciler) SetupWithManager(ctx context.Context, mgr Watches( &clusterv1.MachineDeployment{}, handler.EnqueueRequestsFromMapFunc(r.machineDeploymentToAWSMachineTemplate), + // Only emit events for creation to reconcile in case the MachineDeployment got created after the AWSMachineTemplate was reconciled. + builder.WithPredicates(resourceCreatedPredicate), + ). + Watches( + &clusterv1.MachineSet{}, + handler.EnqueueRequestsFromMapFunc(r.machineSetToAWSMachineTemplate), + // Only emit events for creation to reconcile in case the MachineSet got created after the AWSMachineTemplate was reconciled. + builder.WithPredicates(resourceCreatedPredicate), ) - // Optionally watch KubeadmControlPlane if the CRD exists + // Watch KubeadmControlPlane if they exist. if _, err := mgr.GetRESTMapper().RESTMapping(schema.GroupKind{Group: controlplanev1.GroupVersion.Group, Kind: "KubeadmControlPlane"}, controlplanev1.GroupVersion.Version); err == nil { b = b.Watches(&controlplanev1.KubeadmControlPlane{}, - handler.EnqueueRequestsFromMapFunc(r.kubeadmControlPlaneToAWSMachineTemplate)) + handler.EnqueueRequestsFromMapFunc(r.kubeadmControlPlaneToAWSMachineTemplate), + // Only emit events for creation to reconcile in case the KubeadmControlPlane got created after the AWSMachineTemplate was reconciled. + builder.WithPredicates(resourceCreatedPredicate), + ) } _, err := b.Build(r) @@ -95,7 +109,7 @@ func (r *AWSMachineTemplateReconciler) SetupWithManager(ctx context.Context, mgr // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=awsmachinetemplates/status,verbs=get;update;patch // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=awsclusters,verbs=get;list;watch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch -// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments,verbs=get;list;watch +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments;machinesets,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch // Reconcile populates capacity information for AWSMachineTemplate. @@ -387,7 +401,22 @@ func (r *AWSMachineTemplateReconciler) getKubernetesVersion(ctx context.Context, return "", errors.Wrap(err, "failed to get parent list options") } - // Try to find version from MachineDeployment first + // Try to find version from MachineSet first + machineSetList := &clusterv1.MachineSetList{} + if err := r.List(ctx, machineSetList, listOpts...); err != nil { + return "", errors.Wrap(err, "failed to list MachineSets") + } + + // Find MachineSets that reference this AWSMachineTemplate + for _, ms := range machineSetList.Items { + if ms.Spec.Template.Spec.InfrastructureRef.Kind == awsMachineTemplateKind && + ms.Spec.Template.Spec.InfrastructureRef.Name == template.Name && + ms.Spec.Template.Spec.Version != "" { + return ms.Spec.Template.Spec.Version, nil + } + } + + // If not found, try MachineDeployment. machineDeploymentList := &clusterv1.MachineDeploymentList{} if err := r.List(ctx, machineDeploymentList, listOpts...); err != nil { return "", errors.Wrap(err, "failed to list MachineDeployments") @@ -402,7 +431,7 @@ func (r *AWSMachineTemplateReconciler) getKubernetesVersion(ctx context.Context, } } - // If not found in MachineDeployment, try KubeadmControlPlane + // If not found, try KubeadmControlPlane kcpList := &controlplanev1.KubeadmControlPlaneList{} if err := r.List(ctx, kcpList, listOpts...); err != nil { return "", errors.Wrap(err, "failed to list KubeadmControlPlanes") @@ -492,3 +521,35 @@ func (r *AWSMachineTemplateReconciler) machineDeploymentToAWSMachineTemplate(ctx }, } } + +// machineSetToAWSMachineTemplate maps MachineSet to AWSMachineTemplate reconcile requests. +// This enables the controller to reconcile AWSMachineTemplate when its owner MachineSet is created or updated, +// ensuring that nodeInfo can be populated even if the cache hasn't synced yet. +func (r *AWSMachineTemplateReconciler) machineSetToAWSMachineTemplate(ctx context.Context, o client.Object) []ctrl.Request { + md, ok := o.(*clusterv1.MachineSet) + if !ok { + return nil + } + + // Check if it references an AWSMachineTemplate + if md.Spec.Template.Spec.InfrastructureRef.Kind != awsMachineTemplateKind { + return nil + } + + // Return reconcile request for the referenced AWSMachineTemplate + return []ctrl.Request{ + { + NamespacedName: client.ObjectKey{ + Namespace: md.Namespace, + Name: md.Spec.Template.Spec.InfrastructureRef.Name, + }, + }, + } +} + +var resourceCreatedPredicate = predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + GenericFunc: func(e event.GenericEvent) bool { return true }, +} diff --git a/test/e2e/suites/unmanaged/unmanaged_functional_test.go b/test/e2e/suites/unmanaged/unmanaged_functional_test.go index 17ca07c3f0..e873a7ff35 100644 --- a/test/e2e/suites/unmanaged/unmanaged_functional_test.go +++ b/test/e2e/suites/unmanaged/unmanaged_functional_test.go @@ -354,7 +354,7 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() { Expect(len(workerMachines)).To(Equal(1)) Expect(len(controlPlaneMachines)).To(Equal(1)) - ginkgo.By("Verifying AWSMachineTemplate capacity is populated for autoscaling from zero") + ginkgo.By("Verifying AWSMachineTemplate capacity and nodeInfo is populated for autoscaling from zero") Eventually(func(g Gomega) { awsMachineTemplateList := &infrav1.AWSMachineTemplateList{} g.Expect(e2eCtx.Environment.BootstrapClusterProxy.GetClient().List(ctx, awsMachineTemplateList, client.InNamespace(namespace.Name))).To(Succeed()) @@ -364,32 +364,12 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() { capacity := template.Status.Capacity _, hasCPU := capacity[corev1.ResourceCPU] _, hasMemory := capacity[corev1.ResourceMemory] - if hasCPU && hasMemory { - ginkgo.By(fmt.Sprintf("AWSMachineTemplate %s has capacity populated: %v", template.Name, capacity)) - return - } + g.Expect(hasCPU).To(BeTrue(), "Expected AWSMachineTemplate %s to have .status.capacity for CPU set", template.Name) + g.Expect(hasMemory).To(BeTrue(), "Expected AWSMachineTemplate %s to have .status.capacity for memory set", template.Name) + g.Expect(template.Status.NodeInfo).ToNot(BeNil(), "Expected AWSMachineTemplate %s to have .status.nodeInfo set", template.Name) + g.Expect(template.Status.NodeInfo.Architecture).ToNot(BeEmpty(), "Expected AWSMachineTemplate %s to have .status.nodeInfo.architecture set", template.Name) + g.Expect(template.Status.NodeInfo.OperatingSystem).ToNot(BeEmpty(), "Expected AWSMachineTemplate %s to have .status.nodeInfo.operatingSystem set", template.Name) } - g.Expect(false).To(BeTrue(), "Expected at least one AWSMachineTemplate to have capacity with CPU and Memory") - }, e2eCtx.E2EConfig.GetIntervals(specName, "wait-deployment")...).Should(Succeed()) - - ginkgo.By("Verifying AWSMachineTemplate nodeInfo is populated") - Eventually(func(g Gomega) { - awsMachineTemplateList := &infrav1.AWSMachineTemplateList{} - g.Expect(e2eCtx.Environment.BootstrapClusterProxy.GetClient().List(ctx, awsMachineTemplateList, client.InNamespace(namespace.Name))).To(Succeed()) - g.Expect(awsMachineTemplateList.Items).ToNot(BeEmpty()) - - for _, template := range awsMachineTemplateList.Items { - nodeInfo := template.Status.NodeInfo - if nodeInfo == nil { - continue - } - arch := string(nodeInfo.Architecture) - if (arch == "amd64" || arch == "arm64") && nodeInfo.OperatingSystem != "" { - ginkgo.By(fmt.Sprintf("AWSMachineTemplate %s has nodeInfo populated: %v", template.Name, nodeInfo)) - return - } - } - g.Expect(false).To(BeTrue(), "Expected at least one AWSMachineTemplate to have valid nodeInfo") }, e2eCtx.E2EConfig.GetIntervals(specName, "wait-deployment")...).Should(Succeed()) }) })