From 67ea6d0afda1a53baf042afb08b23d2fc8e3b4dd Mon Sep 17 00:00:00 2001 From: jigisha620 Date: Wed, 12 Feb 2025 14:09:49 -0800 Subject: [PATCH] feat: add NodeRegistrationHealthy status condition to nodepool --- kwok/charts/crds/karpenter.sh_nodepools.yaml | 6 + pkg/apis/crds/karpenter.sh_nodepools.yaml | 6 + pkg/apis/v1/nodepool_status.go | 6 + pkg/controllers/controllers.go | 2 + .../nodeclaim/lifecycle/liveness.go | 30 ++++- .../nodeclaim/lifecycle/liveness_test.go | 44 +++++++ .../nodeclaim/lifecycle/registration.go | 26 ++++ .../nodeclaim/lifecycle/registration_test.go | 2 + .../nodepool/registrationhealth/controller.go | 100 +++++++++++++++ .../nodepool/registrationhealth/suite_test.go | 115 ++++++++++++++++++ 10 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 pkg/controllers/nodepool/registrationhealth/controller.go create mode 100644 pkg/controllers/nodepool/registrationhealth/suite_test.go diff --git a/kwok/charts/crds/karpenter.sh_nodepools.yaml b/kwok/charts/crds/karpenter.sh_nodepools.yaml index 6601e59dbf..1a0c8d64a8 100644 --- a/kwok/charts/crds/karpenter.sh_nodepools.yaml +++ b/kwok/charts/crds/karpenter.sh_nodepools.yaml @@ -498,6 +498,12 @@ spec: - type type: object type: array + nodeClassObservedGeneration: + description: |- + NodeClassObservedGeneration represents the nodeClass generation for referenced nodeClass. If this does not match + the actual NodeClass Generation, NodeRegistrationHealthy status condition on the NodePool will be reset + format: int64 + type: integer resources: additionalProperties: anyOf: diff --git a/pkg/apis/crds/karpenter.sh_nodepools.yaml b/pkg/apis/crds/karpenter.sh_nodepools.yaml index 157aaf13c4..2ff525637c 100644 --- a/pkg/apis/crds/karpenter.sh_nodepools.yaml +++ b/pkg/apis/crds/karpenter.sh_nodepools.yaml @@ -496,6 +496,12 @@ spec: - type type: object type: array + nodeClassObservedGeneration: + description: |- + NodeClassObservedGeneration represents the nodeClass generation for referenced nodeClass. If this does not match + the actual NodeClass Generation, NodeRegistrationHealthy status condition on the NodePool will be reset + format: int64 + type: integer resources: additionalProperties: anyOf: diff --git a/pkg/apis/v1/nodepool_status.go b/pkg/apis/v1/nodepool_status.go index 1b3f974694..0ce19c5fa2 100644 --- a/pkg/apis/v1/nodepool_status.go +++ b/pkg/apis/v1/nodepool_status.go @@ -27,6 +27,8 @@ const ( ConditionTypeValidationSucceeded = "ValidationSucceeded" // ConditionTypeNodeClassReady = "NodeClassReady" condition indicates that underlying nodeClass was resolved and is reporting as Ready ConditionTypeNodeClassReady = "NodeClassReady" + // ConditionTypeNodeRegistrationHealthy = "NodeRegistrationHealthy" condition indicates if a misconfiguration exists that is preventing successful node launch/registrations that requires manual investigation + ConditionTypeNodeRegistrationHealthy = "NodeRegistrationHealthy" ) // NodePoolStatus defines the observed state of NodePool @@ -34,6 +36,10 @@ type NodePoolStatus struct { // Resources is the list of resources that have been provisioned. // +optional Resources v1.ResourceList `json:"resources,omitempty"` + // NodeClassObservedGeneration represents the nodeClass generation for referenced nodeClass. If this does not match + // the actual NodeClass Generation, NodeRegistrationHealthy status condition on the NodePool will be reset + // +optional + NodeClassObservedGeneration int64 `json:"nodeClassObservedGeneration,omitempty"` // Conditions contains signals for health and readiness // +optional Conditions []status.Condition `json:"conditions,omitempty"` diff --git a/pkg/controllers/controllers.go b/pkg/controllers/controllers.go index 682bf172fd..544def960e 100644 --- a/pkg/controllers/controllers.go +++ b/pkg/controllers/controllers.go @@ -50,6 +50,7 @@ import ( nodepoolcounter "sigs.k8s.io/karpenter/pkg/controllers/nodepool/counter" nodepoolhash "sigs.k8s.io/karpenter/pkg/controllers/nodepool/hash" nodepoolreadiness "sigs.k8s.io/karpenter/pkg/controllers/nodepool/readiness" + nodepoolregistrationhealth "sigs.k8s.io/karpenter/pkg/controllers/nodepool/registrationhealth" nodepoolvalidation "sigs.k8s.io/karpenter/pkg/controllers/nodepool/validation" "sigs.k8s.io/karpenter/pkg/controllers/provisioning" "sigs.k8s.io/karpenter/pkg/controllers/state" @@ -88,6 +89,7 @@ func NewControllers( metricsnodepool.NewController(kubeClient, cloudProvider), metricsnode.NewController(cluster), nodepoolreadiness.NewController(kubeClient, cloudProvider), + nodepoolregistrationhealth.NewController(kubeClient, cloudProvider), nodepoolcounter.NewController(kubeClient, cloudProvider, cluster), nodepoolvalidation.NewController(kubeClient, cloudProvider), podevents.NewController(clock, kubeClient, cloudProvider), diff --git a/pkg/controllers/nodeclaim/lifecycle/liveness.go b/pkg/controllers/nodeclaim/lifecycle/liveness.go index fc1a272752..3be2aa313d 100644 --- a/pkg/controllers/nodeclaim/lifecycle/liveness.go +++ b/pkg/controllers/nodeclaim/lifecycle/liveness.go @@ -20,9 +20,13 @@ import ( "context" "time" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/controller-runtime/pkg/log" + "k8s.io/utils/clock" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" v1 "sigs.k8s.io/karpenter/pkg/apis/v1" @@ -61,6 +65,28 @@ func (l *Liveness) Reconcile(ctx context.Context, nodeClaim *v1.NodeClaim) (reco metrics.NodePoolLabel: nodeClaim.Labels[v1.NodePoolLabelKey], metrics.CapacityTypeLabel: nodeClaim.Labels[v1.CapacityTypeLabelKey], }) - + nodePool := &v1.NodePool{} + if err := l.kubeClient.Get(ctx, types.NamespacedName{Name: nodeClaim.Labels[v1.NodePoolLabelKey]}, nodePool); err != nil { + return reconcile.Result{}, err + } + if nodePool.StatusConditions().Get(v1.ConditionTypeNodeRegistrationHealthy).IsUnknown() { + stored := nodePool.DeepCopy() + // If the nodeClaim failed to register during the TTL set NodeRegistrationHealthy status condition on + // NodePool to False. If the launch failed get the launch failure reason and message from nodeClaim. + if launchCondition := nodeClaim.StatusConditions().Get(v1.ConditionTypeLaunched); launchCondition.IsTrue() { + nodePool.StatusConditions().SetFalse(v1.ConditionTypeNodeRegistrationHealthy, "RegistrationFailed", "Failed to register node") + } else { + nodePool.StatusConditions().SetFalse(v1.ConditionTypeNodeRegistrationHealthy, launchCondition.Reason, launchCondition.Message) + } + // We use client.MergeFromWithOptimisticLock because patching a list with a JSON merge patch + // can cause races due to the fact that it fully replaces the list on a change + // Here, we are updating the status condition list + if err := l.kubeClient.Status().Patch(ctx, nodePool, client.MergeFromWithOptions(stored, client.MergeFromWithOptimisticLock{})); client.IgnoreNotFound(err) != nil { + if errors.IsConflict(err) { + return reconcile.Result{Requeue: true}, nil + } + return reconcile.Result{}, err + } + } return reconcile.Result{}, nil } diff --git a/pkg/controllers/nodeclaim/lifecycle/liveness_test.go b/pkg/controllers/nodeclaim/lifecycle/liveness_test.go index 8fe3421782..cd32ebab00 100644 --- a/pkg/controllers/nodeclaim/lifecycle/liveness_test.go +++ b/pkg/controllers/nodeclaim/lifecycle/liveness_test.go @@ -20,6 +20,7 @@ import ( "time" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -78,6 +79,11 @@ var _ = Describe("Liveness", func() { ExpectFinalizersRemoved(ctx, env.Client, nodeClaim) if isManagedNodeClaim { ExpectNotFound(ctx, env.Client, nodeClaim) + nodePool = ExpectExists(ctx, env.Client, nodePool) + nodeRegistrationHealthySC := ExpectStatusConditionExists(nodePool, v1.ConditionTypeNodeRegistrationHealthy) + Expect(nodeRegistrationHealthySC.Status).To(Equal(metav1.ConditionFalse)) + Expect(nodeRegistrationHealthySC.Reason).To(Equal("RegistrationFailed")) + Expect(nodeRegistrationHealthySC.Message).To(Equal("Failed to register node")) } else { ExpectExists(ctx, env.Client, nodeClaim) } @@ -141,6 +147,44 @@ var _ = Describe("Liveness", func() { // If the node hasn't registered in the registration timeframe, then we deprovision the nodeClaim fakeClock.Step(time.Minute * 20) _ = ExpectObjectReconcileFailed(ctx, env.Client, nodeClaimController, nodeClaim) + nodePool = ExpectExists(ctx, env.Client, nodePool) + nodeRegistrationHealthySC := ExpectStatusConditionExists(nodePool, v1.ConditionTypeNodeRegistrationHealthy) + Expect(nodeRegistrationHealthySC.Status).To(Equal(metav1.ConditionFalse)) + Expect(nodeRegistrationHealthySC.Reason).To(Equal(nodeClaim.StatusConditions().Get(v1.ConditionTypeLaunched).Reason)) + Expect(nodeRegistrationHealthySC.Message).To(Equal(nodeClaim.StatusConditions().Get(v1.ConditionTypeLaunched).Message)) + ExpectFinalizersRemoved(ctx, env.Client, nodeClaim) + ExpectNotFound(ctx, env.Client, nodeClaim) + }) + It("should not update NodeRegistrationHealthy status condition if it is already set to True", func() { + nodePool.StatusConditions().SetTrue(v1.ConditionTypeNodeRegistrationHealthy) + nodeClaim := test.NodeClaim(v1.NodeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + v1.NodePoolLabelKey: nodePool.Name, + }, + }, + Spec: v1.NodeClaimSpec{ + Resources: v1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourcePods: resource.MustParse("5"), + fake.ResourceGPUVendorA: resource.MustParse("1"), + }, + }, + }, + }) + cloudProvider.AllowedCreateCalls = 0 // Don't allow Create() calls to succeed + ExpectApplied(ctx, env.Client, nodePool, nodeClaim) + _ = ExpectObjectReconcileFailed(ctx, env.Client, nodeClaimController, nodeClaim) + nodeClaim = ExpectExists(ctx, env.Client, nodeClaim) + + // If the node hasn't registered in the registration timeframe, then we deprovision the nodeClaim + fakeClock.Step(time.Minute * 20) + _ = ExpectObjectReconcileFailed(ctx, env.Client, nodeClaimController, nodeClaim) + nodePool = ExpectExists(ctx, env.Client, nodePool) + // NodeClaim registration failed, but we should not update the NodeRegistrationHealthy status condition if it is already True + Expect(ExpectStatusConditionExists(nodePool, v1.ConditionTypeNodeRegistrationHealthy).Status).To(Equal(metav1.ConditionTrue)) ExpectFinalizersRemoved(ctx, env.Client, nodeClaim) ExpectNotFound(ctx, env.Client, nodeClaim) }) diff --git a/pkg/controllers/nodeclaim/lifecycle/registration.go b/pkg/controllers/nodeclaim/lifecycle/registration.go index 0cbcaf156e..7f9fbc1855 100644 --- a/pkg/controllers/nodeclaim/lifecycle/registration.go +++ b/pkg/controllers/nodeclaim/lifecycle/registration.go @@ -20,6 +20,8 @@ import ( "context" "fmt" + "k8s.io/apimachinery/pkg/types" + "github.com/samber/lo" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -82,9 +84,33 @@ func (r *Registration) Reconcile(ctx context.Context, nodeClaim *v1.NodeClaim) ( metrics.NodesCreatedTotal.Inc(map[string]string{ metrics.NodePoolLabel: nodeClaim.Labels[v1.NodePoolLabelKey], }) + if err := r.updateNodePoolRegistrationHealth(ctx, nodeClaim); err != nil { + if errors.IsConflict(err) { + return reconcile.Result{Requeue: true}, nil + } + return reconcile.Result{}, err + } return reconcile.Result{}, nil } +func (r *Registration) updateNodePoolRegistrationHealth(ctx context.Context, nodeClaim *v1.NodeClaim) error { + nodePool := &v1.NodePool{} + if err := r.kubeClient.Get(ctx, types.NamespacedName{Name: nodeClaim.Labels[v1.NodePoolLabelKey]}, nodePool); err != nil { + return err + } + storedNodePool := nodePool.DeepCopy() + nodePool.StatusConditions().SetTrue(v1.ConditionTypeNodeRegistrationHealthy) + if !equality.Semantic.DeepEqual(storedNodePool, nodePool) { + // We use client.MergeFromWithOptimisticLock because patching a list with a JSON merge patch + // can cause races due to the fact that it fully replaces the list on a change + // Here, we are updating the status condition list + if err := r.kubeClient.Status().Patch(ctx, nodePool, client.MergeFromWithOptions(storedNodePool, client.MergeFromWithOptimisticLock{})); client.IgnoreNotFound(err) != nil { + return err + } + } + return nil +} + func (r *Registration) syncNode(ctx context.Context, nodeClaim *v1.NodeClaim, node *corev1.Node) error { stored := node.DeepCopy() controllerutil.AddFinalizer(node, v1.TerminationFinalizer) diff --git a/pkg/controllers/nodeclaim/lifecycle/registration_test.go b/pkg/controllers/nodeclaim/lifecycle/registration_test.go index 9f9b93459e..ac082a17c8 100644 --- a/pkg/controllers/nodeclaim/lifecycle/registration_test.go +++ b/pkg/controllers/nodeclaim/lifecycle/registration_test.go @@ -66,6 +66,8 @@ var _ = Describe("Registration", func() { if isManagedNodeClaim { Expect(nodeClaim.StatusConditions().Get(v1.ConditionTypeRegistered).IsTrue()).To(BeTrue()) Expect(nodeClaim.Status.NodeName).To(Equal(node.Name)) + nodePool = ExpectExists(ctx, env.Client, nodePool) + Expect(ExpectStatusConditionExists(nodePool, v1.ConditionTypeNodeRegistrationHealthy).Status).To(Equal(metav1.ConditionTrue)) } else { Expect(nodeClaim.StatusConditions().Get(v1.ConditionTypeRegistered).IsUnknown()).To(BeTrue()) Expect(nodeClaim.Status.NodeName).To(Equal("")) diff --git a/pkg/controllers/nodepool/registrationhealth/controller.go b/pkg/controllers/nodepool/registrationhealth/controller.go new file mode 100644 index 0000000000..09dffad335 --- /dev/null +++ b/pkg/controllers/nodepool/registrationhealth/controller.go @@ -0,0 +1,100 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registrationhealth + +import ( + "context" + + "github.com/awslabs/operatorpkg/object" + "github.com/awslabs/operatorpkg/status" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + + "sigs.k8s.io/karpenter/pkg/operator/injection" + + controllerruntime "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/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + v1 "sigs.k8s.io/karpenter/pkg/apis/v1" + "sigs.k8s.io/karpenter/pkg/cloudprovider" + nodepoolutils "sigs.k8s.io/karpenter/pkg/utils/nodepool" +) + +// Controller for the resource +type Controller struct { + kubeClient client.Client + cloudProvider cloudprovider.CloudProvider +} + +// NewController is a constructor +func NewController(kubeClient client.Client, cloudProvider cloudprovider.CloudProvider) *Controller { + return &Controller{ + kubeClient: kubeClient, + cloudProvider: cloudProvider, + } +} + +func (c *Controller) Reconcile(ctx context.Context, nodePool *v1.NodePool) (reconcile.Result, error) { + ctx = injection.WithControllerName(ctx, "nodepool.registrationhealth") + + nodeClass, ok := lo.Find(c.cloudProvider.GetSupportedNodeClasses(), func(nc status.Object) bool { + return object.GVK(nc).GroupKind() == nodePool.Spec.Template.Spec.NodeClassRef.GroupKind() + }) + if !ok { + // Ignore NodePools which aren't using a supported NodeClass. + return reconcile.Result{}, nil + } + if err := c.kubeClient.Get(ctx, client.ObjectKey{Name: nodePool.Spec.Template.Spec.NodeClassRef.Name}, nodeClass); err != nil { + return reconcile.Result{}, client.IgnoreNotFound(err) + } + stored := nodePool.DeepCopy() + // If NodeClass/NodePool have been updated then NodeRegistrationHealthy = Unknown + if (nodePool.Status.NodeClassObservedGeneration != nodeClass.GetGeneration()) || + (nodePool.Generation != nodePool.StatusConditions().Get(v1.ConditionTypeNodeRegistrationHealthy).ObservedGeneration) { + nodePool.StatusConditions().SetUnknown(v1.ConditionTypeNodeRegistrationHealthy) + nodePool.Status.NodeClassObservedGeneration = nodeClass.GetGeneration() + } + + if !equality.Semantic.DeepEqual(stored, nodePool) { + // We use client.MergeFromWithOptimisticLock because patching a list with a JSON merge patch + // can cause races due to the fact that it fully replaces the list on a change + // Here, we are updating the status condition list + if err := c.kubeClient.Status().Patch(ctx, nodePool, client.MergeFromWithOptions(stored, client.MergeFromWithOptimisticLock{})); client.IgnoreNotFound(err) != nil { + if errors.IsConflict(err) { + return reconcile.Result{Requeue: true}, nil + } + return reconcile.Result{}, err + } + } + return reconcile.Result{}, nil +} + +func (c *Controller) Register(_ context.Context, m manager.Manager) error { + b := controllerruntime.NewControllerManagedBy(m). + Named("nodepool.registrationhealth"). + For(&v1.NodePool{}, builder.WithPredicates(nodepoolutils.IsManagedPredicateFuncs(c.cloudProvider))). + WithOptions(controller.Options{MaxConcurrentReconciles: 10}) + for _, nodeClass := range c.cloudProvider.GetSupportedNodeClasses() { + b.Watches(nodeClass, nodepoolutils.NodeClassEventHandler(c.kubeClient)) + } + return b.Complete(reconcile.AsReconciler(m.GetClient(), c)) +} diff --git a/pkg/controllers/nodepool/registrationhealth/suite_test.go b/pkg/controllers/nodepool/registrationhealth/suite_test.go new file mode 100644 index 0000000000..5b90816de9 --- /dev/null +++ b/pkg/controllers/nodepool/registrationhealth/suite_test.go @@ -0,0 +1,115 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registrationhealth_test + +import ( + "context" + "testing" + + "sigs.k8s.io/karpenter/pkg/controllers/nodepool/registrationhealth" + + "github.com/awslabs/operatorpkg/object" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/karpenter/pkg/apis" + v1 "sigs.k8s.io/karpenter/pkg/apis/v1" + "sigs.k8s.io/karpenter/pkg/cloudprovider/fake" + "sigs.k8s.io/karpenter/pkg/test" + . "sigs.k8s.io/karpenter/pkg/test/expectations" + "sigs.k8s.io/karpenter/pkg/test/v1alpha1" + . "sigs.k8s.io/karpenter/pkg/utils/testing" +) + +var ( + controller *registrationhealth.Controller + ctx context.Context + env *test.Environment + cloudProvider *fake.CloudProvider + nodePool *v1.NodePool + nodeClass *v1alpha1.TestNodeClass +) + +func TestAPIs(t *testing.T) { + ctx = TestContextWithLogger(t) + RegisterFailHandler(Fail) + RunSpecs(t, "RegistrationHealth") +} + +var _ = BeforeSuite(func() { + cloudProvider = fake.NewCloudProvider() + env = test.NewEnvironment(test.WithCRDs(apis.CRDs...), test.WithCRDs(v1alpha1.CRDs...)) + controller = registrationhealth.NewController(env.Client, cloudProvider) +}) +var _ = AfterEach(func() { + ExpectCleanedUp(ctx, env.Client) +}) + +var _ = AfterSuite(func() { + Expect(env.Stop()).To(Succeed(), "Failed to stop environment") +}) + +var _ = Describe("RegistrationHealth", func() { + BeforeEach(func() { + nodePool = test.NodePool() + nodeClass = test.NodeClass(v1alpha1.TestNodeClass{ + ObjectMeta: metav1.ObjectMeta{Name: nodePool.Spec.Template.Spec.NodeClassRef.Name}, + }) + nodePool.Spec.Template.Spec.NodeClassRef.Group = object.GVK(nodeClass).Group + nodePool.Spec.Template.Spec.NodeClassRef.Kind = object.GVK(nodeClass).Kind + _ = nodePool.StatusConditions().Clear(v1.ConditionTypeNodeRegistrationHealthy) + }) + It("should ignore setting NodeRegistrationHealthy status condition on NodePools which aren't managed by this instance of Karpenter", func() { + nodePool.Spec.Template.Spec.NodeClassRef = &v1.NodeClassReference{ + Group: "karpenter.test.sh", + Kind: "UnmanagedNodeClass", + Name: "default", + } + ExpectApplied(ctx, env.Client, nodePool, nodeClass) + _ = ExpectObjectReconciled(ctx, env.Client, controller, nodePool) + nodePool = ExpectExists(ctx, env.Client, nodePool) + Expect(nodePool.StatusConditions().Get(v1.ConditionTypeNodeRegistrationHealthy)).To(BeNil()) + }) + It("should not set NodeRegistrationHealthy status condition on nodePool when nodeClass does not exist", func() { + ExpectApplied(ctx, env.Client, nodePool) + ExpectObjectReconciled(ctx, env.Client, controller, nodePool) + nodePool = ExpectExists(ctx, env.Client, nodePool) + Expect(nodePool.StatusConditions().Get(v1.ConditionTypeNodeRegistrationHealthy)).To(BeNil()) + }) + It("should set NodeRegistrationHealthy status condition on nodePool as Unknown if the nodeClass observed generation doesn't match with that on nodePool", func() { + nodePool.StatusConditions().SetFalse(v1.ConditionTypeNodeRegistrationHealthy, "unhealthy", "unhealthy") + nodePool.Status.NodeClassObservedGeneration = int64(1) + ExpectApplied(ctx, env.Client, nodePool, nodeClass) + + nodePool.Spec.Limits = map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("14")} + ExpectApplied(ctx, env.Client, nodePool, nodeClass) + _ = ExpectObjectReconciled(ctx, env.Client, controller, nodePool) + nodePool = ExpectExists(ctx, env.Client, nodePool) + Expect(nodePool.StatusConditions().Get(v1.ConditionTypeNodeRegistrationHealthy).IsUnknown()).To(BeTrue()) + Expect(nodePool.Status.NodeClassObservedGeneration).To(Equal(int64(1))) + }) + It("should set NodeRegistrationHealthy status condition on nodePool as Unknown if the nodePool is updated", func() { + ExpectApplied(ctx, env.Client, nodePool, nodeClass) + _ = ExpectObjectReconciled(ctx, env.Client, controller, nodePool) + nodePool = ExpectExists(ctx, env.Client, nodePool) + Expect(nodePool.StatusConditions().Get(v1.ConditionTypeNodeRegistrationHealthy).IsUnknown()).To(BeTrue()) + Expect(nodePool.Status.NodeClassObservedGeneration).To(Equal(int64(1))) + }) +})