volcano.sh/volcano@v1.9.0/pkg/webhooks/admission/jobs/validate/admit_job_test.go (about) 1 /* 2 Copyright 2019 The Volcano 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 validate 18 19 import ( 20 "context" 21 "strings" 22 "testing" 23 24 admissionv1 "k8s.io/api/admission/v1" 25 26 v1 "k8s.io/api/core/v1" 27 "k8s.io/apimachinery/pkg/api/resource" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 30 "volcano.sh/apis/pkg/apis/batch/v1alpha1" 31 busv1alpha1 "volcano.sh/apis/pkg/apis/bus/v1alpha1" 32 schedulingv1beta2 "volcano.sh/apis/pkg/apis/scheduling/v1beta1" 33 fakeclient "volcano.sh/apis/pkg/client/clientset/versioned/fake" 34 ) 35 36 func TestValidateJobCreate(t *testing.T) { 37 var invTTL int32 = -1 38 var policyExitCode int32 = -1 39 var invMinAvailable int32 = -1 40 namespace := "test" 41 priviledged := true 42 43 testCases := []struct { 44 Name string 45 Job v1alpha1.Job 46 ExpectErr bool 47 reviewResponse admissionv1.AdmissionResponse 48 ret string 49 }{ 50 { 51 Name: "validate valid-job", 52 Job: v1alpha1.Job{ 53 ObjectMeta: metav1.ObjectMeta{ 54 Name: "valid-job", 55 Namespace: namespace, 56 }, 57 Spec: v1alpha1.JobSpec{ 58 MinAvailable: 1, 59 Queue: "default", 60 Tasks: []v1alpha1.TaskSpec{ 61 { 62 Name: "task-1", 63 Replicas: 1, 64 Template: v1.PodTemplateSpec{ 65 ObjectMeta: metav1.ObjectMeta{ 66 Labels: map[string]string{"name": "test"}, 67 }, 68 Spec: v1.PodSpec{ 69 Containers: []v1.Container{ 70 { 71 Name: "fake-name", 72 Image: "busybox:1.24", 73 }, 74 }, 75 }, 76 }, 77 }, 78 }, 79 Policies: []v1alpha1.LifecyclePolicy{ 80 { 81 Event: busv1alpha1.PodEvictedEvent, 82 Action: busv1alpha1.RestartTaskAction, 83 }, 84 }, 85 }, 86 }, 87 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 88 ret: "", 89 ExpectErr: false, 90 }, 91 // duplicate task name 92 { 93 Name: "duplicate-task-job", 94 Job: v1alpha1.Job{ 95 ObjectMeta: metav1.ObjectMeta{ 96 Name: "duplicate-task-job", 97 Namespace: namespace, 98 }, 99 Spec: v1alpha1.JobSpec{ 100 MinAvailable: 1, 101 Queue: "default", 102 Tasks: []v1alpha1.TaskSpec{ 103 { 104 Name: "duplicated-task-1", 105 Replicas: 1, 106 Template: v1.PodTemplateSpec{ 107 ObjectMeta: metav1.ObjectMeta{ 108 Labels: map[string]string{"name": "test"}, 109 }, 110 Spec: v1.PodSpec{ 111 Containers: []v1.Container{ 112 { 113 Name: "fake-name", 114 Image: "busybox:1.24", 115 }, 116 }, 117 }, 118 }, 119 }, 120 { 121 Name: "duplicated-task-1", 122 Replicas: 1, 123 Template: v1.PodTemplateSpec{ 124 ObjectMeta: metav1.ObjectMeta{ 125 Labels: map[string]string{"name": "test"}, 126 }, 127 Spec: v1.PodSpec{ 128 Containers: []v1.Container{ 129 { 130 Name: "fake-name", 131 Image: "busybox:1.24", 132 }, 133 }, 134 }, 135 }, 136 }, 137 }, 138 }, 139 }, 140 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 141 ret: "duplicated task name duplicated-task-1", 142 ExpectErr: true, 143 }, 144 // Duplicated Policy Event 145 { 146 Name: "job-policy-duplicated", 147 Job: v1alpha1.Job{ 148 ObjectMeta: metav1.ObjectMeta{ 149 Name: "job-policy-duplicated", 150 Namespace: namespace, 151 }, 152 Spec: v1alpha1.JobSpec{ 153 MinAvailable: 1, 154 Queue: "default", 155 Tasks: []v1alpha1.TaskSpec{ 156 { 157 Name: "task-1", 158 Replicas: 1, 159 Template: v1.PodTemplateSpec{ 160 ObjectMeta: metav1.ObjectMeta{ 161 Labels: map[string]string{"name": "test"}, 162 }, 163 Spec: v1.PodSpec{ 164 Containers: []v1.Container{ 165 { 166 Name: "fake-name", 167 Image: "busybox:1.24", 168 }, 169 }, 170 }, 171 }, 172 }, 173 }, 174 Policies: []v1alpha1.LifecyclePolicy{ 175 { 176 Event: busv1alpha1.PodFailedEvent, 177 Action: busv1alpha1.AbortJobAction, 178 }, 179 { 180 Event: busv1alpha1.PodFailedEvent, 181 Action: busv1alpha1.RestartJobAction, 182 }, 183 }, 184 }, 185 }, 186 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 187 ret: "duplicate", 188 ExpectErr: true, 189 }, 190 // Min Available illegal 191 { 192 Name: "Min Available illegal", 193 Job: v1alpha1.Job{ 194 ObjectMeta: metav1.ObjectMeta{ 195 Name: "job-min-illegal", 196 Namespace: namespace, 197 }, 198 Spec: v1alpha1.JobSpec{ 199 MinAvailable: 2, 200 Queue: "default", 201 Tasks: []v1alpha1.TaskSpec{ 202 { 203 Name: "task-1", 204 Replicas: 1, 205 Template: v1.PodTemplateSpec{ 206 ObjectMeta: metav1.ObjectMeta{ 207 Labels: map[string]string{"name": "test"}, 208 }, 209 Spec: v1.PodSpec{ 210 Containers: []v1.Container{ 211 { 212 Name: "fake-name", 213 Image: "busybox:1.24", 214 }, 215 }, 216 }, 217 }, 218 }, 219 }, 220 }, 221 }, 222 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 223 ret: "job 'minAvailable' should not be greater than total replicas in tasks", 224 ExpectErr: true, 225 }, 226 // Job Plugin illegal 227 { 228 Name: "Job Plugin illegal", 229 Job: v1alpha1.Job{ 230 ObjectMeta: metav1.ObjectMeta{ 231 Name: "job-plugin-illegal", 232 Namespace: namespace, 233 }, 234 Spec: v1alpha1.JobSpec{ 235 MinAvailable: 1, 236 Queue: "default", 237 Tasks: []v1alpha1.TaskSpec{ 238 { 239 Name: "task-1", 240 Replicas: 1, 241 Template: v1.PodTemplateSpec{ 242 ObjectMeta: metav1.ObjectMeta{ 243 Labels: map[string]string{"name": "test"}, 244 }, 245 Spec: v1.PodSpec{ 246 Containers: []v1.Container{ 247 { 248 Name: "fake-name", 249 Image: "busybox:1.24", 250 }, 251 }, 252 }, 253 }, 254 }, 255 }, 256 Plugins: map[string][]string{ 257 "big_plugin": {}, 258 }, 259 }, 260 }, 261 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 262 ret: "unable to find job plugin: big_plugin", 263 ExpectErr: true, 264 }, 265 // ttl-illegal 266 { 267 Name: "job-ttl-illegal", 268 Job: v1alpha1.Job{ 269 ObjectMeta: metav1.ObjectMeta{ 270 Name: "job-ttl-illegal", 271 Namespace: namespace, 272 }, 273 Spec: v1alpha1.JobSpec{ 274 MinAvailable: 1, 275 Queue: "default", 276 Tasks: []v1alpha1.TaskSpec{ 277 { 278 Name: "task-1", 279 Replicas: 1, 280 Template: v1.PodTemplateSpec{ 281 ObjectMeta: metav1.ObjectMeta{ 282 Labels: map[string]string{"name": "test"}, 283 }, 284 Spec: v1.PodSpec{ 285 Containers: []v1.Container{ 286 { 287 Name: "fake-name", 288 Image: "busybox:1.24", 289 }, 290 }, 291 }, 292 }, 293 }, 294 }, 295 TTLSecondsAfterFinished: &invTTL, 296 }, 297 }, 298 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 299 ret: "'ttlSecondsAfterFinished' cannot be less than zero", 300 ExpectErr: true, 301 }, 302 // min-MinAvailable less than zero 303 { 304 Name: "minAvailable-lessThanZero", 305 Job: v1alpha1.Job{ 306 ObjectMeta: metav1.ObjectMeta{ 307 Name: "minAvailable-lessThanZero", 308 Namespace: namespace, 309 }, 310 Spec: v1alpha1.JobSpec{ 311 MinAvailable: -1, 312 Queue: "default", 313 Tasks: []v1alpha1.TaskSpec{ 314 { 315 Name: "task-1", 316 Replicas: 1, 317 Template: v1.PodTemplateSpec{ 318 ObjectMeta: metav1.ObjectMeta{ 319 Labels: map[string]string{"name": "test"}, 320 }, 321 Spec: v1.PodSpec{ 322 Containers: []v1.Container{ 323 { 324 Name: "fake-name", 325 Image: "busybox:1.24", 326 }, 327 }, 328 }, 329 }, 330 }, 331 }, 332 }, 333 }, 334 reviewResponse: admissionv1.AdmissionResponse{Allowed: false}, 335 ret: "job 'minAvailable' must be >= 0", 336 ExpectErr: true, 337 }, 338 // maxretry less than zero 339 { 340 Name: "maxretry-lessThanZero", 341 Job: v1alpha1.Job{ 342 ObjectMeta: metav1.ObjectMeta{ 343 Name: "maxretry-lessThanZero", 344 Namespace: namespace, 345 }, 346 Spec: v1alpha1.JobSpec{ 347 MinAvailable: 1, 348 MaxRetry: -1, 349 Queue: "default", 350 Tasks: []v1alpha1.TaskSpec{ 351 { 352 Name: "task-1", 353 Replicas: 1, 354 Template: v1.PodTemplateSpec{ 355 ObjectMeta: metav1.ObjectMeta{ 356 Labels: map[string]string{"name": "test"}, 357 }, 358 Spec: v1.PodSpec{ 359 Containers: []v1.Container{ 360 { 361 Name: "fake-name", 362 Image: "busybox:1.24", 363 }, 364 }, 365 }, 366 }, 367 }, 368 }, 369 }, 370 }, 371 reviewResponse: admissionv1.AdmissionResponse{Allowed: false}, 372 ret: "'maxRetry' cannot be less than zero.", 373 ExpectErr: true, 374 }, 375 // no task specified in the job 376 { 377 Name: "no-task", 378 Job: v1alpha1.Job{ 379 ObjectMeta: metav1.ObjectMeta{ 380 Name: "no-task", 381 Namespace: namespace, 382 }, 383 Spec: v1alpha1.JobSpec{ 384 MinAvailable: 1, 385 Queue: "default", 386 Tasks: []v1alpha1.TaskSpec{}, 387 }, 388 }, 389 reviewResponse: admissionv1.AdmissionResponse{Allowed: false}, 390 ret: "No task specified in job spec", 391 ExpectErr: true, 392 }, 393 // replica set less than zero 394 { 395 Name: "replica-lessThanZero", 396 Job: v1alpha1.Job{ 397 ObjectMeta: metav1.ObjectMeta{ 398 Name: "replica-lessThanZero", 399 Namespace: namespace, 400 }, 401 Spec: v1alpha1.JobSpec{ 402 MinAvailable: 1, 403 Queue: "default", 404 Tasks: []v1alpha1.TaskSpec{ 405 { 406 Name: "task-1", 407 Replicas: -1, 408 Template: v1.PodTemplateSpec{ 409 ObjectMeta: metav1.ObjectMeta{ 410 Labels: map[string]string{"name": "test"}, 411 }, 412 Spec: v1.PodSpec{ 413 Containers: []v1.Container{ 414 { 415 Name: "fake-name", 416 Image: "busybox:1.24", 417 }, 418 }, 419 }, 420 }, 421 }, 422 }, 423 }, 424 }, 425 reviewResponse: admissionv1.AdmissionResponse{Allowed: false}, 426 ret: "'replicas' < 0 in task: task-1, job: replica-lessThanZero; job 'minAvailable' should not be greater than total replicas in tasks;", 427 ExpectErr: true, 428 }, 429 // task minAvailable set less than zero 430 { 431 Name: "replica-lessThanZero", 432 Job: v1alpha1.Job{ 433 ObjectMeta: metav1.ObjectMeta{ 434 Name: "taskMinAvailable-lessThanZero", 435 Namespace: namespace, 436 }, 437 Spec: v1alpha1.JobSpec{ 438 MinAvailable: 1, 439 Queue: "default", 440 Tasks: []v1alpha1.TaskSpec{ 441 { 442 Name: "task-1", 443 Replicas: 1, 444 MinAvailable: &invMinAvailable, 445 Template: v1.PodTemplateSpec{ 446 ObjectMeta: metav1.ObjectMeta{ 447 Labels: map[string]string{"name": "test"}, 448 }, 449 Spec: v1.PodSpec{ 450 Containers: []v1.Container{ 451 { 452 Name: "fake-name", 453 Image: "busybox:1.24", 454 }, 455 }, 456 }, 457 }, 458 }, 459 }, 460 }, 461 }, 462 reviewResponse: admissionv1.AdmissionResponse{Allowed: false}, 463 ret: "'minAvailable' < 0 in task: task-1, job: taskMinAvailable-lessThanZero;", 464 ExpectErr: true, 465 }, 466 // task name error 467 { 468 Name: "nonDNS-task", 469 Job: v1alpha1.Job{ 470 ObjectMeta: metav1.ObjectMeta{ 471 Name: "replica-lessThanZero", 472 Namespace: namespace, 473 }, 474 Spec: v1alpha1.JobSpec{ 475 MinAvailable: 1, 476 Queue: "default", 477 Tasks: []v1alpha1.TaskSpec{ 478 { 479 Name: "Task-1", 480 Replicas: 1, 481 Template: v1.PodTemplateSpec{ 482 ObjectMeta: metav1.ObjectMeta{ 483 Labels: map[string]string{"name": "test"}, 484 }, 485 Spec: v1.PodSpec{ 486 Containers: []v1.Container{ 487 { 488 Name: "fake-name", 489 Image: "busybox:1.24", 490 }, 491 }, 492 }, 493 }, 494 }, 495 }, 496 }, 497 }, 498 reviewResponse: admissionv1.AdmissionResponse{Allowed: false}, 499 ret: "[a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')];", 500 ExpectErr: true, 501 }, 502 // Policy Event with exit code 503 { 504 Name: "job-policy-withExitCode", 505 Job: v1alpha1.Job{ 506 ObjectMeta: metav1.ObjectMeta{ 507 Name: "job-policy-withExitCode", 508 Namespace: namespace, 509 }, 510 Spec: v1alpha1.JobSpec{ 511 MinAvailable: 1, 512 Queue: "default", 513 Tasks: []v1alpha1.TaskSpec{ 514 { 515 Name: "task-1", 516 Replicas: 1, 517 Template: v1.PodTemplateSpec{ 518 ObjectMeta: metav1.ObjectMeta{ 519 Labels: map[string]string{"name": "test"}, 520 }, 521 Spec: v1.PodSpec{ 522 Containers: []v1.Container{ 523 { 524 Name: "fake-name", 525 Image: "busybox:1.24", 526 }, 527 }, 528 }, 529 }, 530 }, 531 }, 532 Policies: []v1alpha1.LifecyclePolicy{ 533 { 534 Event: busv1alpha1.PodFailedEvent, 535 Action: busv1alpha1.AbortJobAction, 536 ExitCode: &policyExitCode, 537 }, 538 }, 539 }, 540 }, 541 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 542 ret: "must not specify event and exitCode simultaneously", 543 ExpectErr: true, 544 }, 545 // Both policy event and exit code are nil 546 { 547 Name: "policy-noEvent-noExCode", 548 Job: v1alpha1.Job{ 549 ObjectMeta: metav1.ObjectMeta{ 550 Name: "policy-noEvent-noExCode", 551 Namespace: namespace, 552 }, 553 Spec: v1alpha1.JobSpec{ 554 MinAvailable: 1, 555 Queue: "default", 556 Tasks: []v1alpha1.TaskSpec{ 557 { 558 Name: "task-1", 559 Replicas: 1, 560 Template: v1.PodTemplateSpec{ 561 ObjectMeta: metav1.ObjectMeta{ 562 Labels: map[string]string{"name": "test"}, 563 }, 564 Spec: v1.PodSpec{ 565 Containers: []v1.Container{ 566 { 567 Name: "fake-name", 568 Image: "busybox:1.24", 569 }, 570 }, 571 }, 572 }, 573 }, 574 }, 575 Policies: []v1alpha1.LifecyclePolicy{ 576 { 577 Action: busv1alpha1.AbortJobAction, 578 }, 579 }, 580 }, 581 }, 582 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 583 ret: "either event and exitCode should be specified", 584 ExpectErr: true, 585 }, 586 // invalid policy event 587 { 588 Name: "invalid-policy-event", 589 Job: v1alpha1.Job{ 590 ObjectMeta: metav1.ObjectMeta{ 591 Name: "invalid-policy-event", 592 Namespace: namespace, 593 }, 594 Spec: v1alpha1.JobSpec{ 595 MinAvailable: 1, 596 Queue: "default", 597 Tasks: []v1alpha1.TaskSpec{ 598 { 599 Name: "task-1", 600 Replicas: 1, 601 Template: v1.PodTemplateSpec{ 602 ObjectMeta: metav1.ObjectMeta{ 603 Labels: map[string]string{"name": "test"}, 604 }, 605 Spec: v1.PodSpec{ 606 Containers: []v1.Container{ 607 { 608 Name: "fake-name", 609 Image: "busybox:1.24", 610 }, 611 }, 612 }, 613 }, 614 }, 615 }, 616 Policies: []v1alpha1.LifecyclePolicy{ 617 { 618 Event: busv1alpha1.Event("someFakeEvent"), 619 Action: busv1alpha1.AbortJobAction, 620 }, 621 }, 622 }, 623 }, 624 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 625 ret: "invalid policy event", 626 ExpectErr: true, 627 }, 628 // invalid policy action 629 { 630 Name: "invalid-policy-action", 631 Job: v1alpha1.Job{ 632 ObjectMeta: metav1.ObjectMeta{ 633 Name: "invalid-policy-action", 634 Namespace: namespace, 635 }, 636 Spec: v1alpha1.JobSpec{ 637 MinAvailable: 1, 638 Queue: "default", 639 Tasks: []v1alpha1.TaskSpec{ 640 { 641 Name: "task-1", 642 Replicas: 1, 643 Template: v1.PodTemplateSpec{ 644 ObjectMeta: metav1.ObjectMeta{ 645 Labels: map[string]string{"name": "test"}, 646 }, 647 Spec: v1.PodSpec{ 648 Containers: []v1.Container{ 649 { 650 Name: "fake-name", 651 Image: "busybox:1.24", 652 }, 653 }, 654 }, 655 }, 656 }, 657 }, 658 Policies: []v1alpha1.LifecyclePolicy{ 659 { 660 Event: busv1alpha1.PodEvictedEvent, 661 Action: busv1alpha1.Action("someFakeAction"), 662 }, 663 }, 664 }, 665 }, 666 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 667 ret: "invalid policy action", 668 ExpectErr: true, 669 }, 670 // policy exit-code zero 671 { 672 Name: "policy-extcode-zero", 673 Job: v1alpha1.Job{ 674 ObjectMeta: metav1.ObjectMeta{ 675 Name: "policy-extcode-zero", 676 Namespace: namespace, 677 }, 678 Spec: v1alpha1.JobSpec{ 679 MinAvailable: 1, 680 Queue: "default", 681 Tasks: []v1alpha1.TaskSpec{ 682 { 683 Name: "task-1", 684 Replicas: 1, 685 Template: v1.PodTemplateSpec{ 686 ObjectMeta: metav1.ObjectMeta{ 687 Labels: map[string]string{"name": "test"}, 688 }, 689 Spec: v1.PodSpec{ 690 Containers: []v1.Container{ 691 { 692 Name: "fake-name", 693 Image: "busybox:1.24", 694 }, 695 }, 696 }, 697 }, 698 }, 699 }, 700 Policies: []v1alpha1.LifecyclePolicy{ 701 { 702 Action: busv1alpha1.AbortJobAction, 703 ExitCode: func(i int32) *int32 { 704 return &i 705 }(int32(0)), 706 }, 707 }, 708 }, 709 }, 710 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 711 ret: "0 is not a valid error code", 712 ExpectErr: true, 713 }, 714 // duplicate policy exit-code 715 { 716 Name: "duplicate-exitcode", 717 Job: v1alpha1.Job{ 718 ObjectMeta: metav1.ObjectMeta{ 719 Name: "duplicate-exitcode", 720 Namespace: namespace, 721 }, 722 Spec: v1alpha1.JobSpec{ 723 MinAvailable: 1, 724 Queue: "default", 725 Tasks: []v1alpha1.TaskSpec{ 726 { 727 Name: "task-1", 728 Replicas: 1, 729 Template: v1.PodTemplateSpec{ 730 ObjectMeta: metav1.ObjectMeta{ 731 Labels: map[string]string{"name": "test"}, 732 }, 733 Spec: v1.PodSpec{ 734 Containers: []v1.Container{ 735 { 736 Name: "fake-name", 737 Image: "busybox:1.24", 738 }, 739 }, 740 }, 741 }, 742 }, 743 }, 744 Policies: []v1alpha1.LifecyclePolicy{ 745 { 746 ExitCode: func(i int32) *int32 { 747 return &i 748 }(int32(1)), 749 }, 750 { 751 ExitCode: func(i int32) *int32 { 752 return &i 753 }(int32(1)), 754 }, 755 }, 756 }, 757 }, 758 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 759 ret: "duplicate exitCode 1", 760 ExpectErr: true, 761 }, 762 // Policy with any event and other events 763 { 764 Name: "job-policy-withExitCode", 765 Job: v1alpha1.Job{ 766 ObjectMeta: metav1.ObjectMeta{ 767 Name: "job-policy-withExitCode", 768 Namespace: namespace, 769 }, 770 Spec: v1alpha1.JobSpec{ 771 MinAvailable: 1, 772 Queue: "default", 773 Tasks: []v1alpha1.TaskSpec{ 774 { 775 Name: "task-1", 776 Replicas: 1, 777 Template: v1.PodTemplateSpec{ 778 ObjectMeta: metav1.ObjectMeta{ 779 Labels: map[string]string{"name": "test"}, 780 }, 781 Spec: v1.PodSpec{ 782 Containers: []v1.Container{ 783 { 784 Name: "fake-name", 785 Image: "busybox:1.24", 786 }, 787 }, 788 }, 789 }, 790 }, 791 }, 792 Policies: []v1alpha1.LifecyclePolicy{ 793 { 794 Event: busv1alpha1.AnyEvent, 795 Action: busv1alpha1.AbortJobAction, 796 }, 797 { 798 Event: busv1alpha1.PodFailedEvent, 799 Action: busv1alpha1.RestartJobAction, 800 }, 801 }, 802 }, 803 }, 804 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 805 ret: "if there's * here, no other policy should be here", 806 ExpectErr: true, 807 }, 808 // invalid mount volume 809 { 810 Name: "invalid-mount-volume", 811 Job: v1alpha1.Job{ 812 ObjectMeta: metav1.ObjectMeta{ 813 Name: "invalid-mount-volume", 814 Namespace: namespace, 815 }, 816 Spec: v1alpha1.JobSpec{ 817 MinAvailable: 1, 818 Queue: "default", 819 Tasks: []v1alpha1.TaskSpec{ 820 { 821 Name: "task-1", 822 Replicas: 1, 823 Template: v1.PodTemplateSpec{ 824 ObjectMeta: metav1.ObjectMeta{ 825 Labels: map[string]string{"name": "test"}, 826 }, 827 Spec: v1.PodSpec{ 828 Containers: []v1.Container{ 829 { 830 Name: "fake-name", 831 Image: "busybox:1.24", 832 }, 833 }, 834 }, 835 }, 836 }, 837 }, 838 Policies: []v1alpha1.LifecyclePolicy{ 839 { 840 Event: busv1alpha1.AnyEvent, 841 Action: busv1alpha1.AbortJobAction, 842 }, 843 }, 844 Volumes: []v1alpha1.VolumeSpec{ 845 { 846 MountPath: "", 847 }, 848 }, 849 }, 850 }, 851 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 852 ret: " mountPath is required;", 853 ExpectErr: true, 854 }, 855 // duplicate mount volume 856 { 857 Name: "duplicate-mount-volume", 858 Job: v1alpha1.Job{ 859 ObjectMeta: metav1.ObjectMeta{ 860 Name: "duplicate-mount-volume", 861 Namespace: namespace, 862 }, 863 Spec: v1alpha1.JobSpec{ 864 MinAvailable: 1, 865 Queue: "default", 866 Tasks: []v1alpha1.TaskSpec{ 867 { 868 Name: "task-1", 869 Replicas: 1, 870 Template: v1.PodTemplateSpec{ 871 ObjectMeta: metav1.ObjectMeta{ 872 Labels: map[string]string{"name": "test"}, 873 }, 874 Spec: v1.PodSpec{ 875 Containers: []v1.Container{ 876 { 877 Name: "fake-name", 878 Image: "busybox:1.24", 879 }, 880 }, 881 }, 882 }, 883 }, 884 }, 885 Policies: []v1alpha1.LifecyclePolicy{ 886 { 887 Event: busv1alpha1.AnyEvent, 888 Action: busv1alpha1.AbortJobAction, 889 }, 890 }, 891 Volumes: []v1alpha1.VolumeSpec{ 892 { 893 MountPath: "/var", 894 VolumeClaimName: "pvc1", 895 }, 896 { 897 MountPath: "/var", 898 VolumeClaimName: "pvc2", 899 }, 900 }, 901 }, 902 }, 903 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 904 ret: " duplicated mountPath: /var;", 905 ExpectErr: true, 906 }, 907 { 908 Name: "volume without VolumeClaimName and VolumeClaim", 909 Job: v1alpha1.Job{ 910 ObjectMeta: metav1.ObjectMeta{ 911 Name: "invalid-volume", 912 Namespace: namespace, 913 }, 914 Spec: v1alpha1.JobSpec{ 915 MinAvailable: 1, 916 Queue: "default", 917 Tasks: []v1alpha1.TaskSpec{ 918 { 919 Name: "task-1", 920 Replicas: 1, 921 Template: v1.PodTemplateSpec{ 922 ObjectMeta: metav1.ObjectMeta{ 923 Labels: map[string]string{"name": "test"}, 924 }, 925 Spec: v1.PodSpec{ 926 Containers: []v1.Container{ 927 { 928 Name: "fake-name", 929 Image: "busybox:1.24", 930 }, 931 }, 932 }, 933 }, 934 }, 935 }, 936 Policies: []v1alpha1.LifecyclePolicy{ 937 { 938 Event: busv1alpha1.AnyEvent, 939 Action: busv1alpha1.AbortJobAction, 940 }, 941 }, 942 Volumes: []v1alpha1.VolumeSpec{ 943 { 944 MountPath: "/var", 945 }, 946 { 947 MountPath: "/var", 948 }, 949 }, 950 }, 951 }, 952 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 953 ret: " either VolumeClaim or VolumeClaimName must be specified;", 954 ExpectErr: true, 955 }, 956 // task Policy with any event and other events 957 { 958 Name: "taskpolicy-withAnyandOthrEvent", 959 Job: v1alpha1.Job{ 960 ObjectMeta: metav1.ObjectMeta{ 961 Name: "taskpolicy-withAnyandOthrEvent", 962 Namespace: namespace, 963 }, 964 Spec: v1alpha1.JobSpec{ 965 MinAvailable: 1, 966 Queue: "default", 967 Tasks: []v1alpha1.TaskSpec{ 968 { 969 Name: "task-1", 970 Replicas: 1, 971 Template: v1.PodTemplateSpec{ 972 ObjectMeta: metav1.ObjectMeta{ 973 Labels: map[string]string{"name": "test"}, 974 }, 975 Spec: v1.PodSpec{ 976 Containers: []v1.Container{ 977 { 978 Name: "fake-name", 979 Image: "busybox:1.24", 980 }, 981 }, 982 }, 983 }, 984 Policies: []v1alpha1.LifecyclePolicy{ 985 { 986 Event: busv1alpha1.AnyEvent, 987 Action: busv1alpha1.AbortJobAction, 988 }, 989 { 990 Event: busv1alpha1.PodFailedEvent, 991 Action: busv1alpha1.RestartJobAction, 992 }, 993 }, 994 }, 995 }, 996 }, 997 }, 998 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 999 ret: "if there's * here, no other policy should be here", 1000 ExpectErr: true, 1001 }, 1002 // job with no queue created 1003 { 1004 Name: "job-with-noQueue", 1005 Job: v1alpha1.Job{ 1006 ObjectMeta: metav1.ObjectMeta{ 1007 Name: "job-with-noQueue", 1008 Namespace: namespace, 1009 }, 1010 Spec: v1alpha1.JobSpec{ 1011 MinAvailable: 1, 1012 Queue: "jobQueue", 1013 Tasks: []v1alpha1.TaskSpec{ 1014 { 1015 Name: "task-1", 1016 Replicas: 1, 1017 Template: v1.PodTemplateSpec{ 1018 ObjectMeta: metav1.ObjectMeta{ 1019 Labels: map[string]string{"name": "test"}, 1020 }, 1021 Spec: v1.PodSpec{ 1022 Containers: []v1.Container{ 1023 { 1024 Name: "fake-name", 1025 Image: "busybox:1.24", 1026 }, 1027 }, 1028 }, 1029 }, 1030 }, 1031 }, 1032 }, 1033 }, 1034 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 1035 ret: "unable to find job queue", 1036 ExpectErr: true, 1037 }, 1038 { 1039 Name: "job with priviledged init container", 1040 Job: v1alpha1.Job{ 1041 ObjectMeta: metav1.ObjectMeta{ 1042 Name: "valid-job", 1043 Namespace: namespace, 1044 }, 1045 Spec: v1alpha1.JobSpec{ 1046 MinAvailable: 1, 1047 Queue: "default", 1048 Tasks: []v1alpha1.TaskSpec{ 1049 { 1050 Name: "task-1", 1051 Replicas: 1, 1052 Template: v1.PodTemplateSpec{ 1053 ObjectMeta: metav1.ObjectMeta{ 1054 Labels: map[string]string{"name": "test"}, 1055 }, 1056 Spec: v1.PodSpec{ 1057 InitContainers: []v1.Container{ 1058 { 1059 Name: "init-fake-name", 1060 Image: "busybox:1.24", 1061 SecurityContext: &v1.SecurityContext{ 1062 Privileged: &priviledged, 1063 }, 1064 }, 1065 }, 1066 Containers: []v1.Container{ 1067 { 1068 Name: "fake-name", 1069 Image: "busybox:1.24", 1070 }, 1071 }, 1072 }, 1073 }, 1074 }, 1075 }, 1076 }, 1077 }, 1078 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 1079 ret: "", 1080 ExpectErr: false, 1081 }, 1082 { 1083 Name: "job with priviledged container", 1084 Job: v1alpha1.Job{ 1085 ObjectMeta: metav1.ObjectMeta{ 1086 Name: "valid-job", 1087 Namespace: namespace, 1088 }, 1089 Spec: v1alpha1.JobSpec{ 1090 MinAvailable: 1, 1091 Queue: "default", 1092 Tasks: []v1alpha1.TaskSpec{ 1093 { 1094 Name: "task-1", 1095 Replicas: 1, 1096 Template: v1.PodTemplateSpec{ 1097 ObjectMeta: metav1.ObjectMeta{ 1098 Labels: map[string]string{"name": "test"}, 1099 }, 1100 Spec: v1.PodSpec{ 1101 Containers: []v1.Container{ 1102 { 1103 Name: "fake-name", 1104 Image: "busybox:1.24", 1105 SecurityContext: &v1.SecurityContext{ 1106 Privileged: &priviledged, 1107 }, 1108 }, 1109 }, 1110 }, 1111 }, 1112 }, 1113 }, 1114 }, 1115 }, 1116 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 1117 ret: "", 1118 ExpectErr: false, 1119 }, 1120 { 1121 Name: "job with valid task depends on", 1122 Job: v1alpha1.Job{ 1123 ObjectMeta: metav1.ObjectMeta{ 1124 Name: "job-with-valid-task-depends-on", 1125 Namespace: namespace, 1126 }, 1127 Spec: v1alpha1.JobSpec{ 1128 MinAvailable: 1, 1129 Queue: "default", 1130 Tasks: []v1alpha1.TaskSpec{ 1131 { 1132 Name: "t1", 1133 Replicas: 1, 1134 DependsOn: &v1alpha1.DependsOn{ 1135 Name: []string{"t2"}, 1136 }, 1137 Template: v1.PodTemplateSpec{ 1138 Spec: v1.PodSpec{ 1139 Containers: []v1.Container{ 1140 { 1141 Name: "fake-name", 1142 Image: "busybox:1.24", 1143 }, 1144 }, 1145 }, 1146 }, 1147 }, 1148 { 1149 Name: "t2", 1150 Replicas: 1, 1151 DependsOn: nil, 1152 Template: v1.PodTemplateSpec{ 1153 Spec: v1.PodSpec{ 1154 Containers: []v1.Container{ 1155 { 1156 Name: "fake-name", 1157 Image: "busybox:1.24", 1158 }, 1159 }, 1160 }, 1161 }, 1162 }, 1163 }, 1164 }, 1165 }, 1166 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 1167 ret: "", 1168 ExpectErr: false, 1169 }, 1170 { 1171 Name: "job with invalid task depends on", 1172 Job: v1alpha1.Job{ 1173 ObjectMeta: metav1.ObjectMeta{ 1174 Name: "job-with-invalid-task-depends-on", 1175 Namespace: namespace, 1176 }, 1177 Spec: v1alpha1.JobSpec{ 1178 MinAvailable: 1, 1179 Queue: "default", 1180 Tasks: []v1alpha1.TaskSpec{ 1181 { 1182 Name: "t1", 1183 Replicas: 1, 1184 DependsOn: &v1alpha1.DependsOn{ 1185 Name: []string{"t3"}, 1186 }, 1187 Template: v1.PodTemplateSpec{ 1188 Spec: v1.PodSpec{ 1189 Containers: []v1.Container{ 1190 { 1191 Name: "fake-name", 1192 Image: "busybox:1.24", 1193 }, 1194 }, 1195 }, 1196 }, 1197 }, 1198 { 1199 Name: "t2", 1200 Replicas: 1, 1201 DependsOn: nil, 1202 Template: v1.PodTemplateSpec{ 1203 Spec: v1.PodSpec{ 1204 Containers: []v1.Container{ 1205 { 1206 Name: "fake-name", 1207 Image: "busybox:1.24", 1208 }, 1209 }, 1210 }, 1211 }, 1212 }, 1213 }, 1214 }, 1215 }, 1216 reviewResponse: admissionv1.AdmissionResponse{Allowed: true}, 1217 ret: "job has dependencies between tasks, but doesn't form a directed acyclic graph(DAG)", 1218 ExpectErr: true, 1219 }, 1220 } 1221 1222 for _, testCase := range testCases { 1223 t.Run(testCase.Name, func(t *testing.T) { 1224 defaultqueue := schedulingv1beta2.Queue{ 1225 ObjectMeta: metav1.ObjectMeta{ 1226 Name: "default", 1227 }, 1228 Spec: schedulingv1beta2.QueueSpec{ 1229 Weight: 1, 1230 }, 1231 Status: schedulingv1beta2.QueueStatus{ 1232 State: schedulingv1beta2.QueueStateOpen, 1233 }, 1234 } 1235 // create fake volcano clientset 1236 config.VolcanoClient = fakeclient.NewSimpleClientset() 1237 1238 //create default queue 1239 _, err := config.VolcanoClient.SchedulingV1beta1().Queues().Create(context.TODO(), &defaultqueue, metav1.CreateOptions{}) 1240 if err != nil { 1241 t.Error("Queue Creation Failed") 1242 } 1243 1244 ret := validateJobCreate(&testCase.Job, &testCase.reviewResponse) 1245 //fmt.Printf("test-case name:%s, ret:%v testCase.reviewResponse:%v \n", testCase.Name, ret,testCase.reviewResponse) 1246 if testCase.ExpectErr == true && ret == "" { 1247 t.Errorf("Expect error msg :%s, but got nil.", testCase.ret) 1248 } 1249 if testCase.ExpectErr == true && testCase.reviewResponse.Allowed != false { 1250 t.Errorf("Expect Allowed as false but got true.") 1251 } 1252 if testCase.ExpectErr == true && !strings.Contains(ret, testCase.ret) { 1253 t.Errorf("Expect error msg :%s, but got diff error %v", testCase.ret, ret) 1254 } 1255 1256 if testCase.ExpectErr == false && ret != "" { 1257 t.Errorf("Expect no error, but got error %v", ret) 1258 } 1259 if testCase.ExpectErr == false && testCase.reviewResponse.Allowed != true { 1260 t.Errorf("Expect Allowed as true but got false. %v", testCase.reviewResponse) 1261 } 1262 }) 1263 } 1264 } 1265 1266 func TestValidateJobUpdate(t *testing.T) { 1267 testCases := []struct { 1268 name string 1269 replicas int32 1270 minAvailable int32 1271 addTask bool 1272 mutateTaskName bool 1273 mutateSpec bool 1274 expectErr bool 1275 }{ 1276 { 1277 name: "scale up", 1278 replicas: 6, 1279 minAvailable: 5, 1280 addTask: false, 1281 mutateTaskName: false, 1282 mutateSpec: false, 1283 expectErr: false, 1284 }, 1285 { 1286 name: "invalid scale down with replicas less than minAvailable", 1287 replicas: 4, 1288 minAvailable: 5, 1289 addTask: false, 1290 mutateTaskName: false, 1291 mutateSpec: false, 1292 expectErr: true, 1293 }, 1294 { 1295 name: "scale down", 1296 replicas: 4, 1297 minAvailable: 3, 1298 addTask: false, 1299 mutateTaskName: false, 1300 mutateSpec: false, 1301 expectErr: false, 1302 }, 1303 { 1304 name: "invalid minAvailable", 1305 replicas: 4, 1306 minAvailable: -1, 1307 addTask: false, 1308 mutateTaskName: false, 1309 mutateSpec: false, 1310 expectErr: true, 1311 }, 1312 { 1313 name: "invalid add task", 1314 replicas: 4, 1315 minAvailable: 5, 1316 addTask: true, 1317 mutateTaskName: false, 1318 mutateSpec: false, 1319 expectErr: true, 1320 }, 1321 { 1322 name: "invalid mutate task's fields other than replicas", 1323 replicas: 5, 1324 minAvailable: 5, 1325 addTask: false, 1326 mutateTaskName: true, 1327 mutateSpec: false, 1328 expectErr: true, 1329 }, 1330 { 1331 name: "invalid mutate job's spec other than minAvailable", 1332 replicas: 5, 1333 minAvailable: 5, 1334 addTask: false, 1335 mutateTaskName: false, 1336 mutateSpec: true, 1337 expectErr: true, 1338 }, 1339 } 1340 1341 for _, tc := range testCases { 1342 t.Run(tc.name, func(t *testing.T) { 1343 old := newJob() 1344 new := newJob() 1345 new.ResourceVersion = "502593" 1346 new.Status.Succeeded = 2 1347 1348 new.Spec.MinAvailable = tc.minAvailable 1349 new.Spec.Tasks[0].Replicas = tc.replicas 1350 1351 if tc.addTask { 1352 new.Spec.Tasks = append(new.Spec.Tasks, v1alpha1.TaskSpec{ 1353 Name: "task-2", 1354 Replicas: 5, 1355 Template: v1.PodTemplateSpec{ 1356 ObjectMeta: metav1.ObjectMeta{ 1357 Labels: map[string]string{"name": "test"}, 1358 }, 1359 Spec: v1.PodSpec{ 1360 Containers: []v1.Container{ 1361 { 1362 Name: "fake-name", 1363 Image: "busybox:1.24", 1364 }, 1365 }, 1366 }, 1367 }, 1368 }) 1369 } 1370 if tc.mutateTaskName { 1371 new.Spec.Tasks[0].Name = "mutated-name" 1372 } 1373 if tc.mutateSpec { 1374 new.Spec.Queue = "mutated-queue" 1375 } 1376 1377 err := validateJobUpdate(old, new) 1378 if err != nil && !tc.expectErr { 1379 t.Errorf("Expected no error, but got: %v", err) 1380 } 1381 if err == nil && tc.expectErr { 1382 t.Errorf("Expected error, but got none") 1383 } 1384 }) 1385 } 1386 1387 } 1388 1389 func newJob() *v1alpha1.Job { 1390 return &v1alpha1.Job{ 1391 ObjectMeta: metav1.ObjectMeta{ 1392 Name: "valid-job", 1393 Namespace: "default", 1394 }, 1395 Spec: v1alpha1.JobSpec{ 1396 MinAvailable: 5, 1397 Queue: "default", 1398 Tasks: []v1alpha1.TaskSpec{ 1399 { 1400 Name: "task-1", 1401 Replicas: 5, 1402 Template: v1.PodTemplateSpec{ 1403 ObjectMeta: metav1.ObjectMeta{ 1404 Labels: map[string]string{"name": "test"}, 1405 }, 1406 Spec: v1.PodSpec{ 1407 Containers: []v1.Container{ 1408 { 1409 Name: "fake-name", 1410 Image: "busybox:1.24", 1411 }, 1412 }, 1413 }, 1414 }, 1415 }, 1416 }, 1417 Policies: []v1alpha1.LifecyclePolicy{ 1418 { 1419 Event: busv1alpha1.PodEvictedEvent, 1420 Action: busv1alpha1.RestartTaskAction, 1421 }, 1422 }, 1423 }, 1424 } 1425 } 1426 1427 func TestValidateTaskTopoPolicy(t *testing.T) { 1428 testCases := []struct { 1429 name string 1430 taskSpec v1alpha1.TaskSpec 1431 expect string 1432 }{ 1433 { 1434 name: "test-1", 1435 taskSpec: v1alpha1.TaskSpec{ 1436 Name: "task-1", 1437 Replicas: 5, 1438 TopologyPolicy: v1alpha1.Restricted, 1439 Template: v1.PodTemplateSpec{ 1440 ObjectMeta: metav1.ObjectMeta{ 1441 Labels: map[string]string{"name": "test"}, 1442 }, 1443 Spec: v1.PodSpec{ 1444 Containers: []v1.Container{ 1445 { 1446 Resources: v1.ResourceRequirements{ 1447 Limits: v1.ResourceList{ 1448 v1.ResourceCPU: *resource.NewQuantity(1, ""), 1449 v1.ResourceMemory: *resource.NewQuantity(2000, resource.BinarySI), 1450 }, 1451 }, 1452 }, 1453 }, 1454 }, 1455 }, 1456 }, 1457 expect: "", 1458 }, 1459 { 1460 name: "test-2", 1461 taskSpec: v1alpha1.TaskSpec{ 1462 Name: "task-2", 1463 TopologyPolicy: v1alpha1.Restricted, 1464 Template: v1.PodTemplateSpec{ 1465 Spec: v1.PodSpec{ 1466 Containers: []v1.Container{ 1467 { 1468 Resources: v1.ResourceRequirements{ 1469 Limits: v1.ResourceList{ 1470 v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), 1471 v1.ResourceMemory: *resource.NewQuantity(2000, resource.BinarySI), 1472 }, 1473 }, 1474 }, 1475 }, 1476 }, 1477 }, 1478 }, 1479 expect: "the cpu request isn't an integer", 1480 }, 1481 } 1482 1483 for _, testcase := range testCases { 1484 msg := validateTaskTopoPolicy(testcase.taskSpec, 0) 1485 if !strings.Contains(msg, testcase.expect) { 1486 t.Errorf("%s failed.", testcase.name) 1487 } 1488 } 1489 }