1
15
16 package kube
17
18 import (
19 "context"
20 "testing"
21
22 appsv1 "k8s.io/api/apps/v1"
23 batchv1 "k8s.io/api/batch/v1"
24 corev1 "k8s.io/api/core/v1"
25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26 "k8s.io/apimachinery/pkg/runtime"
27 "k8s.io/apimachinery/pkg/util/intstr"
28 "k8s.io/client-go/kubernetes/fake"
29 )
30
31 const defaultNamespace = metav1.NamespaceDefault
32
33 func Test_ReadyChecker_deploymentReady(t *testing.T) {
34 type args struct {
35 rs *appsv1.ReplicaSet
36 dep *appsv1.Deployment
37 }
38 tests := []struct {
39 name string
40 args args
41 want bool
42 }{
43 {
44 name: "deployment is ready",
45 args: args{
46 rs: newReplicaSet("foo", 1, 1, true),
47 dep: newDeployment("foo", 1, 1, 0, true),
48 },
49 want: true,
50 },
51 {
52 name: "deployment is not ready",
53 args: args{
54 rs: newReplicaSet("foo", 0, 0, true),
55 dep: newDeployment("foo", 1, 1, 0, true),
56 },
57 want: false,
58 },
59 {
60 name: "deployment is ready when maxUnavailable is set",
61 args: args{
62 rs: newReplicaSet("foo", 2, 1, true),
63 dep: newDeployment("foo", 2, 1, 1, true),
64 },
65 want: true,
66 },
67 {
68 name: "deployment is not ready when replicaset generations are out of sync",
69 args: args{
70 rs: newReplicaSet("foo", 1, 1, false),
71 dep: newDeployment("foo", 1, 1, 0, true),
72 },
73 want: false,
74 },
75 {
76 name: "deployment is not ready when deployment generations are out of sync",
77 args: args{
78 rs: newReplicaSet("foo", 1, 1, true),
79 dep: newDeployment("foo", 1, 1, 0, false),
80 },
81 want: false,
82 },
83 {
84 name: "deployment is not ready when generations are out of sync",
85 args: args{
86 rs: newReplicaSet("foo", 1, 1, false),
87 dep: newDeployment("foo", 1, 1, 0, false),
88 },
89 want: false,
90 },
91 }
92 for _, tt := range tests {
93 t.Run(tt.name, func(t *testing.T) {
94 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
95 if got := c.deploymentReady(tt.args.rs, tt.args.dep); got != tt.want {
96 t.Errorf("deploymentReady() = %v, want %v", got, tt.want)
97 }
98 })
99 }
100 }
101
102 func Test_ReadyChecker_replicaSetReady(t *testing.T) {
103 type args struct {
104 rs *appsv1.ReplicaSet
105 }
106 tests := []struct {
107 name string
108 args args
109 want bool
110 }{
111 {
112 name: "replicaSet is ready",
113 args: args{
114 rs: newReplicaSet("foo", 1, 1, true),
115 },
116 want: true,
117 },
118 {
119 name: "replicaSet is not ready when generations are out of sync",
120 args: args{
121 rs: newReplicaSet("foo", 1, 1, false),
122 },
123 want: false,
124 },
125 }
126 for _, tt := range tests {
127 t.Run(tt.name, func(t *testing.T) {
128 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
129 if got := c.replicaSetReady(tt.args.rs); got != tt.want {
130 t.Errorf("replicaSetReady() = %v, want %v", got, tt.want)
131 }
132 })
133 }
134 }
135
136 func Test_ReadyChecker_replicationControllerReady(t *testing.T) {
137 type args struct {
138 rc *corev1.ReplicationController
139 }
140 tests := []struct {
141 name string
142 args args
143 want bool
144 }{
145 {
146 name: "replicationController is ready",
147 args: args{
148 rc: newReplicationController("foo", true),
149 },
150 want: true,
151 },
152 {
153 name: "replicationController is not ready when generations are out of sync",
154 args: args{
155 rc: newReplicationController("foo", false),
156 },
157 want: false,
158 },
159 }
160 for _, tt := range tests {
161 t.Run(tt.name, func(t *testing.T) {
162 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
163 if got := c.replicationControllerReady(tt.args.rc); got != tt.want {
164 t.Errorf("replicationControllerReady() = %v, want %v", got, tt.want)
165 }
166 })
167 }
168 }
169
170 func Test_ReadyChecker_daemonSetReady(t *testing.T) {
171 type args struct {
172 ds *appsv1.DaemonSet
173 }
174 tests := []struct {
175 name string
176 args args
177 want bool
178 }{
179 {
180 name: "daemonset is ready",
181 args: args{
182 ds: newDaemonSet("foo", 0, 1, 1, 1, true),
183 },
184 want: true,
185 },
186 {
187 name: "daemonset is not ready",
188 args: args{
189 ds: newDaemonSet("foo", 0, 0, 1, 1, true),
190 },
191 want: false,
192 },
193 {
194 name: "daemonset pods have not been scheduled successfully",
195 args: args{
196 ds: newDaemonSet("foo", 0, 0, 1, 0, true),
197 },
198 want: false,
199 },
200 {
201 name: "daemonset is ready when maxUnavailable is set",
202 args: args{
203 ds: newDaemonSet("foo", 1, 1, 2, 2, true),
204 },
205 want: true,
206 },
207 {
208 name: "daemonset is not ready when generations are out of sync",
209 args: args{
210 ds: newDaemonSet("foo", 0, 1, 1, 1, false),
211 },
212 want: false,
213 },
214 }
215 for _, tt := range tests {
216 t.Run(tt.name, func(t *testing.T) {
217 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
218 if got := c.daemonSetReady(tt.args.ds); got != tt.want {
219 t.Errorf("daemonSetReady() = %v, want %v", got, tt.want)
220 }
221 })
222 }
223 }
224
225 func Test_ReadyChecker_statefulSetReady(t *testing.T) {
226 type args struct {
227 sts *appsv1.StatefulSet
228 }
229 tests := []struct {
230 name string
231 args args
232 want bool
233 }{
234 {
235 name: "statefulset is ready",
236 args: args{
237 sts: newStatefulSet("foo", 1, 0, 1, 1, true),
238 },
239 want: true,
240 },
241 {
242 name: "statefulset is not ready",
243 args: args{
244 sts: newStatefulSet("foo", 1, 0, 0, 1, true),
245 },
246 want: false,
247 },
248 {
249 name: "statefulset is ready when partition is specified",
250 args: args{
251 sts: newStatefulSet("foo", 2, 1, 2, 1, true),
252 },
253 want: true,
254 },
255 {
256 name: "statefulset is not ready when partition is set",
257 args: args{
258 sts: newStatefulSet("foo", 2, 1, 1, 0, true),
259 },
260 want: false,
261 },
262 {
263 name: "statefulset is ready when partition is set and no change in template",
264 args: args{
265 sts: newStatefulSet("foo", 2, 1, 2, 2, true),
266 },
267 want: true,
268 },
269 {
270 name: "statefulset is ready when partition is greater than replicas",
271 args: args{
272 sts: newStatefulSet("foo", 1, 2, 1, 1, true),
273 },
274 want: true,
275 },
276 {
277 name: "statefulset is not ready when generations are out of sync",
278 args: args{
279 sts: newStatefulSet("foo", 1, 0, 1, 1, false),
280 },
281 want: false,
282 },
283 {
284 name: "statefulset is ready when current revision for current replicas does not match update revision for updated replicas when using partition !=0",
285 args: args{
286 sts: newStatefulSetWithUpdateRevision("foo", 3, 2, 3, 3, "foo-bbbbbbb", true),
287 },
288 want: true,
289 },
290 }
291 for _, tt := range tests {
292 t.Run(tt.name, func(t *testing.T) {
293 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
294 if got := c.statefulSetReady(tt.args.sts); got != tt.want {
295 t.Errorf("statefulSetReady() = %v, want %v", got, tt.want)
296 }
297 })
298 }
299 }
300
301 func Test_ReadyChecker_podsReadyForObject(t *testing.T) {
302 type args struct {
303 namespace string
304 obj runtime.Object
305 }
306 tests := []struct {
307 name string
308 args args
309 existPods []corev1.Pod
310 want bool
311 wantErr bool
312 }{
313 {
314 name: "pods ready for a replicaset",
315 args: args{
316 namespace: defaultNamespace,
317 obj: newReplicaSet("foo", 1, 1, true),
318 },
319 existPods: []corev1.Pod{
320 *newPodWithCondition("foo", corev1.ConditionTrue),
321 },
322 want: true,
323 wantErr: false,
324 },
325 {
326 name: "pods not ready for a replicaset",
327 args: args{
328 namespace: defaultNamespace,
329 obj: newReplicaSet("foo", 1, 1, true),
330 },
331 existPods: []corev1.Pod{
332 *newPodWithCondition("foo", corev1.ConditionFalse),
333 },
334 want: false,
335 wantErr: false,
336 },
337 }
338 for _, tt := range tests {
339 t.Run(tt.name, func(t *testing.T) {
340 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
341 for _, pod := range tt.existPods {
342 if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(context.TODO(), &pod, metav1.CreateOptions{}); err != nil {
343 t.Errorf("Failed to create Pod error: %v", err)
344 return
345 }
346 }
347 got, err := c.podsReadyForObject(context.TODO(), tt.args.namespace, tt.args.obj)
348 if (err != nil) != tt.wantErr {
349 t.Errorf("podsReadyForObject() error = %v, wantErr %v", err, tt.wantErr)
350 return
351 }
352 if got != tt.want {
353 t.Errorf("podsReadyForObject() got = %v, want %v", got, tt.want)
354 }
355 })
356 }
357 }
358
359 func Test_ReadyChecker_jobReady(t *testing.T) {
360 type args struct {
361 job *batchv1.Job
362 }
363 tests := []struct {
364 name string
365 args args
366 want bool
367 wantErr bool
368 }{
369 {
370 name: "job is completed",
371 args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
372 want: true,
373 wantErr: false,
374 },
375 {
376 name: "job is incomplete",
377 args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
378 want: false,
379 wantErr: false,
380 },
381 {
382 name: "job is failed but within BackoffLimit",
383 args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
384 want: false,
385 wantErr: false,
386 },
387 {
388 name: "job is completed with retry",
389 args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
390 want: true,
391 wantErr: false,
392 },
393 {
394 name: "job is failed and beyond BackoffLimit",
395 args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
396 want: false,
397 wantErr: true,
398 },
399 {
400 name: "job is completed single run",
401 args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
402 want: true,
403 wantErr: false,
404 },
405 {
406 name: "job is failed single run",
407 args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
408 want: false,
409 wantErr: true,
410 },
411 {
412 name: "job with null completions",
413 args: args{job: newJob("foo", 0, nil, 1, 0)},
414 want: true,
415 },
416 }
417 for _, tt := range tests {
418 t.Run(tt.name, func(t *testing.T) {
419 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
420 got, err := c.jobReady(tt.args.job)
421 if (err != nil) != tt.wantErr {
422 t.Errorf("jobReady() error = %v, wantErr %v", err, tt.wantErr)
423 return
424 }
425 if got != tt.want {
426 t.Errorf("jobReady() = %v, want %v", got, tt.want)
427 }
428 })
429 }
430 }
431
432 func Test_ReadyChecker_volumeReady(t *testing.T) {
433 type args struct {
434 v *corev1.PersistentVolumeClaim
435 }
436 tests := []struct {
437 name string
438 args args
439 want bool
440 }{
441 {
442 name: "pvc is bound",
443 args: args{
444 v: newPersistentVolumeClaim("foo", corev1.ClaimBound),
445 },
446 want: true,
447 },
448 {
449 name: "pvc is not ready",
450 args: args{
451 v: newPersistentVolumeClaim("foo", corev1.ClaimPending),
452 },
453 want: false,
454 },
455 }
456 for _, tt := range tests {
457 t.Run(tt.name, func(t *testing.T) {
458 c := NewReadyChecker(fake.NewSimpleClientset(), nil)
459 if got := c.volumeReady(tt.args.v); got != tt.want {
460 t.Errorf("volumeReady() = %v, want %v", got, tt.want)
461 }
462 })
463 }
464 }
465
466 func newStatefulSetWithUpdateRevision(name string, replicas, partition, readyReplicas, updatedReplicas int, updateRevision string, generationInSync bool) *appsv1.StatefulSet {
467 ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas, generationInSync)
468 ss.Status.UpdateRevision = updateRevision
469 return ss
470 }
471
472 func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberScheduled, updatedNumberScheduled int, generationInSync bool) *appsv1.DaemonSet {
473 var generation, observedGeneration int64 = 1, 1
474 if !generationInSync {
475 generation = 2
476 }
477 return &appsv1.DaemonSet{
478 ObjectMeta: metav1.ObjectMeta{
479 Name: name,
480 Namespace: defaultNamespace,
481 Generation: generation,
482 },
483 Spec: appsv1.DaemonSetSpec{
484 UpdateStrategy: appsv1.DaemonSetUpdateStrategy{
485 Type: appsv1.RollingUpdateDaemonSetStrategyType,
486 RollingUpdate: &appsv1.RollingUpdateDaemonSet{
487 MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromInt(maxUnavailable); return &i }(),
488 },
489 },
490 Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}},
491 Template: corev1.PodTemplateSpec{
492 ObjectMeta: metav1.ObjectMeta{
493 Name: name,
494 Labels: map[string]string{"name": name},
495 },
496 Spec: corev1.PodSpec{
497 Containers: []corev1.Container{
498 {
499 Image: "nginx",
500 },
501 },
502 },
503 },
504 },
505 Status: appsv1.DaemonSetStatus{
506 DesiredNumberScheduled: int32(desiredNumberScheduled),
507 NumberReady: int32(numberReady),
508 UpdatedNumberScheduled: int32(updatedNumberScheduled),
509 ObservedGeneration: observedGeneration,
510 },
511 }
512 }
513
514 func newStatefulSet(name string, replicas, partition, readyReplicas, updatedReplicas int, generationInSync bool) *appsv1.StatefulSet {
515 var generation, observedGeneration int64 = 1, 1
516 if !generationInSync {
517 generation = 2
518 }
519 return &appsv1.StatefulSet{
520 ObjectMeta: metav1.ObjectMeta{
521 Name: name,
522 Namespace: defaultNamespace,
523 Generation: generation,
524 },
525 Spec: appsv1.StatefulSetSpec{
526 UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
527 Type: appsv1.RollingUpdateStatefulSetStrategyType,
528 RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
529 Partition: intToInt32(partition),
530 },
531 },
532 Replicas: intToInt32(replicas),
533 Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}},
534 Template: corev1.PodTemplateSpec{
535 ObjectMeta: metav1.ObjectMeta{
536 Name: name,
537 Labels: map[string]string{"name": name},
538 },
539 Spec: corev1.PodSpec{
540 Containers: []corev1.Container{
541 {
542 Image: "nginx",
543 },
544 },
545 },
546 },
547 },
548 Status: appsv1.StatefulSetStatus{
549 UpdatedReplicas: int32(updatedReplicas),
550 ReadyReplicas: int32(readyReplicas),
551 ObservedGeneration: observedGeneration,
552 },
553 }
554 }
555
556 func newDeployment(name string, replicas, maxSurge, maxUnavailable int, generationInSync bool) *appsv1.Deployment {
557 var generation, observedGeneration int64 = 1, 1
558 if !generationInSync {
559 generation = 2
560 }
561 return &appsv1.Deployment{
562 ObjectMeta: metav1.ObjectMeta{
563 Name: name,
564 Namespace: defaultNamespace,
565 Generation: generation,
566 },
567 Spec: appsv1.DeploymentSpec{
568 Strategy: appsv1.DeploymentStrategy{
569 Type: appsv1.RollingUpdateDeploymentStrategyType,
570 RollingUpdate: &appsv1.RollingUpdateDeployment{
571 MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromInt(maxUnavailable); return &i }(),
572 MaxSurge: func() *intstr.IntOrString { i := intstr.FromInt(maxSurge); return &i }(),
573 },
574 },
575 Replicas: intToInt32(replicas),
576 Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}},
577 Template: corev1.PodTemplateSpec{
578 ObjectMeta: metav1.ObjectMeta{
579 Name: name,
580 Labels: map[string]string{"name": name},
581 },
582 Spec: corev1.PodSpec{
583 Containers: []corev1.Container{
584 {
585 Image: "nginx",
586 },
587 },
588 },
589 },
590 },
591 Status: appsv1.DeploymentStatus{
592 ObservedGeneration: observedGeneration,
593 },
594 }
595 }
596
597 func newReplicationController(name string, generationInSync bool) *corev1.ReplicationController {
598 var generation, observedGeneration int64 = 1, 1
599 if !generationInSync {
600 generation = 2
601 }
602 return &corev1.ReplicationController{
603 ObjectMeta: metav1.ObjectMeta{
604 Name: name,
605 Generation: generation,
606 },
607 Status: corev1.ReplicationControllerStatus{
608 ObservedGeneration: observedGeneration,
609 },
610 }
611 }
612
613 func newReplicaSet(name string, replicas int, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet {
614 d := newDeployment(name, replicas, 0, 0, generationInSync)
615 return &appsv1.ReplicaSet{
616 ObjectMeta: metav1.ObjectMeta{
617 Name: name,
618 Namespace: defaultNamespace,
619 Labels: d.Spec.Selector.MatchLabels,
620 OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(d, d.GroupVersionKind())},
621 Generation: d.Generation,
622 },
623 Spec: appsv1.ReplicaSetSpec{
624 Selector: d.Spec.Selector,
625 Replicas: intToInt32(replicas),
626 Template: d.Spec.Template,
627 },
628 Status: appsv1.ReplicaSetStatus{
629 ReadyReplicas: int32(readyReplicas),
630 ObservedGeneration: d.Status.ObservedGeneration,
631 },
632 }
633 }
634
635 func newPodWithCondition(name string, podReadyCondition corev1.ConditionStatus) *corev1.Pod {
636 return &corev1.Pod{
637 ObjectMeta: metav1.ObjectMeta{
638 Name: name,
639 Namespace: defaultNamespace,
640 Labels: map[string]string{"name": name},
641 },
642 Spec: corev1.PodSpec{
643 Containers: []corev1.Container{
644 {
645 Image: "nginx",
646 },
647 },
648 },
649 Status: corev1.PodStatus{
650 Conditions: []corev1.PodCondition{
651 {
652 Type: corev1.PodReady,
653 Status: podReadyCondition,
654 },
655 },
656 },
657 }
658 }
659
660 func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPhase) *corev1.PersistentVolumeClaim {
661 return &corev1.PersistentVolumeClaim{
662 ObjectMeta: metav1.ObjectMeta{
663 Name: name,
664 Namespace: defaultNamespace,
665 },
666 Status: corev1.PersistentVolumeClaimStatus{
667 Phase: phase,
668 },
669 }
670 }
671
672 func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job {
673 return &batchv1.Job{
674 ObjectMeta: metav1.ObjectMeta{
675 Name: name,
676 Namespace: defaultNamespace,
677 },
678 Spec: batchv1.JobSpec{
679 BackoffLimit: intToInt32(backoffLimit),
680 Completions: completions,
681 Template: corev1.PodTemplateSpec{
682 ObjectMeta: metav1.ObjectMeta{
683 Name: name,
684 Labels: map[string]string{"name": name},
685 },
686 Spec: corev1.PodSpec{
687 Containers: []corev1.Container{
688 {
689 Image: "nginx",
690 },
691 },
692 },
693 },
694 },
695 Status: batchv1.JobStatus{
696 Succeeded: int32(succeeded),
697 Failed: int32(failed),
698 },
699 }
700 }
701
702 func intToInt32(i int) *int32 {
703 i32 := int32(i)
704 return &i32
705 }
706
View as plain text