Repository: gardener/gardener
Pull request #11779: Add `CheckNodesScaling`
Author: login: RadaBDimitrova
Labels: name: kind/enhancement
description: Enhancement, improvement, extension
name: size/XL
description: Denotes a PR that changes 500-999 lines, ignoring generated files.
name: area/quality
description: Output qualification (tests, checks, scans, automation in general, etc.) related
name: cla: yes
description: Indicates the PR's author has signed the cla-assistant.io CLA.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
Created: 2025-04-01T07:55:39Z | Merged: 2025-04-14T12:55:04Z

**How to categorize this PR?**
<!--
Please select area, kind, and priority for this pull request. This helps the community categorizing it.
Replace below TODOs or exchange the existing identifiers with those that fit best in your opinion.
If multiple identifiers make sense you can also state the commands multiple times, e.g.
  /area control-plane
  /area auto-scaling
  ...

"/area" identifiers:     audit-logging|auto-scaling|backup|compliance|control-plane-migration|control-plane|cost|delivery|dev-productivity|disaster-recovery|documentation|high-availability|logging|metering|monitoring|networking|open-source|ops-productivity|os|performance|quality|robustness|scalability|security|storage|testing|usability|user-management
"/kind" identifiers:     api-change|bug|cleanup|discussion|enhancement|epic|flake|impediment|poc|post-mortem|question|regression|task|technical-debt|test

For Gardener Enhancement Proposals (GEPs), please check the following [documentation](https://github.com/gardener/gardener/tree/master/docs/proposals/README.md) before submitting this pull request.
-->
/area quality
/kind enhancement

**What this PR does / why we need it**:
This PR is a prerequisite for the agreed upon [solution](https://github.com/gardener/gardener/pull/11084#issuecomment-2639137008) to https://github.com/gardener/gardener/issues/10227
Here the scaling logic, which was separated by other checks - example:
https://github.com/gardener/gardener/blob/8f341920a785a66e2761e71e0d8363a8c40e093c/pkg/gardenlet/controller/shoot/care/health.go#L681-L689 is now grouped in the `CheckNodesScaling` function.

**Which issue(s) this PR fixes**:
Part of https://github.com/gardener/gardener/issues/10227

**Special notes for your reviewer**:
N/A

**Release note**:
<!--
Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|noteworthy|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```other operator
NONE
```



Diff:
diff --git a/pkg/gardenlet/controller/shoot/care/health.go b/pkg/gardenlet/controller/shoot/care/health.go
index 9f1cfd2f5a2..6e630426e16 100644
--- a/pkg/gardenlet/controller/shoot/care/health.go
+++ b/pkg/gardenlet/controller/shoot/care/health.go
@@ -645,37 +645,20 @@ func (h *Health) CheckClusterNodes(
 		return nil, err
 	}
 
-	var (
-		nodeList            = convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
-		readyNodes          int
-		registeredNodes     = len(nodeList.Items)
-		desiredMachines     = getDesiredMachineCount(machineDeploymentList.Items)
-		nodeNotManagedByMCM int
-	)
-
+	nodeList := convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
+	// only nodes that are managed by MCM are considered
+	nodesManagedByMCM := []*corev1.Node{}
 	for _, node := range nodeList.Items {
 		if metav1.HasAnnotation(node.ObjectMeta, annotationKeyNotManagedByMCM) && node.Annotations[annotationKeyNotManagedByMCM] == "1" {
-			nodeNotManagedByMCM++
-			continue
-		}
-		if node.Spec.Unschedulable {
 			continue
 		}
-		for _, condition := range node.Status.Conditions {
-			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
-				readyNodes++
-			}
-		}
+		nodesManagedByMCM = append(nodesManagedByMCM, &node)
 	}
-
-	// only nodes that are managed by MCM is considered
-	registeredNodes = registeredNodes - nodeNotManagedByMCM
-
-	machineList := &machinev1alpha1.MachineList{}
-	if registeredNodes != desiredMachines || readyNodes != desiredMachines {
-		if err := h.seedClient.Client().List(ctx, machineList, client.InNamespace(h.shoot.ControlPlaneNamespace)); err != nil {
+	if msg, err := CheckNodesScaling(ctx, h.seedClient.Client(), nodesManagedByMCM, machineDeploymentList, h.shoot.ControlPlaneNamespace); err != nil {
+		if msg == "" {
 			return nil, err
 		}
+		return ptr.To(v1beta1helper.FailedCondition(h.clock, h.shoot.GetInfo().Status.LastOperation, h.conditionThresholds, condition, msg, err.Error())), nil
 	}
 
 	leaseList := &coordinationv1.LeaseList{}
@@ -688,37 +671,6 @@ func (h *Health) CheckClusterNodes(
 		return &c, nil
 	}
 
-	// First check if the MachineDeployments report failed machines. If false then check if the MachineDeployments are
-	// "available". If false then check if there is a regular scale-up happening or if there are machines with an erroneous
-	// phase. Only then check the other MachineDeployment conditions. As last check, check if there is a scale-down happening
-	// (e.g., in case of a rolling-update).
-
-	checkScaleUp := false
-	for _, deployment := range machineDeploymentList.Items {
-		if len(deployment.Status.FailedMachines) > 0 {
-			break
-		}
-
-		for _, condition := range deployment.Status.Conditions {
-			if condition.Type == machinev1alpha1.MachineDeploymentAvailable && condition.Status != machinev1alpha1.ConditionTrue {
-				checkScaleUp = true
-				break
-			}
-		}
-	}
-
-	if checkScaleUp {
-		if err := CheckNodesScalingUp(machineList, readyNodes, desiredMachines); err != nil {
-			c := v1beta1helper.FailedCondition(h.clock, h.shoot.GetInfo().Status.LastOperation, h.conditionThresholds, condition, "NodesScalingUp", err.Error())
-			return &c, nil
-		}
-	}
-
-	if err := CheckNodesScalingDown(machineList, nodeList, registeredNodes, desiredMachines); err != nil {
-		c := v1beta1helper.FailedCondition(h.clock, h.shoot.GetInfo().Status.LastOperation, h.conditionThresholds, condition, "NodesScalingDown", err.Error())
-		return &c, nil
-	}
-
 	if !h.shoot.IsWorkerless && v1beta1helper.SeedSettingDependencyWatchdogProberEnabled(h.seed.GetInfo().Spec.Settings) {
 		leaseList := &coordinationv1.LeaseList{}
 		if err := shootClient.Client().List(ctx, leaseList, client.InNamespace(corev1.NamespaceNodeLease)); err != nil {
@@ -786,8 +738,69 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling checks whether cluster nodes are being scaled up or down. If scaling is in progress, it returns a string describing the action. An error is returned if any failure occurs.
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList []*corev1.Node, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+				break
+			}
+		}
+	}
+
+	// Skip checks if all shoot nodes are ready and match the target node number and if there are no ongoing node drains
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if err := seedClient.List(ctx, machineList, client.InNamespace(controlPlaneNamespace)); err != nil {
+		return "", err
+	}
+
+	// First check if the MachineDeployments report failed machines. If false then check if the MachineDeployments are
+	// "available". If false then check if there is a regular scale-up happening or if there are machines with an erroneous
+	// phase. Only then check the other MachineDeployment conditions. As last check, check if there is a scale-down happening
+	// (e.g., in case of a rolling-update).
+
+	checkScaleUp := false
+	for _, deployment := range machineDeploymentList.Items {
+		if len(deployment.Status.FailedMachines) > 0 {
+			break
+		}
+
+		for _, condition := range deployment.Status.Conditions {
+			if condition.Type == machinev1alpha1.MachineDeploymentAvailable && condition.Status != machinev1alpha1.ConditionTrue {
+				checkScaleUp = true
+				break
+			}
+		}
+	}
+
+	if checkScaleUp {
+		if err := checkNodesScalingUp(machineList, readyAndSchedulableNodes, desiredMachines); err != nil {
+			return "NodesScalingUp", err
+		}
+	}
+
+	if err := checkNodesScalingDown(machineList, nodeList, registeredNodes, desiredMachines); err != nil {
+		return "NodesScalingDown", err
+	}
+
+	return "", nil
+}
+
+func checkNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
 	if readyNodes == desiredMachines {
 		return nil
 	}
@@ -821,8 +834,7 @@ func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, d
 	return fmt.Errorf("%s provisioning and should join the cluster soon", cosmeticMachineMessage(pendingMachines))
 }
 
-// CheckNodesScalingDown returns an error if nodes are being scaled down.
-func CheckNodesScalingDown(machineList *machinev1alpha1.MachineList, nodeList *corev1.NodeList, registeredNodes, desiredMachines int) error {
+func checkNodesScalingDown(machineList *machinev1alpha1.MachineList, nodeList []*corev1.Node, registeredNodes, desiredMachines int) error {
 	if registeredNodes == desiredMachines {
 		return nil
 	}
@@ -837,10 +849,7 @@ func CheckNodesScalingDown(machineList *machinev1alpha1.MachineList, nodeList *c
 	}
 
 	var cordonedNodes int
-	for _, node := range nodeList.Items {
-		if metav1.HasAnnotation(node.ObjectMeta, annotationKeyNotManagedByMCM) && node.Annotations[annotationKeyNotManagedByMCM] == "1" {
-			continue
-		}
+	for _, node := range nodeList {
 		if node.Spec.Unschedulable {
 			machine, ok := nodeNameToMachine[node.Name]
 			if !ok {
diff --git a/pkg/gardenlet/controller/shoot/care/health_test.go b/pkg/gardenlet/controller/shoot/care/health_test.go
index 3db4dc8cc22..fc7e9ef4229 100644
--- a/pkg/gardenlet/controller/shoot/care/health_test.go
+++ b/pkg/gardenlet/controller/shoot/care/health_test.go
@@ -422,6 +422,253 @@ var _ = Describe("health check", func() {
 		)
 	})
 
+	Describe("#CheckNodesScaling", func() {
+		Describe("Scaling up", func() {
+			It("should return true if number of ready nodes equal number of desired machines", func() {
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+						},
+					},
+				}
+				nodeList := []*corev1.Node{
+					{
+						Status: corev1.NodeStatus{
+							Conditions: []corev1.NodeCondition{
+								{
+									Type:   corev1.NodeReady,
+									Status: corev1.ConditionTrue,
+								},
+							},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal(""))
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			It("should return an error if not enough machine objects as desired were created", func() {
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+							Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, []*corev1.Node{}, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough machine objects created yet")))
+			})
+
+			It("should return an error when detecting erroneous machines", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineUnknown},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+							Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, []*corev1.Node{}, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("is erroneous")))
+			})
+
+			It("should return an error when not enough ready nodes are registered", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineRunning},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := []*corev1.Node{{}}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+							Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough ready worker nodes registered in the cluster")))
+			})
+
+			It("should return progressing when detecting a regular scale up (pending status)", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachinePending},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := []*corev1.Node{{}}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+							Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("provisioning and should join the cluster soon")))
+			})
+
+			It("should return progressing when detecting a regular scale up (no status)", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace}},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := []*corev1.Node{{}}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+							Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("provisioning and should join the cluster soon")))
+			})
+		})
+
+		Describe("Scaling down", func() {
+			It("should return true if number of registered nodes equal number of desired machines", func() {
+				nodeList := []*corev1.Node{{}}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{
+					Items: []machinev1alpha1.MachineDeployment{
+						machinev1alpha1.MachineDeployment{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+							Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+						},
+					},
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal(""))
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			It("should return an error if the machine for a cordoned node is not found", func() {
+				nodeList := []*corev1.Node{
+					{Spec: corev1.NodeSpec{Unschedulable: true}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("machine object for cordoned node \"\" not found")))
+			})
+
+			It("should return an error if the machine for a cordoned node is not deleted", func() {
+				var (
+					nodeName = "foo"
+
+					machineList = &machinev1alpha1.MachineList{
+						Items: []machinev1alpha1.Machine{
+							{ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace, Labels: map[string]string{"node": nodeName}}},
+						},
+					}
+					nodeList = []*corev1.Node{
+						{
+							ObjectMeta: metav1.ObjectMeta{Name: nodeName},
+							Spec:       corev1.NodeSpec{Unschedulable: true},
+						},
+					}
+				)
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("found but corresponding machine object does not have a deletion timestamp")))
+			})
+
+			It("should return an error if there are more nodes then machines", func() {
+				nodeList := []*corev1.Node{{}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("too many worker nodes are registered. Exceeding maximum desired machine count")))
+			})
+
+			It("should return progressing for a regular scale down", func() {
+				var (
+					nodeName = "foo"
+
+					machineList = &machinev1alpha1.MachineList{
+						Items: []machinev1alpha1.Machine{
+							{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:         "foo",
+									GenerateName: "obj-",
+									Namespace:    controlPlaneNamespace,
+									Labels:       map[string]string{"node": nodeName},
+									Finalizers:   []string{"in-deletion"},
+								},
+							},
+						},
+					}
+					nodeList = []*corev1.Node{
+						{
+							ObjectMeta: metav1.ObjectMeta{Name: nodeName},
+							Spec:       corev1.NodeSpec{Unschedulable: true},
+						},
+					}
+				)
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+					Expect(fakeClient.Delete(ctx, &machine)).To(Succeed())
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("is waiting to be completely drained from pods")))
+			})
+		})
+	})
+
 	Describe("#CheckIfDependencyWatchdogProberScaledDownControllers", func() {
 		var (
 			deploymentCA  *appsv1.Deployment
@@ -596,162 +843,6 @@ var _ = Describe("health check", func() {
 		)
 	})
 
-	Describe("#CheckNodesScalingUp", func() {
-		It("should return true if number of ready nodes equal number of desired machines", func() {
-			Expect(CheckNodesScalingUp(nil, 1, 1)).To(Succeed())
-		})
-
-		It("should return an error if not enough machine objects as desired were created", func() {
-			Expect(CheckNodesScalingUp(&machinev1alpha1.MachineList{}, 0, 1)).To(MatchError(ContainSubstring("not enough machine objects created yet")))
-		})
-
-		It("should return an error when detecting erroneous machines", func() {
-			machineList := &machinev1alpha1.MachineList{
-				Items: []machinev1alpha1.Machine{
-					{
-						Status: machinev1alpha1.MachineStatus{
-							CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineUnknown},
-						},
-					},
-				},
-			}
-
-			Expect(CheckNodesScalingUp(machineList, 0, 1)).To(MatchError(ContainSubstring("is erroneous")))
-		})
-
-		It("should return an error when not enough ready nodes are registered", func() {
-			machineList := &machinev1alpha1.MachineList{
-				Items: []machinev1alpha1.Machine{
-					{
-						Status: machinev1alpha1.MachineStatus{
-							CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineRunning},
-						},
-					},
-				},
-			}
-
-			Expect(CheckNodesScalingUp(machineList, 0, 1)).To(MatchError(ContainSubstring("not enough ready worker nodes registered in the cluster")))
-		})
-
-		It("should return progressing when detecting a regular scale up (pending status)", func() {
-			machineList := &machinev1alpha1.MachineList{
-				Items: []machinev1alpha1.Machine{
-					{
-						Status: machinev1alpha1.MachineStatus{
-							CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachinePending},
-						},
-					},
-				},
-			}
-
-			Expect(CheckNodesScalingUp(machineList, 0, 1)).To(MatchError(ContainSubstring("is provisioning and should join the cluster soon")))
-		})
-
-		It("should return progressing when detecting a regular scale up (no status)", func() {
-			machineList := &machinev1alpha1.MachineList{
-				Items: []machinev1alpha1.Machine{
-					{},
-				},
-			}
-
-			Expect(CheckNodesScalingUp(machineList, 0, 1)).To(MatchError(ContainSubstring("is provisioning and should join the cluster soon")))
-		})
-	})
-
-	Describe("#CheckNodesScalingDown", func() {
-		It("should return true if number of registered nodes equal number of desired machines", func() {
-			Expect(CheckNodesScalingDown(nil, nil, 1, 1)).To(Succeed())
-		})
-
-		It("should return an error if the machine for a cordoned node is not found", func() {
-			nodeList := &corev1.NodeList{
-				Items: []corev1.Node{
-					{Spec: corev1.NodeSpec{Unschedulable: true}},
-				},
-			}
-
-			Expect(CheckNodesScalingDown(&machinev1alpha1.MachineList{}, nodeList, 2, 1)).To(MatchError(ContainSubstring("machine object for cordoned node \"\" not found")))
-		})
-
-		It("should return an error if the machine for a cordoned node is not deleted", func() {
-			var (
-				nodeName = "foo"
-
-				machineList = &machinev1alpha1.MachineList{
-					Items: []machinev1alpha1.Machine{
-						{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"node": nodeName}}},
-					},
-				}
-				nodeList = &corev1.NodeList{
-					Items: []corev1.Node{
-						{
-							ObjectMeta: metav1.ObjectMeta{Name: nodeName},
-							Spec:       corev1.NodeSpec{Unschedulable: true},
-						},
-					},
-				}
-			)
-
-			Expect(CheckNodesScalingDown(machineList, nodeList, 2, 1)).To(MatchError(ContainSubstring("found but corresponding machine object does not have a deletion timestamp")))
-		})
-
-		It("should return an error if there are more nodes then machines", func() {
-			Expect(CheckNodesScalingDown(&machinev1alpha1.MachineList{}, &corev1.NodeList{Items: []corev1.Node{{}}}, 2, 1)).To(MatchError(ContainSubstring("too many worker nodes are registered. Exceeding maximum desired machine count")))
-		})
-
-		It("should return progressing for a regular scale down", func() {
-			var (
-				nodeName          = "foo"
-				deletionTimestamp = metav1.Now()
-
-				machineList = &machinev1alpha1.MachineList{
-					Items: []machinev1alpha1.Machine{
-						{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &deletionTimestamp, Labels: map[string]string{"node": nodeName}}},
-					},
-				}
-				nodeList = &corev1.NodeList{
-					Items: []corev1.Node{
-						{
-							ObjectMeta: metav1.ObjectMeta{Name: nodeName},
-							Spec:       corev1.NodeSpec{Unschedulable: true},
-						},
-					},
-				}
-			)
-
-			Expect(CheckNodesScalingDown(machineList, nodeList, 2, 1)).To(MatchError(ContainSubstring("is waiting to be completely drained from pods")))
-		})
-
-		It("should ignore node not managed by MCM and return progressing for a regular scale down", func() {
-			var (
-				nodeName          = "foo"
-				deletionTimestamp = metav1.Now()
-
-				machineList = &machinev1alpha1.MachineList{
-					Items: []machinev1alpha1.Machine{
-						{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &deletionTimestamp, Labels: map[string]string{"node": nodeName}}},
-					},
-				}
-				nodeList = &corev1.NodeList{
-					Items: []corev1.Node{
-						{
-							ObjectMeta: metav1.ObjectMeta{Name: nodeName},
-							Spec:       corev1.NodeSpec{Unschedulable: true},
-						},
-						{
-							ObjectMeta: metav1.ObjectMeta{
-								Name:        "bar",
-								Annotations: map[string]string{"node.machine.sapcloud.io/not-managed-by-mcm": "1"},
-							},
-						},
-					},
-				}
-			)
-
-			Expect(CheckNodesScalingDown(machineList, nodeList, 2, 1)).To(MatchError(ContainSubstring("is waiting to be completely drained from pods")))
-		})
-	})
-
 	Describe("ShootConditions", func() {
 		Describe("#NewShootConditions", func() {
 			It("should initialize all conditions", func() {


Reviews:
user: login: Kostov6
body: Partial review
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T13:09:04Z
user: login: Kostov6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T13:45:54Z
user: login: Kostov6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:17:09Z
user: login: timuthy
body: Thanks for the refactoring. Please see the added comments for feedback.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-09T12:16:58Z
user: login: timuthy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T12:18:21Z
user: login: RadaBDimitrova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:26:10Z
user: login: timuthy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T14:03:00Z
user: login: Kostov6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:12:08Z
user: login: timuthy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T05:56:28Z
user: login: Kostov6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:21:35Z
user: login: timuthy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T13:33:12Z
user: login: Kostov6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:52:42Z
user: login: timuthy
body: Apart from https://github.com/gardener/gardener/pull/11779#discussion_r2037477749 the changes look good to me. Thanks for addressing the feedback.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:25:02Z
user: login: timuthy
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T07:37:17Z
user: login: Kostov6
body: /lgtm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T08:03:58Z

Review comments:
diff_hunk: @@ -422,6 +422,262 @@ var _ = Describe("health check", func() {
 		)
 	})
 
+	Describe("#CheckNodesScaling", func() {
+		var (
+			ctrl *gomock.Controller
+			c    *mockclient.MockClient
+		)
+
+		BeforeEach(func() {
+			ctrl = gomock.NewController(GinkgoT())
+			c = mockclient.NewMockClient(ctrl)
+		})
+
+		AfterEach(func() {
+			ctrl.Finish()
+		})
+
+		Describe("Scaling up", func() {
+			It("should return true if number of ready nodes equal number of desired machines", func() {
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
path: pkg/gardenlet/controller/shoot/care/health_test.go
user: login: Kostov6
body: Can you make this test case be 1 node as the previous one?
created_at: 2025-04-01T12:38:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,6 +422,262 @@ var _ = Describe("health check", func() {
 		)
 	})
 
+	Describe("#CheckNodesScaling", func() {
+		var (
+			ctrl *gomock.Controller
+			c    *mockclient.MockClient
+		)
+
+		BeforeEach(func() {
+			ctrl = gomock.NewController(GinkgoT())
+			c = mockclient.NewMockClient(ctrl)
+		})
+
+		AfterEach(func() {
+			ctrl.Finish()
+		})
+
+		Describe("Scaling up", func() {
+			It("should return true if number of ready nodes equal number of desired machines", func() {
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal(""))
+				Expect(err).To(BeNil())
+			})
+
+			It("should return an error if not enough machine objects as desired were created", func() {
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough machine objects created yet")))
+			})
+
+			It("should return an error when detecting erroneous machines", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineUnknown},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
path: pkg/gardenlet/controller/shoot/care/health_test.go
user: login: Kostov6
body: Do we need this? Can't we use `&corev1.NodeList{}` in `CheckNodesScaling`?
created_at: 2025-04-01T12:49:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -786,8 +730,75 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyNodes          int
+		registeredNodes     = len(nodeList.Items)
+		desiredMachines     = getDesiredMachineCount(machineDeploymentList.Items)
+		nodeNotManagedByMCM int
+	)
+
+	for _, node := range nodeList.Items {
+		if metav1.HasAnnotation(node.ObjectMeta, annotationKeyNotManagedByMCM) && node.Annotations[annotationKeyNotManagedByMCM] == "1" {
+			nodeNotManagedByMCM++
+			continue
+		}
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: Kostov6
body: Can you move the filtering of non-MCM nodes outside of `CheckNodesScaling` and provide `CheckNodesScaling` node list of only MCM managed nodes? With this I think we can remove the same check in `checkNodesScalingDown`
created_at: 2025-04-01T13:29:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -786,8 +730,75 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyNodes          int
+		registeredNodes     = len(nodeList.Items)
+		desiredMachines     = getDesiredMachineCount(machineDeploymentList.Items)
+		nodeNotManagedByMCM int
+	)
+
+	for _, node := range nodeList.Items {
+		if metav1.HasAnnotation(node.ObjectMeta, annotationKeyNotManagedByMCM) && node.Annotations[annotationKeyNotManagedByMCM] == "1" {
+			nodeNotManagedByMCM++
+			continue
+		}
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyNodes++
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: Kostov6
body: ```suggestion
				readyAndSchedulableNodes++
```
created_at: 2025-04-01T13:32:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,6 +422,274 @@ var _ = Describe("health check", func() {
 		)
 	})
 
+	Describe("#CheckNodesScaling", func() {
+		var (
+			ctrl *gomock.Controller
+		)
+
+		BeforeEach(func() {
+			ctrl = gomock.NewController(GinkgoT())
+		})
+
+		AfterEach(func() {
+			ctrl.Finish()
+		})
+
+		Describe("Scaling up", func() {
+			It("should return true if number of ready nodes equal number of desired machines", func() {
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal(""))
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			It("should return an error if not enough machine objects as desired were created", func() {
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough machine objects created yet")))
+			})
+
+			It("should return an error when detecting erroneous machines", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineUnknown},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("is erroneous")))
+			})
+
+			It("should return an error when not enough ready nodes are registered", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineRunning},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough ready worker nodes registered in the cluster")))
+			})
+
+			It("should return progressing when detecting a regular scale up (pending status)", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachinePending},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("provisioning and should join the cluster soon")))
+			})
+
+			It("should return progressing when detecting a regular scale up (no status)", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace}},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("provisioning and should join the cluster soon")))
+			})
+		})
+
+		Describe("Scaling down", func() {
+			It("should return true if number of registered nodes equal number of desired machines", func() {
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
path: pkg/gardenlet/controller/shoot/care/health_test.go
user: login: Kostov6
body: Can you include all `machineDeployment` in `machineDeploymentList`?
created_at: 2025-04-01T14:06:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,6 +422,274 @@ var _ = Describe("health check", func() {
 		)
 	})
 
+	Describe("#CheckNodesScaling", func() {
+		var (
+			ctrl *gomock.Controller
+		)
+
+		BeforeEach(func() {
+			ctrl = gomock.NewController(GinkgoT())
+		})
+
+		AfterEach(func() {
+			ctrl.Finish()
+		})
+
+		Describe("Scaling up", func() {
+			It("should return true if number of ready nodes equal number of desired machines", func() {
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal(""))
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			It("should return an error if not enough machine objects as desired were created", func() {
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, &corev1.NodeList{}, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough machine objects created yet")))
+			})
+
+			It("should return an error when detecting erroneous machines", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineUnknown},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("is erroneous")))
+			})
+
+			It("should return an error when not enough ready nodes are registered", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachineRunning},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("not enough ready worker nodes registered in the cluster")))
+			})
+
+			It("should return progressing when detecting a regular scale up (pending status)", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{
+							ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace},
+							Status: machinev1alpha1.MachineStatus{
+								CurrentStatus: machinev1alpha1.CurrentStatus{Phase: machinev1alpha1.MachinePending},
+							},
+						},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("provisioning and should join the cluster soon")))
+			})
+
+			It("should return progressing when detecting a regular scale up (no status)", func() {
+				machineList := &machinev1alpha1.MachineList{
+					Items: []machinev1alpha1.Machine{
+						{ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace}},
+					},
+				}
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+					Status:     machinev1alpha1.MachineDeploymentStatus{Conditions: []machinev1alpha1.MachineDeploymentCondition{{Type: machinev1alpha1.MachineDeploymentAvailable, Status: machinev1alpha1.ConditionFalse}, {}}},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingUp"))
+				Expect(err).To(MatchError(ContainSubstring("provisioning and should join the cluster soon")))
+			})
+		})
+
+		Describe("Scaling down", func() {
+			It("should return true if number of registered nodes equal number of desired machines", func() {
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				machineDeployment := machinev1alpha1.MachineDeployment{
+					ObjectMeta: metav1.ObjectMeta{GenerateName: "deploy", Namespace: controlPlaneNamespace},
+					Spec:       machinev1alpha1.MachineDeploymentSpec{Replicas: int32(1)},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{machineDeployment}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal(""))
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			It("should return an error if the machine for a cordoned node is not found", func() {
+				nodeList := &corev1.NodeList{
+					Items: []corev1.Node{
+						{Spec: corev1.NodeSpec{Unschedulable: true}},
+					},
+				}
+				machineDeploymentList := &machinev1alpha1.MachineDeploymentList{Items: []machinev1alpha1.MachineDeployment{}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, machineDeploymentList, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("machine object for cordoned node \"\" not found")))
+			})
+
+			It("should return an error if the machine for a cordoned node is not deleted", func() {
+				var (
+					nodeName = "foo"
+
+					machineList = &machinev1alpha1.MachineList{
+						Items: []machinev1alpha1.Machine{
+							{ObjectMeta: metav1.ObjectMeta{GenerateName: "obj-", Namespace: controlPlaneNamespace, Labels: map[string]string{"node": nodeName}}},
+						},
+					}
+					nodeList = &corev1.NodeList{
+						Items: []corev1.Node{
+							{
+								ObjectMeta: metav1.ObjectMeta{Name: nodeName},
+								Spec:       corev1.NodeSpec{Unschedulable: true},
+							},
+						},
+					}
+				)
+				for _, machine := range machineList.Items {
+					Expect(fakeClient.Create(ctx, &machine)).To(Succeed())
+				}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("found but corresponding machine object does not have a deletion timestamp")))
+			})
+
+			It("should return an error if there are more nodes then machines", func() {
+				nodeList := &corev1.NodeList{Items: []corev1.Node{{}}}
+				msg, err := CheckNodesScaling(ctx, fakeClient, nodeList, &machinev1alpha1.MachineDeploymentList{}, controlPlaneNamespace)
+				Expect(msg).To(Equal("NodesScalingDown"))
+				Expect(err).To(MatchError(ContainSubstring("too many worker nodes are registered. Exceeding maximum desired machine count")))
+			})
+
+			It("should return progressing for a regular scale down", func() {
+				var (
+					nodeName = "foo"
+
+					machineList = &machinev1alpha1.MachineList{
+						Items: []machinev1alpha1.Machine{
+							{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:         "foo",
+									GenerateName: "obj-",
+									Namespace:    controlPlaneNamespace,
+									Labels:       map[string]string{"node": nodeName},
+									Finalizers:   []string{"foo"},
path: pkg/gardenlet/controller/shoot/care/health_test.go
user: login: Kostov6
body: A little bit more clearer. In multiple places:
```suggestion
									Finalizers:   []string{"in-deletion"},
```
created_at: 2025-04-01T14:08:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,6 +422,274 @@ var _ = Describe("health check", func() {
 		)
 	})
 
+	Describe("#CheckNodesScaling", func() {
+		var (
+			ctrl *gomock.Controller
+		)
+
+		BeforeEach(func() {
+			ctrl = gomock.NewController(GinkgoT())
+		})
+
+		AfterEach(func() {
+			ctrl.Finish()
+		})
+
path: pkg/gardenlet/controller/shoot/care/health_test.go
user: login: Kostov6
body: Do we need `ctrl` ?
created_at: 2025-04-01T14:09:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -645,37 +645,20 @@ func (h *Health) CheckClusterNodes(
 		return nil, err
 	}
 
-	var (
-		nodeList            = convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
-		readyNodes          int
-		registeredNodes     = len(nodeList.Items)
-		desiredMachines     = getDesiredMachineCount(machineDeploymentList.Items)
-		nodeNotManagedByMCM int
-	)
-
+	nodeList := convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
+	// only nodes that are managed by MCM are considered
+	nodesManagedByMCM := &corev1.NodeList{}
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: This `NodeList` seems to be unused. I suppose you wanted it to pass to `CheckNodesScaling `?!

Furthermore, I'm a bit concerned that the nodes are copied again to another slice which can be quite some data for clusters with many nodes, images and volumes. Can we instead work with the already existing `nodeList` (like previous code)?
created_at: 2025-04-09T11:48:47Z
author_association: MEMBER
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: ```suggestion
// CheckNodesScaling checks whether cluster nodes are being scaled up or down. If scaling is in progress, it returns a string describing the action. An error is returned if any failure occurs.
```
created_at: 2025-04-09T12:04:15Z
author_association: MEMBER
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+			}
+		}
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: ```suggestion
	if readyAndSchedulableNodes == desiredMachines {
		return "", nil
	}
	
	machineList := &machinev1alpha1.MachineList{}
```

`registeredNodes == desiredMachines` seems redundant to me. 
created_at: 2025-04-09T12:05:07Z
author_association: MEMBER
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: ```suggestion
				readyAndSchedulableNodes++
				break
```
created_at: 2025-04-09T12:18:21Z
author_association: MEMBER
diff_hunk: @@ -645,37 +645,20 @@ func (h *Health) CheckClusterNodes(
 		return nil, err
 	}
 
-	var (
-		nodeList            = convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
-		readyNodes          int
-		registeredNodes     = len(nodeList.Items)
-		desiredMachines     = getDesiredMachineCount(machineDeploymentList.Items)
-		nodeNotManagedByMCM int
-	)
-
+	nodeList := convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
+	// only nodes that are managed by MCM are considered
+	nodesManagedByMCM := &corev1.NodeList{}
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: RadaBDimitrova
body: Yes, I wanted to use it in `CheckNodesScaling`, it's a mistype, thanks for the catch. 
Concerning the copying, I could use a slice of pointers to the objects instead of returning the original logic, since it has code duplication and that's what I was trying to get rid of: https://github.com/gardener/gardener/blob/fe8e1b28d459b693184ace40b5e74b21de417bfd/pkg/gardenlet/controller/shoot/care/health.go#L656-L660 and https://github.com/gardener/gardener/blob/fe8e1b28d459b693184ace40b5e74b21de417bfd/pkg/gardenlet/controller/shoot/care/health.go#L840-L843 and if in the future we decide to add additional checks that depend on the nodes being managed by MCM, this implicit need for filtering could be forgotten. What do you think?
created_at: 2025-04-09T13:26:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -645,37 +645,20 @@ func (h *Health) CheckClusterNodes(
 		return nil, err
 	}
 
-	var (
-		nodeList            = convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
-		readyNodes          int
-		registeredNodes     = len(nodeList.Items)
-		desiredMachines     = getDesiredMachineCount(machineDeploymentList.Items)
-		nodeNotManagedByMCM int
-	)
-
+	nodeList := convertWorkerPoolToNodesMappingToNodeList(workerPoolToNodes)
+	// only nodes that are managed by MCM are considered
+	nodesManagedByMCM := &corev1.NodeList{}
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: Sounds good to me, thank you 🙂 
created_at: 2025-04-09T14:03:00Z
author_association: MEMBER
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+			}
+		}
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: Kostov6
body: I was thinking initially the same thing when reviewing.  Only counter-argument case that came to my mind is when during a rolling update we have 1 desiredMachine(for example), 2 registered nodes - 1 unready and 1 ready and schedulable. in old code we will get `too many worker nodes are registered. Exceedin ...` error and in new one we will not
1. Is this even possible - with 1 unready can the second ready node be schedulable?
2. Even if this state is transiently possible will the reporting of it even matter?
created_at: 2025-04-09T14:12:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+			}
+		}
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: I'm not sure about how this is related to the comment.

At the moment it says `registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines`.

As a consequence, if `readyAndSchedulableNodes == desiredMachines` it automatically means `registeredNodes == desiredMachines`, because `readyAndSchedulableNodes` is a subset of `registeredNodes`.

Am I missing something? 🤔 
created_at: 2025-04-10T05:56:28Z
author_association: MEMBER
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+			}
+		}
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: Kostov6
body: Basically in case of a rolling update when there is a user pod with a PDB that prevents the node drain. For example:
```
❯ k get machinedeployments.machine.sapcloud.io -A
NAMESPACE             NAME                        READY   DESIRED   UP-TO-DATE   AVAILABLE   AGE
shoot--local--local   shoot--local--local-local   1       1         1            1           28m

❯ k get nodes
NAME                                            STATUS                     ROLES    AGE   VERSION
machine-shoot--local--local-local-57666-9rhtk   Ready                      <none>   10m   v1.32.0
machine-shoot--local--local-local-6cffc-48svp   Ready,SchedulingDisabled   <none>   23m   v1.31.1
```
created_at: 2025-04-10T13:21:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+			}
+		}
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: timuthy
body: I see, thanks for posting this case. Then it's probably better to keep the check as it is.
created_at: 2025-04-10T13:33:11Z
author_association: MEMBER
diff_hunk: @@ -786,8 +738,67 @@ func CheckForExpiredNodeLeases(nodeList *corev1.NodeList, leaseList *coordinatio
 	return nil
 }
 
-// CheckNodesScalingUp returns an error if nodes are being scaled up.
-func CheckNodesScalingUp(machineList *machinev1alpha1.MachineList, readyNodes, desiredMachines int) error {
+// CheckNodesScaling returns an error and a string describing the scaling if there is a scaling happening
+func CheckNodesScaling(ctx context.Context, seedClient client.Client, nodeList *corev1.NodeList, machineDeploymentList *machinev1alpha1.MachineDeploymentList, controlPlaneNamespace string) (string, error) {
+	var (
+		readyAndSchedulableNodes int
+		registeredNodes          = len(nodeList.Items)
+		desiredMachines          = getDesiredMachineCount(machineDeploymentList.Items)
+	)
+
+	for _, node := range nodeList.Items {
+		if node.Spec.Unschedulable {
+			continue
+		}
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
+				readyAndSchedulableNodes++
+			}
+		}
+	}
+
+	machineList := &machinev1alpha1.MachineList{}
+	if registeredNodes == desiredMachines && readyAndSchedulableNodes == desiredMachines {
+		return "", nil
+	}
+
path: pkg/gardenlet/controller/shoot/care/health.go
user: login: Kostov6
body: Maybe it will be good to add something like this above the if:
```
// skip checks if all shoot nodes are ready and match the target node number and if there are no ongoing node drains
```
It took me quite some time to come up with the above case justifying the old if logic
wdyt?
created_at: 2025-04-10T13:52:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: Kostov6
created_at: 2025-04-01T08:21:40Z
body: /assign
author_association: CONTRIBUTOR
user: login: Kostov6
created_at: 2025-04-01T12:28:25Z
body: ```
The function passed to Eventually failed at /home/prow/go/src/github.com/gardener/gardener/test/e2e/gardener/managedseed/create_rotate_delete.go:110 with:
Unexpected error:
    <*fmt.wrapError | 0xc002314440>: 
    error discovering kubernetes version: Get "https://api.e2e-mngdseed-op.garden.external.local.gardener.cloud/version": dial tcp 172.18.255.12:443: connect: connection refused
    {
        msg: "error discovering kubernetes version: Get \"https://api.e2e-mngdseed-op.garden.external.local.gardener.cloud/version\": dial tcp 172.18.255.12:443: connect: connection refused",
        err: <*url.Error | 0xc00230f2f0>{
            Op: "Get",
            URL: "https://api.e2e-mngdseed-op.garden.external.local.gardener.cloud/version",
            Err: <*net.OpError | 0xc002301450>{
                Op: "dial",
                Net: "tcp",
                Source: nil,
                Addr: <*net.TCPAddr | 0xc00230f2c0>{
                    IP: [172, 18, 255, 12],
                    Port: 443,
                    Zone: "",
                },
                Err: <*os.SyscallError | 0xc0023143c0>{
                    Syscall: "connect",
                    Err: <syscall.Errno>0x6f,
                },
            },
        },
    }
occurred failed [FAILED] Timed out after 30.000s.
```
author_association: CONTRIBUTOR
user: login: Kostov6
created_at: 2025-04-01T12:28:33Z
body: /retest
author_association: CONTRIBUTOR
user: login: timuthy
created_at: 2025-04-02T13:00:05Z
body: /assign
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-04-11T07:37:22Z
body: LGTM label has been added.  <details>Git tree hash: 9ff388556483fe42f4b82dc077a860acc355f31f</details>
author_association: CONTRIBUTOR
user: login: timuthy
created_at: 2025-04-14T08:06:24Z
body: /approve
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-04-14T08:06:33Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/gardener/gardener/pull/11779#issuecomment-2800811003" title="Approved">timuthy</a>*

The full list of commands accepted by this bot can be found [here](https://prow.gardener.cloud/command-help?repo=gardener%2Fgardener).

The pull request process is described [here](https://gardener.cloud/docs/contribute/#pull-request-checklist)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/gardener/gardener/blob/master/OWNERS)~~ [timuthy]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: Kostov6
created_at: 2025-04-14T10:26:47Z
body: /retest
author_association: CONTRIBUTOR
user: login: RadaBDimitrova
created_at: 2025-04-14T11:02:26Z
body: /retest
author_association: CONTRIBUTOR