k8s.io/kubernetes@v1.29.3/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 22 "github.com/google/go-cmp/cmp" 23 "github.com/google/go-cmp/cmp/cmpopts" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/labels" 26 "k8s.io/apimachinery/pkg/types" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 29 "k8s.io/apiserver/pkg/registry/rest" 30 utilfeature "k8s.io/apiserver/pkg/util/feature" 31 featuregatetesting "k8s.io/component-base/featuregate/testing" 32 apitesting "k8s.io/kubernetes/pkg/api/testing" 33 "k8s.io/kubernetes/pkg/apis/batch" 34 _ "k8s.io/kubernetes/pkg/apis/batch/install" 35 api "k8s.io/kubernetes/pkg/apis/core" 36 "k8s.io/kubernetes/pkg/features" 37 "k8s.io/utils/pointer" 38 ) 39 40 var ignoreErrValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") 41 42 // TestJobStrategy_PrepareForUpdate tests various scenearios for PrepareForUpdate 43 func TestJobStrategy_PrepareForUpdate(t *testing.T) { 44 validSelector := getValidLabelSelector() 45 validPodTemplateSpec := getValidPodTemplateSpecForSelector(validSelector) 46 47 podFailurePolicy := &batch.PodFailurePolicy{ 48 Rules: []batch.PodFailurePolicyRule{ 49 { 50 Action: batch.PodFailurePolicyActionFailJob, 51 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 52 ContainerName: pointer.String("container-name"), 53 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 54 Values: []int32{1}, 55 }, 56 }, 57 }, 58 } 59 updatedPodFailurePolicy := &batch.PodFailurePolicy{ 60 Rules: []batch.PodFailurePolicyRule{ 61 { 62 Action: batch.PodFailurePolicyActionIgnore, 63 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 64 ContainerName: pointer.String("updated-container-name"), 65 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 66 Values: []int32{2}, 67 }, 68 }, 69 }, 70 } 71 72 cases := map[string]struct { 73 enableJobPodFailurePolicy bool 74 enableJobBackoffLimitPerIndex bool 75 enableJobPodReplacementPolicy bool 76 job batch.Job 77 updatedJob batch.Job 78 wantJob batch.Job 79 }{ 80 "update job with a new field; updated when JobBackoffLimitPerIndex enabled": { 81 enableJobBackoffLimitPerIndex: true, 82 job: batch.Job{ 83 ObjectMeta: getValidObjectMeta(0), 84 Spec: batch.JobSpec{ 85 Selector: validSelector, 86 Template: validPodTemplateSpec, 87 BackoffLimitPerIndex: nil, 88 MaxFailedIndexes: nil, 89 }, 90 }, 91 updatedJob: batch.Job{ 92 ObjectMeta: getValidObjectMeta(0), 93 Spec: batch.JobSpec{ 94 Selector: validSelector, 95 Template: validPodTemplateSpec, 96 BackoffLimitPerIndex: pointer.Int32(1), 97 MaxFailedIndexes: pointer.Int32(1), 98 }, 99 }, 100 wantJob: batch.Job{ 101 ObjectMeta: getValidObjectMeta(1), 102 Spec: batch.JobSpec{ 103 Selector: validSelector, 104 Template: validPodTemplateSpec, 105 BackoffLimitPerIndex: pointer.Int32(1), 106 MaxFailedIndexes: pointer.Int32(1), 107 }, 108 }, 109 }, 110 "update job with a new field; not updated when JobBackoffLimitPerIndex disabled": { 111 enableJobBackoffLimitPerIndex: false, 112 job: batch.Job{ 113 ObjectMeta: getValidObjectMeta(0), 114 Spec: batch.JobSpec{ 115 Selector: validSelector, 116 Template: validPodTemplateSpec, 117 BackoffLimitPerIndex: nil, 118 MaxFailedIndexes: nil, 119 }, 120 }, 121 updatedJob: batch.Job{ 122 ObjectMeta: getValidObjectMeta(0), 123 Spec: batch.JobSpec{ 124 Selector: validSelector, 125 Template: validPodTemplateSpec, 126 BackoffLimitPerIndex: pointer.Int32(1), 127 MaxFailedIndexes: pointer.Int32(1), 128 }, 129 }, 130 wantJob: batch.Job{ 131 ObjectMeta: getValidObjectMeta(0), 132 Spec: batch.JobSpec{ 133 Selector: validSelector, 134 Template: validPodTemplateSpec, 135 BackoffLimitPerIndex: nil, 136 MaxFailedIndexes: nil, 137 }, 138 }, 139 }, 140 "update job with a new field; updated when JobPodFailurePolicy enabled": { 141 enableJobPodFailurePolicy: true, 142 job: batch.Job{ 143 ObjectMeta: getValidObjectMeta(0), 144 Spec: batch.JobSpec{ 145 Selector: validSelector, 146 Template: validPodTemplateSpec, 147 PodFailurePolicy: nil, 148 }, 149 }, 150 updatedJob: batch.Job{ 151 ObjectMeta: getValidObjectMeta(0), 152 Spec: batch.JobSpec{ 153 Selector: validSelector, 154 Template: validPodTemplateSpec, 155 PodFailurePolicy: updatedPodFailurePolicy, 156 }, 157 }, 158 wantJob: batch.Job{ 159 ObjectMeta: getValidObjectMeta(1), 160 Spec: batch.JobSpec{ 161 Selector: validSelector, 162 Template: validPodTemplateSpec, 163 PodFailurePolicy: updatedPodFailurePolicy, 164 }, 165 }, 166 }, 167 "update job with a new field; updated when JobPodReplacementPolicy enabled": { 168 enableJobPodReplacementPolicy: true, 169 job: batch.Job{ 170 ObjectMeta: getValidObjectMeta(0), 171 Spec: batch.JobSpec{ 172 Selector: validSelector, 173 Template: validPodTemplateSpec, 174 PodReplacementPolicy: nil, 175 }, 176 }, 177 updatedJob: batch.Job{ 178 ObjectMeta: getValidObjectMeta(0), 179 Spec: batch.JobSpec{ 180 Selector: validSelector, 181 Template: validPodTemplateSpec, 182 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 183 }, 184 }, 185 wantJob: batch.Job{ 186 ObjectMeta: getValidObjectMeta(1), 187 Spec: batch.JobSpec{ 188 Selector: validSelector, 189 Template: validPodTemplateSpec, 190 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 191 }, 192 }, 193 }, 194 "update job with a new field; not updated when JobPodReplacementPolicy disabled": { 195 enableJobPodReplacementPolicy: false, 196 job: batch.Job{ 197 ObjectMeta: getValidObjectMeta(0), 198 Spec: batch.JobSpec{ 199 Selector: validSelector, 200 Template: validPodTemplateSpec, 201 PodReplacementPolicy: nil, 202 }, 203 }, 204 updatedJob: batch.Job{ 205 ObjectMeta: getValidObjectMeta(0), 206 Spec: batch.JobSpec{ 207 Selector: validSelector, 208 Template: validPodTemplateSpec, 209 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 210 }, 211 }, 212 wantJob: batch.Job{ 213 ObjectMeta: getValidObjectMeta(0), 214 Spec: batch.JobSpec{ 215 Selector: validSelector, 216 Template: validPodTemplateSpec, 217 PodReplacementPolicy: nil, 218 }, 219 }, 220 }, 221 "update job with a new field; not updated when JobPodFailurePolicy disabled": { 222 enableJobPodFailurePolicy: false, 223 job: batch.Job{ 224 ObjectMeta: getValidObjectMeta(0), 225 Spec: batch.JobSpec{ 226 Selector: validSelector, 227 Template: validPodTemplateSpec, 228 PodFailurePolicy: nil, 229 }, 230 }, 231 updatedJob: batch.Job{ 232 ObjectMeta: getValidObjectMeta(0), 233 Spec: batch.JobSpec{ 234 Selector: validSelector, 235 Template: validPodTemplateSpec, 236 PodFailurePolicy: updatedPodFailurePolicy, 237 }, 238 }, 239 wantJob: batch.Job{ 240 ObjectMeta: getValidObjectMeta(0), 241 Spec: batch.JobSpec{ 242 Selector: validSelector, 243 Template: validPodTemplateSpec, 244 PodFailurePolicy: nil, 245 }, 246 }, 247 }, 248 "update pre-existing field; updated when JobPodFailurePolicy enabled": { 249 enableJobPodFailurePolicy: true, 250 job: batch.Job{ 251 ObjectMeta: getValidObjectMeta(0), 252 Spec: batch.JobSpec{ 253 Selector: validSelector, 254 Template: validPodTemplateSpec, 255 PodFailurePolicy: podFailurePolicy, 256 }, 257 }, 258 updatedJob: batch.Job{ 259 ObjectMeta: getValidObjectMeta(0), 260 Spec: batch.JobSpec{ 261 Selector: validSelector, 262 Template: validPodTemplateSpec, 263 PodFailurePolicy: updatedPodFailurePolicy, 264 }, 265 }, 266 wantJob: batch.Job{ 267 ObjectMeta: getValidObjectMeta(1), 268 Spec: batch.JobSpec{ 269 Selector: validSelector, 270 Template: validPodTemplateSpec, 271 PodFailurePolicy: updatedPodFailurePolicy, 272 }, 273 }, 274 }, 275 "update pre-existing field; updated when JobPodFailurePolicy disabled": { 276 enableJobPodFailurePolicy: false, 277 job: batch.Job{ 278 ObjectMeta: getValidObjectMeta(0), 279 Spec: batch.JobSpec{ 280 Selector: validSelector, 281 Template: validPodTemplateSpec, 282 PodFailurePolicy: podFailurePolicy, 283 }, 284 }, 285 updatedJob: batch.Job{ 286 ObjectMeta: getValidObjectMeta(0), 287 Spec: batch.JobSpec{ 288 Selector: validSelector, 289 Template: validPodTemplateSpec, 290 PodFailurePolicy: updatedPodFailurePolicy, 291 }, 292 }, 293 wantJob: batch.Job{ 294 ObjectMeta: getValidObjectMeta(1), 295 Spec: batch.JobSpec{ 296 Selector: validSelector, 297 Template: validPodTemplateSpec, 298 PodFailurePolicy: updatedPodFailurePolicy, 299 }, 300 }, 301 }, 302 "add tracking annotation back": { 303 job: batch.Job{ 304 ObjectMeta: getValidObjectMeta(0), 305 Spec: batch.JobSpec{ 306 Selector: validSelector, 307 Template: validPodTemplateSpec, 308 PodFailurePolicy: podFailurePolicy, 309 }, 310 }, 311 updatedJob: batch.Job{ 312 ObjectMeta: getValidObjectMeta(0), 313 Spec: batch.JobSpec{ 314 Selector: validSelector, 315 Template: validPodTemplateSpec, 316 }, 317 }, 318 wantJob: batch.Job{ 319 ObjectMeta: getValidObjectMeta(1), 320 Spec: batch.JobSpec{ 321 Selector: validSelector, 322 Template: validPodTemplateSpec, 323 }, 324 }, 325 }, 326 "attempt status update and verify it doesn't change": { 327 job: batch.Job{ 328 ObjectMeta: getValidObjectMeta(0), 329 Spec: batch.JobSpec{ 330 Selector: validSelector, 331 Template: validPodTemplateSpec, 332 }, 333 Status: batch.JobStatus{ 334 Active: 1, 335 }, 336 }, 337 updatedJob: batch.Job{ 338 ObjectMeta: getValidObjectMeta(0), 339 Spec: batch.JobSpec{ 340 Selector: validSelector, 341 Template: validPodTemplateSpec, 342 }, 343 Status: batch.JobStatus{ 344 Active: 2, 345 }, 346 }, 347 wantJob: batch.Job{ 348 ObjectMeta: getValidObjectMeta(0), 349 Spec: batch.JobSpec{ 350 Selector: validSelector, 351 Template: validPodTemplateSpec, 352 }, 353 Status: batch.JobStatus{ 354 Active: 1, 355 }, 356 }, 357 }, 358 "ensure generation doesn't change over non spec updates": { 359 job: batch.Job{ 360 ObjectMeta: getValidObjectMeta(0), 361 Spec: batch.JobSpec{ 362 Selector: validSelector, 363 Template: validPodTemplateSpec, 364 }, 365 Status: batch.JobStatus{ 366 Active: 1, 367 }, 368 }, 369 updatedJob: batch.Job{ 370 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 371 Spec: batch.JobSpec{ 372 Selector: validSelector, 373 Template: validPodTemplateSpec, 374 }, 375 Status: batch.JobStatus{ 376 Active: 2, 377 }, 378 }, 379 wantJob: batch.Job{ 380 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 381 Spec: batch.JobSpec{ 382 Selector: validSelector, 383 Template: validPodTemplateSpec, 384 }, 385 Status: batch.JobStatus{ 386 Active: 1, 387 }, 388 }, 389 }, 390 "test updating suspend false->true": { 391 job: batch.Job{ 392 ObjectMeta: getValidObjectMeta(0), 393 Spec: batch.JobSpec{ 394 Selector: validSelector, 395 Template: validPodTemplateSpec, 396 Suspend: pointer.Bool(false), 397 }, 398 }, 399 updatedJob: batch.Job{ 400 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 401 Spec: batch.JobSpec{ 402 Selector: validSelector, 403 Template: validPodTemplateSpec, 404 Suspend: pointer.Bool(true), 405 }, 406 }, 407 wantJob: batch.Job{ 408 ObjectMeta: getValidObjectMetaWithAnnotations(1, map[string]string{"hello": "world"}), 409 Spec: batch.JobSpec{ 410 Selector: validSelector, 411 Template: validPodTemplateSpec, 412 Suspend: pointer.Bool(true), 413 }, 414 }, 415 }, 416 "test updating suspend nil -> true": { 417 job: batch.Job{ 418 ObjectMeta: getValidObjectMeta(0), 419 Spec: batch.JobSpec{ 420 Selector: validSelector, 421 Template: validPodTemplateSpec, 422 }, 423 }, 424 updatedJob: batch.Job{ 425 ObjectMeta: getValidObjectMetaWithAnnotations(0, map[string]string{"hello": "world"}), 426 Spec: batch.JobSpec{ 427 Selector: validSelector, 428 Template: validPodTemplateSpec, 429 Suspend: pointer.Bool(true), 430 }, 431 }, 432 wantJob: batch.Job{ 433 ObjectMeta: getValidObjectMetaWithAnnotations(1, map[string]string{"hello": "world"}), 434 Spec: batch.JobSpec{ 435 Selector: validSelector, 436 Template: validPodTemplateSpec, 437 Suspend: pointer.Bool(true), 438 }, 439 }, 440 }, 441 } 442 443 for name, tc := range cases { 444 t.Run(name, func(t *testing.T) { 445 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy)() 446 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex)() 447 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodReplacementPolicy, tc.enableJobPodReplacementPolicy)() 448 ctx := genericapirequest.NewDefaultContext() 449 450 Strategy.PrepareForUpdate(ctx, &tc.updatedJob, &tc.job) 451 452 if diff := cmp.Diff(tc.wantJob, tc.updatedJob); diff != "" { 453 t.Errorf("Job update differences (-want,+got):\n%s", diff) 454 } 455 }) 456 } 457 } 458 459 // TestJobStrategy_PrepareForCreate tests various scenarios for PrepareForCreate 460 func TestJobStrategy_PrepareForCreate(t *testing.T) { 461 validSelector := getValidLabelSelector() 462 validPodTemplateSpec := getValidPodTemplateSpecForSelector(validSelector) 463 validSelectorWithBatchLabels := &metav1.LabelSelector{MatchLabels: getValidBatchLabelsWithNonBatch()} 464 expectedPodTemplateSpec := getValidPodTemplateSpecForSelector(validSelectorWithBatchLabels) 465 466 podFailurePolicy := &batch.PodFailurePolicy{ 467 Rules: []batch.PodFailurePolicyRule{ 468 { 469 Action: batch.PodFailurePolicyActionFailJob, 470 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 471 ContainerName: pointer.String("container-name"), 472 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 473 Values: []int32{1}, 474 }, 475 }, 476 }, 477 } 478 479 cases := map[string]struct { 480 enableJobPodFailurePolicy bool 481 enableJobBackoffLimitPerIndex bool 482 enableJobPodReplacementPolicy bool 483 job batch.Job 484 wantJob batch.Job 485 }{ 486 "generate selectors": { 487 job: batch.Job{ 488 ObjectMeta: getValidObjectMeta(0), 489 Spec: batch.JobSpec{ 490 Selector: validSelector, 491 ManualSelector: pointer.Bool(false), 492 Template: validPodTemplateSpec, 493 }, 494 }, 495 wantJob: batch.Job{ 496 ObjectMeta: getValidObjectMeta(1), 497 Spec: batch.JobSpec{ 498 Selector: validSelector, 499 ManualSelector: pointer.Bool(false), 500 Template: expectedPodTemplateSpec, 501 }, 502 }, 503 }, 504 "create job with a new fields; JobBackoffLimitPerIndex enabled": { 505 enableJobBackoffLimitPerIndex: true, 506 job: batch.Job{ 507 ObjectMeta: getValidObjectMeta(0), 508 Spec: batch.JobSpec{ 509 Selector: validSelector, 510 ManualSelector: pointer.Bool(false), 511 Template: validPodTemplateSpec, 512 BackoffLimitPerIndex: pointer.Int32(1), 513 MaxFailedIndexes: pointer.Int32(1), 514 }, 515 }, 516 wantJob: batch.Job{ 517 ObjectMeta: getValidObjectMeta(1), 518 Spec: batch.JobSpec{ 519 Selector: validSelector, 520 ManualSelector: pointer.Bool(false), 521 Template: expectedPodTemplateSpec, 522 BackoffLimitPerIndex: pointer.Int32(1), 523 MaxFailedIndexes: pointer.Int32(1), 524 }, 525 }, 526 }, 527 "create job with a new fields; JobBackoffLimitPerIndex disabled": { 528 enableJobBackoffLimitPerIndex: false, 529 job: batch.Job{ 530 ObjectMeta: getValidObjectMeta(0), 531 Spec: batch.JobSpec{ 532 Selector: validSelector, 533 ManualSelector: pointer.Bool(false), 534 Template: validPodTemplateSpec, 535 BackoffLimitPerIndex: pointer.Int32(1), 536 MaxFailedIndexes: pointer.Int32(1), 537 }, 538 }, 539 wantJob: batch.Job{ 540 ObjectMeta: getValidObjectMeta(1), 541 Spec: batch.JobSpec{ 542 Selector: validSelector, 543 ManualSelector: pointer.Bool(false), 544 Template: expectedPodTemplateSpec, 545 BackoffLimitPerIndex: nil, 546 MaxFailedIndexes: nil, 547 }, 548 }, 549 }, 550 "create job with a new field; JobPodFailurePolicy enabled": { 551 enableJobPodFailurePolicy: true, 552 job: batch.Job{ 553 ObjectMeta: getValidObjectMeta(0), 554 Spec: batch.JobSpec{ 555 Selector: validSelector, 556 ManualSelector: pointer.Bool(false), 557 Template: validPodTemplateSpec, 558 PodFailurePolicy: podFailurePolicy, 559 }, 560 }, 561 wantJob: batch.Job{ 562 ObjectMeta: getValidObjectMeta(1), 563 Spec: batch.JobSpec{ 564 Selector: validSelector, 565 ManualSelector: pointer.Bool(false), 566 Template: expectedPodTemplateSpec, 567 PodFailurePolicy: podFailurePolicy, 568 }, 569 }, 570 }, 571 "create job with a new field; JobPodReplacementPolicy enabled": { 572 enableJobPodReplacementPolicy: true, 573 job: batch.Job{ 574 ObjectMeta: getValidObjectMeta(0), 575 Spec: batch.JobSpec{ 576 Selector: validSelector, 577 ManualSelector: pointer.Bool(false), 578 Template: validPodTemplateSpec, 579 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 580 }, 581 }, 582 wantJob: batch.Job{ 583 ObjectMeta: getValidObjectMeta(1), 584 Spec: batch.JobSpec{ 585 Selector: validSelector, 586 ManualSelector: pointer.Bool(false), 587 Template: expectedPodTemplateSpec, 588 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 589 }, 590 }, 591 }, 592 "create job with a new field; JobPodReplacementPolicy disabled": { 593 enableJobPodReplacementPolicy: false, 594 job: batch.Job{ 595 ObjectMeta: getValidObjectMeta(0), 596 Spec: batch.JobSpec{ 597 Selector: validSelector, 598 ManualSelector: pointer.Bool(false), 599 Template: validPodTemplateSpec, 600 PodReplacementPolicy: podReplacementPolicy(batch.Failed), 601 }, 602 }, 603 wantJob: batch.Job{ 604 ObjectMeta: getValidObjectMeta(1), 605 Spec: batch.JobSpec{ 606 Selector: validSelector, 607 ManualSelector: pointer.Bool(false), 608 Template: expectedPodTemplateSpec, 609 PodReplacementPolicy: nil, 610 }, 611 }, 612 }, 613 "create job with a new field; JobPodFailurePolicy disabled": { 614 enableJobPodFailurePolicy: false, 615 job: batch.Job{ 616 ObjectMeta: getValidObjectMeta(0), 617 Spec: batch.JobSpec{ 618 Selector: validSelector, 619 ManualSelector: pointer.Bool(false), 620 Template: validPodTemplateSpec, 621 PodFailurePolicy: podFailurePolicy, 622 }, 623 }, 624 wantJob: batch.Job{ 625 ObjectMeta: getValidObjectMeta(1), 626 Spec: batch.JobSpec{ 627 Selector: validSelector, 628 ManualSelector: pointer.Bool(false), 629 Template: expectedPodTemplateSpec, 630 PodFailurePolicy: nil, 631 }, 632 }, 633 }, 634 "job does not allow setting status on create": { 635 job: batch.Job{ 636 ObjectMeta: getValidObjectMeta(0), 637 Spec: batch.JobSpec{ 638 Selector: validSelector, 639 ManualSelector: pointer.Bool(false), 640 Template: validPodTemplateSpec, 641 }, 642 Status: batch.JobStatus{ 643 Active: 1, 644 }, 645 }, 646 wantJob: batch.Job{ 647 ObjectMeta: getValidObjectMeta(1), 648 Spec: batch.JobSpec{ 649 Selector: validSelector, 650 ManualSelector: pointer.Bool(false), 651 Template: expectedPodTemplateSpec, 652 }, 653 }, 654 }, 655 "create job with pod failure policy using FailIndex action; JobPodFailurePolicy enabled, JobBackoffLimitPerIndex disabled": { 656 enableJobBackoffLimitPerIndex: false, 657 enableJobPodFailurePolicy: true, 658 job: batch.Job{ 659 ObjectMeta: getValidObjectMeta(0), 660 Spec: batch.JobSpec{ 661 Selector: validSelector, 662 ManualSelector: pointer.Bool(false), 663 Template: validPodTemplateSpec, 664 BackoffLimitPerIndex: pointer.Int32(1), 665 PodFailurePolicy: &batch.PodFailurePolicy{ 666 Rules: []batch.PodFailurePolicyRule{ 667 { 668 Action: batch.PodFailurePolicyActionFailIndex, 669 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 670 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 671 Values: []int32{1}, 672 }, 673 }, 674 }, 675 }, 676 }, 677 }, 678 wantJob: batch.Job{ 679 ObjectMeta: getValidObjectMeta(1), 680 Spec: batch.JobSpec{ 681 Selector: validSelector, 682 ManualSelector: pointer.Bool(false), 683 Template: expectedPodTemplateSpec, 684 PodFailurePolicy: &batch.PodFailurePolicy{ 685 Rules: []batch.PodFailurePolicyRule{}, 686 }, 687 }, 688 }, 689 }, 690 "create job with multiple pod failure policy rules, some using FailIndex action; JobPodFailurePolicy enabled, JobBackoffLimitPerIndex disabled": { 691 enableJobBackoffLimitPerIndex: false, 692 enableJobPodFailurePolicy: true, 693 job: batch.Job{ 694 ObjectMeta: getValidObjectMeta(0), 695 Spec: batch.JobSpec{ 696 Selector: validSelector, 697 ManualSelector: pointer.Bool(false), 698 Template: validPodTemplateSpec, 699 BackoffLimitPerIndex: pointer.Int32(1), 700 PodFailurePolicy: &batch.PodFailurePolicy{ 701 Rules: []batch.PodFailurePolicyRule{ 702 { 703 Action: batch.PodFailurePolicyActionFailJob, 704 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 705 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 706 Values: []int32{2}, 707 }, 708 }, 709 { 710 Action: batch.PodFailurePolicyActionFailIndex, 711 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 712 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 713 Values: []int32{1}, 714 }, 715 }, 716 { 717 Action: batch.PodFailurePolicyActionIgnore, 718 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 719 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 720 Values: []int32{13}, 721 }, 722 }, 723 }, 724 }, 725 }, 726 }, 727 wantJob: batch.Job{ 728 ObjectMeta: getValidObjectMeta(1), 729 Spec: batch.JobSpec{ 730 Selector: validSelector, 731 ManualSelector: pointer.Bool(false), 732 Template: expectedPodTemplateSpec, 733 PodFailurePolicy: &batch.PodFailurePolicy{ 734 Rules: []batch.PodFailurePolicyRule{ 735 { 736 Action: batch.PodFailurePolicyActionFailJob, 737 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 738 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 739 Values: []int32{2}, 740 }, 741 }, 742 { 743 Action: batch.PodFailurePolicyActionIgnore, 744 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 745 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 746 Values: []int32{13}, 747 }, 748 }, 749 }, 750 }, 751 }, 752 }, 753 }, 754 } 755 756 for name, tc := range cases { 757 t.Run(name, func(t *testing.T) { 758 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy)() 759 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex)() 760 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodReplacementPolicy, tc.enableJobPodReplacementPolicy)() 761 ctx := genericapirequest.NewDefaultContext() 762 763 Strategy.PrepareForCreate(ctx, &tc.job) 764 765 if diff := cmp.Diff(tc.wantJob, tc.job); diff != "" { 766 t.Errorf("Job pod failure policy (-want,+got):\n%s", diff) 767 } 768 }) 769 } 770 } 771 772 func TestJobStrategy_GarbageCollectionPolicy(t *testing.T) { 773 // Make sure we correctly implement the interface. 774 // Otherwise a typo could silently change the default. 775 var gcds rest.GarbageCollectionDeleteStrategy = Strategy 776 if got, want := gcds.DefaultGarbageCollectionPolicy(genericapirequest.NewContext()), rest.DeleteDependents; got != want { 777 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want) 778 } 779 780 var ( 781 v1Ctx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v1", Resource: "jobs"}) 782 otherVersionCtx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v100", Resource: "jobs"}) 783 ) 784 if got, want := gcds.DefaultGarbageCollectionPolicy(v1Ctx), rest.OrphanDependents; got != want { 785 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want) 786 } 787 if got, want := gcds.DefaultGarbageCollectionPolicy(otherVersionCtx), rest.DeleteDependents; got != want { 788 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want) 789 } 790 } 791 792 func TestJobStrategy_ValidateUpdate(t *testing.T) { 793 ctx := genericapirequest.NewDefaultContext() 794 validSelector := &metav1.LabelSelector{ 795 MatchLabels: map[string]string{"a": "b"}, 796 } 797 validPodTemplateSpec := api.PodTemplateSpec{ 798 ObjectMeta: metav1.ObjectMeta{ 799 Labels: validSelector.MatchLabels, 800 }, 801 Spec: api.PodSpec{ 802 RestartPolicy: api.RestartPolicyOnFailure, 803 DNSPolicy: api.DNSClusterFirst, 804 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 805 }, 806 } 807 validPodTemplateSpecNever := *validPodTemplateSpec.DeepCopy() 808 validPodTemplateSpecNever.Spec.RestartPolicy = api.RestartPolicyNever 809 now := metav1.Now() 810 cases := map[string]struct { 811 enableJobPodFailurePolicy bool 812 enableJobBackoffLimitPerIndex bool 813 job *batch.Job 814 update func(*batch.Job) 815 wantErrs field.ErrorList 816 }{ 817 "update parallelism": { 818 job: &batch.Job{ 819 ObjectMeta: metav1.ObjectMeta{ 820 Name: "myjob", 821 Namespace: metav1.NamespaceDefault, 822 ResourceVersion: "0", 823 }, 824 Spec: batch.JobSpec{ 825 Selector: validSelector, 826 Template: validPodTemplateSpec, 827 ManualSelector: pointer.BoolPtr(true), 828 Parallelism: pointer.Int32Ptr(1), 829 }, 830 }, 831 update: func(job *batch.Job) { 832 job.Spec.Parallelism = pointer.Int32Ptr(2) 833 }, 834 }, 835 "update completions disallowed": { 836 job: &batch.Job{ 837 ObjectMeta: metav1.ObjectMeta{ 838 Name: "myjob", 839 Namespace: metav1.NamespaceDefault, 840 ResourceVersion: "0", 841 }, 842 Spec: batch.JobSpec{ 843 Selector: validSelector, 844 Template: validPodTemplateSpec, 845 ManualSelector: pointer.BoolPtr(true), 846 Parallelism: pointer.Int32Ptr(1), 847 Completions: pointer.Int32Ptr(1), 848 }, 849 }, 850 update: func(job *batch.Job) { 851 job.Spec.Completions = pointer.Int32Ptr(2) 852 }, 853 wantErrs: field.ErrorList{ 854 {Type: field.ErrorTypeInvalid, Field: "spec.completions"}, 855 }, 856 }, 857 "preserving tracking annotation": { 858 job: &batch.Job{ 859 ObjectMeta: metav1.ObjectMeta{ 860 Name: "myjob", 861 Namespace: metav1.NamespaceDefault, 862 ResourceVersion: "0", 863 Annotations: map[string]string{ 864 batch.JobTrackingFinalizer: "", 865 }, 866 }, 867 Spec: batch.JobSpec{ 868 Selector: validSelector, 869 Template: validPodTemplateSpec, 870 ManualSelector: pointer.BoolPtr(true), 871 Parallelism: pointer.Int32Ptr(1), 872 }, 873 }, 874 update: func(job *batch.Job) { 875 job.Annotations["foo"] = "bar" 876 }, 877 }, 878 "deleting user annotation": { 879 job: &batch.Job{ 880 ObjectMeta: metav1.ObjectMeta{ 881 Name: "myjob", 882 Namespace: metav1.NamespaceDefault, 883 ResourceVersion: "0", 884 Annotations: map[string]string{ 885 batch.JobTrackingFinalizer: "", 886 "foo": "bar", 887 }, 888 }, 889 Spec: batch.JobSpec{ 890 Selector: validSelector, 891 Template: validPodTemplateSpec, 892 ManualSelector: pointer.BoolPtr(true), 893 Parallelism: pointer.Int32Ptr(1), 894 }, 895 }, 896 update: func(job *batch.Job) { 897 delete(job.Annotations, "foo") 898 }, 899 }, 900 "updating node selector for unsuspended job disallowed": { 901 job: &batch.Job{ 902 ObjectMeta: metav1.ObjectMeta{ 903 Name: "myjob", 904 Namespace: metav1.NamespaceDefault, 905 ResourceVersion: "0", 906 Annotations: map[string]string{"foo": "bar"}, 907 }, 908 Spec: batch.JobSpec{ 909 Selector: validSelector, 910 Template: validPodTemplateSpec, 911 ManualSelector: pointer.BoolPtr(true), 912 Parallelism: pointer.Int32Ptr(1), 913 }, 914 }, 915 update: func(job *batch.Job) { 916 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 917 }, 918 wantErrs: field.ErrorList{ 919 {Type: field.ErrorTypeInvalid, Field: "spec.template"}, 920 }, 921 }, 922 "updating node selector for suspended but previously started job disallowed": { 923 job: &batch.Job{ 924 ObjectMeta: metav1.ObjectMeta{ 925 Name: "myjob", 926 Namespace: metav1.NamespaceDefault, 927 ResourceVersion: "0", 928 Annotations: map[string]string{"foo": "bar"}, 929 }, 930 Spec: batch.JobSpec{ 931 Selector: validSelector, 932 Template: validPodTemplateSpec, 933 ManualSelector: pointer.BoolPtr(true), 934 Parallelism: pointer.Int32Ptr(1), 935 Suspend: pointer.BoolPtr(true), 936 }, 937 Status: batch.JobStatus{ 938 StartTime: &now, 939 }, 940 }, 941 update: func(job *batch.Job) { 942 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 943 }, 944 wantErrs: field.ErrorList{ 945 {Type: field.ErrorTypeInvalid, Field: "spec.template"}, 946 }, 947 }, 948 "updating node selector for suspended and not previously started job allowed": { 949 job: &batch.Job{ 950 ObjectMeta: metav1.ObjectMeta{ 951 Name: "myjob", 952 Namespace: metav1.NamespaceDefault, 953 ResourceVersion: "0", 954 Annotations: map[string]string{"foo": "bar"}, 955 }, 956 Spec: batch.JobSpec{ 957 Selector: validSelector, 958 Template: validPodTemplateSpec, 959 ManualSelector: pointer.BoolPtr(true), 960 Parallelism: pointer.Int32Ptr(1), 961 Suspend: pointer.BoolPtr(true), 962 }, 963 }, 964 update: func(job *batch.Job) { 965 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 966 }, 967 }, 968 "invalid label selector": { 969 job: &batch.Job{ 970 ObjectMeta: metav1.ObjectMeta{ 971 Name: "myjob", 972 Namespace: metav1.NamespaceDefault, 973 ResourceVersion: "0", 974 Annotations: map[string]string{"foo": "bar"}, 975 }, 976 Spec: batch.JobSpec{ 977 Selector: &metav1.LabelSelector{ 978 MatchLabels: map[string]string{"a": "b"}, 979 MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "key", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"bad value"}}}, 980 }, 981 ManualSelector: pointer.BoolPtr(true), 982 Template: validPodTemplateSpec, 983 }, 984 }, 985 update: func(job *batch.Job) { 986 job.Annotations["hello"] = "world" 987 }, 988 }, 989 "old job has no batch.kubernetes.io labels": { 990 job: &batch.Job{ 991 ObjectMeta: metav1.ObjectMeta{ 992 Name: "myjob", 993 UID: "test", 994 Namespace: metav1.NamespaceDefault, 995 ResourceVersion: "10", 996 Annotations: map[string]string{"hello": "world"}, 997 }, 998 Spec: batch.JobSpec{ 999 Selector: &metav1.LabelSelector{ 1000 MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "test"}, 1001 }, 1002 Parallelism: pointer.Int32(4), 1003 Template: api.PodTemplateSpec{ 1004 ObjectMeta: metav1.ObjectMeta{ 1005 Labels: map[string]string{batch.LegacyJobNameLabel: "myjob", batch.LegacyControllerUidLabel: "test"}, 1006 }, 1007 Spec: api.PodSpec{ 1008 RestartPolicy: api.RestartPolicyOnFailure, 1009 DNSPolicy: api.DNSClusterFirst, 1010 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1011 }, 1012 }, 1013 }, 1014 }, 1015 update: func(job *batch.Job) { 1016 job.Annotations["hello"] = "world" 1017 }, 1018 }, 1019 "old job has all labels": { 1020 job: &batch.Job{ 1021 ObjectMeta: metav1.ObjectMeta{ 1022 Name: "myjob", 1023 UID: "test", 1024 Namespace: metav1.NamespaceDefault, 1025 ResourceVersion: "10", 1026 Annotations: map[string]string{"foo": "bar"}, 1027 }, 1028 Spec: batch.JobSpec{ 1029 Selector: &metav1.LabelSelector{ 1030 MatchLabels: map[string]string{batch.ControllerUidLabel: "test"}, 1031 }, 1032 Parallelism: pointer.Int32(4), 1033 Template: api.PodTemplateSpec{ 1034 ObjectMeta: metav1.ObjectMeta{ 1035 Labels: map[string]string{batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: "test", batch.ControllerUidLabel: "test"}, 1036 }, 1037 Spec: api.PodSpec{ 1038 RestartPolicy: api.RestartPolicyOnFailure, 1039 DNSPolicy: api.DNSClusterFirst, 1040 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1041 }, 1042 }, 1043 }, 1044 }, 1045 update: func(job *batch.Job) { 1046 job.Annotations["hello"] = "world" 1047 }, 1048 }, 1049 "old job is using FailIndex JobBackoffLimitPerIndex is disabled, but FailIndex was already used": { 1050 enableJobPodFailurePolicy: true, 1051 enableJobBackoffLimitPerIndex: false, 1052 job: &batch.Job{ 1053 ObjectMeta: metav1.ObjectMeta{ 1054 Name: "myjob", 1055 Namespace: metav1.NamespaceDefault, 1056 ResourceVersion: "0", 1057 Annotations: map[string]string{"foo": "bar"}, 1058 }, 1059 Spec: batch.JobSpec{ 1060 CompletionMode: completionModePtr(batch.IndexedCompletion), 1061 Completions: pointer.Int32(2), 1062 BackoffLimitPerIndex: pointer.Int32(1), 1063 Selector: validSelector, 1064 ManualSelector: pointer.Bool(true), 1065 Template: validPodTemplateSpecNever, 1066 PodFailurePolicy: &batch.PodFailurePolicy{ 1067 Rules: []batch.PodFailurePolicyRule{ 1068 { 1069 Action: batch.PodFailurePolicyActionFailIndex, 1070 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1071 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1072 Values: []int32{1}, 1073 }, 1074 }, 1075 }, 1076 }, 1077 }, 1078 }, 1079 update: func(job *batch.Job) { 1080 job.Annotations["hello"] = "world" 1081 }, 1082 }, 1083 } 1084 for name, tc := range cases { 1085 t.Run(name, func(t *testing.T) { 1086 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy)() 1087 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex)() 1088 newJob := tc.job.DeepCopy() 1089 tc.update(newJob) 1090 errs := Strategy.ValidateUpdate(ctx, newJob, tc.job) 1091 if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" { 1092 t.Errorf("Unexpected errors (-want,+got):\n%s", diff) 1093 } 1094 }) 1095 } 1096 } 1097 1098 func TestJobStrategy_WarningsOnUpdate(t *testing.T) { 1099 ctx := genericapirequest.NewDefaultContext() 1100 validSelector := &metav1.LabelSelector{ 1101 MatchLabels: map[string]string{"a": "b"}, 1102 } 1103 validPodTemplateSpec := api.PodTemplateSpec{ 1104 ObjectMeta: metav1.ObjectMeta{ 1105 Labels: validSelector.MatchLabels, 1106 }, 1107 Spec: api.PodSpec{ 1108 RestartPolicy: api.RestartPolicyOnFailure, 1109 DNSPolicy: api.DNSClusterFirst, 1110 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1111 }, 1112 } 1113 cases := map[string]struct { 1114 oldJob *batch.Job 1115 job *batch.Job 1116 wantWarningsCount int32 1117 }{ 1118 "generation 0 for both": { 1119 job: &batch.Job{ 1120 ObjectMeta: metav1.ObjectMeta{ 1121 Name: "myjob", 1122 Namespace: metav1.NamespaceDefault, 1123 ResourceVersion: "0", 1124 Generation: 0, 1125 }, 1126 Spec: batch.JobSpec{ 1127 Selector: validSelector, 1128 Template: validPodTemplateSpec, 1129 ManualSelector: pointer.BoolPtr(true), 1130 Parallelism: pointer.Int32Ptr(1), 1131 }, 1132 }, 1133 1134 oldJob: &batch.Job{ 1135 ObjectMeta: metav1.ObjectMeta{ 1136 Name: "myjob", 1137 Namespace: metav1.NamespaceDefault, 1138 ResourceVersion: "0", 1139 Generation: 0, 1140 }, 1141 Spec: batch.JobSpec{ 1142 Selector: validSelector, 1143 Template: validPodTemplateSpec, 1144 ManualSelector: pointer.BoolPtr(true), 1145 Parallelism: pointer.Int32Ptr(1), 1146 }, 1147 }, 1148 }, 1149 "generation 1 for new; force WarningsOnUpdate to check PodTemplate for updates": { 1150 job: &batch.Job{ 1151 ObjectMeta: metav1.ObjectMeta{ 1152 Name: "myjob", 1153 Namespace: metav1.NamespaceDefault, 1154 ResourceVersion: "0", 1155 Generation: 1, 1156 }, 1157 Spec: batch.JobSpec{ 1158 Selector: validSelector, 1159 Template: validPodTemplateSpec, 1160 ManualSelector: pointer.BoolPtr(true), 1161 Parallelism: pointer.Int32Ptr(1), 1162 }, 1163 }, 1164 1165 oldJob: &batch.Job{ 1166 ObjectMeta: metav1.ObjectMeta{ 1167 Name: "myjob", 1168 Namespace: metav1.NamespaceDefault, 1169 ResourceVersion: "0", 1170 Generation: 0, 1171 }, 1172 Spec: batch.JobSpec{ 1173 Selector: validSelector, 1174 Template: validPodTemplateSpec, 1175 ManualSelector: pointer.BoolPtr(true), 1176 Parallelism: pointer.Int32Ptr(1), 1177 }, 1178 }, 1179 }, 1180 "force validation failure in pod template": { 1181 job: &batch.Job{ 1182 ObjectMeta: metav1.ObjectMeta{ 1183 Name: "myjob", 1184 Namespace: metav1.NamespaceDefault, 1185 ResourceVersion: "0", 1186 Generation: 1, 1187 }, 1188 Spec: batch.JobSpec{ 1189 Selector: validSelector, 1190 Template: api.PodTemplateSpec{ 1191 Spec: api.PodSpec{ImagePullSecrets: []api.LocalObjectReference{{Name: ""}}}, 1192 }, 1193 ManualSelector: pointer.BoolPtr(true), 1194 Parallelism: pointer.Int32Ptr(1), 1195 }, 1196 }, 1197 1198 oldJob: &batch.Job{ 1199 ObjectMeta: metav1.ObjectMeta{ 1200 Name: "myjob", 1201 Namespace: metav1.NamespaceDefault, 1202 ResourceVersion: "0", 1203 Generation: 0, 1204 }, 1205 Spec: batch.JobSpec{ 1206 Selector: validSelector, 1207 Template: validPodTemplateSpec, 1208 ManualSelector: pointer.BoolPtr(true), 1209 Parallelism: pointer.Int32Ptr(1), 1210 }, 1211 }, 1212 wantWarningsCount: 1, 1213 }, 1214 "Invalid transition to high parallelism": { 1215 wantWarningsCount: 1, 1216 job: &batch.Job{ 1217 ObjectMeta: metav1.ObjectMeta{ 1218 Name: "myjob2", 1219 Namespace: metav1.NamespaceDefault, 1220 Generation: 1, 1221 ResourceVersion: "0", 1222 }, 1223 Spec: batch.JobSpec{ 1224 CompletionMode: completionModePtr(batch.IndexedCompletion), 1225 Completions: pointer.Int32(100_001), 1226 Parallelism: pointer.Int32(10_001), 1227 Template: validPodTemplateSpec, 1228 }, 1229 }, 1230 oldJob: &batch.Job{ 1231 ObjectMeta: metav1.ObjectMeta{ 1232 Name: "myjob2", 1233 Namespace: metav1.NamespaceDefault, 1234 Generation: 0, 1235 ResourceVersion: "0", 1236 }, 1237 Spec: batch.JobSpec{ 1238 CompletionMode: completionModePtr(batch.IndexedCompletion), 1239 Completions: pointer.Int32(100_001), 1240 Parallelism: pointer.Int32(10_000), 1241 Template: validPodTemplateSpec, 1242 }, 1243 }, 1244 }, 1245 } 1246 for val, tc := range cases { 1247 t.Run(val, func(t *testing.T) { 1248 gotWarnings := Strategy.WarningsOnUpdate(ctx, tc.job, tc.oldJob) 1249 if len(gotWarnings) != int(tc.wantWarningsCount) { 1250 t.Errorf("got warning length of %d but expected %d", len(gotWarnings), tc.wantWarningsCount) 1251 } 1252 }) 1253 } 1254 } 1255 func TestJobStrategy_WarningsOnCreate(t *testing.T) { 1256 ctx := genericapirequest.NewDefaultContext() 1257 1258 theUID := types.UID("1a2b3c4d5e6f7g8h9i0k") 1259 validSelector := &metav1.LabelSelector{ 1260 MatchLabels: map[string]string{"a": "b"}, 1261 } 1262 validPodTemplate := api.PodTemplateSpec{ 1263 ObjectMeta: metav1.ObjectMeta{ 1264 Labels: validSelector.MatchLabels, 1265 }, 1266 Spec: api.PodSpec{ 1267 RestartPolicy: api.RestartPolicyOnFailure, 1268 DNSPolicy: api.DNSClusterFirst, 1269 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1270 }, 1271 } 1272 validSpec := batch.JobSpec{ 1273 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 1274 Selector: nil, 1275 Template: validPodTemplate, 1276 } 1277 1278 testcases := map[string]struct { 1279 job *batch.Job 1280 wantWarningsCount int32 1281 }{ 1282 "happy path job": { 1283 job: &batch.Job{ 1284 ObjectMeta: metav1.ObjectMeta{ 1285 Name: "myjob2", 1286 Namespace: metav1.NamespaceDefault, 1287 UID: theUID, 1288 }, 1289 Spec: validSpec, 1290 }, 1291 }, 1292 "dns invalid name": { 1293 wantWarningsCount: 1, 1294 job: &batch.Job{ 1295 ObjectMeta: metav1.ObjectMeta{ 1296 Name: "my job2", 1297 Namespace: metav1.NamespaceDefault, 1298 UID: theUID, 1299 }, 1300 Spec: validSpec, 1301 }, 1302 }, 1303 "high completions and parallelism": { 1304 wantWarningsCount: 1, 1305 job: &batch.Job{ 1306 ObjectMeta: metav1.ObjectMeta{ 1307 Name: "myjob2", 1308 Namespace: metav1.NamespaceDefault, 1309 UID: theUID, 1310 }, 1311 Spec: batch.JobSpec{ 1312 CompletionMode: completionModePtr(batch.IndexedCompletion), 1313 Parallelism: pointer.Int32(100_001), 1314 Completions: pointer.Int32(100_001), 1315 Template: validPodTemplate, 1316 }, 1317 }, 1318 }, 1319 } 1320 for name, tc := range testcases { 1321 t.Run(name, func(t *testing.T) { 1322 gotWarnings := Strategy.WarningsOnCreate(ctx, tc.job) 1323 if len(gotWarnings) != int(tc.wantWarningsCount) { 1324 t.Errorf("got warning length of %d but expected %d", len(gotWarnings), tc.wantWarningsCount) 1325 } 1326 }) 1327 } 1328 } 1329 func TestJobStrategy_Validate(t *testing.T) { 1330 ctx := genericapirequest.NewDefaultContext() 1331 1332 theUID := getValidUID() 1333 validSelector := getValidLabelSelector() 1334 batchLabels := getValidBatchLabels() 1335 labelsWithNonBatch := getValidBatchLabelsWithNonBatch() 1336 defaultSelector := &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}} 1337 validPodSpec := api.PodSpec{ 1338 RestartPolicy: api.RestartPolicyOnFailure, 1339 DNSPolicy: api.DNSClusterFirst, 1340 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1341 } 1342 validPodSpecNever := *validPodSpec.DeepCopy() 1343 validPodSpecNever.RestartPolicy = api.RestartPolicyNever 1344 validObjectMeta := getValidObjectMeta(0) 1345 testcases := map[string]struct { 1346 enableJobPodFailurePolicy bool 1347 enableJobBackoffLimitPerIndex bool 1348 job *batch.Job 1349 wantJob *batch.Job 1350 wantWarningCount int32 1351 }{ 1352 "valid job with batch labels in pod template": { 1353 job: &batch.Job{ 1354 ObjectMeta: validObjectMeta, 1355 Spec: batch.JobSpec{ 1356 Selector: defaultSelector, 1357 ManualSelector: pointer.Bool(false), 1358 Template: api.PodTemplateSpec{ 1359 ObjectMeta: metav1.ObjectMeta{ 1360 Labels: batchLabels, 1361 }, 1362 Spec: validPodSpec, 1363 }}, 1364 }, 1365 wantJob: &batch.Job{ 1366 ObjectMeta: validObjectMeta, 1367 Spec: batch.JobSpec{ 1368 Selector: defaultSelector, 1369 ManualSelector: pointer.Bool(false), 1370 Template: api.PodTemplateSpec{ 1371 ObjectMeta: metav1.ObjectMeta{ 1372 Labels: batchLabels, 1373 }, 1374 Spec: validPodSpec, 1375 }}, 1376 }, 1377 }, 1378 "valid job with batch and non-batch labels in pod template": { 1379 job: &batch.Job{ 1380 ObjectMeta: validObjectMeta, 1381 Spec: batch.JobSpec{ 1382 Selector: defaultSelector, 1383 ManualSelector: pointer.Bool(false), 1384 Template: api.PodTemplateSpec{ 1385 ObjectMeta: metav1.ObjectMeta{ 1386 Labels: labelsWithNonBatch, 1387 }, 1388 Spec: validPodSpec, 1389 }}, 1390 }, 1391 wantJob: &batch.Job{ 1392 ObjectMeta: validObjectMeta, 1393 Spec: batch.JobSpec{ 1394 Selector: defaultSelector, 1395 ManualSelector: pointer.Bool(false), 1396 Template: api.PodTemplateSpec{ 1397 ObjectMeta: metav1.ObjectMeta{ 1398 Labels: labelsWithNonBatch, 1399 }, 1400 Spec: validPodSpec, 1401 }}, 1402 }, 1403 }, 1404 "job with non-batch labels and without batch labels in pod template": { 1405 job: &batch.Job{ 1406 ObjectMeta: validObjectMeta, 1407 Spec: batch.JobSpec{ 1408 Selector: defaultSelector, 1409 ManualSelector: pointer.Bool(false), 1410 Template: api.PodTemplateSpec{ 1411 ObjectMeta: metav1.ObjectMeta{ 1412 Labels: map[string]string{}, 1413 }, 1414 Spec: validPodSpec, 1415 }}, 1416 }, 1417 wantJob: &batch.Job{ 1418 ObjectMeta: validObjectMeta, 1419 Spec: batch.JobSpec{ 1420 Selector: defaultSelector, 1421 ManualSelector: pointer.Bool(false), 1422 Template: api.PodTemplateSpec{ 1423 ObjectMeta: metav1.ObjectMeta{ 1424 Labels: map[string]string{}, 1425 }, 1426 Spec: validPodSpec, 1427 }}, 1428 }, 1429 wantWarningCount: 5, 1430 }, 1431 "no labels in job": { 1432 job: &batch.Job{ 1433 ObjectMeta: validObjectMeta, 1434 Spec: batch.JobSpec{ 1435 Selector: defaultSelector, 1436 Template: api.PodTemplateSpec{ 1437 Spec: validPodSpec, 1438 }}, 1439 }, 1440 wantJob: &batch.Job{ 1441 ObjectMeta: validObjectMeta, 1442 Spec: batch.JobSpec{ 1443 Selector: defaultSelector, 1444 Template: api.PodTemplateSpec{ 1445 Spec: validPodSpec, 1446 }}, 1447 }, 1448 wantWarningCount: 5, 1449 }, 1450 "manual selector; do not generate labels": { 1451 job: &batch.Job{ 1452 ObjectMeta: validObjectMeta, 1453 Spec: batch.JobSpec{ 1454 Selector: validSelector, 1455 Template: api.PodTemplateSpec{ 1456 ObjectMeta: metav1.ObjectMeta{ 1457 Labels: validSelector.MatchLabels, 1458 }, 1459 Spec: validPodSpec, 1460 }, 1461 Completions: pointer.Int32Ptr(2), 1462 ManualSelector: pointer.BoolPtr(true), 1463 }, 1464 }, 1465 wantJob: &batch.Job{ 1466 ObjectMeta: validObjectMeta, 1467 Spec: batch.JobSpec{ 1468 Selector: validSelector, 1469 Template: api.PodTemplateSpec{ 1470 ObjectMeta: metav1.ObjectMeta{ 1471 Labels: validSelector.MatchLabels, 1472 }, 1473 Spec: validPodSpec, 1474 }, 1475 Completions: pointer.Int32Ptr(2), 1476 ManualSelector: pointer.BoolPtr(true), 1477 }, 1478 }, 1479 }, 1480 "valid job with extended configuration": { 1481 job: &batch.Job{ 1482 ObjectMeta: validObjectMeta, 1483 Spec: batch.JobSpec{ 1484 Selector: defaultSelector, 1485 ManualSelector: pointer.Bool(false), 1486 Template: api.PodTemplateSpec{ 1487 ObjectMeta: metav1.ObjectMeta{ 1488 Labels: labelsWithNonBatch, 1489 }, 1490 Spec: validPodSpec, 1491 }, 1492 Completions: pointer.Int32Ptr(2), 1493 Suspend: pointer.BoolPtr(true), 1494 TTLSecondsAfterFinished: pointer.Int32Ptr(0), 1495 CompletionMode: completionModePtr(batch.IndexedCompletion), 1496 }, 1497 }, 1498 wantJob: &batch.Job{ 1499 ObjectMeta: validObjectMeta, 1500 Spec: batch.JobSpec{ 1501 Selector: defaultSelector, 1502 ManualSelector: pointer.Bool(false), 1503 Template: api.PodTemplateSpec{ 1504 ObjectMeta: metav1.ObjectMeta{ 1505 Labels: labelsWithNonBatch, 1506 }, 1507 Spec: validPodSpec, 1508 }, 1509 Completions: pointer.Int32Ptr(2), 1510 Suspend: pointer.BoolPtr(true), 1511 TTLSecondsAfterFinished: pointer.Int32Ptr(0), 1512 CompletionMode: completionModePtr(batch.IndexedCompletion), 1513 }, 1514 }, 1515 }, 1516 "fail validation due to invalid volume spec": { 1517 job: &batch.Job{ 1518 ObjectMeta: validObjectMeta, 1519 Spec: batch.JobSpec{ 1520 Selector: defaultSelector, 1521 ManualSelector: pointer.Bool(false), 1522 Template: api.PodTemplateSpec{ 1523 ObjectMeta: metav1.ObjectMeta{ 1524 Labels: labelsWithNonBatch, 1525 }, 1526 Spec: api.PodSpec{ 1527 RestartPolicy: api.RestartPolicyOnFailure, 1528 DNSPolicy: api.DNSClusterFirst, 1529 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1530 Volumes: []api.Volume{{Name: "volume-name"}}, 1531 }, 1532 }, 1533 }, 1534 }, 1535 wantJob: &batch.Job{ 1536 ObjectMeta: validObjectMeta, 1537 Spec: batch.JobSpec{ 1538 Selector: defaultSelector, 1539 ManualSelector: pointer.Bool(false), 1540 Template: api.PodTemplateSpec{ 1541 ObjectMeta: metav1.ObjectMeta{ 1542 Labels: labelsWithNonBatch, 1543 }, 1544 Spec: api.PodSpec{ 1545 RestartPolicy: api.RestartPolicyOnFailure, 1546 DNSPolicy: api.DNSClusterFirst, 1547 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1548 Volumes: []api.Volume{{Name: "volume-name"}}, 1549 }, 1550 }, 1551 }, 1552 }, 1553 wantWarningCount: 1, 1554 }, 1555 "FailIndex action; when JobBackoffLimitPerIndex is disabled - validation error": { 1556 enableJobPodFailurePolicy: true, 1557 enableJobBackoffLimitPerIndex: false, 1558 job: &batch.Job{ 1559 ObjectMeta: validObjectMeta, 1560 Spec: batch.JobSpec{ 1561 Selector: validSelector, 1562 ManualSelector: pointer.Bool(true), 1563 Template: api.PodTemplateSpec{ 1564 ObjectMeta: metav1.ObjectMeta{ 1565 Labels: validSelector.MatchLabels, 1566 }, 1567 Spec: validPodSpecNever, 1568 }, 1569 PodFailurePolicy: &batch.PodFailurePolicy{ 1570 Rules: []batch.PodFailurePolicyRule{ 1571 { 1572 Action: batch.PodFailurePolicyActionFailIndex, 1573 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1574 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1575 Values: []int32{1}, 1576 }, 1577 }, 1578 }, 1579 }, 1580 }, 1581 }, 1582 wantJob: &batch.Job{ 1583 ObjectMeta: validObjectMeta, 1584 Spec: batch.JobSpec{ 1585 Selector: validSelector, 1586 ManualSelector: pointer.Bool(true), 1587 Template: api.PodTemplateSpec{ 1588 ObjectMeta: metav1.ObjectMeta{ 1589 Labels: validSelector.MatchLabels, 1590 }, 1591 Spec: validPodSpecNever, 1592 }, 1593 PodFailurePolicy: &batch.PodFailurePolicy{ 1594 Rules: []batch.PodFailurePolicyRule{ 1595 { 1596 Action: batch.PodFailurePolicyActionFailIndex, 1597 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1598 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1599 Values: []int32{1}, 1600 }, 1601 }, 1602 }, 1603 }, 1604 }, 1605 }, 1606 wantWarningCount: 1, 1607 }, 1608 "FailIndex action; when JobBackoffLimitPerIndex is enabled, but not used - validation error": { 1609 enableJobPodFailurePolicy: true, 1610 enableJobBackoffLimitPerIndex: true, 1611 job: &batch.Job{ 1612 ObjectMeta: validObjectMeta, 1613 Spec: batch.JobSpec{ 1614 Selector: validSelector, 1615 ManualSelector: pointer.Bool(true), 1616 Template: api.PodTemplateSpec{ 1617 ObjectMeta: metav1.ObjectMeta{ 1618 Labels: validSelector.MatchLabels, 1619 }, 1620 Spec: validPodSpecNever, 1621 }, 1622 PodFailurePolicy: &batch.PodFailurePolicy{ 1623 Rules: []batch.PodFailurePolicyRule{ 1624 { 1625 Action: batch.PodFailurePolicyActionFailIndex, 1626 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1627 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1628 Values: []int32{1}, 1629 }, 1630 }, 1631 }, 1632 }, 1633 }, 1634 }, 1635 wantJob: &batch.Job{ 1636 ObjectMeta: validObjectMeta, 1637 Spec: batch.JobSpec{ 1638 Selector: validSelector, 1639 ManualSelector: pointer.Bool(true), 1640 Template: api.PodTemplateSpec{ 1641 ObjectMeta: metav1.ObjectMeta{ 1642 Labels: validSelector.MatchLabels, 1643 }, 1644 Spec: validPodSpecNever, 1645 }, 1646 PodFailurePolicy: &batch.PodFailurePolicy{ 1647 Rules: []batch.PodFailurePolicyRule{ 1648 { 1649 Action: batch.PodFailurePolicyActionFailIndex, 1650 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1651 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1652 Values: []int32{1}, 1653 }, 1654 }, 1655 }, 1656 }, 1657 }, 1658 }, 1659 wantWarningCount: 1, 1660 }, 1661 "FailIndex action; when JobBackoffLimitPerIndex is enabled and used - no error": { 1662 enableJobPodFailurePolicy: true, 1663 enableJobBackoffLimitPerIndex: true, 1664 job: &batch.Job{ 1665 ObjectMeta: validObjectMeta, 1666 Spec: batch.JobSpec{ 1667 CompletionMode: completionModePtr(batch.IndexedCompletion), 1668 Completions: pointer.Int32(2), 1669 BackoffLimitPerIndex: pointer.Int32(1), 1670 Selector: validSelector, 1671 ManualSelector: pointer.Bool(true), 1672 Template: api.PodTemplateSpec{ 1673 ObjectMeta: metav1.ObjectMeta{ 1674 Labels: validSelector.MatchLabels, 1675 }, 1676 Spec: validPodSpecNever, 1677 }, 1678 PodFailurePolicy: &batch.PodFailurePolicy{ 1679 Rules: []batch.PodFailurePolicyRule{ 1680 { 1681 Action: batch.PodFailurePolicyActionFailIndex, 1682 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1683 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1684 Values: []int32{1}, 1685 }, 1686 }, 1687 }, 1688 }, 1689 }, 1690 }, 1691 wantJob: &batch.Job{ 1692 ObjectMeta: validObjectMeta, 1693 Spec: batch.JobSpec{ 1694 CompletionMode: completionModePtr(batch.IndexedCompletion), 1695 Completions: pointer.Int32(2), 1696 BackoffLimitPerIndex: pointer.Int32(1), 1697 Selector: validSelector, 1698 ManualSelector: pointer.Bool(true), 1699 Template: api.PodTemplateSpec{ 1700 ObjectMeta: metav1.ObjectMeta{ 1701 Labels: validSelector.MatchLabels, 1702 }, 1703 Spec: validPodSpecNever, 1704 }, 1705 PodFailurePolicy: &batch.PodFailurePolicy{ 1706 Rules: []batch.PodFailurePolicyRule{ 1707 { 1708 Action: batch.PodFailurePolicyActionFailIndex, 1709 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 1710 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 1711 Values: []int32{1}, 1712 }, 1713 }, 1714 }, 1715 }, 1716 }, 1717 }, 1718 }, 1719 } 1720 for name, tc := range testcases { 1721 t.Run(name, func(t *testing.T) { 1722 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobPodFailurePolicy, tc.enableJobPodFailurePolicy)() 1723 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobBackoffLimitPerIndex, tc.enableJobBackoffLimitPerIndex)() 1724 errs := Strategy.Validate(ctx, tc.job) 1725 if len(errs) != int(tc.wantWarningCount) { 1726 t.Errorf("want warnings %d but got %d, errors: %v", tc.wantWarningCount, len(errs), errs) 1727 } 1728 if diff := cmp.Diff(tc.wantJob, tc.job); diff != "" { 1729 t.Errorf("Unexpected job (-want,+got):\n%s", diff) 1730 } 1731 }) 1732 } 1733 } 1734 1735 func TestStrategy_ResetFields(t *testing.T) { 1736 resetFields := Strategy.GetResetFields() 1737 if len(resetFields) != 1 { 1738 t.Errorf("ResetFields should have 1 element, but have %d", len(resetFields)) 1739 } 1740 } 1741 1742 func TestJobStatusStrategy_ResetFields(t *testing.T) { 1743 resetFields := StatusStrategy.GetResetFields() 1744 if len(resetFields) != 1 { 1745 t.Errorf("ResetFields should have 1 element, but have %d", len(resetFields)) 1746 } 1747 } 1748 1749 func TestStatusStrategy_PrepareForUpdate(t *testing.T) { 1750 ctx := genericapirequest.NewDefaultContext() 1751 validSelector := &metav1.LabelSelector{ 1752 MatchLabels: map[string]string{"a": "b"}, 1753 } 1754 validPodTemplateSpec := api.PodTemplateSpec{ 1755 ObjectMeta: metav1.ObjectMeta{ 1756 Labels: validSelector.MatchLabels, 1757 }, 1758 Spec: api.PodSpec{ 1759 RestartPolicy: api.RestartPolicyOnFailure, 1760 DNSPolicy: api.DNSClusterFirst, 1761 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1762 }, 1763 } 1764 validObjectMeta := metav1.ObjectMeta{ 1765 Name: "myjob", 1766 Namespace: metav1.NamespaceDefault, 1767 ResourceVersion: "10", 1768 } 1769 1770 cases := map[string]struct { 1771 job *batch.Job 1772 newJob *batch.Job 1773 wantJob *batch.Job 1774 }{ 1775 "job must allow status updates": { 1776 job: &batch.Job{ 1777 ObjectMeta: validObjectMeta, 1778 Spec: batch.JobSpec{ 1779 Selector: validSelector, 1780 Template: validPodTemplateSpec, 1781 Parallelism: pointer.Int32(4), 1782 }, 1783 Status: batch.JobStatus{ 1784 Active: 11, 1785 }, 1786 }, 1787 newJob: &batch.Job{ 1788 ObjectMeta: validObjectMeta, 1789 Spec: batch.JobSpec{ 1790 Selector: validSelector, 1791 Template: validPodTemplateSpec, 1792 Parallelism: pointer.Int32(4), 1793 }, 1794 Status: batch.JobStatus{ 1795 Active: 12, 1796 }, 1797 }, 1798 wantJob: &batch.Job{ 1799 ObjectMeta: validObjectMeta, 1800 Spec: batch.JobSpec{ 1801 Selector: validSelector, 1802 Template: validPodTemplateSpec, 1803 Parallelism: pointer.Int32(4), 1804 }, 1805 Status: batch.JobStatus{ 1806 Active: 12, 1807 }, 1808 }, 1809 }, 1810 "parallelism changes not allowed": { 1811 job: &batch.Job{ 1812 ObjectMeta: validObjectMeta, 1813 Spec: batch.JobSpec{ 1814 Selector: validSelector, 1815 Template: validPodTemplateSpec, 1816 Parallelism: pointer.Int32(3), 1817 }, 1818 }, 1819 newJob: &batch.Job{ 1820 ObjectMeta: validObjectMeta, 1821 Spec: batch.JobSpec{ 1822 Selector: validSelector, 1823 Template: validPodTemplateSpec, 1824 Parallelism: pointer.Int32(4), 1825 }, 1826 }, 1827 wantJob: &batch.Job{ 1828 ObjectMeta: validObjectMeta, 1829 Spec: batch.JobSpec{ 1830 Selector: validSelector, 1831 Template: validPodTemplateSpec, 1832 Parallelism: pointer.Int32(3), 1833 }, 1834 }, 1835 }, 1836 } 1837 for name, tc := range cases { 1838 t.Run(name, func(t *testing.T) { 1839 StatusStrategy.PrepareForUpdate(ctx, tc.newJob, tc.job) 1840 if diff := cmp.Diff(tc.wantJob, tc.newJob); diff != "" { 1841 t.Errorf("Unexpected job (-want,+got):\n%s", diff) 1842 } 1843 }) 1844 } 1845 } 1846 1847 func TestStatusStrategy_ValidateUpdate(t *testing.T) { 1848 ctx := genericapirequest.NewDefaultContext() 1849 validSelector := &metav1.LabelSelector{ 1850 MatchLabels: map[string]string{"a": "b"}, 1851 } 1852 validPodTemplateSpec := api.PodTemplateSpec{ 1853 ObjectMeta: metav1.ObjectMeta{ 1854 Labels: validSelector.MatchLabels, 1855 }, 1856 Spec: api.PodSpec{ 1857 RestartPolicy: api.RestartPolicyOnFailure, 1858 DNSPolicy: api.DNSClusterFirst, 1859 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1860 }, 1861 } 1862 1863 cases := map[string]struct { 1864 job *batch.Job 1865 newJob *batch.Job 1866 wantJob *batch.Job 1867 }{ 1868 "incoming resource version on update should not be mutated": { 1869 job: &batch.Job{ 1870 ObjectMeta: metav1.ObjectMeta{ 1871 Name: "myjob", 1872 Namespace: metav1.NamespaceDefault, 1873 ResourceVersion: "10", 1874 }, 1875 Spec: batch.JobSpec{ 1876 Selector: validSelector, 1877 Template: validPodTemplateSpec, 1878 Parallelism: pointer.Int32(4), 1879 }, 1880 }, 1881 newJob: &batch.Job{ 1882 ObjectMeta: metav1.ObjectMeta{ 1883 Name: "myjob", 1884 Namespace: metav1.NamespaceDefault, 1885 ResourceVersion: "9", 1886 }, 1887 Spec: batch.JobSpec{ 1888 Selector: validSelector, 1889 Template: validPodTemplateSpec, 1890 Parallelism: pointer.Int32(4), 1891 }, 1892 }, 1893 wantJob: &batch.Job{ 1894 ObjectMeta: metav1.ObjectMeta{ 1895 Name: "myjob", 1896 Namespace: metav1.NamespaceDefault, 1897 ResourceVersion: "9", 1898 }, 1899 Spec: batch.JobSpec{ 1900 Selector: validSelector, 1901 Template: validPodTemplateSpec, 1902 Parallelism: pointer.Int32(4), 1903 }, 1904 }, 1905 }, 1906 } 1907 for name, tc := range cases { 1908 t.Run(name, func(t *testing.T) { 1909 errs := StatusStrategy.ValidateUpdate(ctx, tc.newJob, tc.job) 1910 if len(errs) != 0 { 1911 t.Errorf("Unexpected error %v", errs) 1912 } 1913 if diff := cmp.Diff(tc.wantJob, tc.newJob); diff != "" { 1914 t.Errorf("Unexpected job (-want,+got):\n%s", diff) 1915 } 1916 }) 1917 } 1918 } 1919 1920 func TestJobStrategy_GetAttrs(t *testing.T) { 1921 validSelector := &metav1.LabelSelector{ 1922 MatchLabels: map[string]string{"a": "b"}, 1923 } 1924 validPodTemplateSpec := api.PodTemplateSpec{ 1925 ObjectMeta: metav1.ObjectMeta{ 1926 Labels: validSelector.MatchLabels, 1927 }, 1928 Spec: api.PodSpec{ 1929 RestartPolicy: api.RestartPolicyOnFailure, 1930 DNSPolicy: api.DNSClusterFirst, 1931 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1932 }, 1933 } 1934 1935 cases := map[string]struct { 1936 job *batch.Job 1937 wantErr string 1938 nonJobObject *api.Pod 1939 }{ 1940 "valid job with no labels": { 1941 job: &batch.Job{ 1942 ObjectMeta: metav1.ObjectMeta{ 1943 Name: "myjob", 1944 Namespace: metav1.NamespaceDefault, 1945 ResourceVersion: "0", 1946 }, 1947 Spec: batch.JobSpec{ 1948 Selector: validSelector, 1949 Template: validPodTemplateSpec, 1950 ManualSelector: pointer.BoolPtr(true), 1951 Parallelism: pointer.Int32Ptr(1), 1952 }, 1953 }, 1954 }, 1955 "valid job with a label": { 1956 job: &batch.Job{ 1957 ObjectMeta: metav1.ObjectMeta{ 1958 Name: "myjob", 1959 Namespace: metav1.NamespaceDefault, 1960 ResourceVersion: "0", 1961 Labels: map[string]string{"a": "b"}, 1962 }, 1963 Spec: batch.JobSpec{ 1964 Selector: validSelector, 1965 Template: validPodTemplateSpec, 1966 ManualSelector: pointer.BoolPtr(true), 1967 Parallelism: pointer.Int32Ptr(1), 1968 }, 1969 }, 1970 }, 1971 "pod instead": { 1972 job: nil, 1973 nonJobObject: &api.Pod{}, 1974 wantErr: "given object is not a job.", 1975 }, 1976 } 1977 for name, tc := range cases { 1978 t.Run(name, func(t *testing.T) { 1979 if tc.job == nil { 1980 _, _, err := GetAttrs(tc.nonJobObject) 1981 if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { 1982 t.Errorf("Unexpected errors (-want,+got):\n%s", diff) 1983 } 1984 } else { 1985 gotLabels, _, err := GetAttrs(tc.job) 1986 if err != nil { 1987 t.Errorf("Error %s supposed to be nil", err.Error()) 1988 } 1989 if diff := cmp.Diff(labels.Set(tc.job.ObjectMeta.Labels), gotLabels); diff != "" { 1990 t.Errorf("Unexpected attrs (-want,+got):\n%s", diff) 1991 } 1992 } 1993 }) 1994 } 1995 } 1996 1997 func TestJobToSelectiableFields(t *testing.T) { 1998 apitesting.TestSelectableFieldLabelConversionsOfKind(t, 1999 "batch/v1", 2000 "Job", 2001 JobToSelectableFields(&batch.Job{}), 2002 nil, 2003 ) 2004 } 2005 2006 func completionModePtr(m batch.CompletionMode) *batch.CompletionMode { 2007 return &m 2008 } 2009 2010 func podReplacementPolicy(m batch.PodReplacementPolicy) *batch.PodReplacementPolicy { 2011 return &m 2012 } 2013 2014 func getValidObjectMeta(generation int64) metav1.ObjectMeta { 2015 return getValidObjectMetaWithAnnotations(generation, nil) 2016 } 2017 2018 func getValidUID() types.UID { 2019 return "1a2b3c4d5e6f7g8h9i0k" 2020 } 2021 2022 func getValidObjectMetaWithAnnotations(generation int64, annotations map[string]string) metav1.ObjectMeta { 2023 return metav1.ObjectMeta{ 2024 Name: "myjob", 2025 UID: getValidUID(), 2026 Namespace: metav1.NamespaceDefault, 2027 Generation: generation, 2028 Annotations: annotations, 2029 } 2030 } 2031 2032 func getValidLabelSelector() *metav1.LabelSelector { 2033 return &metav1.LabelSelector{ 2034 MatchLabels: map[string]string{"a": "b"}, 2035 } 2036 } 2037 2038 func getValidBatchLabels() map[string]string { 2039 theUID := getValidUID() 2040 return map[string]string{batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: string(theUID), batch.ControllerUidLabel: string(theUID)} 2041 } 2042 2043 func getValidBatchLabelsWithNonBatch() map[string]string { 2044 theUID := getValidUID() 2045 return map[string]string{"a": "b", batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: string(theUID), batch.ControllerUidLabel: string(theUID)} 2046 } 2047 2048 func getValidPodTemplateSpecForSelector(validSelector *metav1.LabelSelector) api.PodTemplateSpec { 2049 return api.PodTemplateSpec{ 2050 ObjectMeta: metav1.ObjectMeta{ 2051 Labels: validSelector.MatchLabels, 2052 }, 2053 Spec: api.PodSpec{ 2054 RestartPolicy: api.RestartPolicyOnFailure, 2055 DNSPolicy: api.DNSClusterFirst, 2056 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 2057 }, 2058 } 2059 }