1
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
191
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
223
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
250
251
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
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
283 return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
284 }
285 if reqcount == 1 {
286
287 return false, &v1.Pod{}, nil
288 }
289
290
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
324 return true, nil, errors.New("requests comes in more than four times.")
325 }
326
327
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
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
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
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
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 }
545
View as plain text