k8s.io/kubernetes@v1.29.3/pkg/scheduler/util/utils_test.go (about) 1 /* 2 Copyright 2017 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 util 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "syscall" 24 "testing" 25 "time" 26 27 "github.com/google/go-cmp/cmp" 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/util/net" 32 clientsetfake "k8s.io/client-go/kubernetes/fake" 33 clienttesting "k8s.io/client-go/testing" 34 "k8s.io/klog/v2" 35 extenderv1 "k8s.io/kube-scheduler/extender/v1" 36 ) 37 38 func TestGetPodFullName(t *testing.T) { 39 pod := &v1.Pod{ 40 ObjectMeta: metav1.ObjectMeta{ 41 Namespace: "test", 42 Name: "pod", 43 }, 44 } 45 got := GetPodFullName(pod) 46 expected := fmt.Sprintf("%s_%s", pod.Name, pod.Namespace) 47 if got != expected { 48 t.Errorf("Got wrong full name, got: %s, expected: %s", got, expected) 49 } 50 } 51 52 func newPriorityPodWithStartTime(name string, priority int32, startTime time.Time) *v1.Pod { 53 return &v1.Pod{ 54 ObjectMeta: metav1.ObjectMeta{ 55 Name: name, 56 }, 57 Spec: v1.PodSpec{ 58 Priority: &priority, 59 }, 60 Status: v1.PodStatus{ 61 StartTime: &metav1.Time{Time: startTime}, 62 }, 63 } 64 } 65 66 func TestGetEarliestPodStartTime(t *testing.T) { 67 var priority int32 = 1 68 currentTime := time.Now() 69 tests := []struct { 70 name string 71 pods []*v1.Pod 72 expectedStartTime *metav1.Time 73 }{ 74 { 75 name: "Pods length is 0", 76 pods: []*v1.Pod{}, 77 expectedStartTime: nil, 78 }, 79 { 80 name: "generate new startTime", 81 pods: []*v1.Pod{ 82 newPriorityPodWithStartTime("pod1", 1, currentTime.Add(-time.Second)), 83 { 84 ObjectMeta: metav1.ObjectMeta{ 85 Name: "pod2", 86 }, 87 Spec: v1.PodSpec{ 88 Priority: &priority, 89 }, 90 }, 91 }, 92 expectedStartTime: &metav1.Time{Time: currentTime.Add(-time.Second)}, 93 }, 94 { 95 name: "Pod with earliest start time last in the list", 96 pods: []*v1.Pod{ 97 newPriorityPodWithStartTime("pod1", 1, currentTime.Add(time.Second)), 98 newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)), 99 newPriorityPodWithStartTime("pod3", 2, currentTime), 100 }, 101 expectedStartTime: &metav1.Time{Time: currentTime}, 102 }, 103 { 104 name: "Pod with earliest start time first in the list", 105 pods: []*v1.Pod{ 106 newPriorityPodWithStartTime("pod1", 2, currentTime), 107 newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)), 108 newPriorityPodWithStartTime("pod3", 2, currentTime.Add(2*time.Second)), 109 }, 110 expectedStartTime: &metav1.Time{Time: currentTime}, 111 }, 112 } 113 for _, test := range tests { 114 t.Run(test.name, func(t *testing.T) { 115 startTime := GetEarliestPodStartTime(&extenderv1.Victims{Pods: test.pods}) 116 if !startTime.Equal(test.expectedStartTime) { 117 t.Errorf("startTime is not the expected result,got %v, expected %v", startTime, test.expectedStartTime) 118 } 119 }) 120 } 121 } 122 123 func TestMoreImportantPod(t *testing.T) { 124 currentTime := time.Now() 125 pod1 := newPriorityPodWithStartTime("pod1", 1, currentTime) 126 pod2 := newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)) 127 pod3 := newPriorityPodWithStartTime("pod3", 2, currentTime) 128 129 tests := map[string]struct { 130 p1 *v1.Pod 131 p2 *v1.Pod 132 expected bool 133 }{ 134 "Pod with higher priority": { 135 p1: pod1, 136 p2: pod2, 137 expected: false, 138 }, 139 "Pod with older created time": { 140 p1: pod2, 141 p2: pod3, 142 expected: false, 143 }, 144 "Pods with same start time": { 145 p1: pod3, 146 p2: pod1, 147 expected: true, 148 }, 149 } 150 151 for k, v := range tests { 152 t.Run(k, func(t *testing.T) { 153 got := MoreImportantPod(v.p1, v.p2) 154 if got != v.expected { 155 t.Errorf("expected %t but got %t", v.expected, got) 156 } 157 }) 158 } 159 } 160 161 func TestRemoveNominatedNodeName(t *testing.T) { 162 tests := []struct { 163 name string 164 currentNominatedNodeName string 165 newNominatedNodeName string 166 expectedPatchRequests int 167 expectedPatchData string 168 }{ 169 { 170 name: "Should make patch request to clear node name", 171 currentNominatedNodeName: "node1", 172 expectedPatchRequests: 1, 173 expectedPatchData: `{"status":{"nominatedNodeName":null}}`, 174 }, 175 { 176 name: "Should not make patch request if nominated node is already cleared", 177 currentNominatedNodeName: "", 178 expectedPatchRequests: 0, 179 }, 180 } 181 for _, test := range tests { 182 t.Run(test.name, func(t *testing.T) { 183 actualPatchRequests := 0 184 var actualPatchData string 185 cs := &clientsetfake.Clientset{} 186 cs.AddReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) { 187 actualPatchRequests++ 188 patch := action.(clienttesting.PatchAction) 189 actualPatchData = string(patch.GetPatch()) 190 // For this test, we don't care about the result of the patched pod, just that we got the expected 191 // patch request, so just returning &v1.Pod{} here is OK because scheduler doesn't use the response. 192 return true, &v1.Pod{}, nil 193 }) 194 195 pod := &v1.Pod{ 196 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 197 Status: v1.PodStatus{NominatedNodeName: test.currentNominatedNodeName}, 198 } 199 200 ctx, cancel := context.WithCancel(context.Background()) 201 defer cancel() 202 if err := ClearNominatedNodeName(ctx, cs, pod); err != nil { 203 t.Fatalf("Error calling removeNominatedNodeName: %v", err) 204 } 205 206 if actualPatchRequests != test.expectedPatchRequests { 207 t.Fatalf("Actual patch requests (%d) dos not equal expected patch requests (%d)", actualPatchRequests, test.expectedPatchRequests) 208 } 209 210 if test.expectedPatchRequests > 0 && actualPatchData != test.expectedPatchData { 211 t.Fatalf("Patch data mismatch: Actual was %v, but expected %v", actualPatchData, test.expectedPatchData) 212 } 213 }) 214 } 215 } 216 217 func TestPatchPodStatus(t *testing.T) { 218 tests := []struct { 219 name string 220 pod v1.Pod 221 client *clientsetfake.Clientset 222 // validateErr checks if error returned from PatchPodStatus is expected one or not. 223 // (true means error is expected one.) 224 validateErr func(goterr error) bool 225 statusToUpdate v1.PodStatus 226 }{ 227 { 228 name: "Should update pod conditions successfully", 229 client: clientsetfake.NewSimpleClientset(), 230 pod: v1.Pod{ 231 ObjectMeta: metav1.ObjectMeta{ 232 Namespace: "ns", 233 Name: "pod1", 234 }, 235 Spec: v1.PodSpec{ 236 ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}}, 237 }, 238 }, 239 statusToUpdate: v1.PodStatus{ 240 Conditions: []v1.PodCondition{ 241 { 242 Type: v1.PodScheduled, 243 Status: v1.ConditionFalse, 244 }, 245 }, 246 }, 247 }, 248 { 249 // ref: #101697, #94626 - ImagePullSecrets are allowed to have empty secret names 250 // which would fail the 2-way merge patch generation on Pod patches 251 // due to the mergeKey being the name field 252 name: "Should update pod conditions successfully on a pod Spec with secrets with empty name", 253 client: clientsetfake.NewSimpleClientset(), 254 pod: v1.Pod{ 255 ObjectMeta: metav1.ObjectMeta{ 256 Namespace: "ns", 257 Name: "pod1", 258 }, 259 Spec: v1.PodSpec{ 260 // this will serialize to imagePullSecrets:[{}] 261 ImagePullSecrets: make([]v1.LocalObjectReference, 1), 262 }, 263 }, 264 statusToUpdate: v1.PodStatus{ 265 Conditions: []v1.PodCondition{ 266 { 267 Type: v1.PodScheduled, 268 Status: v1.ConditionFalse, 269 }, 270 }, 271 }, 272 }, 273 { 274 name: "retry patch request when an 'connection refused' error is returned", 275 client: func() *clientsetfake.Clientset { 276 client := clientsetfake.NewSimpleClientset() 277 278 reqcount := 0 279 client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) { 280 defer func() { reqcount++ }() 281 if reqcount == 0 { 282 // return an connection refused error for the first patch request. 283 return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED) 284 } 285 if reqcount == 1 { 286 // not return error for the second patch request. 287 return false, &v1.Pod{}, nil 288 } 289 290 // return error if requests comes in more than three times. 291 return true, nil, errors.New("requests comes in more than three times.") 292 }) 293 294 return client 295 }(), 296 pod: v1.Pod{ 297 ObjectMeta: metav1.ObjectMeta{ 298 Namespace: "ns", 299 Name: "pod1", 300 }, 301 Spec: v1.PodSpec{ 302 ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}}, 303 }, 304 }, 305 statusToUpdate: v1.PodStatus{ 306 Conditions: []v1.PodCondition{ 307 { 308 Type: v1.PodScheduled, 309 Status: v1.ConditionFalse, 310 }, 311 }, 312 }, 313 }, 314 { 315 name: "only 4 retries at most", 316 client: func() *clientsetfake.Clientset { 317 client := clientsetfake.NewSimpleClientset() 318 319 reqcount := 0 320 client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) { 321 defer func() { reqcount++ }() 322 if reqcount >= 4 { 323 // return error if requests comes in more than four times. 324 return true, nil, errors.New("requests comes in more than four times.") 325 } 326 327 // return an connection refused error for the first patch request. 328 return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED) 329 }) 330 331 return client 332 }(), 333 pod: v1.Pod{ 334 ObjectMeta: metav1.ObjectMeta{ 335 Namespace: "ns", 336 Name: "pod1", 337 }, 338 Spec: v1.PodSpec{ 339 ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}}, 340 }, 341 }, 342 validateErr: net.IsConnectionRefused, 343 statusToUpdate: v1.PodStatus{ 344 Conditions: []v1.PodCondition{ 345 { 346 Type: v1.PodScheduled, 347 Status: v1.ConditionFalse, 348 }, 349 }, 350 }, 351 }, 352 } 353 354 for _, tc := range tests { 355 t.Run(tc.name, func(t *testing.T) { 356 client := tc.client 357 _, err := client.CoreV1().Pods(tc.pod.Namespace).Create(context.TODO(), &tc.pod, metav1.CreateOptions{}) 358 if err != nil { 359 t.Fatal(err) 360 } 361 362 ctx, cancel := context.WithCancel(context.Background()) 363 defer cancel() 364 err = PatchPodStatus(ctx, client, &tc.pod, &tc.statusToUpdate) 365 if err != nil && tc.validateErr == nil { 366 // shouldn't be error 367 t.Fatal(err) 368 } 369 if tc.validateErr != nil { 370 if !tc.validateErr(err) { 371 t.Fatalf("Returned unexpected error: %v", err) 372 } 373 return 374 } 375 376 retrievedPod, err := client.CoreV1().Pods(tc.pod.Namespace).Get(ctx, tc.pod.Name, metav1.GetOptions{}) 377 if err != nil { 378 t.Fatal(err) 379 } 380 381 if diff := cmp.Diff(tc.statusToUpdate, retrievedPod.Status); diff != "" { 382 t.Errorf("unexpected pod status (-want,+got):\n%s", diff) 383 } 384 }) 385 } 386 } 387 388 // Test_As tests the As function with Pod. 389 func Test_As_Pod(t *testing.T) { 390 tests := []struct { 391 name string 392 oldObj interface{} 393 newObj interface{} 394 wantOldObj *v1.Pod 395 wantNewObj *v1.Pod 396 wantErr bool 397 }{ 398 { 399 name: "nil old Pod", 400 oldObj: nil, 401 newObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 402 wantOldObj: nil, 403 wantNewObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 404 }, 405 { 406 name: "nil new Pod", 407 oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 408 newObj: nil, 409 wantOldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 410 wantNewObj: nil, 411 }, 412 { 413 name: "two different kinds of objects", 414 oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 415 newObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 416 wantErr: true, 417 }, 418 } 419 420 for _, tc := range tests { 421 t.Run(tc.name, func(t *testing.T) { 422 gotOld, gotNew, err := As[*v1.Pod](tc.oldObj, tc.newObj) 423 if err != nil && !tc.wantErr { 424 t.Fatalf("unexpected error: %v", err) 425 } 426 if tc.wantErr { 427 if err == nil { 428 t.Fatalf("expected error, but got nil") 429 } 430 return 431 } 432 433 if diff := cmp.Diff(tc.wantOldObj, gotOld); diff != "" { 434 t.Errorf("unexpected old object (-want,+got):\n%s", diff) 435 } 436 if diff := cmp.Diff(tc.wantNewObj, gotNew); diff != "" { 437 t.Errorf("unexpected new object (-want,+got):\n%s", diff) 438 } 439 }) 440 } 441 } 442 443 // Test_As_Node tests the As function with Node. 444 func Test_As_Node(t *testing.T) { 445 tests := []struct { 446 name string 447 oldObj interface{} 448 newObj interface{} 449 wantOldObj *v1.Node 450 wantNewObj *v1.Node 451 wantErr bool 452 }{ 453 { 454 name: "nil old Node", 455 oldObj: nil, 456 newObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 457 wantOldObj: nil, 458 wantNewObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 459 }, 460 { 461 name: "nil new Node", 462 oldObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 463 newObj: nil, 464 wantOldObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 465 wantNewObj: nil, 466 }, 467 } 468 469 for _, tc := range tests { 470 t.Run(tc.name, func(t *testing.T) { 471 gotOld, gotNew, err := As[*v1.Node](tc.oldObj, tc.newObj) 472 if err != nil && !tc.wantErr { 473 t.Fatalf("unexpected error: %v", err) 474 } 475 if tc.wantErr { 476 if err == nil { 477 t.Fatalf("expected error, but got nil") 478 } 479 return 480 } 481 482 if diff := cmp.Diff(tc.wantOldObj, gotOld); diff != "" { 483 t.Errorf("unexpected old object (-want,+got):\n%s", diff) 484 } 485 if diff := cmp.Diff(tc.wantNewObj, gotNew); diff != "" { 486 t.Errorf("unexpected new object (-want,+got):\n%s", diff) 487 } 488 }) 489 } 490 } 491 492 // Test_As_KMetadata tests the As function with Pod. 493 func Test_As_KMetadata(t *testing.T) { 494 tests := []struct { 495 name string 496 oldObj interface{} 497 newObj interface{} 498 wantErr bool 499 }{ 500 { 501 name: "nil old Pod", 502 oldObj: nil, 503 newObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 504 wantErr: false, 505 }, 506 { 507 name: "nil new Pod", 508 oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 509 newObj: nil, 510 wantErr: false, 511 }, 512 { 513 name: "two different kinds of objects", 514 oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 515 newObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 516 wantErr: false, 517 }, 518 { 519 name: "unknown old type", 520 oldObj: "unknown type", 521 wantErr: true, 522 }, 523 { 524 name: "unknown new type", 525 newObj: "unknown type", 526 wantErr: true, 527 }, 528 } 529 530 for _, tc := range tests { 531 t.Run(tc.name, func(t *testing.T) { 532 _, _, err := As[klog.KMetadata](tc.oldObj, tc.newObj) 533 if err != nil && !tc.wantErr { 534 t.Fatalf("unexpected error: %v", err) 535 } 536 if tc.wantErr { 537 if err == nil { 538 t.Fatalf("expected error, but got nil") 539 } 540 return 541 } 542 }) 543 } 544 }