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