k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/registry/batch/job/strategy_test.go (about) 1 /* 2 Copyright 2015 The Kubernetes 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 job 18 19 import ( 20 "testing" 21 "time" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/google/go-cmp/cmp/cmpopts" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/labels" 27 "k8s.io/apimachinery/pkg/types" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 30 "k8s.io/apiserver/pkg/registry/rest" 31 utilfeature "k8s.io/apiserver/pkg/util/feature" 32 featuregatetesting "k8s.io/component-base/featuregate/testing" 33 apitesting "k8s.io/kubernetes/pkg/api/testing" 34 "k8s.io/kubernetes/pkg/apis/batch" 35 _ "k8s.io/kubernetes/pkg/apis/batch/install" 36 api "k8s.io/kubernetes/pkg/apis/core" 37 "k8s.io/kubernetes/pkg/features" 38 "k8s.io/utils/ptr" 39 ) 40 41 var ignoreErrValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") 42 43 // TestJobStrategy_PrepareForUpdate tests various scenarios for PrepareForUpdate 44 func TestJobStrategy_PrepareForUpdate(t *testing.T) { 45 validSelector := getValidLabelSelector() 46 validPodTemplateSpec := getValidPodTemplateSpecForSelector(validSelector) 47 48 podFailurePolicy := &batch.PodFailurePolicy{ 49 Rules: []batch.PodFailurePolicyRule{ 50 { 51 Action: batch.PodFailurePolicyActionFailJob, 52 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 53 ContainerName: ptr.To("container-name"), 54 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 55 Values: []int32{1}, 56 }, 57 }, 58 }, 59 } 60 updatedPodFailurePolicy := &batch.PodFailurePolicy{ 61 Rules: []batch.PodFailurePolicyRule{ 62 { 63 Action: batch.PodFailurePolicyActionIgnore, 64 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 65 ContainerName: ptr.To("updated-container-name"), 66 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 67 Values: []int32{2}, 68 }, 69 }, 70 }, 71 } 72 successPolicy := &batch.SuccessPolicy{ 73 Rules: []batch.SuccessPolicyRule{ 74 { 75 SucceededIndexes: ptr.To("1,3-7"), 76 SucceededCount: ptr.To[int32](4), 77 }, 78 }, 79 } 80 updatedSuccessPolicy := &batch.SuccessPolicy{ 81 Rules: []batch.SuccessPolicyRule{ 82 { 83 SucceededIndexes: ptr.To("1,3-7"), 84 SucceededCount: ptr.To[int32](5), 85 }, 86 }, 87 } 88 89 cases := map[string]struct { 90 enableJobPodFailurePolicy bool 91 enableJobBackoffLimitPerIndex bool 92 enableJobPodReplacementPolicy bool 93 enableJobSuccessPolicy bool 94 job batch.Job 95 updatedJob batch.Job 96 wantJob batch.Job 97 }{ 98 "update job with a new field; updated when JobSuccessPolicy enabled": { 99 enableJobSuccessPolicy: true, 100 job: batch.Job{ 101 ObjectMeta: getValidObjectMeta(0), 102 Spec: batch.JobSpec{ 103 Selector: validSelector, 104 Template: validPodTemplateSpec, 105 SuccessPolicy: nil, 106 }, 107 }, 108 updatedJob: batch.Job{ 109 ObjectMeta: getValidObjectMeta(0), 110 Spec: batch.JobSpec{ 111 Selector: validSelector, 112 Template: validPodTemplateSpec, 113 SuccessPolicy: updatedSuccessPolicy, 114 }, 115 }, 116 wantJob: batch.Job{ 117 ObjectMeta: getValidObjectMeta(1), 118 Spec: batch.JobSpec{ 119 Selector: validSelector, 120 Template: validPodTemplateSpec, 121 SuccessPolicy: updatedSuccessPolicy, 122 }, 123 }, 124 }, 125 "update pre-existing field; updated when JobSuccessPolicy enabled": { 126 enableJobSuccessPolicy: true, 127 job: batch.Job{ 128 ObjectMeta: getValidObjectMeta(0), 129 Spec: batch.JobSpec{ 130 Selector: validSelector, 131 Template: validPodTemplateSpec, 132 SuccessPolicy: successPolicy, 133 }, 134 }, 135 updatedJob: batch.Job{ 136 ObjectMeta: getValidObjectMeta(0), 137 Spec: batch.JobSpec{ 138 Selector: validSelector, 139 Template: validPodTemplateSpec, 140 SuccessPolicy: updatedSuccessPolicy, 141 }, 142 }, 143 wantJob: batch.Job{ 144 ObjectMeta: getValidObjectMeta(1), 145 Spec: batch.JobSpec{ 146 Selector: validSelector, 147 Template: validPodTemplateSpec, 148 SuccessPolicy: updatedSuccessPolicy, 149 }, 150 }, 151 }, 152 "update job with a new field: not update when JobSuccessPolicy disabled": { 153 enableJobSuccessPolicy: false, 154 job: batch.Job{ 155 ObjectMeta: getValidObjectMeta(0), 156 Spec: batch.JobSpec{ 157 Selector: validSelector, 158 Template: validPodTemplateSpec, 159 SuccessPolicy: nil, 160 }, 161 }, 162 updatedJob: batch.Job{ 163 ObjectMeta: getValidObjectMeta(0), 164 Spec: batch.JobSpec{ 165 Selector: validSelector, 166 Template: validPodTemplateSpec, 167 SuccessPolicy: updatedSuccessPolicy, 168 }, 169 }, 170 wantJob: batch.Job{ 171 ObjectMeta: getValidObjectMeta(0), 172 Spec: batch.JobSpec{ 173 Selector: validSelector, 174 Template: validPodTemplateSpec, 175 SuccessPolicy: nil, 176 }, 177 }, 178 }, 179 "update pre-existing field; updated when JobSuccessPolicy disabled": { 180 enableJobSuccessPolicy: false, 181 job: batch.Job{ 182 ObjectMeta: getValidObjectMeta(0), 183 Spec: batch.JobSpec{ 184 Selector: validSelector, 185 Template: validPodTemplateSpec, 186 SuccessPolicy: successPolicy, 187 }, 188 }, 189 updatedJob: batch.Job{ 190 ObjectMeta: getValidObjectMeta(0), 191 Spec: batch.JobSpec{ 192 Selector: validSelector, 193 Template: validPodTemplateSpec, 194 SuccessPolicy: updatedSuccessPolicy, 195 }, 196 }, 197 wantJob: batch.Job{ 198 ObjectMeta: getValidObjectMeta(1), 199 Spec: batch.JobSpec{ 200 Selector: validSelector, 201 Template: validPodTemplateSpec, 202 SuccessPolicy: updatedSuccessPolicy, 203 }, 204 }, 205 }, 206 "update job with a new field; updated when JobBackoffLimitPerIndex enabled": { 207 enableJobBackoffLimitPerIndex: true, 208 job: batch.Job{ 209 ObjectMeta: getValidObjectMeta(0), 210 Spec: batch.JobSpec{ 211 Selector: validSelector, 212 Template: validPodTemplateSpec, 213 BackoffLimitPerIndex: nil, 214 MaxFailedIndexes: nil, 215 }, 216 }, 217 updatedJob: batch.Job{ 218 ObjectMeta: getValidObjectMeta(0), 219 Spec: batch.JobSpec{ 220 Selector: validSelector, 221 Template: validPodTemplateSpec, 222 BackoffLimitPerIndex: ptr.To[int32](1), 223 MaxFailedIndexes: ptr.To[int32](1), 224 }, 225 }, 226 wantJob: batch.Job{ 227 ObjectMeta: getValidObjectMeta(1), 228 Spec: batch.JobSpec{ 229 Selector: validSelector, 230 Template: validPodTemplateSpec, 231 BackoffLimitPerIndex: ptr.To[int32](1), 232 MaxFailedIndexes: ptr.To[int32](1), 233 }, 234 }, 235 }, 236 "update job with a new field; not updated when JobBackoffLimitPerIndex disabled": { 237 enableJobBackoffLimitPerIndex: false, 238 job: batch.Job{ 239 ObjectMeta: getValidObjectMeta(0), 240 Spec: batch.JobSpec{ 241 Selector: validSelector, 242 Template: validPodTemplateSpec, 243 BackoffLimitPerIndex: nil, 244 MaxFailedIndexes: nil, 245 }, 246 }, 247 updatedJob: batch.Job{ 248 ObjectMeta: getValidObjectMeta(0), 249 Spec: batch.JobSpec{ 250 Selector: validSelector, 251 Template: validPodTemplateSpec, 252 BackoffLimitPerIndex: ptr.To[int32](1), 253 MaxFailedIndexes: ptr.To[int32](1), 254 }, 255 }, 256 wantJob: batch.Job{ 257 ObjectMeta: getValidObjectMeta(0), 258 Spec: batch.JobSpec{ 259 Selector: validSelector, 260 Template: validPodTemplateSpec, 261 BackoffLimitPerIndex: nil, 262 MaxFailedIndexes: nil, 263 }, 264 }, 265 }, 266 "update job with a new field; updated when JobPodFailurePolicy enabled": { 267 enableJobPodFailurePolicy: true, 268 job: batch.Job{ 269 ObjectMeta: getValidObjectMeta(0), 270 Spec: batch.JobSpec{ 271 Selector: validSelector, 272 Template: validPodTemplateSpec, 273 PodFailurePolicy: nil, 274 }, 275 }, 276 updatedJob: batch.Job{ 277 ObjectMeta: getValidObjectMeta(0), 278 Spec: batch.JobSpec{ 279 Selector: validSelector, 280 Template: validPodTemplateSpec, 281 PodFailurePolicy: updatedPodFailurePolicy, 282 }, 283 }, 284 wantJob: batch.Job{ 285 ObjectMeta: getValidObjectMeta(1), 286 Spec: batch.JobSpec{ 287 Selector: validSelector, 288 Template: validPodTemplateSpec, 289 PodFailurePolicy: updatedPodFailurePolicy, 290 }, 291 }, 292 }, 293 "update job with a new field; updated when JobPodReplacementPolicy enabled": { 294 enableJobPodReplacementPolicy: true, 295 job: batch.Job{ 296 ObjectMeta: getValidObjectMeta(0), 297 Spec: batch.JobSpec{ 298 Selector: validSelector, 299 Template: validPodTemplateSpec, 300 PodReplacementPolicy: nil, 301 }, 302 }, 303 updatedJob: batch.Job{ 304 ObjectMeta: getValidObjectMeta(0), 305 Spec: batch.JobSpec{ 306 Selector: validSelector, 307 Template: validPodTemplateSpec, 308 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 309 }, 310 }, 311 wantJob: batch.Job{ 312 ObjectMeta: getValidObjectMeta(1), 313 Spec: batch.JobSpec{ 314 Selector: validSelector, 315 Template: validPodTemplateSpec, 316 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 317 }, 318 }, 319 }, 320 "update job with a new field; not updated when JobPodReplacementPolicy disabled": { 321 enableJobPodReplacementPolicy: false, 322 job: batch.Job{ 323 ObjectMeta: getValidObjectMeta(0), 324 Spec: batch.JobSpec{ 325 Selector: validSelector, 326 Template: validPodTemplateSpec, 327 PodReplacementPolicy: nil, 328 }, 329 }, 330 updatedJob: batch.Job{ 331 ObjectMeta: getValidObjectMeta(0), 332 Spec: batch.JobSpec{ 333 Selector: validSelector, 334 Template: validPodTemplateSpec, 335 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 336 }, 337 }, 338 wantJob: batch.Job{ 339 ObjectMeta: getValidObjectMeta(0), 340 Spec: batch.JobSpec{ 341 Selector: validSelector, 342 Template: validPodTemplateSpec, 343 PodReplacementPolicy: nil, 344 }, 345 }, 346 }, 347 "update job with a new field; not updated when JobPodFailurePolicy disabled": { 348 enableJobPodFailurePolicy: false, 349 job: batch.Job{ 350 ObjectMeta: getValidObjectMeta(0), 351 Spec: batch.JobSpec{ 352 Selector: validSelector, 353 Template: validPodTemplateSpec, 354 PodFailurePolicy: nil, 355 }, 356 }, 357 updatedJob: batch.Job{ 358 ObjectMeta: getValidObjectMeta(0), 359 Spec: batch.JobSpec{ 360 Selector: validSelector, 361 Template: validPodTemplateSpec, 362 PodFailurePolicy: updatedPodFailurePolicy, 363 }, 364 }, 365 wantJob: batch.Job{ 366 ObjectMeta: getValidObjectMeta(0), 367 Spec: batch.JobSpec{ 368 Selector: validSelector, 369 Template: validPodTemplateSpec, 370 PodFailurePolicy: nil, 371 }, 372 }, 373 }, 374 "update pre-existing field; updated when JobPodFailurePolicy enabled": { 375 enableJobPodFailurePolicy: true, 376 job: batch.Job{ 377 ObjectMeta: getValidObjectMeta(0), 378 Spec: batch.JobSpec{ 379 Selector: validSelector, 380 Template: validPodTemplateSpec, 381 PodFailurePolicy: podFailurePolicy, 382 }, 383 }, 384 updatedJob: batch.Job{ 385 ObjectMeta: getValidObjectMeta(0), 386 Spec: batch.JobSpec{ 387 Selector: validSelector, 388 Template: validPodTemplateSpec, 389 PodFailurePolicy: updatedPodFailurePolicy, 390 }, 391 }, 392 wantJob: batch.Job{ 393 ObjectMeta: getValidObjectMeta(1), 394 Spec: batch.JobSpec{ 395 Selector: validSelector, 396 Template: validPodTemplateSpec, 397 PodFailurePolicy: updatedPodFailurePolicy, 398 }, 399 }, 400 }, 401 "update pre-existing field; updated when JobPodFailurePolicy disabled": { 402 enableJobPodFailurePolicy: false, 403 job: batch.Job{ 404 ObjectMeta: getValidObjectMeta(0), 405 Spec: batch.JobSpec{ 406 Selector: validSelector, 407 Template: validPodTemplateSpec, 408 PodFailurePolicy: podFailurePolicy, 409 }, 410 }, 411 updatedJob: batch.Job{ 412 ObjectMeta: getValidObjectMeta(0), 413 Spec: batch.JobSpec{ 414 Selector: validSelector, 415 Template: validPodTemplateSpec, 416 PodFailurePolicy: updatedPodFailurePolicy, 417 }, 418 }, 419 wantJob: batch.Job{ 420 ObjectMeta: getValidObjectMeta(1), 421 Spec: batch.JobSpec{ 422 Selector: validSelector, 423 Template: validPodTemplateSpec, 424 PodFailurePolicy: updatedPodFailurePolicy, 425 }, 426 }, 427 }, 428 "add tracking annotation back": { 429 job: batch.Job{ 430 ObjectMeta: getValidObjectMeta(0), 431 Spec: batch.JobSpec{ 432 Selector: validSelector, 433 Template: validPodTemplateSpec, 434 PodFailurePolicy: podFailurePolicy, 435 }, 436 }, 437 updatedJob: batch.Job{ 438 ObjectMeta: getValidObjectMeta(0), 439 Spec: batch.JobSpec{ 440 Selector: validSelector, 441 Template: validPodTemplateSpec, 442 }, 443 }, 444 wantJob: batch.Job{ 445 ObjectMeta: getValidObjectMeta(1), 446 Spec: batch.JobSpec{ 447 Selector: validSelector, 448 Template: validPodTemplateSpec, 449 }, 450 }, 451 }, 452 "attempt status update and verify it doesn't change": { 453 job: batch.Job{ 454 ObjectMeta: getValidObjectMeta(0), 455 Spec: batch.JobSpec{ 456 Selector: validSelector, 457 Template: validPodTemplateSpec, 458 }, 459 Status: batch.JobStatus{ 460 Active: 1, 461 }, 462 }, 463 updatedJob: batch.Job{ 464 ObjectMeta: getValidObjectMeta(0), 465 Spec: batch.JobSpec{ 466 Selector: validSelector, 467 Template: validPodTemplateSpec, 468 }, 469 Status: batch.JobStatus{ 470 Active: 2, 471 }, 472 }, 473 wantJob: batch.Job{ 474 ObjectMeta: getValidObjectMeta(0), 475 Spec: batch.JobSpec{ 476 Selector: validSelector, 477 Template: validPodTemplateSpec, 478 }, 479 Status: batch.JobStatus{ 480 Active: 1, 481 }, 482 }, 483 }, 484 "ensure generation doesn't change over non spec updates": { 485 job: batch.Job{ 486 ObjectMeta: getValidObjectMeta(0), 487 Spec: batch.JobSpec{ 488 Selector: validSelector, 489 Template: validPodTemplateSpec, 490 }, 491 Status: batch.JobStatus{ 492 Active: 1, 493 }, 494 }, 495 updatedJob: batch.Job{ 496 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 497 Spec: batch.JobSpec{ 498 Selector: validSelector, 499 Template: validPodTemplateSpec, 500 }, 501 Status: batch.JobStatus{ 502 Active: 2, 503 }, 504 }, 505 wantJob: batch.Job{ 506 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 507 Spec: batch.JobSpec{ 508 Selector: validSelector, 509 Template: validPodTemplateSpec, 510 }, 511 Status: batch.JobStatus{ 512 Active: 1, 513 }, 514 }, 515 }, 516 "test updating suspend false->true": { 517 job: batch.Job{ 518 ObjectMeta: getValidObjectMeta(0), 519 Spec: batch.JobSpec{ 520 Selector: validSelector, 521 Template: validPodTemplateSpec, 522 Suspend: ptr.To(false), 523 }, 524 }, 525 updatedJob: batch.Job{ 526 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 527 Spec: batch.JobSpec{ 528 Selector: validSelector, 529 Template: validPodTemplateSpec, 530 Suspend: ptr.To(true), 531 }, 532 }, 533 wantJob: batch.Job{ 534 ObjectMeta: getValidObjectMetaWithAnnotations(1, map[string]string{"hello": "world"}), 535 Spec: batch.JobSpec{ 536 Selector: validSelector, 537 Template: validPodTemplateSpec, 538 Suspend: ptr.To(true), 539 }, 540 }, 541 }, 542 "test updating suspend nil -> true": { 543 job: batch.Job{ 544 ObjectMeta: getValidObjectMeta(0), 545 Spec: batch.JobSpec{ 546 Selector: validSelector, 547 Template: validPodTemplateSpec, 548 }, 549 }, 550 updatedJob: batch.Job{ 551 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 552 Spec: batch.JobSpec{ 553 Selector: validSelector, 554 Template: validPodTemplateSpec, 555 Suspend: ptr.To(true), 556 }, 557 }, 558 wantJob: batch.Job{ 559 ObjectMeta: getValidObjectMetaWithAnnotations(1, map[string]string{"hello": "world"}), 560 Spec: batch.JobSpec{ 561 Selector: validSelector, 562 Template: validPodTemplateSpec, 563 Suspend: ptr.To(true), 564 }, 565 }, 566 }, 567 } 568 569 for name, tc := range cases { 570 t.Run(name, func(t *testing.T) { 571 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy) 572 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex) 573 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodReplacementPolicy, tc.enableJobPodReplacementPolicy) 574 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobSuccessPolicy, tc.enableJobSuccessPolicy) 575 ctx := genericapirequest.NewDefaultContext() 576 577 Strategy.PrepareForUpdate(ctx, &tc.updatedJob, &tc.job) 578 579 if diff := cmp.Diff(tc.wantJob, tc.updatedJob); diff != "" { 580 t.Errorf("Job update differences (-want,+got):\n%s", diff) 581 } 582 }) 583 } 584 } 585 586 // TestJobStrategy_PrepareForCreate tests various scenarios for PrepareForCreate 587 func TestJobStrategy_PrepareForCreate(t *testing.T) { 588 validSelector := getValidLabelSelector() 589 validPodTemplateSpec := getValidPodTemplateSpecForSelector(validSelector) 590 validSelectorWithBatchLabels := &metav1.LabelSelector{MatchLabels: getValidBatchLabelsWithNonBatch()} 591 expectedPodTemplateSpec := getValidPodTemplateSpecForSelector(validSelectorWithBatchLabels) 592 593 podFailurePolicy := &batch.PodFailurePolicy{ 594 Rules: []batch.PodFailurePolicyRule{ 595 { 596 Action: batch.PodFailurePolicyActionFailJob, 597 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 598 ContainerName: ptr.To("container-name"), 599 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 600 Values: []int32{1}, 601 }, 602 }, 603 }, 604 } 605 successPolicy := &batch.SuccessPolicy{ 606 Rules: []batch.SuccessPolicyRule{ 607 { 608 SucceededIndexes: ptr.To("1,3-7"), 609 SucceededCount: ptr.To[int32](4), 610 }, 611 }, 612 } 613 614 cases := map[string]struct { 615 enableJobPodFailurePolicy bool 616 enableJobBackoffLimitPerIndex bool 617 enableJobPodReplacementPolicy bool 618 enableJobManageBy bool 619 enableJobSuccessPolicy bool 620 job batch.Job 621 wantJob batch.Job 622 }{ 623 "generate selectors": { 624 job: batch.Job{ 625 ObjectMeta: getValidObjectMeta(0), 626 Spec: batch.JobSpec{ 627 Selector: validSelector, 628 ManualSelector: ptr.To(false), 629 Template: validPodTemplateSpec, 630 }, 631 }, 632 wantJob: batch.Job{ 633 ObjectMeta: getValidObjectMeta(1), 634 Spec: batch.JobSpec{ 635 Selector: validSelector, 636 ManualSelector: ptr.To(false), 637 Template: expectedPodTemplateSpec, 638 }, 639 }, 640 }, 641 "create job with a new field; JobSuccessPolicy enabled": { 642 enableJobSuccessPolicy: true, 643 job: batch.Job{ 644 ObjectMeta: getValidObjectMeta(0), 645 Spec: batch.JobSpec{ 646 Selector: validSelector, 647 ManualSelector: ptr.To(false), 648 Template: validPodTemplateSpec, 649 SuccessPolicy: successPolicy, 650 }, 651 }, 652 wantJob: batch.Job{ 653 ObjectMeta: getValidObjectMeta(1), 654 Spec: batch.JobSpec{ 655 Selector: validSelector, 656 ManualSelector: ptr.To(false), 657 Template: expectedPodTemplateSpec, 658 SuccessPolicy: successPolicy, 659 }, 660 }, 661 }, 662 "create job with a new field; JobSuccessPolicy disabled": { 663 enableJobSuccessPolicy: false, 664 job: batch.Job{ 665 ObjectMeta: getValidObjectMeta(0), 666 Spec: batch.JobSpec{ 667 Selector: validSelector, 668 ManualSelector: ptr.To(false), 669 Template: validPodTemplateSpec, 670 SuccessPolicy: successPolicy, 671 }, 672 }, 673 wantJob: batch.Job{ 674 ObjectMeta: getValidObjectMeta(1), 675 Spec: batch.JobSpec{ 676 Selector: validSelector, 677 ManualSelector: ptr.To(false), 678 Template: validPodTemplateSpec, 679 SuccessPolicy: nil, 680 }, 681 }, 682 }, 683 "create job with a new fields; JobBackoffLimitPerIndex enabled": { 684 enableJobBackoffLimitPerIndex: true, 685 job: batch.Job{ 686 ObjectMeta: getValidObjectMeta(0), 687 Spec: batch.JobSpec{ 688 Selector: validSelector, 689 ManualSelector: ptr.To(false), 690 Template: validPodTemplateSpec, 691 BackoffLimitPerIndex: ptr.To[int32](1), 692 MaxFailedIndexes: ptr.To[int32](1), 693 }, 694 }, 695 wantJob: batch.Job{ 696 ObjectMeta: getValidObjectMeta(1), 697 Spec: batch.JobSpec{ 698 Selector: validSelector, 699 ManualSelector: ptr.To(false), 700 Template: expectedPodTemplateSpec, 701 BackoffLimitPerIndex: ptr.To[int32](1), 702 MaxFailedIndexes: ptr.To[int32](1), 703 }, 704 }, 705 }, 706 "create job with a new fields; JobBackoffLimitPerIndex disabled": { 707 enableJobBackoffLimitPerIndex: false, 708 job: batch.Job{ 709 ObjectMeta: getValidObjectMeta(0), 710 Spec: batch.JobSpec{ 711 Selector: validSelector, 712 ManualSelector: ptr.To(false), 713 Template: validPodTemplateSpec, 714 BackoffLimitPerIndex: ptr.To[int32](1), 715 MaxFailedIndexes: ptr.To[int32](1), 716 }, 717 }, 718 wantJob: batch.Job{ 719 ObjectMeta: getValidObjectMeta(1), 720 Spec: batch.JobSpec{ 721 Selector: validSelector, 722 ManualSelector: ptr.To(false), 723 Template: expectedPodTemplateSpec, 724 BackoffLimitPerIndex: nil, 725 MaxFailedIndexes: nil, 726 }, 727 }, 728 }, 729 "create job with a new field; JobPodFailurePolicy enabled": { 730 enableJobPodFailurePolicy: true, 731 job: batch.Job{ 732 ObjectMeta: getValidObjectMeta(0), 733 Spec: batch.JobSpec{ 734 Selector: validSelector, 735 ManualSelector: ptr.To(false), 736 Template: validPodTemplateSpec, 737 PodFailurePolicy: podFailurePolicy, 738 }, 739 }, 740 wantJob: batch.Job{ 741 ObjectMeta: getValidObjectMeta(1), 742 Spec: batch.JobSpec{ 743 Selector: validSelector, 744 ManualSelector: ptr.To(false), 745 Template: expectedPodTemplateSpec, 746 PodFailurePolicy: podFailurePolicy, 747 }, 748 }, 749 }, 750 "create job with a new field; JobPodReplacementPolicy enabled": { 751 enableJobPodReplacementPolicy: true, 752 job: batch.Job{ 753 ObjectMeta: getValidObjectMeta(0), 754 Spec: batch.JobSpec{ 755 Selector: validSelector, 756 ManualSelector: ptr.To(false), 757 Template: validPodTemplateSpec, 758 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 759 }, 760 }, 761 wantJob: batch.Job{ 762 ObjectMeta: getValidObjectMeta(1), 763 Spec: batch.JobSpec{ 764 Selector: validSelector, 765 ManualSelector: ptr.To(false), 766 Template: expectedPodTemplateSpec, 767 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 768 }, 769 }, 770 }, 771 "create job with a new field; JobPodReplacementPolicy disabled": { 772 enableJobPodReplacementPolicy: false, 773 job: batch.Job{ 774 ObjectMeta: getValidObjectMeta(0), 775 Spec: batch.JobSpec{ 776 Selector: validSelector, 777 ManualSelector: ptr.To(false), 778 Template: validPodTemplateSpec, 779 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 780 }, 781 }, 782 wantJob: batch.Job{ 783 ObjectMeta: getValidObjectMeta(1), 784 Spec: batch.JobSpec{ 785 Selector: validSelector, 786 ManualSelector: ptr.To(false), 787 Template: expectedPodTemplateSpec, 788 PodReplacementPolicy: nil, 789 }, 790 }, 791 }, 792 "create job with a new field; JobPodFailurePolicy disabled": { 793 enableJobPodFailurePolicy: false, 794 job: batch.Job{ 795 ObjectMeta: getValidObjectMeta(0), 796 Spec: batch.JobSpec{ 797 Selector: validSelector, 798 ManualSelector: ptr.To(false), 799 Template: validPodTemplateSpec, 800 PodFailurePolicy: podFailurePolicy, 801 }, 802 }, 803 wantJob: batch.Job{ 804 ObjectMeta: getValidObjectMeta(1), 805 Spec: batch.JobSpec{ 806 Selector: validSelector, 807 ManualSelector: ptr.To(false), 808 Template: expectedPodTemplateSpec, 809 PodFailurePolicy: nil, 810 }, 811 }, 812 }, 813 "job does not allow setting status on create": { 814 job: batch.Job{ 815 ObjectMeta: getValidObjectMeta(0), 816 Spec: batch.JobSpec{ 817 Selector: validSelector, 818 ManualSelector: ptr.To(false), 819 Template: validPodTemplateSpec, 820 }, 821 Status: batch.JobStatus{ 822 Active: 1, 823 }, 824 }, 825 wantJob: batch.Job{ 826 ObjectMeta: getValidObjectMeta(1), 827 Spec: batch.JobSpec{ 828 Selector: validSelector, 829 ManualSelector: ptr.To(false), 830 Template: expectedPodTemplateSpec, 831 }, 832 }, 833 }, 834 "create job with pod failure policy using FailIndex action; JobPodFailurePolicy enabled, JobBackoffLimitPerIndex disabled": { 835 enableJobBackoffLimitPerIndex: false, 836 enableJobPodFailurePolicy: true, 837 job: batch.Job{ 838 ObjectMeta: getValidObjectMeta(0), 839 Spec: batch.JobSpec{ 840 Selector: validSelector, 841 ManualSelector: ptr.To(false), 842 Template: validPodTemplateSpec, 843 BackoffLimitPerIndex: ptr.To[int32](1), 844 PodFailurePolicy: &batch.PodFailurePolicy{ 845 Rules: []batch.PodFailurePolicyRule{ 846 { 847 Action: batch.PodFailurePolicyActionFailIndex, 848 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 849 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 850 Values: []int32{1}, 851 }, 852 }, 853 }, 854 }, 855 }, 856 }, 857 wantJob: batch.Job{ 858 ObjectMeta: getValidObjectMeta(1), 859 Spec: batch.JobSpec{ 860 Selector: validSelector, 861 ManualSelector: ptr.To(false), 862 Template: expectedPodTemplateSpec, 863 PodFailurePolicy: &batch.PodFailurePolicy{ 864 Rules: []batch.PodFailurePolicyRule{}, 865 }, 866 }, 867 }, 868 }, 869 "create job with multiple pod failure policy rules, some using FailIndex action; JobPodFailurePolicy enabled, JobBackoffLimitPerIndex disabled": { 870 enableJobBackoffLimitPerIndex: false, 871 enableJobPodFailurePolicy: true, 872 job: batch.Job{ 873 ObjectMeta: getValidObjectMeta(0), 874 Spec: batch.JobSpec{ 875 Selector: validSelector, 876 ManualSelector: ptr.To(false), 877 Template: validPodTemplateSpec, 878 BackoffLimitPerIndex: ptr.To[int32](1), 879 PodFailurePolicy: &batch.PodFailurePolicy{ 880 Rules: []batch.PodFailurePolicyRule{ 881 { 882 Action: batch.PodFailurePolicyActionFailJob, 883 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 884 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 885 Values: []int32{2}, 886 }, 887 }, 888 { 889 Action: batch.PodFailurePolicyActionFailIndex, 890 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 891 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 892 Values: []int32{1}, 893 }, 894 }, 895 { 896 Action: batch.PodFailurePolicyActionIgnore, 897 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 898 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 899 Values: []int32{13}, 900 }, 901 }, 902 }, 903 }, 904 }, 905 }, 906 wantJob: batch.Job{ 907 ObjectMeta: getValidObjectMeta(1), 908 Spec: batch.JobSpec{ 909 Selector: validSelector, 910 ManualSelector: ptr.To(false), 911 Template: expectedPodTemplateSpec, 912 PodFailurePolicy: &batch.PodFailurePolicy{ 913 Rules: []batch.PodFailurePolicyRule{ 914 { 915 Action: batch.PodFailurePolicyActionFailJob, 916 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 917 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 918 Values: []int32{2}, 919 }, 920 }, 921 { 922 Action: batch.PodFailurePolicyActionIgnore, 923 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 924 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 925 Values: []int32{13}, 926 }, 927 }, 928 }, 929 }, 930 }, 931 }, 932 }, 933 "managedBy field is dropped when the feature gate is disabled": { 934 enableJobManageBy: false, 935 job: batch.Job{ 936 ObjectMeta: getValidObjectMeta(0), 937 Spec: batch.JobSpec{ 938 Selector: validSelector, 939 ManualSelector: ptr.To(false), 940 Template: validPodTemplateSpec, 941 ManagedBy: ptr.To("custom-controller-name"), 942 }, 943 }, 944 wantJob: batch.Job{ 945 ObjectMeta: getValidObjectMeta(1), 946 Spec: batch.JobSpec{ 947 Selector: validSelector, 948 ManualSelector: ptr.To(false), 949 Template: expectedPodTemplateSpec, 950 }, 951 }, 952 }, 953 "managedBy field is set when the feature gate is enabled": { 954 enableJobManageBy: true, 955 job: batch.Job{ 956 ObjectMeta: getValidObjectMeta(0), 957 Spec: batch.JobSpec{ 958 Selector: validSelector, 959 ManualSelector: ptr.To(false), 960 Template: validPodTemplateSpec, 961 ManagedBy: ptr.To("custom-controller-name"), 962 }, 963 }, 964 wantJob: batch.Job{ 965 ObjectMeta: getValidObjectMeta(1), 966 Spec: batch.JobSpec{ 967 Selector: validSelector, 968 ManualSelector: ptr.To(false), 969 Template: expectedPodTemplateSpec, 970 ManagedBy: ptr.To("custom-controller-name"), 971 }, 972 }, 973 }, 974 } 975 976 for name, tc := range cases { 977 t.Run(name, func(t *testing.T) { 978 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy) 979 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex) 980 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodReplacementPolicy, tc.enableJobPodReplacementPolicy) 981 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobManagedBy, tc.enableJobManageBy) 982 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobSuccessPolicy, tc.enableJobSuccessPolicy) 983 ctx := genericapirequest.NewDefaultContext() 984 985 Strategy.PrepareForCreate(ctx, &tc.job) 986 987 if diff := cmp.Diff(tc.wantJob, tc.job); diff != "" { 988 t.Errorf("Job pod failure policy (-want,+got):\n%s", diff) 989 } 990 }) 991 } 992 } 993 994 func TestJobStrategy_GarbageCollectionPolicy(t *testing.T) { 995 // Make sure we correctly implement the interface. 996 // Otherwise a typo could silently change the default. 997 var gcds rest.GarbageCollectionDeleteStrategy = Strategy 998 if got, want := gcds.DefaultGarbageCollectionPolicy(genericapirequest.NewContext()), rest.DeleteDependents; got != want { 999 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want) 1000 } 1001 1002 var ( 1003 v1Ctx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v1", Resource: "jobs"}) 1004 otherVersionCtx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v100", Resource: "jobs"}) 1005 ) 1006 if got, want := gcds.DefaultGarbageCollectionPolicy(v1Ctx), rest.OrphanDependents; got != want { 1007 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want) 1008 } 1009 if got, want := gcds.DefaultGarbageCollectionPolicy(otherVersionCtx), rest.DeleteDependents; got != want { 1010 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want) 1011 } 1012 } 1013 1014 func TestJobStrategy_ValidateUpdate(t *testing.T) { 1015 ctx := genericapirequest.NewDefaultContext() 1016 validSelector := &metav1.LabelSelector{ 1017 MatchLabels: map[string]string{"a": "b"}, 1018 } 1019 validPodTemplateSpec := api.PodTemplateSpec{ 1020 ObjectMeta: metav1.ObjectMeta{ 1021 Labels: validSelector.MatchLabels, 1022 }, 1023 Spec: api.PodSpec{ 1024 RestartPolicy: api.RestartPolicyOnFailure, 1025 DNSPolicy: api.DNSClusterFirst, 1026 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1027 }, 1028 } 1029 validPodTemplateSpecNever := *validPodTemplateSpec.DeepCopy() 1030 validPodTemplateSpecNever.Spec.RestartPolicy = api.RestartPolicyNever 1031 now := metav1.Now() 1032 cases := map[string]struct { 1033 enableJobPodFailurePolicy bool 1034 enableJobBackoffLimitPerIndex bool 1035 job *batch.Job 1036 update func(*batch.Job) 1037 wantErrs field.ErrorList 1038 }{ 1039 "update parallelism": { 1040 job: &batch.Job{ 1041 ObjectMeta: metav1.ObjectMeta{ 1042 Name: "myjob", 1043 Namespace: metav1.NamespaceDefault, 1044 ResourceVersion: "0", 1045 }, 1046 Spec: batch.JobSpec{ 1047 Selector: validSelector, 1048 Template: validPodTemplateSpec, 1049 ManualSelector: ptr.To(true), 1050 Parallelism: ptr.To[int32](1), 1051 }, 1052 }, 1053 update: func(job *batch.Job) { 1054 job.Spec.Parallelism = ptr.To[int32](2) 1055 }, 1056 }, 1057 "update completions disallowed": { 1058 job: &batch.Job{ 1059 ObjectMeta: metav1.ObjectMeta{ 1060 Name: "myjob", 1061 Namespace: metav1.NamespaceDefault, 1062 ResourceVersion: "0", 1063 }, 1064 Spec: batch.JobSpec{ 1065 Selector: validSelector, 1066 Template: validPodTemplateSpec, 1067 ManualSelector: ptr.To(true), 1068 Parallelism: ptr.To[int32](1), 1069 Completions: ptr.To[int32](1), 1070 }, 1071 }, 1072 update: func(job *batch.Job) { 1073 job.Spec.Completions = ptr.To[int32](2) 1074 }, 1075 wantErrs: field.ErrorList{ 1076 {Type: field.ErrorTypeInvalid, Field: "spec.completions"}, 1077 }, 1078 }, 1079 "preserving tracking annotation": { 1080 job: &batch.Job{ 1081 ObjectMeta: metav1.ObjectMeta{ 1082 Name: "myjob", 1083 Namespace: metav1.NamespaceDefault, 1084 ResourceVersion: "0", 1085 Annotations: map[string]string{ 1086 batch.JobTrackingFinalizer: "", 1087 }, 1088 }, 1089 Spec: batch.JobSpec{ 1090 Selector: validSelector, 1091 Template: validPodTemplateSpec, 1092 ManualSelector: ptr.To(true), 1093 Parallelism: ptr.To[int32](1), 1094 }, 1095 }, 1096 update: func(job *batch.Job) { 1097 job.Annotations["foo"] = "bar" 1098 }, 1099 }, 1100 "deleting user annotation": { 1101 job: &batch.Job{ 1102 ObjectMeta: metav1.ObjectMeta{ 1103 Name: "myjob", 1104 Namespace: metav1.NamespaceDefault, 1105 ResourceVersion: "0", 1106 Annotations: map[string]string{ 1107 batch.JobTrackingFinalizer: "", 1108 "foo": "bar", 1109 }, 1110 }, 1111 Spec: batch.JobSpec{ 1112 Selector: validSelector, 1113 Template: validPodTemplateSpec, 1114 ManualSelector: ptr.To(true), 1115 Parallelism: ptr.To[int32](1), 1116 }, 1117 }, 1118 update: func(job *batch.Job) { 1119 delete(job.Annotations, "foo") 1120 }, 1121 }, 1122 "updating node selector for unsuspended job disallowed": { 1123 job: &batch.Job{ 1124 ObjectMeta: metav1.ObjectMeta{ 1125 Name: "myjob", 1126 Namespace: metav1.NamespaceDefault, 1127 ResourceVersion: "0", 1128 Annotations: map[string]string{"foo": "bar"}, 1129 }, 1130 Spec: batch.JobSpec{ 1131 Selector: validSelector, 1132 Template: validPodTemplateSpec, 1133 ManualSelector: ptr.To(true), 1134 Parallelism: ptr.To[int32](1), 1135 }, 1136 }, 1137 update: func(job *batch.Job) { 1138 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 1139 }, 1140 wantErrs: field.ErrorList{ 1141 {Type: field.ErrorTypeInvalid, Field: "spec.template"}, 1142 }, 1143 }, 1144 "updating node selector for suspended but previously started job disallowed": { 1145 job: &batch.Job{ 1146 ObjectMeta: metav1.ObjectMeta{ 1147 Name: "myjob", 1148 Namespace: metav1.NamespaceDefault, 1149 ResourceVersion: "0", 1150 Annotations: map[string]string{"foo": "bar"}, 1151 }, 1152 Spec: batch.JobSpec{ 1153 Selector: validSelector, 1154 Template: validPodTemplateSpec, 1155 ManualSelector: ptr.To(true), 1156 Parallelism: ptr.To[int32](1), 1157 Suspend: ptr.To(true), 1158 }, 1159 Status: batch.JobStatus{ 1160 StartTime: &now, 1161 }, 1162 }, 1163 update: func(job *batch.Job) { 1164 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 1165 }, 1166 wantErrs: field.ErrorList{ 1167 {Type: field.ErrorTypeInvalid, Field: "spec.template"}, 1168 }, 1169 }, 1170 "updating node selector for suspended and not previously started job allowed": { 1171 job: &batch.Job{ 1172 ObjectMeta: metav1.ObjectMeta{ 1173 Name: "myjob", 1174 Namespace: metav1.NamespaceDefault, 1175 ResourceVersion: "0", 1176 Annotations: map[string]string{"foo": "bar"}, 1177 }, 1178 Spec: batch.JobSpec{ 1179 Selector: validSelector, 1180 Template: validPodTemplateSpec, 1181 ManualSelector: ptr.To(true), 1182 Parallelism: ptr.To[int32](1), 1183 Suspend: ptr.To(true), 1184 }, 1185 }, 1186 update: func(job *batch.Job) { 1187 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 1188 }, 1189 }, 1190 "invalid label selector": { 1191 job: &batch.Job{ 1192 ObjectMeta: metav1.ObjectMeta{ 1193 Name: "myjob", 1194 Namespace: metav1.NamespaceDefault, 1195 ResourceVersion: "0", 1196 Annotations: map[string]string{"foo": "bar"}, 1197 }, 1198 Spec: batch.JobSpec{ 1199 Selector: &metav1.LabelSelector{ 1200 MatchLabels: map[string]string{"a": "b"}, 1201 MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "key", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"bad value"}}}, 1202 }, 1203 ManualSelector: ptr.To(true), 1204 Template: validPodTemplateSpec, 1205 }, 1206 }, 1207 update: func(job *batch.Job) { 1208 job.Annotations["hello"] = "world" 1209 }, 1210 }, 1211 "old job has no batch.kubernetes.io labels": { 1212 job: &batch.Job{ 1213 ObjectMeta: metav1.ObjectMeta{ 1214 Name: "myjob", 1215 UID: "test", 1216 Namespace: metav1.NamespaceDefault, 1217 ResourceVersion: "10", 1218 Annotations: map[string]string{"hello": "world"}, 1219 }, 1220 Spec: batch.JobSpec{ 1221 Selector: &metav1.LabelSelector{ 1222 MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "test"}, 1223 }, 1224 Parallelism: ptr.To[int32](4), 1225 Template: api.PodTemplateSpec{ 1226 ObjectMeta: metav1.ObjectMeta{ 1227 Labels: map[string]string{batch.LegacyJobNameLabel: "myjob", batch.LegacyControllerUidLabel: "test"}, 1228 }, 1229 Spec: api.PodSpec{ 1230 RestartPolicy: api.RestartPolicyOnFailure, 1231 DNSPolicy: api.DNSClusterFirst, 1232 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1233 }, 1234 }, 1235 }, 1236 }, 1237 update: func(job *batch.Job) { 1238 job.Annotations["hello"] = "world" 1239 }, 1240 }, 1241 "old job has all labels": { 1242 job: &batch.Job{ 1243 ObjectMeta: metav1.ObjectMeta{ 1244 Name: "myjob", 1245 UID: "test", 1246 Namespace: metav1.NamespaceDefault, 1247 ResourceVersion: "10", 1248 Annotations: map[string]string{"foo": "bar"}, 1249 }, 1250 Spec: batch.JobSpec{ 1251 Selector: &metav1.LabelSelector{ 1252 MatchLabels: map[string]string{batch.ControllerUidLabel: "test"}, 1253 }, 1254 Parallelism: ptr.To[int32](4), 1255 Template: api.PodTemplateSpec{ 1256 ObjectMeta: metav1.ObjectMeta{ 1257 Labels: map[string]string{batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: "test", batch.ControllerUidLabel: "test"}, 1258 }, 1259 Spec: api.PodSpec{ 1260 RestartPolicy: api.RestartPolicyOnFailure, 1261 DNSPolicy: api.DNSClusterFirst, 1262 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1263 }, 1264 }, 1265 }, 1266 }, 1267 update: func(job *batch.Job) { 1268 job.Annotations["hello"] = "world" 1269 }, 1270 }, 1271 "old job is using FailIndex JobBackoffLimitPerIndex is disabled, but FailIndex was already used": { 1272 enableJobPodFailurePolicy: true, 1273 enableJobBackoffLimitPerIndex: false, 1274 job: &batch.Job{ 1275 ObjectMeta: metav1.ObjectMeta{ 1276 Name: "myjob", 1277 Namespace: metav1.NamespaceDefault, 1278 ResourceVersion: "0", 1279 Annotations: map[string]string{"foo": "bar"}, 1280 }, 1281 Spec: batch.JobSpec{ 1282 CompletionMode: completionModePtr(batch.IndexedCompletion), 1283 Completions: ptr.To[int32](2), 1284 BackoffLimitPerIndex: ptr.To[int32](1), 1285 Selector: validSelector, 1286 ManualSelector: ptr.To(true), 1287 Template: validPodTemplateSpecNever, 1288 PodFailurePolicy: &batch.PodFailurePolicy{ 1289 Rules: []batch.PodFailurePolicyRule{ 1290 { 1291 Action: batch.PodFailurePolicyActionFailIndex, 1292 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1293 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1294 Values: []int32{1}, 1295 }, 1296 }, 1297 }, 1298 }, 1299 }, 1300 }, 1301 update: func(job *batch.Job) { 1302 job.Annotations["hello"] = "world" 1303 }, 1304 }, 1305 } 1306 for name, tc := range cases { 1307 t.Run(name, func(t *testing.T) { 1308 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy) 1309 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex) 1310 newJob := tc.job.DeepCopy() 1311 tc.update(newJob) 1312 errs := Strategy.ValidateUpdate(ctx, newJob, tc.job) 1313 if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" { 1314 t.Errorf("Unexpected errors (-want,+got):\n%s", diff) 1315 } 1316 }) 1317 } 1318 } 1319 1320 func TestJobStrategy_WarningsOnUpdate(t *testing.T) { 1321 ctx := genericapirequest.NewDefaultContext() 1322 validSelector := &metav1.LabelSelector{ 1323 MatchLabels: map[string]string{"a": "b"}, 1324 } 1325 validPodTemplateSpec := api.PodTemplateSpec{ 1326 ObjectMeta: metav1.ObjectMeta{ 1327 Labels: validSelector.MatchLabels, 1328 }, 1329 Spec: api.PodSpec{ 1330 RestartPolicy: api.RestartPolicyOnFailure, 1331 DNSPolicy: api.DNSClusterFirst, 1332 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1333 }, 1334 } 1335 cases := map[string]struct { 1336 oldJob *batch.Job 1337 job *batch.Job 1338 wantWarningsCount int32 1339 }{ 1340 "generation 0 for both": { 1341 job: &batch.Job{ 1342 ObjectMeta: metav1.ObjectMeta{ 1343 Name: "myjob", 1344 Namespace: metav1.NamespaceDefault, 1345 ResourceVersion: "0", 1346 Generation: 0, 1347 }, 1348 Spec: batch.JobSpec{ 1349 Selector: validSelector, 1350 Template: validPodTemplateSpec, 1351 ManualSelector: ptr.To(true), 1352 Parallelism: ptr.To[int32](1), 1353 }, 1354 }, 1355 1356 oldJob: &batch.Job{ 1357 ObjectMeta: metav1.ObjectMeta{ 1358 Name: "myjob", 1359 Namespace: metav1.NamespaceDefault, 1360 ResourceVersion: "0", 1361 Generation: 0, 1362 }, 1363 Spec: batch.JobSpec{ 1364 Selector: validSelector, 1365 Template: validPodTemplateSpec, 1366 ManualSelector: ptr.To(true), 1367 Parallelism: ptr.To[int32](1), 1368 }, 1369 }, 1370 }, 1371 "generation 1 for new; force WarningsOnUpdate to check PodTemplate for updates": { 1372 job: &batch.Job{ 1373 ObjectMeta: metav1.ObjectMeta{ 1374 Name: "myjob", 1375 Namespace: metav1.NamespaceDefault, 1376 ResourceVersion: "0", 1377 Generation: 1, 1378 }, 1379 Spec: batch.JobSpec{ 1380 Selector: validSelector, 1381 Template: validPodTemplateSpec, 1382 ManualSelector: ptr.To(true), 1383 Parallelism: ptr.To[int32](1), 1384 }, 1385 }, 1386 1387 oldJob: &batch.Job{ 1388 ObjectMeta: metav1.ObjectMeta{ 1389 Name: "myjob", 1390 Namespace: metav1.NamespaceDefault, 1391 ResourceVersion: "0", 1392 Generation: 0, 1393 }, 1394 Spec: batch.JobSpec{ 1395 Selector: validSelector, 1396 Template: validPodTemplateSpec, 1397 ManualSelector: ptr.To(true), 1398 Parallelism: ptr.To[int32](1), 1399 }, 1400 }, 1401 }, 1402 "force validation failure in pod template": { 1403 job: &batch.Job{ 1404 ObjectMeta: metav1.ObjectMeta{ 1405 Name: "myjob", 1406 Namespace: metav1.NamespaceDefault, 1407 ResourceVersion: "0", 1408 Generation: 1, 1409 }, 1410 Spec: batch.JobSpec{ 1411 Selector: validSelector, 1412 Template: api.PodTemplateSpec{ 1413 Spec: api.PodSpec{ImagePullSecrets: []api.LocalObjectReference{{Name: ""}}}, 1414 }, 1415 ManualSelector: ptr.To(true), 1416 Parallelism: ptr.To[int32](1), 1417 }, 1418 }, 1419 1420 oldJob: &batch.Job{ 1421 ObjectMeta: metav1.ObjectMeta{ 1422 Name: "myjob", 1423 Namespace: metav1.NamespaceDefault, 1424 ResourceVersion: "0", 1425 Generation: 0, 1426 }, 1427 Spec: batch.JobSpec{ 1428 Selector: validSelector, 1429 Template: validPodTemplateSpec, 1430 ManualSelector: ptr.To(true), 1431 Parallelism: ptr.To[int32](1), 1432 }, 1433 }, 1434 wantWarningsCount: 1, 1435 }, 1436 "Invalid transition to high parallelism": { 1437 wantWarningsCount: 1, 1438 job: &batch.Job{ 1439 ObjectMeta: metav1.ObjectMeta{ 1440 Name: "myjob2", 1441 Namespace: metav1.NamespaceDefault, 1442 Generation: 1, 1443 ResourceVersion: "0", 1444 }, 1445 Spec: batch.JobSpec{ 1446 CompletionMode: completionModePtr(batch.IndexedCompletion), 1447 Completions: ptr.To[int32](100_001), 1448 Parallelism: ptr.To[int32](10_001), 1449 Template: validPodTemplateSpec, 1450 }, 1451 }, 1452 oldJob: &batch.Job{ 1453 ObjectMeta: metav1.ObjectMeta{ 1454 Name: "myjob2", 1455 Namespace: metav1.NamespaceDefault, 1456 Generation: 0, 1457 ResourceVersion: "0", 1458 }, 1459 Spec: batch.JobSpec{ 1460 CompletionMode: completionModePtr(batch.IndexedCompletion), 1461 Completions: ptr.To[int32](100_001), 1462 Parallelism: ptr.To[int32](10_000), 1463 Template: validPodTemplateSpec, 1464 }, 1465 }, 1466 }, 1467 } 1468 for val, tc := range cases { 1469 t.Run(val, func(t *testing.T) { 1470 gotWarnings := Strategy.WarningsOnUpdate(ctx, tc.job, tc.oldJob) 1471 if len(gotWarnings) != int(tc.wantWarningsCount) { 1472 t.Errorf("got warning length of %d but expected %d", len(gotWarnings), tc.wantWarningsCount) 1473 } 1474 }) 1475 } 1476 } 1477 func TestJobStrategy_WarningsOnCreate(t *testing.T) { 1478 ctx := genericapirequest.NewDefaultContext() 1479 1480 theUID := types.UID("1a2b3c4d5e6f7g8h9i0k") 1481 validSelector := &metav1.LabelSelector{ 1482 MatchLabels: map[string]string{"a": "b"}, 1483 } 1484 validPodTemplate := api.PodTemplateSpec{ 1485 ObjectMeta: metav1.ObjectMeta{ 1486 Labels: validSelector.MatchLabels, 1487 }, 1488 Spec: api.PodSpec{ 1489 RestartPolicy: api.RestartPolicyOnFailure, 1490 DNSPolicy: api.DNSClusterFirst, 1491 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1492 }, 1493 } 1494 validSpec := batch.JobSpec{ 1495 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 1496 Selector: nil, 1497 Template: validPodTemplate, 1498 } 1499 1500 testcases := map[string]struct { 1501 job *batch.Job 1502 wantWarningsCount int32 1503 }{ 1504 "happy path job": { 1505 job: &batch.Job{ 1506 ObjectMeta: metav1.ObjectMeta{ 1507 Name: "myjob2", 1508 Namespace: metav1.NamespaceDefault, 1509 UID: theUID, 1510 }, 1511 Spec: validSpec, 1512 }, 1513 }, 1514 "dns invalid name": { 1515 wantWarningsCount: 1, 1516 job: &batch.Job{ 1517 ObjectMeta: metav1.ObjectMeta{ 1518 Name: "my job2", 1519 Namespace: metav1.NamespaceDefault, 1520 UID: theUID, 1521 }, 1522 Spec: validSpec, 1523 }, 1524 }, 1525 "high completions and parallelism": { 1526 wantWarningsCount: 1, 1527 job: &batch.Job{ 1528 ObjectMeta: metav1.ObjectMeta{ 1529 Name: "myjob2", 1530 Namespace: metav1.NamespaceDefault, 1531 UID: theUID, 1532 }, 1533 Spec: batch.JobSpec{ 1534 CompletionMode: completionModePtr(batch.IndexedCompletion), 1535 Parallelism: ptr.To[int32](100_001), 1536 Completions: ptr.To[int32](100_001), 1537 Template: validPodTemplate, 1538 }, 1539 }, 1540 }, 1541 } 1542 for name, tc := range testcases { 1543 t.Run(name, func(t *testing.T) { 1544 gotWarnings := Strategy.WarningsOnCreate(ctx, tc.job) 1545 if len(gotWarnings) != int(tc.wantWarningsCount) { 1546 t.Errorf("got warning length of %d but expected %d", len(gotWarnings), tc.wantWarningsCount) 1547 } 1548 }) 1549 } 1550 } 1551 func TestJobStrategy_Validate(t *testing.T) { 1552 ctx := genericapirequest.NewDefaultContext() 1553 1554 theUID := getValidUID() 1555 validSelector := getValidLabelSelector() 1556 batchLabels := getValidBatchLabels() 1557 labelsWithNonBatch := getValidBatchLabelsWithNonBatch() 1558 defaultSelector := &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}} 1559 validPodSpec := api.PodSpec{ 1560 RestartPolicy: api.RestartPolicyOnFailure, 1561 DNSPolicy: api.DNSClusterFirst, 1562 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1563 } 1564 validPodSpecNever := *validPodSpec.DeepCopy() 1565 validPodSpecNever.RestartPolicy = api.RestartPolicyNever 1566 validObjectMeta := getValidObjectMeta(0) 1567 testcases := map[string]struct { 1568 enableJobPodFailurePolicy bool 1569 enableJobBackoffLimitPerIndex bool 1570 job *batch.Job 1571 wantJob *batch.Job 1572 wantWarningCount int32 1573 }{ 1574 "valid job with batch labels in pod template": { 1575 job: &batch.Job{ 1576 ObjectMeta: validObjectMeta, 1577 Spec: batch.JobSpec{ 1578 Selector: defaultSelector, 1579 ManualSelector: ptr.To(false), 1580 Template: api.PodTemplateSpec{ 1581 ObjectMeta: metav1.ObjectMeta{ 1582 Labels: batchLabels, 1583 }, 1584 Spec: validPodSpec, 1585 }}, 1586 }, 1587 wantJob: &batch.Job{ 1588 ObjectMeta: validObjectMeta, 1589 Spec: batch.JobSpec{ 1590 Selector: defaultSelector, 1591 ManualSelector: ptr.To(false), 1592 Template: api.PodTemplateSpec{ 1593 ObjectMeta: metav1.ObjectMeta{ 1594 Labels: batchLabels, 1595 }, 1596 Spec: validPodSpec, 1597 }}, 1598 }, 1599 }, 1600 "valid job with batch and non-batch labels in pod template": { 1601 job: &batch.Job{ 1602 ObjectMeta: validObjectMeta, 1603 Spec: batch.JobSpec{ 1604 Selector: defaultSelector, 1605 ManualSelector: ptr.To(false), 1606 Template: api.PodTemplateSpec{ 1607 ObjectMeta: metav1.ObjectMeta{ 1608 Labels: labelsWithNonBatch, 1609 }, 1610 Spec: validPodSpec, 1611 }}, 1612 }, 1613 wantJob: &batch.Job{ 1614 ObjectMeta: validObjectMeta, 1615 Spec: batch.JobSpec{ 1616 Selector: defaultSelector, 1617 ManualSelector: ptr.To(false), 1618 Template: api.PodTemplateSpec{ 1619 ObjectMeta: metav1.ObjectMeta{ 1620 Labels: labelsWithNonBatch, 1621 }, 1622 Spec: validPodSpec, 1623 }}, 1624 }, 1625 }, 1626 "job with non-batch labels and without batch labels in pod template": { 1627 job: &batch.Job{ 1628 ObjectMeta: validObjectMeta, 1629 Spec: batch.JobSpec{ 1630 Selector: defaultSelector, 1631 ManualSelector: ptr.To(false), 1632 Template: api.PodTemplateSpec{ 1633 ObjectMeta: metav1.ObjectMeta{ 1634 Labels: map[string]string{}, 1635 }, 1636 Spec: validPodSpec, 1637 }}, 1638 }, 1639 wantJob: &batch.Job{ 1640 ObjectMeta: validObjectMeta, 1641 Spec: batch.JobSpec{ 1642 Selector: defaultSelector, 1643 ManualSelector: ptr.To(false), 1644 Template: api.PodTemplateSpec{ 1645 ObjectMeta: metav1.ObjectMeta{ 1646 Labels: map[string]string{}, 1647 }, 1648 Spec: validPodSpec, 1649 }}, 1650 }, 1651 wantWarningCount: 5, 1652 }, 1653 "no labels in job": { 1654 job: &batch.Job{ 1655 ObjectMeta: validObjectMeta, 1656 Spec: batch.JobSpec{ 1657 Selector: defaultSelector, 1658 Template: api.PodTemplateSpec{ 1659 Spec: validPodSpec, 1660 }}, 1661 }, 1662 wantJob: &batch.Job{ 1663 ObjectMeta: validObjectMeta, 1664 Spec: batch.JobSpec{ 1665 Selector: defaultSelector, 1666 Template: api.PodTemplateSpec{ 1667 Spec: validPodSpec, 1668 }}, 1669 }, 1670 wantWarningCount: 5, 1671 }, 1672 "manual selector; do not generate labels": { 1673 job: &batch.Job{ 1674 ObjectMeta: validObjectMeta, 1675 Spec: batch.JobSpec{ 1676 Selector: validSelector, 1677 Template: api.PodTemplateSpec{ 1678 ObjectMeta: metav1.ObjectMeta{ 1679 Labels: validSelector.MatchLabels, 1680 }, 1681 Spec: validPodSpec, 1682 }, 1683 Completions: ptr.To[int32](2), 1684 ManualSelector: ptr.To(true), 1685 }, 1686 }, 1687 wantJob: &batch.Job{ 1688 ObjectMeta: validObjectMeta, 1689 Spec: batch.JobSpec{ 1690 Selector: validSelector, 1691 Template: api.PodTemplateSpec{ 1692 ObjectMeta: metav1.ObjectMeta{ 1693 Labels: validSelector.MatchLabels, 1694 }, 1695 Spec: validPodSpec, 1696 }, 1697 Completions: ptr.To[int32](2), 1698 ManualSelector: ptr.To(true), 1699 }, 1700 }, 1701 }, 1702 "valid job with extended configuration": { 1703 job: &batch.Job{ 1704 ObjectMeta: validObjectMeta, 1705 Spec: batch.JobSpec{ 1706 Selector: defaultSelector, 1707 ManualSelector: ptr.To(false), 1708 Template: api.PodTemplateSpec{ 1709 ObjectMeta: metav1.ObjectMeta{ 1710 Labels: labelsWithNonBatch, 1711 }, 1712 Spec: validPodSpec, 1713 }, 1714 Completions: ptr.To[int32](2), 1715 Suspend: ptr.To(true), 1716 TTLSecondsAfterFinished: ptr.To[int32](0), 1717 CompletionMode: completionModePtr(batch.IndexedCompletion), 1718 }, 1719 }, 1720 wantJob: &batch.Job{ 1721 ObjectMeta: validObjectMeta, 1722 Spec: batch.JobSpec{ 1723 Selector: defaultSelector, 1724 ManualSelector: ptr.To(false), 1725 Template: api.PodTemplateSpec{ 1726 ObjectMeta: metav1.ObjectMeta{ 1727 Labels: labelsWithNonBatch, 1728 }, 1729 Spec: validPodSpec, 1730 }, 1731 Completions: ptr.To[int32](2), 1732 Suspend: ptr.To(true), 1733 TTLSecondsAfterFinished: ptr.To[int32](0), 1734 CompletionMode: completionModePtr(batch.IndexedCompletion), 1735 }, 1736 }, 1737 }, 1738 "fail validation due to invalid volume spec": { 1739 job: &batch.Job{ 1740 ObjectMeta: validObjectMeta, 1741 Spec: batch.JobSpec{ 1742 Selector: defaultSelector, 1743 ManualSelector: ptr.To(false), 1744 Template: api.PodTemplateSpec{ 1745 ObjectMeta: metav1.ObjectMeta{ 1746 Labels: labelsWithNonBatch, 1747 }, 1748 Spec: api.PodSpec{ 1749 RestartPolicy: api.RestartPolicyOnFailure, 1750 DNSPolicy: api.DNSClusterFirst, 1751 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1752 Volumes: []api.Volume{{Name: "volume-name"}}, 1753 }, 1754 }, 1755 }, 1756 }, 1757 wantJob: &batch.Job{ 1758 ObjectMeta: validObjectMeta, 1759 Spec: batch.JobSpec{ 1760 Selector: defaultSelector, 1761 ManualSelector: ptr.To(false), 1762 Template: api.PodTemplateSpec{ 1763 ObjectMeta: metav1.ObjectMeta{ 1764 Labels: labelsWithNonBatch, 1765 }, 1766 Spec: api.PodSpec{ 1767 RestartPolicy: api.RestartPolicyOnFailure, 1768 DNSPolicy: api.DNSClusterFirst, 1769 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1770 Volumes: []api.Volume{{Name: "volume-name"}}, 1771 }, 1772 }, 1773 }, 1774 }, 1775 wantWarningCount: 1, 1776 }, 1777 "FailIndex action; when JobBackoffLimitPerIndex is disabled - validation error": { 1778 enableJobPodFailurePolicy: true, 1779 enableJobBackoffLimitPerIndex: false, 1780 job: &batch.Job{ 1781 ObjectMeta: validObjectMeta, 1782 Spec: batch.JobSpec{ 1783 Selector: validSelector, 1784 ManualSelector: ptr.To(true), 1785 Template: api.PodTemplateSpec{ 1786 ObjectMeta: metav1.ObjectMeta{ 1787 Labels: validSelector.MatchLabels, 1788 }, 1789 Spec: validPodSpecNever, 1790 }, 1791 PodFailurePolicy: &batch.PodFailurePolicy{ 1792 Rules: []batch.PodFailurePolicyRule{ 1793 { 1794 Action: batch.PodFailurePolicyActionFailIndex, 1795 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1796 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1797 Values: []int32{1}, 1798 }, 1799 }, 1800 }, 1801 }, 1802 }, 1803 }, 1804 wantJob: &batch.Job{ 1805 ObjectMeta: validObjectMeta, 1806 Spec: batch.JobSpec{ 1807 Selector: validSelector, 1808 ManualSelector: ptr.To(true), 1809 Template: api.PodTemplateSpec{ 1810 ObjectMeta: metav1.ObjectMeta{ 1811 Labels: validSelector.MatchLabels, 1812 }, 1813 Spec: validPodSpecNever, 1814 }, 1815 PodFailurePolicy: &batch.PodFailurePolicy{ 1816 Rules: []batch.PodFailurePolicyRule{ 1817 { 1818 Action: batch.PodFailurePolicyActionFailIndex, 1819 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1820 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1821 Values: []int32{1}, 1822 }, 1823 }, 1824 }, 1825 }, 1826 }, 1827 }, 1828 wantWarningCount: 1, 1829 }, 1830 "FailIndex action; when JobBackoffLimitPerIndex is enabled, but not used - validation error": { 1831 enableJobPodFailurePolicy: true, 1832 enableJobBackoffLimitPerIndex: true, 1833 job: &batch.Job{ 1834 ObjectMeta: validObjectMeta, 1835 Spec: batch.JobSpec{ 1836 Selector: validSelector, 1837 ManualSelector: ptr.To(true), 1838 Template: api.PodTemplateSpec{ 1839 ObjectMeta: metav1.ObjectMeta{ 1840 Labels: validSelector.MatchLabels, 1841 }, 1842 Spec: validPodSpecNever, 1843 }, 1844 PodFailurePolicy: &batch.PodFailurePolicy{ 1845 Rules: []batch.PodFailurePolicyRule{ 1846 { 1847 Action: batch.PodFailurePolicyActionFailIndex, 1848 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1849 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1850 Values: []int32{1}, 1851 }, 1852 }, 1853 }, 1854 }, 1855 }, 1856 }, 1857 wantJob: &batch.Job{ 1858 ObjectMeta: validObjectMeta, 1859 Spec: batch.JobSpec{ 1860 Selector: validSelector, 1861 ManualSelector: ptr.To(true), 1862 Template: api.PodTemplateSpec{ 1863 ObjectMeta: metav1.ObjectMeta{ 1864 Labels: validSelector.MatchLabels, 1865 }, 1866 Spec: validPodSpecNever, 1867 }, 1868 PodFailurePolicy: &batch.PodFailurePolicy{ 1869 Rules: []batch.PodFailurePolicyRule{ 1870 { 1871 Action: batch.PodFailurePolicyActionFailIndex, 1872 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1873 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1874 Values: []int32{1}, 1875 }, 1876 }, 1877 }, 1878 }, 1879 }, 1880 }, 1881 wantWarningCount: 1, 1882 }, 1883 "FailIndex action; when JobBackoffLimitPerIndex is enabled and used - no error": { 1884 enableJobPodFailurePolicy: true, 1885 enableJobBackoffLimitPerIndex: true, 1886 job: &batch.Job{ 1887 ObjectMeta: validObjectMeta, 1888 Spec: batch.JobSpec{ 1889 CompletionMode: completionModePtr(batch.IndexedCompletion), 1890 Completions: ptr.To[int32](2), 1891 BackoffLimitPerIndex: ptr.To[int32](1), 1892 Selector: validSelector, 1893 ManualSelector: ptr.To(true), 1894 Template: api.PodTemplateSpec{ 1895 ObjectMeta: metav1.ObjectMeta{ 1896 Labels: validSelector.MatchLabels, 1897 }, 1898 Spec: validPodSpecNever, 1899 }, 1900 PodFailurePolicy: &batch.PodFailurePolicy{ 1901 Rules: []batch.PodFailurePolicyRule{ 1902 { 1903 Action: batch.PodFailurePolicyActionFailIndex, 1904 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1905 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1906 Values: []int32{1}, 1907 }, 1908 }, 1909 }, 1910 }, 1911 }, 1912 }, 1913 wantJob: &batch.Job{ 1914 ObjectMeta: validObjectMeta, 1915 Spec: batch.JobSpec{ 1916 CompletionMode: completionModePtr(batch.IndexedCompletion), 1917 Completions: ptr.To[int32](2), 1918 BackoffLimitPerIndex: ptr.To[int32](1), 1919 Selector: validSelector, 1920 ManualSelector: ptr.To(true), 1921 Template: api.PodTemplateSpec{ 1922 ObjectMeta: metav1.ObjectMeta{ 1923 Labels: validSelector.MatchLabels, 1924 }, 1925 Spec: validPodSpecNever, 1926 }, 1927 PodFailurePolicy: &batch.PodFailurePolicy{ 1928 Rules: []batch.PodFailurePolicyRule{ 1929 { 1930 Action: batch.PodFailurePolicyActionFailIndex, 1931 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1932 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1933 Values: []int32{1}, 1934 }, 1935 }, 1936 }, 1937 }, 1938 }, 1939 }, 1940 }, 1941 } 1942 for name, tc := range testcases { 1943 t.Run(name, func(t *testing.T) { 1944 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy) 1945 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex) 1946 errs := Strategy.Validate(ctx, tc.job) 1947 if len(errs) != int(tc.wantWarningCount) { 1948 t.Errorf("want warnings %d but got %d, errors: %v", tc.wantWarningCount, len(errs), errs) 1949 } 1950 if diff := cmp.Diff(tc.wantJob, tc.job); diff != "" { 1951 t.Errorf("Unexpected job (-want,+got):\n%s", diff) 1952 } 1953 }) 1954 } 1955 } 1956 1957 func TestStrategy_ResetFields(t *testing.T) { 1958 resetFields := Strategy.GetResetFields() 1959 if len(resetFields) != 1 { 1960 t.Errorf("ResetFields should have 1 element, but have %d", len(resetFields)) 1961 } 1962 } 1963 1964 func TestJobStatusStrategy_ResetFields(t *testing.T) { 1965 resetFields := StatusStrategy.GetResetFields() 1966 if len(resetFields) != 1 { 1967 t.Errorf("ResetFields should have 1 element, but have %d", len(resetFields)) 1968 } 1969 } 1970 1971 func TestStatusStrategy_PrepareForUpdate(t *testing.T) { 1972 ctx := genericapirequest.NewDefaultContext() 1973 validSelector := &metav1.LabelSelector{ 1974 MatchLabels: map[string]string{"a": "b"}, 1975 } 1976 validPodTemplateSpec := api.PodTemplateSpec{ 1977 ObjectMeta: metav1.ObjectMeta{ 1978 Labels: validSelector.MatchLabels, 1979 }, 1980 Spec: api.PodSpec{ 1981 RestartPolicy: api.RestartPolicyOnFailure, 1982 DNSPolicy: api.DNSClusterFirst, 1983 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1984 }, 1985 } 1986 validObjectMeta := metav1.ObjectMeta{ 1987 Name: "myjob", 1988 Namespace: metav1.NamespaceDefault, 1989 ResourceVersion: "10", 1990 } 1991 1992 cases := map[string]struct { 1993 job *batch.Job 1994 newJob *batch.Job 1995 wantJob *batch.Job 1996 }{ 1997 "job must allow status updates": { 1998 job: &batch.Job{ 1999 ObjectMeta: validObjectMeta, 2000 Spec: batch.JobSpec{ 2001 Selector: validSelector, 2002 Template: validPodTemplateSpec, 2003 Parallelism: ptr.To[int32](4), 2004 }, 2005 Status: batch.JobStatus{ 2006 Active: 11, 2007 }, 2008 }, 2009 newJob: &batch.Job{ 2010 ObjectMeta: validObjectMeta, 2011 Spec: batch.JobSpec{ 2012 Selector: validSelector, 2013 Template: validPodTemplateSpec, 2014 Parallelism: ptr.To[int32](4), 2015 }, 2016 Status: batch.JobStatus{ 2017 Active: 12, 2018 }, 2019 }, 2020 wantJob: &batch.Job{ 2021 ObjectMeta: validObjectMeta, 2022 Spec: batch.JobSpec{ 2023 Selector: validSelector, 2024 Template: validPodTemplateSpec, 2025 Parallelism: ptr.To[int32](4), 2026 }, 2027 Status: batch.JobStatus{ 2028 Active: 12, 2029 }, 2030 }, 2031 }, 2032 "parallelism changes not allowed": { 2033 job: &batch.Job{ 2034 ObjectMeta: validObjectMeta, 2035 Spec: batch.JobSpec{ 2036 Selector: validSelector, 2037 Template: validPodTemplateSpec, 2038 Parallelism: ptr.To[int32](3), 2039 }, 2040 }, 2041 newJob: &batch.Job{ 2042 ObjectMeta: validObjectMeta, 2043 Spec: batch.JobSpec{ 2044 Selector: validSelector, 2045 Template: validPodTemplateSpec, 2046 Parallelism: ptr.To[int32](4), 2047 }, 2048 }, 2049 wantJob: &batch.Job{ 2050 ObjectMeta: validObjectMeta, 2051 Spec: batch.JobSpec{ 2052 Selector: validSelector, 2053 Template: validPodTemplateSpec, 2054 Parallelism: ptr.To[int32](3), 2055 }, 2056 }, 2057 }, 2058 } 2059 for name, tc := range cases { 2060 t.Run(name, func(t *testing.T) { 2061 StatusStrategy.PrepareForUpdate(ctx, tc.newJob, tc.job) 2062 if diff := cmp.Diff(tc.wantJob, tc.newJob); diff != "" { 2063 t.Errorf("Unexpected job (-want,+got):\n%s", diff) 2064 } 2065 }) 2066 } 2067 } 2068 2069 func TestStatusStrategy_ValidateUpdate(t *testing.T) { 2070 ctx := genericapirequest.NewDefaultContext() 2071 validSelector := &metav1.LabelSelector{ 2072 MatchLabels: map[string]string{"a": "b"}, 2073 } 2074 validPodTemplateSpec := api.PodTemplateSpec{ 2075 ObjectMeta: metav1.ObjectMeta{ 2076 Labels: validSelector.MatchLabels, 2077 }, 2078 Spec: api.PodSpec{ 2079 RestartPolicy: api.RestartPolicyOnFailure, 2080 DNSPolicy: api.DNSClusterFirst, 2081 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 2082 }, 2083 } 2084 validObjectMeta := metav1.ObjectMeta{ 2085 Name: "myjob", 2086 Namespace: metav1.NamespaceDefault, 2087 ResourceVersion: "10", 2088 } 2089 validSuccessPolicy := &batch.SuccessPolicy{ 2090 Rules: []batch.SuccessPolicyRule{{ 2091 SucceededIndexes: ptr.To("0-2"), 2092 }}, 2093 } 2094 now := metav1.Now() 2095 nowPlusMinute := metav1.Time{Time: now.Add(time.Minute)} 2096 2097 cases := map[string]struct { 2098 enableJobManagedBy bool 2099 enableJobSuccessPolicy bool 2100 2101 job *batch.Job 2102 newJob *batch.Job 2103 wantJob *batch.Job 2104 wantErrs field.ErrorList 2105 }{ 2106 "incoming resource version on update should not be mutated": { 2107 job: &batch.Job{ 2108 ObjectMeta: metav1.ObjectMeta{ 2109 Name: "myjob", 2110 Namespace: metav1.NamespaceDefault, 2111 ResourceVersion: "10", 2112 }, 2113 Spec: batch.JobSpec{ 2114 Selector: validSelector, 2115 Template: validPodTemplateSpec, 2116 Parallelism: ptr.To[int32](4), 2117 }, 2118 }, 2119 newJob: &batch.Job{ 2120 ObjectMeta: metav1.ObjectMeta{ 2121 Name: "myjob", 2122 Namespace: metav1.NamespaceDefault, 2123 ResourceVersion: "9", 2124 }, 2125 Spec: batch.JobSpec{ 2126 Selector: validSelector, 2127 Template: validPodTemplateSpec, 2128 Parallelism: ptr.To[int32](4), 2129 }, 2130 }, 2131 wantJob: &batch.Job{ 2132 ObjectMeta: metav1.ObjectMeta{ 2133 Name: "myjob", 2134 Namespace: metav1.NamespaceDefault, 2135 ResourceVersion: "9", 2136 }, 2137 Spec: batch.JobSpec{ 2138 Selector: validSelector, 2139 Template: validPodTemplateSpec, 2140 Parallelism: ptr.To[int32](4), 2141 }, 2142 }, 2143 }, 2144 "invalid addition of both Failed=True and Complete=True; allowed because feature gate disabled": { 2145 enableJobManagedBy: false, 2146 job: &batch.Job{ 2147 ObjectMeta: validObjectMeta, 2148 }, 2149 newJob: &batch.Job{ 2150 ObjectMeta: validObjectMeta, 2151 Status: batch.JobStatus{ 2152 StartTime: &now, 2153 CompletionTime: &now, 2154 Conditions: []batch.JobCondition{ 2155 { 2156 Type: batch.JobComplete, 2157 Status: api.ConditionTrue, 2158 }, 2159 { 2160 Type: batch.JobFailed, 2161 Status: api.ConditionTrue, 2162 }, 2163 }, 2164 }, 2165 }, 2166 }, 2167 "invalid addition of both Failed=True and Complete=True": { 2168 enableJobManagedBy: true, 2169 job: &batch.Job{ 2170 ObjectMeta: validObjectMeta, 2171 }, 2172 newJob: &batch.Job{ 2173 ObjectMeta: validObjectMeta, 2174 Status: batch.JobStatus{ 2175 StartTime: &now, 2176 CompletionTime: &now, 2177 Conditions: []batch.JobCondition{ 2178 { 2179 Type: batch.JobComplete, 2180 Status: api.ConditionTrue, 2181 }, 2182 { 2183 Type: batch.JobFailed, 2184 Status: api.ConditionTrue, 2185 }, 2186 }, 2187 }, 2188 }, 2189 wantErrs: field.ErrorList{ 2190 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 2191 }, 2192 }, 2193 "completionTime can be removed to fix still running job": { 2194 enableJobManagedBy: true, 2195 job: &batch.Job{ 2196 ObjectMeta: validObjectMeta, 2197 Status: batch.JobStatus{ 2198 StartTime: &now, 2199 CompletionTime: &now, 2200 }, 2201 }, 2202 newJob: &batch.Job{ 2203 ObjectMeta: validObjectMeta, 2204 Status: batch.JobStatus{ 2205 StartTime: &now, 2206 }, 2207 }, 2208 }, 2209 "invalid attempt to transition to Failed=True without startTime": { 2210 enableJobManagedBy: true, 2211 job: &batch.Job{ 2212 ObjectMeta: validObjectMeta, 2213 }, 2214 newJob: &batch.Job{ 2215 ObjectMeta: validObjectMeta, 2216 Status: batch.JobStatus{ 2217 Conditions: []batch.JobCondition{ 2218 { 2219 Type: batch.JobFailed, 2220 Status: api.ConditionTrue, 2221 }, 2222 }, 2223 }, 2224 }, 2225 wantErrs: field.ErrorList{ 2226 {Type: field.ErrorTypeRequired, Field: "status.startTime"}, 2227 }, 2228 }, 2229 "invalid attempt to transition to Complete=True without startTime": { 2230 enableJobManagedBy: true, 2231 job: &batch.Job{ 2232 ObjectMeta: validObjectMeta, 2233 }, 2234 newJob: &batch.Job{ 2235 ObjectMeta: validObjectMeta, 2236 Status: batch.JobStatus{ 2237 CompletionTime: &now, 2238 Conditions: []batch.JobCondition{ 2239 { 2240 Type: batch.JobComplete, 2241 Status: api.ConditionTrue, 2242 }, 2243 }, 2244 }, 2245 }, 2246 wantErrs: field.ErrorList{ 2247 {Type: field.ErrorTypeRequired, Field: "status.startTime"}, 2248 }, 2249 }, 2250 "invalid attempt to transition to Complete=True with active > 0": { 2251 enableJobManagedBy: true, 2252 job: &batch.Job{ 2253 ObjectMeta: validObjectMeta, 2254 }, 2255 newJob: &batch.Job{ 2256 ObjectMeta: validObjectMeta, 2257 Status: batch.JobStatus{ 2258 StartTime: &now, 2259 CompletionTime: &now, 2260 Active: 1, 2261 Conditions: []batch.JobCondition{ 2262 { 2263 Type: batch.JobComplete, 2264 Status: api.ConditionTrue, 2265 }, 2266 }, 2267 }, 2268 }, 2269 wantErrs: field.ErrorList{ 2270 {Type: field.ErrorTypeInvalid, Field: "status.active"}, 2271 }, 2272 }, 2273 "transition to Failed condition with terminating>0 and ready>0": { 2274 enableJobManagedBy: true, 2275 job: &batch.Job{ 2276 ObjectMeta: validObjectMeta, 2277 }, 2278 newJob: &batch.Job{ 2279 ObjectMeta: validObjectMeta, 2280 Status: batch.JobStatus{ 2281 StartTime: &now, 2282 Conditions: []batch.JobCondition{ 2283 { 2284 Type: batch.JobFailed, 2285 Status: api.ConditionTrue, 2286 }, 2287 }, 2288 Terminating: ptr.To[int32](1), 2289 Ready: ptr.To[int32](1), 2290 }, 2291 }, 2292 }, 2293 "invalid attempt to transition to Failed=True with uncountedTerminatedPods.Failed>0": { 2294 enableJobManagedBy: true, 2295 job: &batch.Job{ 2296 ObjectMeta: validObjectMeta, 2297 }, 2298 newJob: &batch.Job{ 2299 ObjectMeta: validObjectMeta, 2300 Status: batch.JobStatus{ 2301 StartTime: &now, 2302 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2303 Failed: []types.UID{"a"}, 2304 }, 2305 Conditions: []batch.JobCondition{ 2306 { 2307 Type: batch.JobFailed, 2308 Status: api.ConditionTrue, 2309 }, 2310 }, 2311 }, 2312 }, 2313 wantErrs: field.ErrorList{ 2314 {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods"}, 2315 }, 2316 }, 2317 "invalid attempt to update uncountedTerminatedPods.Succeeded for Complete job": { 2318 enableJobManagedBy: true, 2319 job: &batch.Job{ 2320 ObjectMeta: validObjectMeta, 2321 Status: batch.JobStatus{ 2322 StartTime: &now, 2323 CompletionTime: &now, 2324 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2325 Failed: []types.UID{"a"}, 2326 }, 2327 Conditions: []batch.JobCondition{ 2328 { 2329 Type: batch.JobComplete, 2330 Status: api.ConditionTrue, 2331 }, 2332 }, 2333 }, 2334 }, 2335 newJob: &batch.Job{ 2336 ObjectMeta: validObjectMeta, 2337 Status: batch.JobStatus{ 2338 StartTime: &now, 2339 CompletionTime: &now, 2340 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2341 Failed: []types.UID{"b"}, 2342 }, 2343 Conditions: []batch.JobCondition{ 2344 { 2345 Type: batch.JobComplete, 2346 Status: api.ConditionTrue, 2347 }, 2348 }, 2349 }, 2350 }, 2351 wantErrs: field.ErrorList{ 2352 {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods"}, 2353 }, 2354 }, 2355 "non-empty uncountedTerminatedPods for complete job, unrelated update": { 2356 enableJobManagedBy: true, 2357 job: &batch.Job{ 2358 ObjectMeta: validObjectMeta, 2359 Status: batch.JobStatus{ 2360 StartTime: &now, 2361 CompletionTime: &now, 2362 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2363 Failed: []types.UID{"a"}, 2364 }, 2365 Conditions: []batch.JobCondition{ 2366 { 2367 Type: batch.JobComplete, 2368 Status: api.ConditionTrue, 2369 }, 2370 }, 2371 }, 2372 }, 2373 newJob: &batch.Job{ 2374 ObjectMeta: validObjectMeta, 2375 Status: batch.JobStatus{ 2376 StartTime: &now, 2377 CompletionTime: &now, 2378 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2379 Failed: []types.UID{"a"}, 2380 }, 2381 Conditions: []batch.JobCondition{ 2382 { 2383 Type: batch.JobComplete, 2384 Status: api.ConditionTrue, 2385 }, 2386 { 2387 Type: batch.JobConditionType("CustomJobCondition"), 2388 Status: api.ConditionTrue, 2389 }, 2390 }, 2391 }, 2392 }, 2393 }, 2394 "invalid attempt to transition to Complete=True with uncountedTerminatedPods.Succeeded>0": { 2395 enableJobManagedBy: true, 2396 job: &batch.Job{ 2397 ObjectMeta: validObjectMeta, 2398 }, 2399 newJob: &batch.Job{ 2400 ObjectMeta: validObjectMeta, 2401 Status: batch.JobStatus{ 2402 StartTime: &now, 2403 CompletionTime: &now, 2404 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2405 Succeeded: []types.UID{"a"}, 2406 }, 2407 Conditions: []batch.JobCondition{ 2408 { 2409 Type: batch.JobComplete, 2410 Status: api.ConditionTrue, 2411 }, 2412 }, 2413 }, 2414 }, 2415 wantErrs: field.ErrorList{ 2416 {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods"}, 2417 }, 2418 }, 2419 "invalid addition Complete=True without setting CompletionTime": { 2420 enableJobManagedBy: true, 2421 job: &batch.Job{ 2422 ObjectMeta: validObjectMeta, 2423 }, 2424 newJob: &batch.Job{ 2425 ObjectMeta: validObjectMeta, 2426 Status: batch.JobStatus{ 2427 StartTime: &now, 2428 Conditions: []batch.JobCondition{ 2429 { 2430 Type: batch.JobComplete, 2431 Status: api.ConditionTrue, 2432 }, 2433 }, 2434 }, 2435 }, 2436 wantErrs: field.ErrorList{ 2437 {Type: field.ErrorTypeRequired, Field: "status.completionTime"}, 2438 }, 2439 }, 2440 "invalid attempt to remove completionTime": { 2441 enableJobManagedBy: true, 2442 job: &batch.Job{ 2443 ObjectMeta: validObjectMeta, 2444 Status: batch.JobStatus{ 2445 CompletionTime: &now, 2446 Conditions: []batch.JobCondition{ 2447 { 2448 Type: batch.JobComplete, 2449 Status: api.ConditionTrue, 2450 }, 2451 }, 2452 }, 2453 }, 2454 newJob: &batch.Job{ 2455 ObjectMeta: validObjectMeta, 2456 Status: batch.JobStatus{ 2457 CompletionTime: nil, 2458 StartTime: &now, 2459 Conditions: []batch.JobCondition{ 2460 { 2461 Type: batch.JobComplete, 2462 Status: api.ConditionTrue, 2463 }, 2464 }, 2465 }, 2466 }, 2467 wantErrs: field.ErrorList{ 2468 {Type: field.ErrorTypeRequired, Field: "status.completionTime"}, 2469 }, 2470 }, 2471 "verify startTime can be cleared for suspended job": { 2472 enableJobManagedBy: true, 2473 job: &batch.Job{ 2474 ObjectMeta: validObjectMeta, 2475 Spec: batch.JobSpec{ 2476 Suspend: ptr.To(true), 2477 }, 2478 Status: batch.JobStatus{ 2479 StartTime: &now, 2480 }, 2481 }, 2482 newJob: &batch.Job{ 2483 ObjectMeta: validObjectMeta, 2484 Spec: batch.JobSpec{ 2485 Suspend: ptr.To(true), 2486 }, 2487 Status: batch.JobStatus{ 2488 StartTime: nil, 2489 }, 2490 }, 2491 }, 2492 "verify startTime cannot be removed for unsuspended job": { 2493 enableJobManagedBy: true, 2494 job: &batch.Job{ 2495 ObjectMeta: validObjectMeta, 2496 Status: batch.JobStatus{ 2497 StartTime: &now, 2498 }, 2499 }, 2500 newJob: &batch.Job{ 2501 ObjectMeta: validObjectMeta, 2502 Status: batch.JobStatus{ 2503 StartTime: nil, 2504 }, 2505 }, 2506 wantErrs: field.ErrorList{ 2507 {Type: field.ErrorTypeRequired, Field: "status.startTime"}, 2508 }, 2509 }, 2510 "verify startTime cannot be updated for unsuspended job": { 2511 enableJobManagedBy: true, 2512 job: &batch.Job{ 2513 ObjectMeta: validObjectMeta, 2514 Status: batch.JobStatus{ 2515 StartTime: &now, 2516 }, 2517 }, 2518 newJob: &batch.Job{ 2519 ObjectMeta: validObjectMeta, 2520 Status: batch.JobStatus{ 2521 StartTime: &nowPlusMinute, 2522 }, 2523 }, 2524 wantErrs: field.ErrorList{ 2525 {Type: field.ErrorTypeRequired, Field: "status.startTime"}, 2526 }, 2527 }, 2528 "invalid attempt to set completionTime before startTime": { 2529 enableJobManagedBy: true, 2530 job: &batch.Job{ 2531 ObjectMeta: validObjectMeta, 2532 Status: batch.JobStatus{ 2533 StartTime: &nowPlusMinute, 2534 }, 2535 }, 2536 newJob: &batch.Job{ 2537 ObjectMeta: validObjectMeta, 2538 Status: batch.JobStatus{ 2539 StartTime: &nowPlusMinute, 2540 CompletionTime: &now, 2541 Conditions: []batch.JobCondition{ 2542 { 2543 Type: batch.JobComplete, 2544 Status: api.ConditionTrue, 2545 }, 2546 }, 2547 }, 2548 }, 2549 wantErrs: field.ErrorList{ 2550 {Type: field.ErrorTypeInvalid, Field: "status.completionTime"}, 2551 }, 2552 }, 2553 "invalid attempt to modify completionTime": { 2554 enableJobManagedBy: true, 2555 job: &batch.Job{ 2556 ObjectMeta: validObjectMeta, 2557 Status: batch.JobStatus{ 2558 CompletionTime: &now, 2559 Conditions: []batch.JobCondition{ 2560 { 2561 Type: batch.JobComplete, 2562 Status: api.ConditionTrue, 2563 }, 2564 }, 2565 }, 2566 }, 2567 newJob: &batch.Job{ 2568 ObjectMeta: validObjectMeta, 2569 Status: batch.JobStatus{ 2570 CompletionTime: &nowPlusMinute, 2571 StartTime: &now, 2572 Conditions: []batch.JobCondition{ 2573 { 2574 Type: batch.JobComplete, 2575 Status: api.ConditionTrue, 2576 }, 2577 }, 2578 }, 2579 }, 2580 wantErrs: field.ErrorList{ 2581 {Type: field.ErrorTypeInvalid, Field: "status.completionTime"}, 2582 }, 2583 }, 2584 "invalid removal of terminal condition Failed=True": { 2585 enableJobManagedBy: true, 2586 job: &batch.Job{ 2587 ObjectMeta: validObjectMeta, 2588 Status: batch.JobStatus{ 2589 Conditions: []batch.JobCondition{ 2590 { 2591 Type: batch.JobFailed, 2592 Status: api.ConditionTrue, 2593 }, 2594 }, 2595 }, 2596 }, 2597 newJob: &batch.Job{ 2598 ObjectMeta: validObjectMeta, 2599 }, 2600 wantErrs: field.ErrorList{ 2601 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 2602 }, 2603 }, 2604 "invalid removal of terminal condition Complete=True": { 2605 enableJobManagedBy: true, 2606 job: &batch.Job{ 2607 ObjectMeta: validObjectMeta, 2608 Status: batch.JobStatus{ 2609 Conditions: []batch.JobCondition{ 2610 { 2611 Type: batch.JobComplete, 2612 Status: api.ConditionTrue, 2613 }, 2614 }, 2615 }, 2616 }, 2617 newJob: &batch.Job{ 2618 ObjectMeta: validObjectMeta, 2619 }, 2620 wantErrs: field.ErrorList{ 2621 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 2622 }, 2623 }, 2624 "invalid removal of terminal condition FailureTarget=True": { 2625 enableJobManagedBy: true, 2626 job: &batch.Job{ 2627 ObjectMeta: validObjectMeta, 2628 Status: batch.JobStatus{ 2629 Conditions: []batch.JobCondition{ 2630 { 2631 Type: batch.JobFailureTarget, 2632 Status: api.ConditionTrue, 2633 }, 2634 }, 2635 }, 2636 }, 2637 newJob: &batch.Job{ 2638 ObjectMeta: validObjectMeta, 2639 }, 2640 wantErrs: field.ErrorList{ 2641 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 2642 }, 2643 }, 2644 "invalid addition of FailureTarget=True when Complete=True": { 2645 enableJobManagedBy: true, 2646 job: &batch.Job{ 2647 ObjectMeta: validObjectMeta, 2648 Status: batch.JobStatus{ 2649 StartTime: &now, 2650 CompletionTime: &now, 2651 Conditions: []batch.JobCondition{ 2652 { 2653 Type: batch.JobComplete, 2654 Status: api.ConditionTrue, 2655 }, 2656 }, 2657 }, 2658 }, 2659 newJob: &batch.Job{ 2660 ObjectMeta: validObjectMeta, 2661 Status: batch.JobStatus{ 2662 StartTime: &now, 2663 CompletionTime: &now, 2664 Conditions: []batch.JobCondition{ 2665 { 2666 Type: batch.JobComplete, 2667 Status: api.ConditionTrue, 2668 }, 2669 { 2670 Type: batch.JobFailureTarget, 2671 Status: api.ConditionTrue, 2672 }, 2673 }, 2674 }, 2675 }, 2676 wantErrs: field.ErrorList{ 2677 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 2678 }, 2679 }, 2680 "invalid attempt setting of CompletionTime when there is no Complete condition": { 2681 enableJobManagedBy: true, 2682 job: &batch.Job{ 2683 ObjectMeta: validObjectMeta, 2684 }, 2685 newJob: &batch.Job{ 2686 ObjectMeta: validObjectMeta, 2687 Status: batch.JobStatus{ 2688 CompletionTime: &now, 2689 }, 2690 }, 2691 wantErrs: field.ErrorList{ 2692 {Type: field.ErrorTypeInvalid, Field: "status.completionTime"}, 2693 }, 2694 }, 2695 "invalid CompletionTime when there is no Complete condition, but allowed": { 2696 enableJobManagedBy: true, 2697 job: &batch.Job{ 2698 ObjectMeta: validObjectMeta, 2699 Status: batch.JobStatus{ 2700 CompletionTime: &now, 2701 }, 2702 }, 2703 newJob: &batch.Job{ 2704 ObjectMeta: validObjectMeta, 2705 Status: batch.JobStatus{ 2706 CompletionTime: &now, 2707 Active: 1, 2708 }, 2709 }, 2710 }, 2711 "invalid attempt setting CompletedIndexes when non-indexed completion mode is used": { 2712 enableJobManagedBy: true, 2713 job: &batch.Job{ 2714 ObjectMeta: validObjectMeta, 2715 Spec: batch.JobSpec{ 2716 Completions: ptr.To[int32](5), 2717 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 2718 }, 2719 }, 2720 newJob: &batch.Job{ 2721 ObjectMeta: validObjectMeta, 2722 Spec: batch.JobSpec{ 2723 Completions: ptr.To[int32](5), 2724 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 2725 }, 2726 Status: batch.JobStatus{ 2727 StartTime: &now, 2728 CompletedIndexes: "0", 2729 }, 2730 }, 2731 wantErrs: field.ErrorList{ 2732 {Type: field.ErrorTypeInvalid, Field: "status.completedIndexes"}, 2733 }, 2734 }, 2735 "invalid because CompletedIndexes set when non-indexed completion mode is used; but allowed": { 2736 enableJobManagedBy: true, 2737 job: &batch.Job{ 2738 ObjectMeta: validObjectMeta, 2739 Spec: batch.JobSpec{ 2740 Completions: ptr.To[int32](5), 2741 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 2742 }, 2743 Status: batch.JobStatus{ 2744 CompletedIndexes: "0", 2745 }, 2746 }, 2747 newJob: &batch.Job{ 2748 ObjectMeta: validObjectMeta, 2749 Spec: batch.JobSpec{ 2750 Completions: ptr.To[int32](5), 2751 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 2752 }, 2753 Status: batch.JobStatus{ 2754 CompletedIndexes: "0", 2755 Active: 1, 2756 }, 2757 }, 2758 }, 2759 "invalid attempt setting FailedIndexes when not backoffLimitPerIndex": { 2760 enableJobManagedBy: true, 2761 job: &batch.Job{ 2762 ObjectMeta: validObjectMeta, 2763 Spec: batch.JobSpec{ 2764 Completions: ptr.To[int32](5), 2765 CompletionMode: completionModePtr(batch.IndexedCompletion), 2766 }, 2767 }, 2768 newJob: &batch.Job{ 2769 ObjectMeta: validObjectMeta, 2770 Spec: batch.JobSpec{ 2771 Completions: ptr.To[int32](5), 2772 CompletionMode: completionModePtr(batch.IndexedCompletion), 2773 }, 2774 Status: batch.JobStatus{ 2775 FailedIndexes: ptr.To("0"), 2776 }, 2777 }, 2778 wantErrs: field.ErrorList{ 2779 {Type: field.ErrorTypeInvalid, Field: "status.failedIndexes"}, 2780 }, 2781 }, 2782 "invalid attempt to decrease the failed counter": { 2783 enableJobManagedBy: true, 2784 job: &batch.Job{ 2785 ObjectMeta: validObjectMeta, 2786 Spec: batch.JobSpec{ 2787 Completions: ptr.To[int32](5), 2788 }, 2789 Status: batch.JobStatus{ 2790 Failed: 3, 2791 }, 2792 }, 2793 newJob: &batch.Job{ 2794 ObjectMeta: validObjectMeta, 2795 Spec: batch.JobSpec{ 2796 Completions: ptr.To[int32](5), 2797 }, 2798 Status: batch.JobStatus{ 2799 Failed: 1, 2800 }, 2801 }, 2802 wantErrs: field.ErrorList{ 2803 {Type: field.ErrorTypeInvalid, Field: "status.failed"}, 2804 }, 2805 }, 2806 "invalid attempt to decrease the succeeded counter": { 2807 enableJobManagedBy: true, 2808 job: &batch.Job{ 2809 ObjectMeta: validObjectMeta, 2810 Spec: batch.JobSpec{ 2811 Completions: ptr.To[int32](5), 2812 }, 2813 Status: batch.JobStatus{ 2814 Succeeded: 3, 2815 }, 2816 }, 2817 newJob: &batch.Job{ 2818 ObjectMeta: validObjectMeta, 2819 Spec: batch.JobSpec{ 2820 Completions: ptr.To[int32](5), 2821 }, 2822 Status: batch.JobStatus{ 2823 Succeeded: 1, 2824 }, 2825 }, 2826 wantErrs: field.ErrorList{ 2827 {Type: field.ErrorTypeInvalid, Field: "status.succeeded"}, 2828 }, 2829 }, 2830 "invalid attempt to set bad format for CompletedIndexes": { 2831 enableJobManagedBy: true, 2832 job: &batch.Job{ 2833 ObjectMeta: validObjectMeta, 2834 Spec: batch.JobSpec{ 2835 Completions: ptr.To[int32](5), 2836 CompletionMode: completionModePtr(batch.IndexedCompletion), 2837 }, 2838 }, 2839 newJob: &batch.Job{ 2840 ObjectMeta: validObjectMeta, 2841 Spec: batch.JobSpec{ 2842 Completions: ptr.To[int32](5), 2843 CompletionMode: completionModePtr(batch.IndexedCompletion), 2844 }, 2845 Status: batch.JobStatus{ 2846 CompletedIndexes: "invalid format", 2847 }, 2848 }, 2849 wantErrs: field.ErrorList{ 2850 {Type: field.ErrorTypeInvalid, Field: "status.completedIndexes"}, 2851 }, 2852 }, 2853 "invalid format for CompletedIndexes, but allowed": { 2854 enableJobManagedBy: true, 2855 job: &batch.Job{ 2856 ObjectMeta: validObjectMeta, 2857 Spec: batch.JobSpec{ 2858 Completions: ptr.To[int32](5), 2859 CompletionMode: completionModePtr(batch.IndexedCompletion), 2860 }, 2861 Status: batch.JobStatus{ 2862 CompletedIndexes: "invalid format", 2863 }, 2864 }, 2865 newJob: &batch.Job{ 2866 ObjectMeta: validObjectMeta, 2867 Spec: batch.JobSpec{ 2868 Completions: ptr.To[int32](5), 2869 CompletionMode: completionModePtr(batch.IndexedCompletion), 2870 }, 2871 Status: batch.JobStatus{ 2872 CompletedIndexes: "invalid format", 2873 Active: 1, 2874 }, 2875 }, 2876 }, 2877 "invalid attempt to set bad format for FailedIndexes": { 2878 enableJobManagedBy: true, 2879 job: &batch.Job{ 2880 ObjectMeta: validObjectMeta, 2881 Spec: batch.JobSpec{ 2882 Completions: ptr.To[int32](5), 2883 CompletionMode: completionModePtr(batch.IndexedCompletion), 2884 BackoffLimitPerIndex: ptr.To[int32](1), 2885 }, 2886 }, 2887 newJob: &batch.Job{ 2888 ObjectMeta: validObjectMeta, 2889 Spec: batch.JobSpec{ 2890 Completions: ptr.To[int32](5), 2891 CompletionMode: completionModePtr(batch.IndexedCompletion), 2892 BackoffLimitPerIndex: ptr.To[int32](1), 2893 }, 2894 Status: batch.JobStatus{ 2895 FailedIndexes: ptr.To("invalid format"), 2896 }, 2897 }, 2898 wantErrs: field.ErrorList{ 2899 {Type: field.ErrorTypeInvalid, Field: "status.failedIndexes"}, 2900 }, 2901 }, 2902 "invalid format for FailedIndexes, but allowed": { 2903 enableJobManagedBy: true, 2904 job: &batch.Job{ 2905 ObjectMeta: validObjectMeta, 2906 Spec: batch.JobSpec{ 2907 Completions: ptr.To[int32](5), 2908 CompletionMode: completionModePtr(batch.IndexedCompletion), 2909 BackoffLimitPerIndex: ptr.To[int32](1), 2910 }, 2911 Status: batch.JobStatus{ 2912 FailedIndexes: ptr.To("invalid format"), 2913 }, 2914 }, 2915 newJob: &batch.Job{ 2916 ObjectMeta: validObjectMeta, 2917 Spec: batch.JobSpec{ 2918 Completions: ptr.To[int32](5), 2919 CompletionMode: completionModePtr(batch.IndexedCompletion), 2920 BackoffLimitPerIndex: ptr.To[int32](1), 2921 }, 2922 Status: batch.JobStatus{ 2923 FailedIndexes: ptr.To("invalid format"), 2924 Active: 1, 2925 }, 2926 }, 2927 }, 2928 "more ready pods than active, but allowed": { 2929 enableJobManagedBy: true, 2930 job: &batch.Job{ 2931 ObjectMeta: validObjectMeta, 2932 Spec: batch.JobSpec{ 2933 Completions: ptr.To[int32](5), 2934 }, 2935 Status: batch.JobStatus{ 2936 Active: 1, 2937 Ready: ptr.To[int32](2), 2938 }, 2939 }, 2940 newJob: &batch.Job{ 2941 ObjectMeta: validObjectMeta, 2942 Spec: batch.JobSpec{ 2943 Completions: ptr.To[int32](5), 2944 }, 2945 Status: batch.JobStatus{ 2946 Active: 1, 2947 Ready: ptr.To[int32](2), 2948 Succeeded: 1, 2949 }, 2950 }, 2951 }, 2952 "invalid addition of both FailureTarget=True and Complete=True": { 2953 enableJobManagedBy: true, 2954 job: &batch.Job{ 2955 ObjectMeta: validObjectMeta, 2956 }, 2957 newJob: &batch.Job{ 2958 ObjectMeta: validObjectMeta, 2959 Status: batch.JobStatus{ 2960 StartTime: &now, 2961 CompletionTime: &now, 2962 Conditions: []batch.JobCondition{ 2963 { 2964 Type: batch.JobComplete, 2965 Status: api.ConditionTrue, 2966 }, 2967 { 2968 Type: batch.JobFailureTarget, 2969 Status: api.ConditionTrue, 2970 }, 2971 }, 2972 }, 2973 }, 2974 wantErrs: field.ErrorList{ 2975 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 2976 }, 2977 }, 2978 "invalid failedIndexes, which overlap with completedIndexes": { 2979 enableJobManagedBy: true, 2980 job: &batch.Job{ 2981 ObjectMeta: validObjectMeta, 2982 Spec: batch.JobSpec{ 2983 Completions: ptr.To[int32](5), 2984 CompletionMode: completionModePtr(batch.IndexedCompletion), 2985 }, 2986 Status: batch.JobStatus{ 2987 FailedIndexes: ptr.To("0,2"), 2988 CompletedIndexes: "3-4", 2989 }, 2990 }, 2991 newJob: &batch.Job{ 2992 ObjectMeta: validObjectMeta, 2993 Spec: batch.JobSpec{ 2994 Completions: ptr.To[int32](5), 2995 CompletionMode: completionModePtr(batch.IndexedCompletion), 2996 }, 2997 Status: batch.JobStatus{ 2998 FailedIndexes: ptr.To("0,2"), 2999 CompletedIndexes: "2-4", 3000 }, 3001 }, 3002 wantErrs: field.ErrorList{ 3003 {Type: field.ErrorTypeInvalid, Field: "status.failedIndexes"}, 3004 }, 3005 }, 3006 "failedIndexes overlap with completedIndexes, unrelated field change": { 3007 enableJobManagedBy: true, 3008 job: &batch.Job{ 3009 ObjectMeta: validObjectMeta, 3010 Spec: batch.JobSpec{ 3011 Completions: ptr.To[int32](5), 3012 CompletionMode: completionModePtr(batch.IndexedCompletion), 3013 }, 3014 Status: batch.JobStatus{ 3015 FailedIndexes: ptr.To("0,2"), 3016 CompletedIndexes: "2-4", 3017 }, 3018 }, 3019 newJob: &batch.Job{ 3020 ObjectMeta: validObjectMeta, 3021 Spec: batch.JobSpec{ 3022 Completions: ptr.To[int32](5), 3023 CompletionMode: completionModePtr(batch.IndexedCompletion), 3024 }, 3025 Status: batch.JobStatus{ 3026 FailedIndexes: ptr.To("0,2"), 3027 CompletedIndexes: "2-4", 3028 Active: 1, 3029 }, 3030 }, 3031 }, 3032 "invalid addition of SuccessCriteriaMet for NonIndexed Job": { 3033 enableJobSuccessPolicy: true, 3034 job: &batch.Job{ 3035 ObjectMeta: validObjectMeta, 3036 Spec: batch.JobSpec{ 3037 SuccessPolicy: validSuccessPolicy, 3038 }, 3039 }, 3040 newJob: &batch.Job{ 3041 ObjectMeta: validObjectMeta, 3042 Spec: batch.JobSpec{ 3043 SuccessPolicy: validSuccessPolicy, 3044 }, 3045 Status: batch.JobStatus{ 3046 Conditions: []batch.JobCondition{{ 3047 Type: batch.JobSuccessCriteriaMet, 3048 Status: api.ConditionTrue, 3049 }}, 3050 }, 3051 }, 3052 wantErrs: field.ErrorList{ 3053 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3054 }, 3055 }, 3056 "invalid addition of SuccessCriteriaMet for Job with Failed": { 3057 enableJobSuccessPolicy: true, 3058 job: &batch.Job{ 3059 ObjectMeta: validObjectMeta, 3060 Spec: batch.JobSpec{ 3061 CompletionMode: completionModePtr(batch.IndexedCompletion), 3062 Completions: ptr.To[int32](10), 3063 SuccessPolicy: validSuccessPolicy, 3064 }, 3065 Status: batch.JobStatus{ 3066 Conditions: []batch.JobCondition{{ 3067 Type: batch.JobFailed, 3068 Status: api.ConditionTrue, 3069 }}, 3070 }, 3071 }, 3072 newJob: &batch.Job{ 3073 ObjectMeta: validObjectMeta, 3074 Spec: batch.JobSpec{ 3075 CompletionMode: completionModePtr(batch.IndexedCompletion), 3076 Completions: ptr.To[int32](10), 3077 SuccessPolicy: validSuccessPolicy, 3078 }, 3079 Status: batch.JobStatus{ 3080 Conditions: []batch.JobCondition{ 3081 { 3082 Type: batch.JobFailed, 3083 Status: api.ConditionTrue, 3084 }, 3085 { 3086 Type: batch.JobSuccessCriteriaMet, 3087 Status: api.ConditionTrue, 3088 }, 3089 }, 3090 }, 3091 }, 3092 wantErrs: field.ErrorList{ 3093 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3094 }, 3095 }, 3096 "invalid addition of Failed for Job with SuccessCriteriaMet": { 3097 enableJobSuccessPolicy: true, 3098 job: &batch.Job{ 3099 ObjectMeta: validObjectMeta, 3100 Spec: batch.JobSpec{ 3101 CompletionMode: completionModePtr(batch.IndexedCompletion), 3102 Completions: ptr.To[int32](10), 3103 SuccessPolicy: validSuccessPolicy, 3104 }, 3105 Status: batch.JobStatus{ 3106 Conditions: []batch.JobCondition{{ 3107 Type: batch.JobSuccessCriteriaMet, 3108 Status: api.ConditionTrue, 3109 }}, 3110 }, 3111 }, 3112 newJob: &batch.Job{ 3113 ObjectMeta: validObjectMeta, 3114 Spec: batch.JobSpec{ 3115 CompletionMode: completionModePtr(batch.IndexedCompletion), 3116 Completions: ptr.To[int32](10), 3117 SuccessPolicy: validSuccessPolicy, 3118 }, 3119 Status: batch.JobStatus{ 3120 Conditions: []batch.JobCondition{ 3121 { 3122 Type: batch.JobSuccessCriteriaMet, 3123 Status: api.ConditionTrue, 3124 }, 3125 { 3126 Type: batch.JobFailed, 3127 Status: api.ConditionTrue, 3128 }, 3129 }, 3130 }, 3131 }, 3132 wantErrs: field.ErrorList{ 3133 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3134 }, 3135 }, 3136 "invalid addition of SuccessCriteriaMet for Job with FailureTarget": { 3137 enableJobSuccessPolicy: true, 3138 job: &batch.Job{ 3139 ObjectMeta: validObjectMeta, 3140 Spec: batch.JobSpec{ 3141 CompletionMode: completionModePtr(batch.IndexedCompletion), 3142 Completions: ptr.To[int32](10), 3143 SuccessPolicy: validSuccessPolicy, 3144 }, 3145 Status: batch.JobStatus{ 3146 Conditions: []batch.JobCondition{{ 3147 Type: batch.JobFailureTarget, 3148 Status: api.ConditionTrue, 3149 }}, 3150 }, 3151 }, 3152 newJob: &batch.Job{ 3153 ObjectMeta: validObjectMeta, 3154 Spec: batch.JobSpec{ 3155 CompletionMode: completionModePtr(batch.IndexedCompletion), 3156 Completions: ptr.To[int32](10), 3157 SuccessPolicy: validSuccessPolicy, 3158 }, 3159 Status: batch.JobStatus{ 3160 Conditions: []batch.JobCondition{ 3161 { 3162 Type: batch.JobFailureTarget, 3163 Status: api.ConditionTrue, 3164 }, 3165 { 3166 Type: batch.JobSuccessCriteriaMet, 3167 Status: api.ConditionTrue, 3168 }, 3169 }, 3170 }, 3171 }, 3172 wantErrs: field.ErrorList{ 3173 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3174 }, 3175 }, 3176 "invalid addition of FailureTarget for Job with SuccessCriteriaMet": { 3177 enableJobSuccessPolicy: true, 3178 job: &batch.Job{ 3179 ObjectMeta: validObjectMeta, 3180 Spec: batch.JobSpec{ 3181 CompletionMode: completionModePtr(batch.IndexedCompletion), 3182 Completions: ptr.To[int32](10), 3183 SuccessPolicy: validSuccessPolicy, 3184 }, 3185 Status: batch.JobStatus{ 3186 Conditions: []batch.JobCondition{{ 3187 Type: batch.JobSuccessCriteriaMet, 3188 Status: api.ConditionTrue, 3189 }}, 3190 }, 3191 }, 3192 newJob: &batch.Job{ 3193 ObjectMeta: validObjectMeta, 3194 Spec: batch.JobSpec{ 3195 CompletionMode: completionModePtr(batch.IndexedCompletion), 3196 Completions: ptr.To[int32](10), 3197 SuccessPolicy: validSuccessPolicy, 3198 }, 3199 Status: batch.JobStatus{ 3200 Conditions: []batch.JobCondition{ 3201 { 3202 Type: batch.JobSuccessCriteriaMet, 3203 Status: api.ConditionTrue, 3204 }, 3205 { 3206 Type: batch.JobFailureTarget, 3207 Status: api.ConditionTrue, 3208 }, 3209 }, 3210 }, 3211 }, 3212 wantErrs: field.ErrorList{ 3213 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3214 }, 3215 }, 3216 "invalid addition of SuccessCriteriaMet for Job with Complete": { 3217 enableJobSuccessPolicy: true, 3218 job: &batch.Job{ 3219 ObjectMeta: validObjectMeta, 3220 Spec: batch.JobSpec{ 3221 CompletionMode: completionModePtr(batch.IndexedCompletion), 3222 Completions: ptr.To[int32](10), 3223 SuccessPolicy: validSuccessPolicy, 3224 }, 3225 Status: batch.JobStatus{ 3226 Conditions: []batch.JobCondition{{ 3227 Type: batch.JobComplete, 3228 Status: api.ConditionTrue, 3229 }}, 3230 }, 3231 }, 3232 newJob: &batch.Job{ 3233 ObjectMeta: validObjectMeta, 3234 Spec: batch.JobSpec{ 3235 CompletionMode: completionModePtr(batch.IndexedCompletion), 3236 Completions: ptr.To[int32](10), 3237 SuccessPolicy: validSuccessPolicy, 3238 }, 3239 Status: batch.JobStatus{ 3240 Conditions: []batch.JobCondition{ 3241 { 3242 Type: batch.JobComplete, 3243 Status: api.ConditionTrue, 3244 }, 3245 { 3246 Type: batch.JobSuccessCriteriaMet, 3247 Status: api.ConditionTrue, 3248 }, 3249 }, 3250 }, 3251 }, 3252 wantErrs: field.ErrorList{ 3253 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3254 }, 3255 }, 3256 "valid addition of Complete for Job with SuccessCriteriaMet": { 3257 enableJobSuccessPolicy: true, 3258 job: &batch.Job{ 3259 ObjectMeta: validObjectMeta, 3260 Spec: batch.JobSpec{ 3261 CompletionMode: completionModePtr(batch.IndexedCompletion), 3262 Completions: ptr.To[int32](10), 3263 SuccessPolicy: validSuccessPolicy, 3264 }, 3265 Status: batch.JobStatus{ 3266 Conditions: []batch.JobCondition{{ 3267 Type: batch.JobSuccessCriteriaMet, 3268 Status: api.ConditionTrue, 3269 }}, 3270 }, 3271 }, 3272 newJob: &batch.Job{ 3273 ObjectMeta: validObjectMeta, 3274 Spec: batch.JobSpec{ 3275 CompletionMode: completionModePtr(batch.IndexedCompletion), 3276 Completions: ptr.To[int32](10), 3277 SuccessPolicy: validSuccessPolicy, 3278 }, 3279 Status: batch.JobStatus{ 3280 Conditions: []batch.JobCondition{ 3281 { 3282 Type: batch.JobSuccessCriteriaMet, 3283 Status: api.ConditionTrue, 3284 }, 3285 { 3286 Type: batch.JobComplete, 3287 Status: api.ConditionTrue, 3288 }, 3289 }, 3290 }, 3291 }, 3292 }, 3293 "invalid addition of SuccessCriteriaMet for Job without SuccessPolicy": { 3294 enableJobSuccessPolicy: true, 3295 job: &batch.Job{ 3296 ObjectMeta: validObjectMeta, 3297 Spec: batch.JobSpec{ 3298 CompletionMode: completionModePtr(batch.IndexedCompletion), 3299 Completions: ptr.To[int32](10), 3300 }, 3301 }, 3302 newJob: &batch.Job{ 3303 ObjectMeta: validObjectMeta, 3304 Spec: batch.JobSpec{ 3305 CompletionMode: completionModePtr(batch.IndexedCompletion), 3306 Completions: ptr.To[int32](10), 3307 }, 3308 Status: batch.JobStatus{ 3309 Conditions: []batch.JobCondition{ 3310 { 3311 Type: batch.JobSuccessCriteriaMet, 3312 Status: api.ConditionTrue, 3313 }, 3314 }, 3315 }, 3316 }, 3317 wantErrs: field.ErrorList{ 3318 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3319 }, 3320 }, 3321 "invalid addition of Complete for Job with SuccessPolicy unless SuccessCriteriaMet": { 3322 enableJobSuccessPolicy: true, 3323 job: &batch.Job{ 3324 ObjectMeta: validObjectMeta, 3325 Spec: batch.JobSpec{ 3326 CompletionMode: completionModePtr(batch.IndexedCompletion), 3327 Completions: ptr.To[int32](10), 3328 SuccessPolicy: validSuccessPolicy, 3329 }, 3330 }, 3331 newJob: &batch.Job{ 3332 ObjectMeta: validObjectMeta, 3333 Spec: batch.JobSpec{ 3334 CompletionMode: completionModePtr(batch.IndexedCompletion), 3335 Completions: ptr.To[int32](10), 3336 SuccessPolicy: validSuccessPolicy, 3337 }, 3338 Status: batch.JobStatus{ 3339 Conditions: []batch.JobCondition{ 3340 { 3341 Type: batch.JobComplete, 3342 Status: api.ConditionTrue, 3343 }, 3344 }, 3345 }, 3346 }, 3347 wantErrs: field.ErrorList{ 3348 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3349 }, 3350 }, 3351 "invalid disabling of SuccessCriteriaMet for Job": { 3352 enableJobSuccessPolicy: true, 3353 job: &batch.Job{ 3354 ObjectMeta: validObjectMeta, 3355 Spec: batch.JobSpec{ 3356 CompletionMode: completionModePtr(batch.IndexedCompletion), 3357 Completions: ptr.To[int32](10), 3358 SuccessPolicy: validSuccessPolicy, 3359 }, 3360 Status: batch.JobStatus{ 3361 Conditions: []batch.JobCondition{{ 3362 Type: batch.JobSuccessCriteriaMet, 3363 Status: api.ConditionTrue, 3364 }}, 3365 }, 3366 }, 3367 newJob: &batch.Job{ 3368 ObjectMeta: validObjectMeta, 3369 Spec: batch.JobSpec{ 3370 CompletionMode: completionModePtr(batch.IndexedCompletion), 3371 Completions: ptr.To[int32](10), 3372 SuccessPolicy: validSuccessPolicy, 3373 }, 3374 Status: batch.JobStatus{ 3375 Conditions: []batch.JobCondition{{ 3376 Type: batch.JobComplete, 3377 Status: api.ConditionFalse, 3378 }}, 3379 }, 3380 }, 3381 wantErrs: field.ErrorList{ 3382 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3383 }, 3384 }, 3385 "invalid removing of SuccessCriteriaMet for Job": { 3386 enableJobSuccessPolicy: true, 3387 job: &batch.Job{ 3388 ObjectMeta: validObjectMeta, 3389 Spec: batch.JobSpec{ 3390 CompletionMode: completionModePtr(batch.IndexedCompletion), 3391 Completions: ptr.To[int32](10), 3392 SuccessPolicy: validSuccessPolicy, 3393 }, 3394 Status: batch.JobStatus{ 3395 Conditions: []batch.JobCondition{{ 3396 Type: batch.JobSuccessCriteriaMet, 3397 Status: api.ConditionTrue, 3398 }}, 3399 }, 3400 }, 3401 newJob: &batch.Job{ 3402 ObjectMeta: validObjectMeta, 3403 Spec: batch.JobSpec{ 3404 CompletionMode: completionModePtr(batch.IndexedCompletion), 3405 Completions: ptr.To[int32](10), 3406 SuccessPolicy: validSuccessPolicy, 3407 }, 3408 }, 3409 wantErrs: field.ErrorList{ 3410 {Type: field.ErrorTypeInvalid, Field: "status.conditions"}, 3411 }, 3412 }, 3413 } 3414 for name, tc := range cases { 3415 t.Run(name, func(t *testing.T) { 3416 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobManagedBy, tc.enableJobManagedBy) 3417 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobSuccessPolicy, tc.enableJobSuccessPolicy) 3418 3419 errs := StatusStrategy.ValidateUpdate(ctx, tc.newJob, tc.job) 3420 if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" { 3421 t.Errorf("Unexpected errors (-want,+got):\n%s", diff) 3422 } 3423 if tc.wantJob != nil { 3424 if diff := cmp.Diff(tc.wantJob, tc.newJob); diff != "" { 3425 t.Errorf("Unexpected job (-want,+got):\n%s", diff) 3426 } 3427 } 3428 }) 3429 } 3430 } 3431 3432 func TestJobStrategy_GetAttrs(t *testing.T) { 3433 validSelector := &metav1.LabelSelector{ 3434 MatchLabels: map[string]string{"a": "b"}, 3435 } 3436 validPodTemplateSpec := api.PodTemplateSpec{ 3437 ObjectMeta: metav1.ObjectMeta{ 3438 Labels: validSelector.MatchLabels, 3439 }, 3440 Spec: api.PodSpec{ 3441 RestartPolicy: api.RestartPolicyOnFailure, 3442 DNSPolicy: api.DNSClusterFirst, 3443 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 3444 }, 3445 } 3446 3447 cases := map[string]struct { 3448 job *batch.Job 3449 wantErr string 3450 nonJobObject *api.Pod 3451 }{ 3452 "valid job with no labels": { 3453 job: &batch.Job{ 3454 ObjectMeta: metav1.ObjectMeta{ 3455 Name: "myjob", 3456 Namespace: metav1.NamespaceDefault, 3457 ResourceVersion: "0", 3458 }, 3459 Spec: batch.JobSpec{ 3460 Selector: validSelector, 3461 Template: validPodTemplateSpec, 3462 ManualSelector: ptr.To(true), 3463 Parallelism: ptr.To[int32](1), 3464 }, 3465 }, 3466 }, 3467 "valid job with a label": { 3468 job: &batch.Job{ 3469 ObjectMeta: metav1.ObjectMeta{ 3470 Name: "myjob", 3471 Namespace: metav1.NamespaceDefault, 3472 ResourceVersion: "0", 3473 Labels: map[string]string{"a": "b"}, 3474 }, 3475 Spec: batch.JobSpec{ 3476 Selector: validSelector, 3477 Template: validPodTemplateSpec, 3478 ManualSelector: ptr.To(true), 3479 Parallelism: ptr.To[int32](1), 3480 }, 3481 }, 3482 }, 3483 "pod instead": { 3484 job: nil, 3485 nonJobObject: &api.Pod{}, 3486 wantErr: "given object is not a job.", 3487 }, 3488 } 3489 for name, tc := range cases { 3490 t.Run(name, func(t *testing.T) { 3491 if tc.job == nil { 3492 _, _, err := GetAttrs(tc.nonJobObject) 3493 if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { 3494 t.Errorf("Unexpected errors (-want,+got):\n%s", diff) 3495 } 3496 } else { 3497 gotLabels, _, err := GetAttrs(tc.job) 3498 if err != nil { 3499 t.Errorf("Error %s supposed to be nil", err.Error()) 3500 } 3501 if diff := cmp.Diff(labels.Set(tc.job.ObjectMeta.Labels), gotLabels); diff != "" { 3502 t.Errorf("Unexpected attrs (-want,+got):\n%s", diff) 3503 } 3504 } 3505 }) 3506 } 3507 } 3508 3509 func TestJobToSelectiableFields(t *testing.T) { 3510 apitesting.TestSelectableFieldLabelConversionsOfKind(t, 3511 "batch/v1", 3512 "Job", 3513 JobToSelectableFields(&batch.Job{}), 3514 nil, 3515 ) 3516 } 3517 3518 func completionModePtr(m batch.CompletionMode) *batch.CompletionMode { 3519 return &m 3520 } 3521 3522 func podReplacementPolicy(m batch.PodReplacementPolicy) *batch.PodReplacementPolicy { 3523 return &m 3524 } 3525 3526 func getValidObjectMeta(generation int64) metav1.ObjectMeta { 3527 return getValidObjectMetaWithAnnotations(generation, nil) 3528 } 3529 3530 func getValidUID() types.UID { 3531 return "1a2b3c4d5e6f7g8h9i0k" 3532 } 3533 3534 func getValidObjectMetaWithAnnotations(generation int64, annotations map[string]string) metav1.ObjectMeta { 3535 return metav1.ObjectMeta{ 3536 Name: "myjob", 3537 UID: getValidUID(), 3538 Namespace: metav1.NamespaceDefault, 3539 Generation: generation, 3540 Annotations: annotations, 3541 } 3542 } 3543 3544 func getValidLabelSelector() *metav1.LabelSelector { 3545 return &metav1.LabelSelector{ 3546 MatchLabels: map[string]string{"a": "b"}, 3547 } 3548 } 3549 3550 func getValidBatchLabels() map[string]string { 3551 theUID := getValidUID() 3552 return map[string]string{batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: string(theUID), batch.ControllerUidLabel: string(theUID)} 3553 } 3554 3555 func getValidBatchLabelsWithNonBatch() map[string]string { 3556 theUID := getValidUID() 3557 return map[string]string{"a": "b", batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: string(theUID), batch.ControllerUidLabel: string(theUID)} 3558 } 3559 3560 func getValidPodTemplateSpecForSelector(validSelector *metav1.LabelSelector) api.PodTemplateSpec { 3561 return api.PodTemplateSpec{ 3562 ObjectMeta: metav1.ObjectMeta{ 3563 Labels: validSelector.MatchLabels, 3564 }, 3565 Spec: api.PodSpec{ 3566 RestartPolicy: api.RestartPolicyOnFailure, 3567 DNSPolicy: api.DNSClusterFirst, 3568 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 3569 }, 3570 } 3571 }