1
16
17 package drain
18
19 import (
20 "errors"
21 "io"
22 "net/http"
23 "net/url"
24 "os"
25 "reflect"
26 "strings"
27 "sync/atomic"
28 "testing"
29 "time"
30
31 "github.com/spf13/cobra"
32 appsv1 "k8s.io/api/apps/v1"
33 batchv1 "k8s.io/api/batch/v1"
34 corev1 "k8s.io/api/core/v1"
35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36 "k8s.io/apimachinery/pkg/runtime"
37 "k8s.io/apimachinery/pkg/runtime/schema"
38 "k8s.io/apimachinery/pkg/util/strategicpatch"
39 "k8s.io/cli-runtime/pkg/genericiooptions"
40 "k8s.io/client-go/rest/fake"
41 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
42 cmdutil "k8s.io/kubectl/pkg/cmd/util"
43 "k8s.io/kubectl/pkg/drain"
44 "k8s.io/kubectl/pkg/scheme"
45 utilpointer "k8s.io/utils/pointer"
46 )
47
48 const (
49 EvictionMethod = "Eviction"
50 DeleteMethod = "Delete"
51 )
52
53 var node *corev1.Node
54 var cordonedNode *corev1.Node
55
56 func TestMain(m *testing.M) {
57
58 node = &corev1.Node{
59 ObjectMeta: metav1.ObjectMeta{
60 Name: "node",
61 CreationTimestamp: metav1.Time{Time: time.Now()},
62 },
63 Status: corev1.NodeStatus{},
64 }
65
66
67 cordonedNode = node.DeepCopy()
68 cordonedNode.Spec.Unschedulable = true
69 os.Exit(m.Run())
70 }
71
72 func TestCordon(t *testing.T) {
73 tests := []struct {
74 description string
75 node *corev1.Node
76 expected *corev1.Node
77 cmd func(cmdutil.Factory, genericiooptions.IOStreams) *cobra.Command
78 arg string
79 expectFatal bool
80 }{
81 {
82 description: "node/node syntax",
83 node: cordonedNode,
84 expected: node,
85 cmd: NewCmdUncordon,
86 arg: "node/node",
87 expectFatal: false,
88 },
89 {
90 description: "uncordon for real",
91 node: cordonedNode,
92 expected: node,
93 cmd: NewCmdUncordon,
94 arg: "node",
95 expectFatal: false,
96 },
97 {
98 description: "uncordon does nothing",
99 node: node,
100 expected: node,
101 cmd: NewCmdUncordon,
102 arg: "node",
103 expectFatal: false,
104 },
105 {
106 description: "cordon does nothing",
107 node: cordonedNode,
108 expected: cordonedNode,
109 cmd: NewCmdCordon,
110 arg: "node",
111 expectFatal: false,
112 },
113 {
114 description: "cordon for real",
115 node: node,
116 expected: cordonedNode,
117 cmd: NewCmdCordon,
118 arg: "node",
119 expectFatal: false,
120 },
121 {
122 description: "cordon missing node",
123 node: node,
124 expected: node,
125 cmd: NewCmdCordon,
126 arg: "bar",
127 expectFatal: true,
128 },
129 {
130 description: "uncordon missing node",
131 node: node,
132 expected: node,
133 cmd: NewCmdUncordon,
134 arg: "bar",
135 expectFatal: true,
136 },
137 {
138 description: "cordon for multiple nodes",
139 node: node,
140 expected: cordonedNode,
141 cmd: NewCmdCordon,
142 arg: "node node1 node2",
143 expectFatal: false,
144 },
145 {
146 description: "uncordon for multiple nodes",
147 node: cordonedNode,
148 expected: node,
149 cmd: NewCmdUncordon,
150 arg: "node node1 node2",
151 expectFatal: false,
152 },
153 }
154
155 for _, test := range tests {
156 t.Run(test.description, func(t *testing.T) {
157 tf := cmdtesting.NewTestFactory()
158 defer tf.Cleanup()
159
160 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
161 ns := scheme.Codecs.WithoutConversion()
162
163 newNode := &corev1.Node{}
164 updated := false
165 tf.Client = &fake.RESTClient{
166 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
167 NegotiatedSerializer: ns,
168 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
169 m := &MyReq{req}
170 switch {
171 case m.isFor("GET", "/nodes/node1"):
172 fallthrough
173 case m.isFor("GET", "/nodes/node2"):
174 fallthrough
175 case m.isFor("GET", "/nodes/node"):
176 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.node)}, nil
177 case m.isFor("GET", "/nodes/bar"):
178 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("nope")}, nil
179 case m.isFor("PATCH", "/nodes/node1"):
180 fallthrough
181 case m.isFor("PATCH", "/nodes/node2"):
182 fallthrough
183 case m.isFor("PATCH", "/nodes/node"):
184 data, err := io.ReadAll(req.Body)
185 if err != nil {
186 t.Fatalf("%s: unexpected error: %v", test.description, err)
187 }
188 defer req.Body.Close()
189 oldJSON, err := runtime.Encode(codec, node)
190 if err != nil {
191 t.Fatalf("%s: unexpected error: %v", test.description, err)
192 }
193 appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{})
194 if err != nil {
195 t.Fatalf("%s: unexpected error: %v", test.description, err)
196 }
197 if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil {
198 t.Fatalf("%s: unexpected error: %v", test.description, err)
199 }
200 if !reflect.DeepEqual(test.expected.Spec, newNode.Spec) {
201 t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec.Unschedulable, newNode.Spec.Unschedulable)
202 }
203 updated = true
204 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
205 default:
206 t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
207 return nil, nil
208 }
209 }),
210 }
211 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
212
213 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
214 cmd := test.cmd(tf, ioStreams)
215
216 var recovered interface{}
217 sawFatal := false
218 func() {
219 defer func() {
220
221 recovered = recover()
222
223 cmdutil.DefaultBehaviorOnFatal()
224 }()
225 cmdutil.BehaviorOnFatal(func(e string, code int) {
226 sawFatal = true
227 panic(e)
228 })
229 cmd.SetArgs(strings.Split(test.arg, " "))
230 cmd.Execute()
231 }()
232
233 switch {
234 case recovered != nil && !sawFatal:
235 t.Fatalf("got panic: %v", recovered)
236 case test.expectFatal:
237 if !sawFatal {
238 t.Fatalf("%s: unexpected non-error", test.description)
239 }
240 if updated {
241 t.Fatalf("%s: unexpected update", test.description)
242 }
243 case !test.expectFatal && sawFatal:
244 t.Fatalf("%s: unexpected error", test.description)
245 case !reflect.DeepEqual(test.expected.Spec, test.node.Spec) && !updated:
246 t.Fatalf("%s: node never updated", test.description)
247 }
248 })
249 }
250 }
251
252 func TestDrain(t *testing.T) {
253 labels := make(map[string]string)
254 labels["my_key"] = "my_value"
255
256 rc := corev1.ReplicationController{
257 ObjectMeta: metav1.ObjectMeta{
258 Name: "rc",
259 Namespace: "default",
260 CreationTimestamp: metav1.Time{Time: time.Now()},
261 Labels: labels,
262 },
263 Spec: corev1.ReplicationControllerSpec{
264 Selector: labels,
265 },
266 }
267
268 rcPod := corev1.Pod{
269 ObjectMeta: metav1.ObjectMeta{
270 Name: "bar",
271 Namespace: "default",
272 CreationTimestamp: metav1.Time{Time: time.Now()},
273 Labels: labels,
274 OwnerReferences: []metav1.OwnerReference{
275 {
276 APIVersion: "v1",
277 Kind: "ReplicationController",
278 Name: "rc",
279 UID: "123",
280 BlockOwnerDeletion: utilpointer.BoolPtr(true),
281 Controller: utilpointer.BoolPtr(true),
282 },
283 },
284 },
285 Spec: corev1.PodSpec{
286 NodeName: "node",
287 },
288 }
289
290 ds := appsv1.DaemonSet{
291 ObjectMeta: metav1.ObjectMeta{
292 Name: "ds",
293 Namespace: "default",
294 CreationTimestamp: metav1.Time{Time: time.Now()},
295 },
296 Spec: appsv1.DaemonSetSpec{
297 Selector: &metav1.LabelSelector{MatchLabels: labels},
298 },
299 }
300
301 dsPod := corev1.Pod{
302 ObjectMeta: metav1.ObjectMeta{
303 Name: "bar",
304 Namespace: "default",
305 CreationTimestamp: metav1.Time{Time: time.Now()},
306 Labels: labels,
307 OwnerReferences: []metav1.OwnerReference{
308 {
309 APIVersion: "apps/v1",
310 Kind: "DaemonSet",
311 Name: "ds",
312 BlockOwnerDeletion: utilpointer.BoolPtr(true),
313 Controller: utilpointer.BoolPtr(true),
314 },
315 },
316 },
317 Spec: corev1.PodSpec{
318 NodeName: "node",
319 },
320 }
321
322 dsTerminatedPod := corev1.Pod{
323 ObjectMeta: metav1.ObjectMeta{
324 Name: "bar",
325 Namespace: "default",
326 CreationTimestamp: metav1.Time{Time: time.Now()},
327 Labels: labels,
328 OwnerReferences: []metav1.OwnerReference{
329 {
330 APIVersion: "apps/v1",
331 Kind: "DaemonSet",
332 Name: "ds",
333 BlockOwnerDeletion: utilpointer.BoolPtr(true),
334 Controller: utilpointer.BoolPtr(true),
335 },
336 },
337 },
338 Spec: corev1.PodSpec{
339 NodeName: "node",
340 },
341 Status: corev1.PodStatus{
342 Phase: corev1.PodSucceeded,
343 },
344 }
345
346 dsPodWithEmptyDir := corev1.Pod{
347 ObjectMeta: metav1.ObjectMeta{
348 Name: "bar",
349 Namespace: "default",
350 CreationTimestamp: metav1.Time{Time: time.Now()},
351 Labels: labels,
352 OwnerReferences: []metav1.OwnerReference{
353 {
354 APIVersion: "apps/v1",
355 Kind: "DaemonSet",
356 Name: "ds",
357 BlockOwnerDeletion: utilpointer.BoolPtr(true),
358 Controller: utilpointer.BoolPtr(true),
359 },
360 },
361 },
362 Spec: corev1.PodSpec{
363 NodeName: "node",
364 Volumes: []corev1.Volume{
365 {
366 Name: "scratch",
367 VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
368 },
369 },
370 },
371 }
372
373 orphanedDsPod := corev1.Pod{
374 ObjectMeta: metav1.ObjectMeta{
375 Name: "bar",
376 Namespace: "default",
377 CreationTimestamp: metav1.Time{Time: time.Now()},
378 Labels: labels,
379 },
380 Spec: corev1.PodSpec{
381 NodeName: "node",
382 },
383 }
384
385 job := batchv1.Job{
386 ObjectMeta: metav1.ObjectMeta{
387 Name: "job",
388 Namespace: "default",
389 CreationTimestamp: metav1.Time{Time: time.Now()},
390 },
391 Spec: batchv1.JobSpec{
392 Selector: &metav1.LabelSelector{MatchLabels: labels},
393 },
394 }
395
396 jobPod := corev1.Pod{
397 ObjectMeta: metav1.ObjectMeta{
398 Name: "bar",
399 Namespace: "default",
400 CreationTimestamp: metav1.Time{Time: time.Now()},
401 Labels: labels,
402 OwnerReferences: []metav1.OwnerReference{
403 {
404 APIVersion: "v1",
405 Kind: "Job",
406 Name: "job",
407 BlockOwnerDeletion: utilpointer.BoolPtr(true),
408 Controller: utilpointer.BoolPtr(true),
409 },
410 },
411 },
412 Spec: corev1.PodSpec{
413 NodeName: "node",
414 Volumes: []corev1.Volume{
415 {
416 Name: "scratch",
417 VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
418 },
419 },
420 },
421 }
422
423 terminatedJobPodWithLocalStorage := corev1.Pod{
424 ObjectMeta: metav1.ObjectMeta{
425 Name: "bar",
426 Namespace: "default",
427 CreationTimestamp: metav1.Time{Time: time.Now()},
428 Labels: labels,
429 OwnerReferences: []metav1.OwnerReference{
430 {
431 APIVersion: "v1",
432 Kind: "Job",
433 Name: "job",
434 BlockOwnerDeletion: utilpointer.BoolPtr(true),
435 Controller: utilpointer.BoolPtr(true),
436 },
437 },
438 },
439 Spec: corev1.PodSpec{
440 NodeName: "node",
441 Volumes: []corev1.Volume{
442 {
443 Name: "scratch",
444 VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
445 },
446 },
447 },
448 Status: corev1.PodStatus{
449 Phase: corev1.PodSucceeded,
450 },
451 }
452
453 rs := appsv1.ReplicaSet{
454 ObjectMeta: metav1.ObjectMeta{
455 Name: "rs",
456 Namespace: "default",
457 CreationTimestamp: metav1.Time{Time: time.Now()},
458 Labels: labels,
459 },
460 Spec: appsv1.ReplicaSetSpec{
461 Selector: &metav1.LabelSelector{MatchLabels: labels},
462 },
463 }
464
465 rsPod := corev1.Pod{
466 ObjectMeta: metav1.ObjectMeta{
467 Name: "bar",
468 Namespace: "default",
469 CreationTimestamp: metav1.Time{Time: time.Now()},
470 Labels: labels,
471 OwnerReferences: []metav1.OwnerReference{
472 {
473 APIVersion: "v1",
474 Kind: "ReplicaSet",
475 Name: "rs",
476 BlockOwnerDeletion: utilpointer.BoolPtr(true),
477 Controller: utilpointer.BoolPtr(true),
478 },
479 },
480 },
481 Spec: corev1.PodSpec{
482 NodeName: "node",
483 },
484 }
485
486 nakedPod := corev1.Pod{
487 ObjectMeta: metav1.ObjectMeta{
488 Name: "bar",
489 Namespace: "default",
490 CreationTimestamp: metav1.Time{Time: time.Now()},
491 Labels: labels,
492 },
493 Spec: corev1.PodSpec{
494 NodeName: "node",
495 },
496 }
497
498 emptydirPod := corev1.Pod{
499 ObjectMeta: metav1.ObjectMeta{
500 Name: "bar",
501 Namespace: "default",
502 CreationTimestamp: metav1.Time{Time: time.Now()},
503 Labels: labels,
504 },
505 Spec: corev1.PodSpec{
506 NodeName: "node",
507 Volumes: []corev1.Volume{
508 {
509 Name: "scratch",
510 VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
511 },
512 },
513 },
514 }
515 emptydirTerminatedPod := corev1.Pod{
516 ObjectMeta: metav1.ObjectMeta{
517 Name: "bar",
518 Namespace: "default",
519 CreationTimestamp: metav1.Time{Time: time.Now()},
520 Labels: labels,
521 },
522 Spec: corev1.PodSpec{
523 NodeName: "node",
524 Volumes: []corev1.Volume{
525 {
526 Name: "scratch",
527 VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
528 },
529 },
530 },
531 Status: corev1.PodStatus{
532 Phase: corev1.PodFailed,
533 },
534 }
535
536 tests := []struct {
537 description string
538 node *corev1.Node
539 expected *corev1.Node
540 pods []corev1.Pod
541 rcs []corev1.ReplicationController
542 replicaSets []appsv1.ReplicaSet
543 args []string
544 failUponEvictionOrDeletion bool
545 expectWarning string
546 expectFatal bool
547 expectDelete bool
548 expectOutputToContain string
549 }{
550 {
551 description: "RC-managed pod",
552 node: node,
553 expected: cordonedNode,
554 pods: []corev1.Pod{rcPod},
555 rcs: []corev1.ReplicationController{rc},
556 args: []string{"node"},
557 expectFatal: false,
558 expectDelete: true,
559 expectOutputToContain: "node/node drained",
560 },
561 {
562 description: "DS-managed pod",
563 node: node,
564 expected: cordonedNode,
565 pods: []corev1.Pod{dsPod},
566 rcs: []corev1.ReplicationController{rc},
567 args: []string{"node"},
568 expectFatal: true,
569 expectDelete: false,
570 },
571 {
572 description: "DS-managed terminated pod",
573 node: node,
574 expected: cordonedNode,
575 pods: []corev1.Pod{dsTerminatedPod},
576 rcs: []corev1.ReplicationController{rc},
577 args: []string{"node"},
578 expectFatal: false,
579 expectDelete: true,
580 expectOutputToContain: "node/node drained",
581 },
582 {
583 description: "orphaned DS-managed pod",
584 node: node,
585 expected: cordonedNode,
586 pods: []corev1.Pod{orphanedDsPod},
587 rcs: []corev1.ReplicationController{},
588 args: []string{"node"},
589 expectFatal: true,
590 expectDelete: false,
591 },
592 {
593 description: "orphaned DS-managed pod with --force",
594 node: node,
595 expected: cordonedNode,
596 pods: []corev1.Pod{orphanedDsPod},
597 rcs: []corev1.ReplicationController{},
598 args: []string{"node", "--force"},
599 expectFatal: false,
600 expectDelete: true,
601 expectWarning: "Warning: deleting Pods that declare no controller: default/bar",
602 expectOutputToContain: "node/node drained",
603 },
604 {
605 description: "DS-managed pod with --ignore-daemonsets",
606 node: node,
607 expected: cordonedNode,
608 pods: []corev1.Pod{dsPod},
609 rcs: []corev1.ReplicationController{rc},
610 args: []string{"node", "--ignore-daemonsets"},
611 expectFatal: false,
612 expectDelete: false,
613 expectOutputToContain: "node/node drained",
614 },
615 {
616 description: "DS-managed pod with emptyDir with --ignore-daemonsets",
617 node: node,
618 expected: cordonedNode,
619 pods: []corev1.Pod{dsPodWithEmptyDir},
620 rcs: []corev1.ReplicationController{rc},
621 args: []string{"node", "--ignore-daemonsets"},
622 expectWarning: "Warning: ignoring DaemonSet-managed Pods: default/bar",
623 expectFatal: false,
624 expectDelete: false,
625 expectOutputToContain: "node/node drained",
626 },
627 {
628 description: "Job-managed pod with local storage",
629 node: node,
630 expected: cordonedNode,
631 pods: []corev1.Pod{jobPod},
632 rcs: []corev1.ReplicationController{rc},
633 args: []string{"node", "--force", "--delete-emptydir-data=true"},
634 expectFatal: false,
635 expectDelete: true,
636 expectOutputToContain: "node/node drained",
637 },
638 {
639 description: "Ensure compatibility for --delete-local-data until fully deprecated",
640 node: node,
641 expected: cordonedNode,
642 pods: []corev1.Pod{jobPod},
643 rcs: []corev1.ReplicationController{rc},
644 args: []string{"node", "--force", "--delete-local-data=true"},
645 expectFatal: false,
646 expectDelete: true,
647 expectOutputToContain: "node/node drained",
648 },
649 {
650 description: "Job-managed terminated pod",
651 node: node,
652 expected: cordonedNode,
653 pods: []corev1.Pod{terminatedJobPodWithLocalStorage},
654 rcs: []corev1.ReplicationController{rc},
655 args: []string{"node"},
656 expectFatal: false,
657 expectDelete: true,
658 expectOutputToContain: "node/node drained",
659 },
660 {
661 description: "RS-managed pod",
662 node: node,
663 expected: cordonedNode,
664 pods: []corev1.Pod{rsPod},
665 replicaSets: []appsv1.ReplicaSet{rs},
666 args: []string{"node"},
667 expectFatal: false,
668 expectDelete: true,
669 expectOutputToContain: "node/node drained",
670 },
671 {
672 description: "naked pod",
673 node: node,
674 expected: cordonedNode,
675 pods: []corev1.Pod{nakedPod},
676 rcs: []corev1.ReplicationController{},
677 args: []string{"node"},
678 expectFatal: true,
679 expectDelete: false,
680 },
681 {
682 description: "naked pod with --force",
683 node: node,
684 expected: cordonedNode,
685 pods: []corev1.Pod{nakedPod},
686 rcs: []corev1.ReplicationController{},
687 args: []string{"node", "--force"},
688 expectFatal: false,
689 expectDelete: true,
690 expectOutputToContain: "node/node drained",
691 },
692 {
693 description: "pod with EmptyDir",
694 node: node,
695 expected: cordonedNode,
696 pods: []corev1.Pod{emptydirPod},
697 args: []string{"node", "--force"},
698 expectFatal: true,
699 expectDelete: false,
700 },
701 {
702 description: "terminated pod with emptyDir",
703 node: node,
704 expected: cordonedNode,
705 pods: []corev1.Pod{emptydirTerminatedPod},
706 rcs: []corev1.ReplicationController{rc},
707 args: []string{"node"},
708 expectFatal: false,
709 expectDelete: true,
710 expectOutputToContain: "node/node drained",
711 },
712 {
713 description: "pod with EmptyDir and --delete-emptydir-data",
714 node: node,
715 expected: cordonedNode,
716 pods: []corev1.Pod{emptydirPod},
717 args: []string{"node", "--force", "--delete-emptydir-data=true"},
718 expectFatal: false,
719 expectDelete: true,
720 expectOutputToContain: "node/node drained",
721 },
722 {
723 description: "empty node",
724 node: node,
725 expected: cordonedNode,
726 pods: []corev1.Pod{},
727 rcs: []corev1.ReplicationController{rc},
728 args: []string{"node"},
729 expectFatal: false,
730 expectDelete: false,
731 expectOutputToContain: "node/node drained",
732 },
733 {
734 description: "fail to list pods",
735 node: node,
736 expected: cordonedNode,
737 pods: []corev1.Pod{rsPod},
738 replicaSets: []appsv1.ReplicaSet{rs},
739 args: []string{"node"},
740 expectFatal: true,
741 expectDelete: true,
742 failUponEvictionOrDeletion: true,
743 },
744 }
745
746 testEviction := false
747 for i := 0; i < 2; i++ {
748 testEviction = !testEviction
749 var currMethod string
750 if testEviction {
751 currMethod = EvictionMethod
752 } else {
753 currMethod = DeleteMethod
754 }
755 for _, test := range tests {
756 t.Run(test.description, func(t *testing.T) {
757 newNode := &corev1.Node{}
758 var deletions, evictions int32
759 tf := cmdtesting.NewTestFactory()
760 defer tf.Cleanup()
761
762 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
763 ns := scheme.Codecs.WithoutConversion()
764
765 tf.Client = &fake.RESTClient{
766 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
767 NegotiatedSerializer: ns,
768 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
769 m := &MyReq{req}
770 switch {
771 case req.Method == "GET" && req.URL.Path == "/api":
772 apiVersions := metav1.APIVersions{
773 Versions: []string{"v1"},
774 }
775 return cmdtesting.GenResponseWithJsonEncodedBody(apiVersions)
776 case req.Method == "GET" && req.URL.Path == "/apis":
777 groupList := metav1.APIGroupList{
778 Groups: []metav1.APIGroup{
779 {
780 Name: "policy",
781 PreferredVersion: metav1.GroupVersionForDiscovery{
782 GroupVersion: "policy/v1",
783 },
784 },
785 },
786 }
787 return cmdtesting.GenResponseWithJsonEncodedBody(groupList)
788 case req.Method == "GET" && req.URL.Path == "/api/v1":
789 resourceList := metav1.APIResourceList{
790 GroupVersion: "v1",
791 }
792 if testEviction {
793 resourceList.APIResources = []metav1.APIResource{
794 {
795 Name: drain.EvictionSubresource,
796 Kind: drain.EvictionKind,
797 Group: "policy",
798 Version: "v1",
799 },
800 }
801 }
802 return cmdtesting.GenResponseWithJsonEncodedBody(resourceList)
803 case m.isFor("GET", "/nodes/node"):
804 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.node)}, nil
805 case m.isFor("GET", "/namespaces/default/replicationcontrollers/rc"):
806 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.rcs[0])}, nil
807 case m.isFor("GET", "/namespaces/default/daemonsets/ds"):
808 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &ds)}, nil
809 case m.isFor("GET", "/namespaces/default/daemonsets/missing-ds"):
810 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.DaemonSet{})}, nil
811 case m.isFor("GET", "/namespaces/default/jobs/job"):
812 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &job)}, nil
813 case m.isFor("GET", "/namespaces/default/replicasets/rs"):
814 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.replicaSets[0])}, nil
815 case m.isFor("GET", "/namespaces/default/pods/bar"):
816 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
817 case m.isFor("GET", "/pods"):
818 if test.failUponEvictionOrDeletion && atomic.LoadInt32(&evictions) > 0 || atomic.LoadInt32(&deletions) > 0 {
819 return nil, errors.New("request failed")
820 }
821 values, err := url.ParseQuery(req.URL.RawQuery)
822 if err != nil {
823 t.Fatalf("%s: unexpected error: %v", test.description, err)
824 }
825 getParams := make(url.Values)
826 getParams["fieldSelector"] = []string{"spec.nodeName=node"}
827 getParams["limit"] = []string{"500"}
828 if !reflect.DeepEqual(getParams, values) {
829 t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, getParams, values)
830 }
831 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.PodList{Items: test.pods})}, nil
832 case m.isFor("GET", "/replicationcontrollers"):
833 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationControllerList{Items: test.rcs})}, nil
834 case m.isFor("PATCH", "/nodes/node"):
835 data, err := io.ReadAll(req.Body)
836 if err != nil {
837 t.Fatalf("%s: unexpected error: %v", test.description, err)
838 }
839 defer req.Body.Close()
840 oldJSON, err := runtime.Encode(codec, node)
841 if err != nil {
842 t.Fatalf("%s: unexpected error: %v", test.description, err)
843 }
844 appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{})
845 if err != nil {
846 t.Fatalf("%s: unexpected error: %v", test.description, err)
847 }
848 if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil {
849 t.Fatalf("%s: unexpected error: %v", test.description, err)
850 }
851 if !reflect.DeepEqual(test.expected.Spec, newNode.Spec) {
852 t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec, newNode.Spec)
853 }
854 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
855 case m.isFor("DELETE", "/namespaces/default/pods/bar"):
856 atomic.AddInt32(&deletions, 1)
857 if test.failUponEvictionOrDeletion {
858 return nil, errors.New("request failed")
859 }
860 return &http.Response{StatusCode: http.StatusNoContent, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.pods[0])}, nil
861 case m.isFor("POST", "/namespaces/default/pods/bar/eviction"):
862 atomic.AddInt32(&evictions, 1)
863 if test.failUponEvictionOrDeletion {
864 return nil, errors.New("request failed")
865 }
866 return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &metav1.Status{})}, nil
867 default:
868 t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
869 return nil, nil
870 }
871 }),
872 }
873 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
874
875 ioStreams, _, outBuf, errBuf := genericiooptions.NewTestIOStreams()
876 cmd := NewCmdDrain(tf, ioStreams)
877
878 var recovered interface{}
879 sawFatal := false
880 fatalMsg := ""
881 func() {
882 defer func() {
883
884 recovered = recover()
885
886 cmdutil.DefaultBehaviorOnFatal()
887 }()
888 cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true; fatalMsg = e; panic(e) })
889 cmd.SetArgs(test.args)
890 cmd.Execute()
891 }()
892 switch {
893 case recovered != nil && !sawFatal:
894 t.Fatalf("got panic: %v", recovered)
895 case test.expectFatal && !sawFatal:
896 t.Fatalf("%s: unexpected non-error when using %s", test.description, currMethod)
897 case !test.expectFatal && sawFatal:
898 t.Fatalf("%s: unexpected error when using %s: %s", test.description, currMethod, fatalMsg)
899 }
900
901 deleted := deletions > 0
902 evicted := evictions > 0
903
904 if test.expectDelete {
905
906 if !testEviction && !deleted {
907 t.Fatalf("%s: pod never deleted", test.description)
908 }
909
910 if testEviction {
911 if !evicted {
912 t.Fatalf("%s: pod never evicted", test.description)
913 }
914 if evictions > 1 {
915 t.Fatalf("%s: asked to evict same pod %d too many times", test.description, evictions-1)
916 }
917 }
918 }
919 if !test.expectDelete {
920 if deleted {
921 t.Fatalf("%s: unexpected delete when using %s", test.description, currMethod)
922 }
923 if deletions > 1 {
924 t.Fatalf("%s: asked to deleted same pod %d too many times", test.description, deletions-1)
925 }
926 }
927 if deleted && evicted {
928 t.Fatalf("%s: same pod deleted %d times and evicted %d times", test.description, deletions, evictions)
929 }
930
931 if len(test.expectWarning) > 0 {
932 if len(errBuf.String()) == 0 {
933 t.Fatalf("%s: expected warning, but found no stderr output", test.description)
934 }
935
936
937 if a, e := errBuf.String(), test.expectWarning; !strings.Contains(a, e) {
938 t.Fatalf("%s: actual warning message did not match expected warning message.\n Expecting:\n%v\n Got:\n%v", test.description, e, a)
939 }
940 }
941
942 if len(test.expectOutputToContain) > 0 {
943 out := outBuf.String()
944 if !strings.Contains(out, test.expectOutputToContain) {
945 t.Fatalf("%s: expected output to contain: %s\nGot:\n%s", test.description, test.expectOutputToContain, out)
946 }
947 }
948 })
949 }
950 }
951 }
952
953 type MyReq struct {
954 Request *http.Request
955 }
956
957 func (m *MyReq) isFor(method string, path string) bool {
958 req := m.Request
959
960 return method == req.Method && (req.URL.Path == path ||
961 req.URL.Path == strings.Join([]string{"/api/v1", path}, "") ||
962 req.URL.Path == strings.Join([]string{"/apis/apps/v1", path}, "") ||
963 req.URL.Path == strings.Join([]string{"/apis/batch/v1", path}, ""))
964 }
965
View as plain text