Skip to content

Commit 3802594

Browse files
authored
Merge pull request #6273 from fische/fix-taint-unselected-node
Stop (un)tainting nodes from unselected node groups.
2 parents 00fbbe1 + e8e3ad0 commit 3802594

File tree

2 files changed

+144
-3
lines changed

2 files changed

+144
-3
lines changed

cluster-autoscaler/core/static_autoscaler.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,9 @@ func (a *StaticAutoscaler) cleanUpIfRequired() {
229229
if allNodes, err := a.AllNodeLister().List(); err != nil {
230230
klog.Errorf("Failed to list ready nodes, not cleaning up taints: %v", err)
231231
} else {
232-
taints.CleanAllToBeDeleted(allNodes,
232+
// Make sure we are only cleaning taints from selected node groups.
233+
selectedNodes := filterNodesFromSelectedGroups(a.CloudProvider, allNodes...)
234+
taints.CleanAllToBeDeleted(selectedNodes,
233235
a.AutoscalingContext.ClientSet, a.Recorder, a.CordonNodeBeforeTerminate)
234236
if a.AutoscalingContext.AutoscalingOptions.MaxBulkSoftTaintCount == 0 {
235237
// Clean old taints if soft taints handling is disabled
@@ -656,7 +658,14 @@ func (a *StaticAutoscaler) RunOnce(currentTime time.Time) caerrors.AutoscalerErr
656658
scaleDownStatus.Result == scaledownstatus.ScaleDownNoUnneeded) &&
657659
a.AutoscalingContext.AutoscalingOptions.MaxBulkSoftTaintCount != 0 {
658660
taintableNodes := a.scaleDownPlanner.UnneededNodes()
659-
untaintableNodes := subtractNodes(allNodes, taintableNodes)
661+
662+
// Make sure we are only cleaning taints from selected node groups.
663+
selectedNodes := filterNodesFromSelectedGroups(a.CloudProvider, allNodes...)
664+
665+
// This is a sanity check to make sure `taintableNodes` only includes
666+
// nodes from selected nodes.
667+
taintableNodes = intersectNodes(selectedNodes, taintableNodes)
668+
untaintableNodes := subtractNodes(selectedNodes, taintableNodes)
660669
actuation.UpdateSoftDeletionTaints(a.AutoscalingContext, taintableNodes, untaintableNodes)
661670
}
662671

@@ -972,6 +981,18 @@ func (a *StaticAutoscaler) obtainNodeLists() ([]*apiv1.Node, []*apiv1.Node, caer
972981
return allNodes, readyNodes, nil
973982
}
974983

984+
func filterNodesFromSelectedGroups(cp cloudprovider.CloudProvider, nodes ...*apiv1.Node) []*apiv1.Node {
985+
filtered := make([]*apiv1.Node, 0, len(nodes))
986+
for _, n := range nodes {
987+
if ng, err := cp.NodeGroupForNode(n); err != nil {
988+
klog.Errorf("Failed to get a node group node node: %v", err)
989+
} else if ng != nil {
990+
filtered = append(filtered, n)
991+
}
992+
}
993+
return filtered
994+
}
995+
975996
func (a *StaticAutoscaler) updateClusterState(allNodes []*apiv1.Node, nodeInfosForGroups map[string]*schedulerframework.NodeInfo, currentTime time.Time) caerrors.AutoscalerError {
976997
err := a.clusterStateRegistry.UpdateNodes(allNodes, nodeInfosForGroups, currentTime)
977998
if err != nil {
@@ -1070,6 +1091,25 @@ func subtractNodes(a []*apiv1.Node, b []*apiv1.Node) []*apiv1.Node {
10701091
return subtractNodesByName(a, nodeNames(b))
10711092
}
10721093

1094+
func filterNodesByName(nodes []*apiv1.Node, names []string) []*apiv1.Node {
1095+
c := make([]*apiv1.Node, 0, len(names))
1096+
filterSet := make(map[string]bool, len(names))
1097+
for _, name := range names {
1098+
filterSet[name] = true
1099+
}
1100+
for _, n := range nodes {
1101+
if filterSet[n.Name] {
1102+
c = append(c, n)
1103+
}
1104+
}
1105+
return c
1106+
}
1107+
1108+
// intersectNodes gives intersection of 2 node lists
1109+
func intersectNodes(a []*apiv1.Node, b []*apiv1.Node) []*apiv1.Node {
1110+
return filterNodesByName(a, nodeNames(b))
1111+
}
1112+
10731113
func nodeNames(ns []*apiv1.Node) []string {
10741114
names := make([]string, len(ns))
10751115
for i, node := range ns {

cluster-autoscaler/core/static_autoscaler_test.go

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package core
1818

1919
import (
2020
"bytes"
21+
stdcontext "context"
2122
"flag"
2223
"fmt"
2324
"os"
@@ -459,7 +460,7 @@ func TestStaticAutoscalerRunOnce(t *testing.T) {
459460
mock.AssertExpectationsForObjects(t, allPodListerMock,
460461
podDisruptionBudgetListerMock, daemonSetListerMock, onScaleUpMock, onScaleDownMock)
461462

462-
// Scale up to node gorup min size.
463+
// Scale up to node group min size.
463464
readyNodeLister.SetNodes([]*apiv1.Node{n4})
464465
allNodeLister.SetNodes([]*apiv1.Node{n4})
465466
allPodListerMock.On("List").Return([]*apiv1.Pod{}, nil).Twice()
@@ -1337,6 +1338,106 @@ func TestStaticAutoscalerRunOnceWithFilteringOnUpcomingNodesEnabledNoScaleUp(t *
13371338
podDisruptionBudgetListerMock, daemonSetListerMock, onScaleUpMock, onScaleDownMock)
13381339
}
13391340

1341+
// We should not touch taints from unselected node groups.
1342+
func TestStaticAutoscalerRunOnceWithUnselectedNodeGroups(t *testing.T) {
1343+
n1 := BuildTestNode("n1", 1000, 1000)
1344+
n1.Spec.Taints = append(n1.Spec.Taints, apiv1.Taint{
1345+
Key: taints.DeletionCandidateTaint,
1346+
Value: fmt.Sprint(time.Now().Unix()),
1347+
Effect: apiv1.TaintEffectPreferNoSchedule,
1348+
})
1349+
SetNodeReadyState(n1, true, time.Now())
1350+
n2 := BuildTestNode("n2", 1000, 1000)
1351+
n2.Spec.Taints = append(n2.Spec.Taints, apiv1.Taint{
1352+
Key: taints.DeletionCandidateTaint,
1353+
Value: fmt.Sprint(time.Now().Unix()),
1354+
Effect: apiv1.TaintEffectPreferNoSchedule,
1355+
})
1356+
SetNodeReadyState(n2, true, time.Now())
1357+
1358+
p1 := BuildTestPod("p1", 600, 100)
1359+
p1.Spec.NodeName = n1.Name
1360+
1361+
// set minimal cloud provider where only ng1 is defined as selected node group
1362+
provider := testprovider.NewTestCloudProvider(nil, nil)
1363+
provider.AddNodeGroup("ng1", 1, 10, 1)
1364+
provider.AddNode("ng1", n1)
1365+
assert.NotNil(t, provider)
1366+
1367+
tests := map[string]struct {
1368+
node *apiv1.Node
1369+
pods []*apiv1.Pod
1370+
expectedTaints []apiv1.Taint
1371+
}{
1372+
"Node from selected node groups can get their deletion candidate taints removed": {
1373+
node: n1,
1374+
pods: []*apiv1.Pod{p1},
1375+
expectedTaints: []apiv1.Taint{},
1376+
},
1377+
"Node from non-selected node groups should keep their deletion candidate taints": {
1378+
node: n2,
1379+
pods: nil,
1380+
expectedTaints: n2.Spec.Taints,
1381+
},
1382+
}
1383+
1384+
for name, test := range tests {
1385+
// prevent issues with scoping, we should be able to get rid of that with Go 1.22
1386+
test := test
1387+
t.Run(name, func(t *testing.T) {
1388+
t.Parallel()
1389+
// Create fake listers for the generated nodes, nothing returned by the rest (but the ones used in the tested path have to be defined).
1390+
readyNodeLister := kubernetes.NewTestNodeLister([]*apiv1.Node{test.node})
1391+
allNodeLister := kubernetes.NewTestNodeLister([]*apiv1.Node{test.node})
1392+
allPodListerMock := kubernetes.NewTestPodLister(test.pods)
1393+
daemonSetLister, err := kubernetes.NewTestDaemonSetLister(nil)
1394+
assert.NoError(t, err)
1395+
listerRegistry := kube_util.NewListerRegistry(allNodeLister, readyNodeLister, allPodListerMock,
1396+
kubernetes.NewTestPodDisruptionBudgetLister(nil), daemonSetLister,
1397+
nil, nil, nil, nil)
1398+
1399+
// Create context with minimal autoscalingOptions that guarantee we reach the tested logic.
1400+
autoscalingOptions := config.AutoscalingOptions{
1401+
ScaleDownEnabled: true,
1402+
MaxBulkSoftTaintCount: 10,
1403+
MaxBulkSoftTaintTime: 3 * time.Second,
1404+
}
1405+
processorCallbacks := newStaticAutoscalerProcessorCallbacks()
1406+
clientset := fake.NewSimpleClientset(test.node)
1407+
context, err := NewScaleTestAutoscalingContext(autoscalingOptions, clientset, listerRegistry, provider, processorCallbacks, nil)
1408+
assert.NoError(t, err)
1409+
1410+
// Create CSR with unhealthy cluster protection effectively disabled, to guarantee we reach the tested logic.
1411+
clusterStateConfig := clusterstate.ClusterStateRegistryConfig{
1412+
OkTotalUnreadyCount: 1,
1413+
}
1414+
clusterState := clusterstate.NewClusterStateRegistry(provider, clusterStateConfig, context.LogRecorder, NewBackoff(), nodegroupconfig.NewDefaultNodeGroupConfigProcessor(autoscalingOptions.NodeGroupDefaults))
1415+
1416+
// Setting the Actuator is necessary for testing any scale-down logic, it shouldn't have anything to do in this test.
1417+
sdActuator := actuation.NewActuator(&context, clusterState, deletiontracker.NewNodeDeletionTracker(0*time.Second), options.NodeDeleteOptions{}, nil, NewTestProcessors(&context).NodeGroupConfigProcessor)
1418+
context.ScaleDownActuator = sdActuator
1419+
1420+
// Fake planner that keeps track of the scale-down candidates passed to UpdateClusterState.
1421+
sdPlanner := &candidateTrackingFakePlanner{}
1422+
1423+
autoscaler := &StaticAutoscaler{
1424+
AutoscalingContext: &context,
1425+
clusterStateRegistry: clusterState,
1426+
scaleDownPlanner: sdPlanner,
1427+
scaleDownActuator: sdActuator,
1428+
processors: NewTestProcessors(&context),
1429+
processorCallbacks: processorCallbacks,
1430+
}
1431+
1432+
err = autoscaler.RunOnce(time.Now().Add(5 * time.Hour))
1433+
assert.NoError(t, err)
1434+
newNode, err := clientset.CoreV1().Nodes().Get(stdcontext.TODO(), test.node.Name, metav1.GetOptions{})
1435+
assert.NoError(t, err)
1436+
assert.Equal(t, test.expectedTaints, newNode.Spec.Taints)
1437+
})
1438+
}
1439+
}
1440+
13401441
func TestStaticAutoscalerRunOnceWithBypassedSchedulers(t *testing.T) {
13411442
bypassedScheduler := "bypassed-scheduler"
13421443
nonBypassedScheduler := "non-bypassed-scheduler"

0 commit comments

Comments
 (0)