k8s.io/kubernetes@v1.29.3/pkg/scheduler/internal/cache/node_tree_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package cache 18 19 import ( 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 24 v1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/klog/v2/ktesting" 27 ) 28 29 var allNodes = []*v1.Node{ 30 // Node 0: a node without any region-zone label 31 { 32 ObjectMeta: metav1.ObjectMeta{ 33 Name: "node-0", 34 }, 35 }, 36 // Node 1: a node with region label only 37 { 38 ObjectMeta: metav1.ObjectMeta{ 39 Name: "node-1", 40 Labels: map[string]string{ 41 v1.LabelTopologyRegion: "region-1", 42 }, 43 }, 44 }, 45 // Node 2: a node with zone label only 46 { 47 ObjectMeta: metav1.ObjectMeta{ 48 Name: "node-2", 49 Labels: map[string]string{ 50 v1.LabelTopologyZone: "zone-2", 51 }, 52 }, 53 }, 54 // Node 3: a node with proper region and zone labels 55 { 56 ObjectMeta: metav1.ObjectMeta{ 57 Name: "node-3", 58 Labels: map[string]string{ 59 v1.LabelTopologyRegion: "region-1", 60 v1.LabelTopologyZone: "zone-2", 61 }, 62 }, 63 }, 64 // Node 4: a node with proper region and zone labels 65 { 66 ObjectMeta: metav1.ObjectMeta{ 67 Name: "node-4", 68 Labels: map[string]string{ 69 v1.LabelTopologyRegion: "region-1", 70 v1.LabelTopologyZone: "zone-2", 71 }, 72 }, 73 }, 74 // Node 5: a node with proper region and zone labels in a different zone, same region as above 75 { 76 ObjectMeta: metav1.ObjectMeta{ 77 Name: "node-5", 78 Labels: map[string]string{ 79 v1.LabelTopologyRegion: "region-1", 80 v1.LabelTopologyZone: "zone-3", 81 }, 82 }, 83 }, 84 // Node 6: a node with proper region and zone labels in a new region and zone 85 { 86 ObjectMeta: metav1.ObjectMeta{ 87 Name: "node-6", 88 Labels: map[string]string{ 89 v1.LabelTopologyRegion: "region-2", 90 v1.LabelTopologyZone: "zone-2", 91 }, 92 }, 93 }, 94 // Node 7: a node with proper region and zone labels in a region and zone as node-6 95 { 96 ObjectMeta: metav1.ObjectMeta{ 97 Name: "node-7", 98 Labels: map[string]string{ 99 v1.LabelTopologyRegion: "region-2", 100 v1.LabelTopologyZone: "zone-2", 101 }, 102 }, 103 }, 104 // Node 8: a node with proper region and zone labels in a region and zone as node-6 105 { 106 ObjectMeta: metav1.ObjectMeta{ 107 Name: "node-8", 108 Labels: map[string]string{ 109 v1.LabelTopologyRegion: "region-2", 110 v1.LabelTopologyZone: "zone-2", 111 }, 112 }, 113 }, 114 // Node 9: a node with zone + region label and the deprecated zone + region label 115 { 116 ObjectMeta: metav1.ObjectMeta{ 117 Name: "node-9", 118 Labels: map[string]string{ 119 v1.LabelTopologyRegion: "region-2", 120 v1.LabelTopologyZone: "zone-2", 121 v1.LabelFailureDomainBetaRegion: "region-2", 122 v1.LabelFailureDomainBetaZone: "zone-2", 123 }, 124 }, 125 }, 126 // Node 10: a node with only the deprecated zone + region labels 127 { 128 ObjectMeta: metav1.ObjectMeta{ 129 Name: "node-10", 130 Labels: map[string]string{ 131 v1.LabelFailureDomainBetaRegion: "region-2", 132 v1.LabelFailureDomainBetaZone: "zone-3", 133 }, 134 }, 135 }, 136 } 137 138 func verifyNodeTree(t *testing.T, nt *nodeTree, expectedTree map[string][]string) { 139 expectedNumNodes := int(0) 140 for _, na := range expectedTree { 141 expectedNumNodes += len(na) 142 } 143 if numNodes := nt.numNodes; numNodes != expectedNumNodes { 144 t.Errorf("unexpected nodeTree.numNodes. Expected: %v, Got: %v", expectedNumNodes, numNodes) 145 } 146 if diff := cmp.Diff(expectedTree, nt.tree); diff != "" { 147 t.Errorf("Unexpected node tree (-want, +got):\n%s", diff) 148 } 149 if len(nt.zones) != len(expectedTree) { 150 t.Errorf("Number of zones in nodeTree.zones is not expected. Expected: %v, Got: %v", len(expectedTree), len(nt.zones)) 151 } 152 for _, z := range nt.zones { 153 if _, ok := expectedTree[z]; !ok { 154 t.Errorf("zone %v is not expected to exist in nodeTree.zones", z) 155 } 156 } 157 } 158 159 func TestNodeTree_AddNode(t *testing.T) { 160 tests := []struct { 161 name string 162 nodesToAdd []*v1.Node 163 expectedTree map[string][]string 164 }{ 165 { 166 name: "single node no labels", 167 nodesToAdd: allNodes[:1], 168 expectedTree: map[string][]string{"": {"node-0"}}, 169 }, 170 { 171 name: "same node specified twice", 172 nodesToAdd: []*v1.Node{allNodes[0], allNodes[0]}, 173 expectedTree: map[string][]string{"": {"node-0"}}, 174 }, 175 { 176 name: "mix of nodes with and without proper labels", 177 nodesToAdd: allNodes[:4], 178 expectedTree: map[string][]string{ 179 "": {"node-0"}, 180 "region-1:\x00:": {"node-1"}, 181 ":\x00:zone-2": {"node-2"}, 182 "region-1:\x00:zone-2": {"node-3"}, 183 }, 184 }, 185 { 186 name: "mix of nodes with and without proper labels and some zones with multiple nodes", 187 nodesToAdd: allNodes[:7], 188 expectedTree: map[string][]string{ 189 "": {"node-0"}, 190 "region-1:\x00:": {"node-1"}, 191 ":\x00:zone-2": {"node-2"}, 192 "region-1:\x00:zone-2": {"node-3", "node-4"}, 193 "region-1:\x00:zone-3": {"node-5"}, 194 "region-2:\x00:zone-2": {"node-6"}, 195 }, 196 }, 197 { 198 name: "nodes also using deprecated zone/region label", 199 nodesToAdd: allNodes[9:], 200 expectedTree: map[string][]string{ 201 "region-2:\x00:zone-2": {"node-9"}, 202 "region-2:\x00:zone-3": {"node-10"}, 203 }, 204 }, 205 } 206 207 for _, test := range tests { 208 t.Run(test.name, func(t *testing.T) { 209 logger, _ := ktesting.NewTestContext(t) 210 nt := newNodeTree(logger, nil) 211 for _, n := range test.nodesToAdd { 212 nt.addNode(logger, n) 213 } 214 verifyNodeTree(t, nt, test.expectedTree) 215 }) 216 } 217 } 218 219 func TestNodeTree_RemoveNode(t *testing.T) { 220 tests := []struct { 221 name string 222 existingNodes []*v1.Node 223 nodesToRemove []*v1.Node 224 expectedTree map[string][]string 225 expectError bool 226 }{ 227 { 228 name: "remove a single node with no labels", 229 existingNodes: allNodes[:7], 230 nodesToRemove: allNodes[:1], 231 expectedTree: map[string][]string{ 232 "region-1:\x00:": {"node-1"}, 233 ":\x00:zone-2": {"node-2"}, 234 "region-1:\x00:zone-2": {"node-3", "node-4"}, 235 "region-1:\x00:zone-3": {"node-5"}, 236 "region-2:\x00:zone-2": {"node-6"}, 237 }, 238 }, 239 { 240 name: "remove a few nodes including one from a zone with multiple nodes", 241 existingNodes: allNodes[:7], 242 nodesToRemove: allNodes[1:4], 243 expectedTree: map[string][]string{ 244 "": {"node-0"}, 245 "region-1:\x00:zone-2": {"node-4"}, 246 "region-1:\x00:zone-3": {"node-5"}, 247 "region-2:\x00:zone-2": {"node-6"}, 248 }, 249 }, 250 { 251 name: "remove all nodes", 252 existingNodes: allNodes[:7], 253 nodesToRemove: allNodes[:7], 254 expectedTree: map[string][]string{}, 255 }, 256 { 257 name: "remove non-existing node", 258 existingNodes: nil, 259 nodesToRemove: allNodes[:5], 260 expectedTree: map[string][]string{}, 261 expectError: true, 262 }, 263 } 264 265 for _, test := range tests { 266 t.Run(test.name, func(t *testing.T) { 267 logger, _ := ktesting.NewTestContext(t) 268 nt := newNodeTree(logger, test.existingNodes) 269 for _, n := range test.nodesToRemove { 270 err := nt.removeNode(logger, n) 271 if test.expectError == (err == nil) { 272 t.Errorf("unexpected returned error value: %v", err) 273 } 274 } 275 verifyNodeTree(t, nt, test.expectedTree) 276 }) 277 } 278 } 279 280 func TestNodeTree_UpdateNode(t *testing.T) { 281 tests := []struct { 282 name string 283 existingNodes []*v1.Node 284 nodeToUpdate *v1.Node 285 expectedTree map[string][]string 286 }{ 287 { 288 name: "update a node without label", 289 existingNodes: allNodes[:7], 290 nodeToUpdate: &v1.Node{ 291 ObjectMeta: metav1.ObjectMeta{ 292 Name: "node-0", 293 Labels: map[string]string{ 294 v1.LabelTopologyRegion: "region-1", 295 v1.LabelTopologyZone: "zone-2", 296 }, 297 }, 298 }, 299 expectedTree: map[string][]string{ 300 "region-1:\x00:": {"node-1"}, 301 ":\x00:zone-2": {"node-2"}, 302 "region-1:\x00:zone-2": {"node-3", "node-4", "node-0"}, 303 "region-1:\x00:zone-3": {"node-5"}, 304 "region-2:\x00:zone-2": {"node-6"}, 305 }, 306 }, 307 { 308 name: "update the only existing node", 309 existingNodes: allNodes[:1], 310 nodeToUpdate: &v1.Node{ 311 ObjectMeta: metav1.ObjectMeta{ 312 Name: "node-0", 313 Labels: map[string]string{ 314 v1.LabelTopologyRegion: "region-1", 315 v1.LabelTopologyZone: "zone-2", 316 }, 317 }, 318 }, 319 expectedTree: map[string][]string{ 320 "region-1:\x00:zone-2": {"node-0"}, 321 }, 322 }, 323 { 324 name: "update non-existing node", 325 existingNodes: allNodes[:1], 326 nodeToUpdate: &v1.Node{ 327 ObjectMeta: metav1.ObjectMeta{ 328 Name: "node-new", 329 Labels: map[string]string{ 330 v1.LabelTopologyRegion: "region-1", 331 v1.LabelTopologyZone: "zone-2", 332 }, 333 }, 334 }, 335 expectedTree: map[string][]string{ 336 "": {"node-0"}, 337 "region-1:\x00:zone-2": {"node-new"}, 338 }, 339 }, 340 } 341 342 for _, test := range tests { 343 t.Run(test.name, func(t *testing.T) { 344 logger, _ := ktesting.NewTestContext(t) 345 nt := newNodeTree(logger, test.existingNodes) 346 var oldNode *v1.Node 347 for _, n := range allNodes { 348 if n.Name == test.nodeToUpdate.Name { 349 oldNode = n 350 break 351 } 352 } 353 if oldNode == nil { 354 oldNode = &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "nonexisting-node"}} 355 } 356 nt.updateNode(logger, oldNode, test.nodeToUpdate) 357 verifyNodeTree(t, nt, test.expectedTree) 358 }) 359 } 360 } 361 362 func TestNodeTree_List(t *testing.T) { 363 tests := []struct { 364 name string 365 nodesToAdd []*v1.Node 366 expectedOutput []string 367 }{ 368 { 369 name: "empty tree", 370 nodesToAdd: nil, 371 expectedOutput: nil, 372 }, 373 { 374 name: "one node", 375 nodesToAdd: allNodes[:1], 376 expectedOutput: []string{"node-0"}, 377 }, 378 { 379 name: "four nodes", 380 nodesToAdd: allNodes[:4], 381 expectedOutput: []string{"node-0", "node-1", "node-2", "node-3"}, 382 }, 383 { 384 name: "all nodes", 385 nodesToAdd: allNodes[:9], 386 expectedOutput: []string{"node-0", "node-1", "node-2", "node-3", "node-5", "node-6", "node-4", "node-7", "node-8"}, 387 }, 388 } 389 390 for _, test := range tests { 391 t.Run(test.name, func(t *testing.T) { 392 logger, _ := ktesting.NewTestContext(t) 393 nt := newNodeTree(logger, test.nodesToAdd) 394 395 output, err := nt.list() 396 if err != nil { 397 t.Fatal(err) 398 } 399 if diff := cmp.Diff(test.expectedOutput, output); diff != "" { 400 t.Errorf("Unexpected output (-want, +got):\n%s", diff) 401 } 402 }) 403 } 404 } 405 406 func TestNodeTree_List_Exhausted(t *testing.T) { 407 logger, _ := ktesting.NewTestContext(t) 408 nt := newNodeTree(logger, allNodes[:9]) 409 nt.numNodes++ 410 _, err := nt.list() 411 if err == nil { 412 t.Fatal("Expected an error from zone exhaustion") 413 } 414 } 415 416 func TestNodeTreeMultiOperations(t *testing.T) { 417 tests := []struct { 418 name string 419 nodesToAdd []*v1.Node 420 nodesToRemove []*v1.Node 421 operations []string 422 expectedOutput []string 423 }{ 424 { 425 name: "add and remove all nodes", 426 nodesToAdd: allNodes[2:9], 427 nodesToRemove: allNodes[2:9], 428 operations: []string{"add", "add", "add", "remove", "remove", "remove"}, 429 expectedOutput: nil, 430 }, 431 { 432 name: "add and remove some nodes", 433 nodesToAdd: allNodes[2:9], 434 nodesToRemove: allNodes[2:9], 435 operations: []string{"add", "add", "add", "remove"}, 436 expectedOutput: []string{"node-3", "node-4"}, 437 }, 438 { 439 name: "remove three nodes", 440 nodesToAdd: allNodes[2:9], 441 nodesToRemove: allNodes[2:9], 442 operations: []string{"add", "add", "add", "remove", "remove", "remove", "add"}, 443 expectedOutput: []string{"node-5"}, 444 }, 445 { 446 name: "add more nodes to an exhausted zone", 447 nodesToAdd: append(allNodes[4:9:9], allNodes[3]), 448 nodesToRemove: nil, 449 operations: []string{"add", "add", "add", "add", "add", "add"}, 450 expectedOutput: []string{"node-4", "node-5", "node-6", "node-3", "node-7", "node-8"}, 451 }, 452 { 453 name: "remove zone and add new", 454 nodesToAdd: append(allNodes[3:5:5], allNodes[6:8]...), 455 nodesToRemove: allNodes[3:5], 456 operations: []string{"add", "add", "remove", "add", "add", "remove"}, 457 expectedOutput: []string{"node-6", "node-7"}, 458 }, 459 } 460 461 for _, test := range tests { 462 t.Run(test.name, func(t *testing.T) { 463 logger, _ := ktesting.NewTestContext(t) 464 nt := newNodeTree(logger, nil) 465 addIndex := 0 466 removeIndex := 0 467 for _, op := range test.operations { 468 switch op { 469 case "add": 470 if addIndex >= len(test.nodesToAdd) { 471 t.Error("more add operations than nodesToAdd") 472 } else { 473 nt.addNode(logger, test.nodesToAdd[addIndex]) 474 addIndex++ 475 } 476 case "remove": 477 if removeIndex >= len(test.nodesToRemove) { 478 t.Error("more remove operations than nodesToRemove") 479 } else { 480 nt.removeNode(logger, test.nodesToRemove[removeIndex]) 481 removeIndex++ 482 } 483 default: 484 t.Errorf("unknown operation: %v", op) 485 } 486 } 487 output, err := nt.list() 488 if err != nil { 489 t.Fatal(err) 490 } 491 if diff := cmp.Diff(test.expectedOutput, output); diff != "" { 492 t.Errorf("Unexpected output (-want, +got):\n%s", diff) 493 } 494 }) 495 } 496 }