k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/batch/validation/validation_test.go (about) 1 /* 2 Copyright 2016 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 validation 18 19 import ( 20 "errors" 21 _ "time/tzdata" 22 23 "fmt" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "github.com/google/go-cmp/cmp/cmpopts" 29 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/types" 32 "k8s.io/apimachinery/pkg/util/validation/field" 33 "k8s.io/kubernetes/pkg/apis/batch" 34 api "k8s.io/kubernetes/pkg/apis/core" 35 corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" 36 "k8s.io/utils/pointer" 37 "k8s.io/utils/ptr" 38 ) 39 40 var ( 41 timeZoneEmpty = "" 42 timeZoneLocal = "LOCAL" 43 timeZoneUTC = "UTC" 44 timeZoneCorrect = "Europe/Rome" 45 timeZoneBadPrefix = " Europe/Rome" 46 timeZoneBadSuffix = "Europe/Rome " 47 timeZoneBadName = "Europe/InvalidRome" 48 timeZoneEmptySpace = " " 49 ) 50 51 var ignoreErrValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") 52 53 func getValidManualSelector() *metav1.LabelSelector { 54 return &metav1.LabelSelector{ 55 MatchLabels: map[string]string{"a": "b"}, 56 } 57 } 58 59 func getValidPodTemplateSpecForManual(selector *metav1.LabelSelector) api.PodTemplateSpec { 60 return api.PodTemplateSpec{ 61 ObjectMeta: metav1.ObjectMeta{ 62 Labels: selector.MatchLabels, 63 }, 64 Spec: api.PodSpec{ 65 RestartPolicy: api.RestartPolicyOnFailure, 66 DNSPolicy: api.DNSClusterFirst, 67 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 68 }, 69 } 70 } 71 72 func getValidGeneratedSelector() *metav1.LabelSelector { 73 return &metav1.LabelSelector{ 74 MatchLabels: map[string]string{batch.ControllerUidLabel: "1a2b3c", batch.LegacyControllerUidLabel: "1a2b3c", batch.JobNameLabel: "myjob", batch.LegacyJobNameLabel: "myjob"}, 75 } 76 } 77 78 func getValidPodTemplateSpecForGenerated(selector *metav1.LabelSelector) api.PodTemplateSpec { 79 return api.PodTemplateSpec{ 80 ObjectMeta: metav1.ObjectMeta{ 81 Labels: selector.MatchLabels, 82 }, 83 Spec: api.PodSpec{ 84 RestartPolicy: api.RestartPolicyOnFailure, 85 DNSPolicy: api.DNSClusterFirst, 86 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 87 InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 88 }, 89 } 90 } 91 92 func TestValidateJob(t *testing.T) { 93 validJobObjectMeta := metav1.ObjectMeta{ 94 Name: "myjob", 95 Namespace: metav1.NamespaceDefault, 96 UID: types.UID("1a2b3c"), 97 } 98 validManualSelector := getValidManualSelector() 99 failedPodReplacement := batch.Failed 100 terminatingOrFailedPodReplacement := batch.TerminatingOrFailed 101 validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector) 102 validGeneratedSelector := getValidGeneratedSelector() 103 validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) 104 validPodTemplateSpecForGeneratedRestartPolicyNever := getValidPodTemplateSpecForGenerated(validGeneratedSelector) 105 validPodTemplateSpecForGeneratedRestartPolicyNever.Spec.RestartPolicy = api.RestartPolicyNever 106 validHostNetPodTemplateSpec := func() api.PodTemplateSpec { 107 spec := getValidPodTemplateSpecForGenerated(validGeneratedSelector) 108 spec.Spec.SecurityContext = &api.PodSecurityContext{ 109 HostNetwork: true, 110 } 111 spec.Spec.Containers[0].Ports = []api.ContainerPort{{ 112 ContainerPort: 12345, 113 Protocol: api.ProtocolTCP, 114 }} 115 return spec 116 }() 117 118 successCases := map[string]struct { 119 opts JobValidationOptions 120 job batch.Job 121 }{ 122 "valid success policy": { 123 opts: JobValidationOptions{RequirePrefixedLabels: true}, 124 job: batch.Job{ 125 ObjectMeta: validJobObjectMeta, 126 Spec: batch.JobSpec{ 127 Selector: validGeneratedSelector, 128 CompletionMode: completionModePtr(batch.IndexedCompletion), 129 Completions: ptr.To[int32](10), 130 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 131 SuccessPolicy: &batch.SuccessPolicy{ 132 Rules: []batch.SuccessPolicyRule{ 133 { 134 SucceededCount: ptr.To[int32](1), 135 SucceededIndexes: ptr.To("0,2,4"), 136 }, 137 { 138 SucceededIndexes: ptr.To("1,3,5-9"), 139 }, 140 }, 141 }, 142 }, 143 }, 144 }, 145 "valid pod failure policy": { 146 opts: JobValidationOptions{RequirePrefixedLabels: true}, 147 job: batch.Job{ 148 ObjectMeta: validJobObjectMeta, 149 Spec: batch.JobSpec{ 150 Selector: validGeneratedSelector, 151 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 152 PodFailurePolicy: &batch.PodFailurePolicy{ 153 Rules: []batch.PodFailurePolicyRule{{ 154 Action: batch.PodFailurePolicyActionIgnore, 155 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 156 Type: api.DisruptionTarget, 157 Status: api.ConditionTrue, 158 }}, 159 }, { 160 Action: batch.PodFailurePolicyActionFailJob, 161 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 162 Type: api.PodConditionType("CustomConditionType"), 163 Status: api.ConditionFalse, 164 }}, 165 }, { 166 Action: batch.PodFailurePolicyActionCount, 167 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 168 ContainerName: pointer.String("abc"), 169 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 170 Values: []int32{1, 2, 3}, 171 }, 172 }, { 173 Action: batch.PodFailurePolicyActionIgnore, 174 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 175 ContainerName: pointer.String("def"), 176 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 177 Values: []int32{4}, 178 }, 179 }, { 180 Action: batch.PodFailurePolicyActionFailJob, 181 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 182 Operator: batch.PodFailurePolicyOnExitCodesOpNotIn, 183 Values: []int32{5, 6, 7}, 184 }, 185 }}, 186 }, 187 }, 188 }, 189 }, 190 "valid pod failure policy with FailIndex": { 191 job: batch.Job{ 192 ObjectMeta: validJobObjectMeta, 193 Spec: batch.JobSpec{ 194 CompletionMode: completionModePtr(batch.IndexedCompletion), 195 Completions: pointer.Int32(2), 196 BackoffLimitPerIndex: pointer.Int32(1), 197 Selector: validGeneratedSelector, 198 ManualSelector: pointer.Bool(true), 199 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 200 PodFailurePolicy: &batch.PodFailurePolicy{ 201 Rules: []batch.PodFailurePolicyRule{{ 202 Action: batch.PodFailurePolicyActionFailIndex, 203 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 204 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 205 Values: []int32{10}, 206 }, 207 }}, 208 }, 209 }, 210 }, 211 }, 212 "valid manual selector": { 213 opts: JobValidationOptions{RequirePrefixedLabels: true}, 214 job: batch.Job{ 215 ObjectMeta: metav1.ObjectMeta{ 216 Name: "myjob", 217 Namespace: metav1.NamespaceDefault, 218 UID: types.UID("1a2b3c"), 219 Annotations: map[string]string{"foo": "bar"}, 220 }, 221 Spec: batch.JobSpec{ 222 Selector: validManualSelector, 223 ManualSelector: pointer.Bool(true), 224 Template: validPodTemplateSpecForManual, 225 }, 226 }, 227 }, 228 "valid generated selector": { 229 opts: JobValidationOptions{RequirePrefixedLabels: true}, 230 job: batch.Job{ 231 ObjectMeta: metav1.ObjectMeta{ 232 Name: "myjob", 233 Namespace: metav1.NamespaceDefault, 234 UID: types.UID("1a2b3c"), 235 }, 236 Spec: batch.JobSpec{ 237 Selector: validGeneratedSelector, 238 Template: validPodTemplateSpecForGenerated, 239 }, 240 }, 241 }, 242 "valid pod replacement": { 243 opts: JobValidationOptions{RequirePrefixedLabels: true}, 244 job: batch.Job{ 245 ObjectMeta: metav1.ObjectMeta{ 246 Name: "myjob", 247 Namespace: metav1.NamespaceDefault, 248 UID: types.UID("1a2b3c"), 249 }, 250 Spec: batch.JobSpec{ 251 Selector: validGeneratedSelector, 252 Template: validPodTemplateSpecForGenerated, 253 PodReplacementPolicy: &terminatingOrFailedPodReplacement, 254 }, 255 }, 256 }, 257 "valid pod replacement with failed": { 258 opts: JobValidationOptions{RequirePrefixedLabels: true}, 259 job: batch.Job{ 260 ObjectMeta: metav1.ObjectMeta{ 261 Name: "myjob", 262 Namespace: metav1.NamespaceDefault, 263 UID: types.UID("1a2b3c"), 264 }, 265 Spec: batch.JobSpec{ 266 Selector: validGeneratedSelector, 267 Template: validPodTemplateSpecForGenerated, 268 PodReplacementPolicy: &failedPodReplacement, 269 }, 270 }, 271 }, 272 "valid hostnet": { 273 opts: JobValidationOptions{RequirePrefixedLabels: true}, 274 job: batch.Job{ 275 ObjectMeta: metav1.ObjectMeta{ 276 Name: "myjob", 277 Namespace: metav1.NamespaceDefault, 278 UID: types.UID("1a2b3c"), 279 }, 280 Spec: batch.JobSpec{ 281 Selector: validGeneratedSelector, 282 Template: validHostNetPodTemplateSpec, 283 }, 284 }, 285 }, 286 "valid NonIndexed completion mode": { 287 opts: JobValidationOptions{RequirePrefixedLabels: true}, 288 job: batch.Job{ 289 ObjectMeta: metav1.ObjectMeta{ 290 Name: "myjob", 291 Namespace: metav1.NamespaceDefault, 292 UID: types.UID("1a2b3c"), 293 }, 294 Spec: batch.JobSpec{ 295 Selector: validGeneratedSelector, 296 Template: validPodTemplateSpecForGenerated, 297 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 298 }, 299 }, 300 }, 301 "valid Indexed completion mode": { 302 opts: JobValidationOptions{RequirePrefixedLabels: true}, 303 job: batch.Job{ 304 ObjectMeta: metav1.ObjectMeta{ 305 Name: "myjob", 306 Namespace: metav1.NamespaceDefault, 307 UID: types.UID("1a2b3c"), 308 }, 309 Spec: batch.JobSpec{ 310 Selector: validGeneratedSelector, 311 Template: validPodTemplateSpecForGenerated, 312 CompletionMode: completionModePtr(batch.IndexedCompletion), 313 Completions: pointer.Int32(2), 314 Parallelism: pointer.Int32(100000), 315 }, 316 }, 317 }, 318 "valid parallelism and maxFailedIndexes for high completions when backoffLimitPerIndex is used": { 319 job: batch.Job{ 320 ObjectMeta: validJobObjectMeta, 321 Spec: batch.JobSpec{ 322 Completions: pointer.Int32(100_000), 323 Parallelism: pointer.Int32(100_000), 324 MaxFailedIndexes: pointer.Int32(100_000), 325 BackoffLimitPerIndex: pointer.Int32(1), 326 CompletionMode: completionModePtr(batch.IndexedCompletion), 327 Selector: validGeneratedSelector, 328 Template: validPodTemplateSpecForGenerated, 329 }, 330 }, 331 opts: JobValidationOptions{RequirePrefixedLabels: true}, 332 }, 333 "valid parallelism and maxFailedIndexes for unlimited completions when backoffLimitPerIndex is used": { 334 job: batch.Job{ 335 ObjectMeta: validJobObjectMeta, 336 Spec: batch.JobSpec{ 337 Completions: pointer.Int32(1_000_000_000), 338 Parallelism: pointer.Int32(10_000), 339 MaxFailedIndexes: pointer.Int32(10_000), 340 BackoffLimitPerIndex: pointer.Int32(1), 341 CompletionMode: completionModePtr(batch.IndexedCompletion), 342 Selector: validGeneratedSelector, 343 Template: validPodTemplateSpecForGenerated, 344 }, 345 }, 346 opts: JobValidationOptions{RequirePrefixedLabels: true}, 347 }, 348 "valid job tracking annotation": { 349 opts: JobValidationOptions{ 350 RequirePrefixedLabels: true, 351 }, 352 job: batch.Job{ 353 ObjectMeta: metav1.ObjectMeta{ 354 Name: "myjob", 355 Namespace: metav1.NamespaceDefault, 356 UID: types.UID("1a2b3c"), 357 }, 358 Spec: batch.JobSpec{ 359 Selector: validGeneratedSelector, 360 Template: validPodTemplateSpecForGenerated, 361 }, 362 }, 363 }, 364 "valid batch labels": { 365 opts: JobValidationOptions{ 366 RequirePrefixedLabels: true, 367 }, 368 job: batch.Job{ 369 ObjectMeta: metav1.ObjectMeta{ 370 Name: "myjob", 371 Namespace: metav1.NamespaceDefault, 372 UID: types.UID("1a2b3c"), 373 }, 374 Spec: batch.JobSpec{ 375 Selector: validGeneratedSelector, 376 Template: validPodTemplateSpecForGenerated, 377 }, 378 }, 379 }, 380 "do not allow new batch labels": { 381 opts: JobValidationOptions{ 382 RequirePrefixedLabels: false, 383 }, 384 job: batch.Job{ 385 ObjectMeta: metav1.ObjectMeta{ 386 Name: "myjob", 387 Namespace: metav1.NamespaceDefault, 388 UID: types.UID("1a2b3c"), 389 }, 390 Spec: batch.JobSpec{ 391 Selector: &metav1.LabelSelector{ 392 MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c"}, 393 }, 394 Template: api.PodTemplateSpec{ 395 ObjectMeta: metav1.ObjectMeta{ 396 Labels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c", batch.LegacyJobNameLabel: "myjob"}, 397 }, 398 Spec: api.PodSpec{ 399 RestartPolicy: api.RestartPolicyOnFailure, 400 DNSPolicy: api.DNSClusterFirst, 401 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 402 InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 403 }, 404 }, 405 }, 406 }, 407 }, 408 "valid managedBy field": { 409 opts: JobValidationOptions{RequirePrefixedLabels: true}, 410 job: batch.Job{ 411 ObjectMeta: validJobObjectMeta, 412 Spec: batch.JobSpec{ 413 Selector: validGeneratedSelector, 414 Template: validPodTemplateSpecForGenerated, 415 ManagedBy: ptr.To("example.com/foo"), 416 }, 417 }, 418 }, 419 } 420 for k, v := range successCases { 421 t.Run(k, func(t *testing.T) { 422 if errs := ValidateJob(&v.job, v.opts); len(errs) != 0 { 423 t.Errorf("Got unexpected validation errors: %v", errs) 424 } 425 }) 426 } 427 negative := int32(-1) 428 negative64 := int64(-1) 429 errorCases := map[string]struct { 430 opts JobValidationOptions 431 job batch.Job 432 }{ 433 `spec.managedBy: Too long: may not be longer than 63`: { 434 opts: JobValidationOptions{RequirePrefixedLabels: true}, 435 job: batch.Job{ 436 ObjectMeta: validJobObjectMeta, 437 Spec: batch.JobSpec{ 438 Selector: validGeneratedSelector, 439 Template: validPodTemplateSpecForGenerated, 440 ManagedBy: ptr.To("example.com/" + strings.Repeat("x", 60)), 441 }, 442 }, 443 }, 444 `spec.managedBy: Invalid value: "invalid custom controller name": must be a domain-prefixed path (such as "acme.io/foo")`: { 445 opts: JobValidationOptions{RequirePrefixedLabels: true}, 446 job: batch.Job{ 447 ObjectMeta: validJobObjectMeta, 448 Spec: batch.JobSpec{ 449 Selector: validGeneratedSelector, 450 Template: validPodTemplateSpecForGenerated, 451 ManagedBy: ptr.To("invalid custom controller name"), 452 }, 453 }, 454 }, 455 `spec.successPolicy: Invalid value: batch.SuccessPolicy{Rules:[]batch.SuccessPolicyRule{}}: requires indexed completion mode`: { 456 job: batch.Job{ 457 ObjectMeta: validJobObjectMeta, 458 Spec: batch.JobSpec{ 459 Selector: validGeneratedSelector, 460 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 461 SuccessPolicy: &batch.SuccessPolicy{ 462 Rules: []batch.SuccessPolicyRule{}, 463 }, 464 }, 465 }, 466 opts: JobValidationOptions{RequirePrefixedLabels: true}, 467 }, 468 `spec.successPolicy.rules: Required value: at least one rules must be specified when the successPolicy is specified`: { 469 job: batch.Job{ 470 ObjectMeta: validJobObjectMeta, 471 Spec: batch.JobSpec{ 472 Selector: validGeneratedSelector, 473 CompletionMode: completionModePtr(batch.IndexedCompletion), 474 Completions: ptr.To[int32](5), 475 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 476 SuccessPolicy: &batch.SuccessPolicy{}, 477 }, 478 }, 479 opts: JobValidationOptions{RequirePrefixedLabels: true}, 480 }, 481 `spec.successPolicy.rules[0]: Required value: at least one of succeededCount or succeededIndexes must be specified`: { 482 job: batch.Job{ 483 ObjectMeta: validJobObjectMeta, 484 Spec: batch.JobSpec{ 485 Selector: validGeneratedSelector, 486 CompletionMode: completionModePtr(batch.IndexedCompletion), 487 Completions: ptr.To[int32](5), 488 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 489 SuccessPolicy: &batch.SuccessPolicy{ 490 Rules: []batch.SuccessPolicyRule{{ 491 SucceededCount: nil, 492 SucceededIndexes: nil, 493 }}, 494 }, 495 }, 496 }, 497 opts: JobValidationOptions{RequirePrefixedLabels: true}, 498 }, 499 `spec.successPolicy.rules[0].succeededIndexes: Invalid value: "invalid-format": error parsing succeededIndexes: cannot convert string to integer for index: "invalid"`: { 500 job: batch.Job{ 501 ObjectMeta: validJobObjectMeta, 502 Spec: batch.JobSpec{ 503 Selector: validGeneratedSelector, 504 CompletionMode: completionModePtr(batch.IndexedCompletion), 505 Completions: ptr.To[int32](5), 506 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 507 SuccessPolicy: &batch.SuccessPolicy{ 508 Rules: []batch.SuccessPolicyRule{{ 509 SucceededIndexes: ptr.To("invalid-format"), 510 }}, 511 }, 512 }, 513 }, 514 opts: JobValidationOptions{RequirePrefixedLabels: true}, 515 }, 516 `spec.successPolicy.rules[0].succeededIndexes: Too long: must have at most 65536 bytes`: { 517 job: batch.Job{ 518 ObjectMeta: validJobObjectMeta, 519 Spec: batch.JobSpec{ 520 Selector: validGeneratedSelector, 521 CompletionMode: completionModePtr(batch.IndexedCompletion), 522 Completions: ptr.To[int32](5), 523 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 524 SuccessPolicy: &batch.SuccessPolicy{ 525 Rules: []batch.SuccessPolicyRule{{ 526 SucceededIndexes: ptr.To(strings.Repeat("1", maxJobSuccessPolicySucceededIndexesLimit+1)), 527 }}, 528 }, 529 }, 530 }, 531 opts: JobValidationOptions{RequirePrefixedLabels: true}, 532 }, 533 `spec.successPolicy.rules[0].succeededCount: must be greater than or equal to 0`: { 534 job: batch.Job{ 535 ObjectMeta: validJobObjectMeta, 536 Spec: batch.JobSpec{ 537 Selector: validGeneratedSelector, 538 CompletionMode: completionModePtr(batch.IndexedCompletion), 539 Completions: ptr.To[int32](5), 540 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 541 SuccessPolicy: &batch.SuccessPolicy{ 542 Rules: []batch.SuccessPolicyRule{{ 543 SucceededCount: ptr.To[int32](-1), 544 }}, 545 }, 546 }, 547 }, 548 opts: JobValidationOptions{RequirePrefixedLabels: true}, 549 }, 550 `spec.successPolicy.rules[0].succeededCount: Invalid value: 6: must be less than or equal to 5 (the number of specified completions)`: { 551 job: batch.Job{ 552 ObjectMeta: validJobObjectMeta, 553 Spec: batch.JobSpec{ 554 Selector: validGeneratedSelector, 555 CompletionMode: completionModePtr(batch.IndexedCompletion), 556 Completions: ptr.To[int32](5), 557 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 558 SuccessPolicy: &batch.SuccessPolicy{ 559 Rules: []batch.SuccessPolicyRule{{ 560 SucceededCount: ptr.To[int32](6), 561 }}, 562 }, 563 }, 564 }, 565 opts: JobValidationOptions{RequirePrefixedLabels: true}, 566 }, 567 `spec.successPolicy.rules[0].succeededCount: Invalid value: 4: must be less than or equal to 3 (the number of indexes in the specified succeededIndexes field)`: { 568 job: batch.Job{ 569 ObjectMeta: validJobObjectMeta, 570 Spec: batch.JobSpec{ 571 Selector: validGeneratedSelector, 572 CompletionMode: completionModePtr(batch.IndexedCompletion), 573 Completions: ptr.To[int32](5), 574 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 575 SuccessPolicy: &batch.SuccessPolicy{ 576 Rules: []batch.SuccessPolicyRule{{ 577 SucceededCount: ptr.To[int32](4), 578 SucceededIndexes: ptr.To("0-2"), 579 }}, 580 }, 581 }, 582 }, 583 opts: JobValidationOptions{RequirePrefixedLabels: true}, 584 }, 585 `spec.successPolicy.rules: Too many: 21: must have at most 20 items`: { 586 job: batch.Job{ 587 ObjectMeta: validJobObjectMeta, 588 Spec: batch.JobSpec{ 589 Selector: validGeneratedSelector, 590 CompletionMode: completionModePtr(batch.IndexedCompletion), 591 Completions: ptr.To[int32](5), 592 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 593 SuccessPolicy: &batch.SuccessPolicy{ 594 Rules: func() []batch.SuccessPolicyRule { 595 var rules []batch.SuccessPolicyRule 596 for i := 0; i < 21; i++ { 597 rules = append(rules, batch.SuccessPolicyRule{ 598 SucceededCount: ptr.To[int32](5), 599 }) 600 } 601 return rules 602 }(), 603 }, 604 }, 605 }, 606 opts: JobValidationOptions{RequirePrefixedLabels: true}, 607 }, 608 `spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: { 609 job: batch.Job{ 610 ObjectMeta: validJobObjectMeta, 611 Spec: batch.JobSpec{ 612 Selector: validGeneratedSelector, 613 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 614 PodFailurePolicy: &batch.PodFailurePolicy{ 615 Rules: []batch.PodFailurePolicyRule{{ 616 Action: batch.PodFailurePolicyActionFailJob, 617 }}, 618 }, 619 }, 620 }, 621 opts: JobValidationOptions{RequirePrefixedLabels: true}, 622 }, 623 `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Duplicate value: 11`: { 624 job: batch.Job{ 625 ObjectMeta: validJobObjectMeta, 626 Spec: batch.JobSpec{ 627 Selector: validGeneratedSelector, 628 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 629 PodFailurePolicy: &batch.PodFailurePolicy{ 630 Rules: []batch.PodFailurePolicyRule{{ 631 Action: batch.PodFailurePolicyActionFailJob, 632 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 633 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 634 Values: []int32{11, 11}, 635 }, 636 }}, 637 }, 638 }, 639 }, 640 opts: JobValidationOptions{RequirePrefixedLabels: true}, 641 }, 642 `spec.podFailurePolicy.rules[0].onExitCodes.values: Too many: 256: must have at most 255 items`: { 643 job: batch.Job{ 644 ObjectMeta: validJobObjectMeta, 645 Spec: batch.JobSpec{ 646 Selector: validGeneratedSelector, 647 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 648 PodFailurePolicy: &batch.PodFailurePolicy{ 649 Rules: []batch.PodFailurePolicyRule{{ 650 Action: batch.PodFailurePolicyActionFailJob, 651 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 652 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 653 Values: func() (values []int32) { 654 tooManyValues := make([]int32, maxPodFailurePolicyOnExitCodesValues+1) 655 for i := range tooManyValues { 656 tooManyValues[i] = int32(i) 657 } 658 return tooManyValues 659 }(), 660 }, 661 }}, 662 }, 663 }, 664 }, 665 opts: JobValidationOptions{RequirePrefixedLabels: true}, 666 }, 667 `spec.podFailurePolicy.rules: Too many: 21: must have at most 20 items`: { 668 job: batch.Job{ 669 ObjectMeta: validJobObjectMeta, 670 Spec: batch.JobSpec{ 671 Selector: validGeneratedSelector, 672 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 673 PodFailurePolicy: &batch.PodFailurePolicy{ 674 Rules: func() []batch.PodFailurePolicyRule { 675 tooManyRules := make([]batch.PodFailurePolicyRule, maxPodFailurePolicyRules+1) 676 for i := range tooManyRules { 677 tooManyRules[i] = batch.PodFailurePolicyRule{ 678 Action: batch.PodFailurePolicyActionFailJob, 679 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 680 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 681 Values: []int32{int32(i + 1)}, 682 }, 683 } 684 } 685 return tooManyRules 686 }(), 687 }, 688 }, 689 }, 690 opts: JobValidationOptions{RequirePrefixedLabels: true}, 691 }, 692 `spec.podFailurePolicy.rules[0].onPodConditions: Too many: 21: must have at most 20 items`: { 693 job: batch.Job{ 694 ObjectMeta: validJobObjectMeta, 695 Spec: batch.JobSpec{ 696 Selector: validGeneratedSelector, 697 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 698 PodFailurePolicy: &batch.PodFailurePolicy{ 699 Rules: []batch.PodFailurePolicyRule{{ 700 Action: batch.PodFailurePolicyActionFailJob, 701 OnPodConditions: func() []batch.PodFailurePolicyOnPodConditionsPattern { 702 tooManyPatterns := make([]batch.PodFailurePolicyOnPodConditionsPattern, maxPodFailurePolicyOnPodConditionsPatterns+1) 703 for i := range tooManyPatterns { 704 tooManyPatterns[i] = batch.PodFailurePolicyOnPodConditionsPattern{ 705 Type: api.PodConditionType(fmt.Sprintf("CustomType_%d", i)), 706 Status: api.ConditionTrue, 707 } 708 } 709 return tooManyPatterns 710 }(), 711 }}, 712 }, 713 }, 714 }, 715 opts: JobValidationOptions{RequirePrefixedLabels: true}, 716 }, 717 `spec.podFailurePolicy.rules[0].onExitCodes.values[2]: Duplicate value: 13`: { 718 job: batch.Job{ 719 ObjectMeta: validJobObjectMeta, 720 Spec: batch.JobSpec{ 721 Selector: validGeneratedSelector, 722 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 723 PodFailurePolicy: &batch.PodFailurePolicy{ 724 Rules: []batch.PodFailurePolicyRule{{ 725 Action: batch.PodFailurePolicyActionFailJob, 726 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 727 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 728 Values: []int32{12, 13, 13, 13}, 729 }, 730 }}, 731 }, 732 }, 733 }, 734 opts: JobValidationOptions{RequirePrefixedLabels: true}, 735 }, 736 `spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{19, 11}: must be ordered`: { 737 job: batch.Job{ 738 ObjectMeta: validJobObjectMeta, 739 Spec: batch.JobSpec{ 740 Selector: validGeneratedSelector, 741 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 742 PodFailurePolicy: &batch.PodFailurePolicy{ 743 Rules: []batch.PodFailurePolicyRule{{ 744 Action: batch.PodFailurePolicyActionFailJob, 745 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 746 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 747 Values: []int32{19, 11}, 748 }, 749 }}, 750 }, 751 }, 752 }, 753 opts: JobValidationOptions{RequirePrefixedLabels: true}, 754 }, 755 `spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{}: at least one value is required`: { 756 job: batch.Job{ 757 ObjectMeta: validJobObjectMeta, 758 Spec: batch.JobSpec{ 759 Selector: validGeneratedSelector, 760 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 761 PodFailurePolicy: &batch.PodFailurePolicy{ 762 Rules: []batch.PodFailurePolicyRule{{ 763 Action: batch.PodFailurePolicyActionFailJob, 764 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 765 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 766 Values: []int32{}, 767 }, 768 }}, 769 }, 770 }, 771 }, 772 opts: JobValidationOptions{RequirePrefixedLabels: true}, 773 }, 774 `spec.podFailurePolicy.rules[0].action: Required value: valid values: ["Count" "FailIndex" "FailJob" "Ignore"]`: { 775 job: batch.Job{ 776 ObjectMeta: validJobObjectMeta, 777 Spec: batch.JobSpec{ 778 Selector: validGeneratedSelector, 779 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 780 PodFailurePolicy: &batch.PodFailurePolicy{ 781 Rules: []batch.PodFailurePolicyRule{{ 782 Action: "", 783 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 784 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 785 Values: []int32{1, 2, 3}, 786 }, 787 }}, 788 }, 789 }, 790 }, 791 opts: JobValidationOptions{RequirePrefixedLabels: true}, 792 }, 793 `spec.podFailurePolicy.rules[0].onExitCodes.operator: Required value: valid values: ["In" "NotIn"]`: { 794 job: batch.Job{ 795 ObjectMeta: validJobObjectMeta, 796 Spec: batch.JobSpec{ 797 Selector: validGeneratedSelector, 798 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 799 PodFailurePolicy: &batch.PodFailurePolicy{ 800 Rules: []batch.PodFailurePolicyRule{{ 801 Action: batch.PodFailurePolicyActionFailJob, 802 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 803 Operator: "", 804 Values: []int32{1, 2, 3}, 805 }, 806 }}, 807 }, 808 }, 809 }, 810 opts: JobValidationOptions{RequirePrefixedLabels: true}, 811 }, 812 `spec.podFailurePolicy.rules[0]: Invalid value: specifying both OnExitCodes and OnPodConditions is not supported`: { 813 job: batch.Job{ 814 ObjectMeta: validJobObjectMeta, 815 Spec: batch.JobSpec{ 816 Selector: validGeneratedSelector, 817 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 818 PodFailurePolicy: &batch.PodFailurePolicy{ 819 Rules: []batch.PodFailurePolicyRule{{ 820 Action: batch.PodFailurePolicyActionFailJob, 821 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 822 ContainerName: pointer.String("abc"), 823 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 824 Values: []int32{1, 2, 3}, 825 }, 826 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 827 Type: api.DisruptionTarget, 828 Status: api.ConditionTrue, 829 }}, 830 }}, 831 }, 832 }, 833 }, 834 opts: JobValidationOptions{RequirePrefixedLabels: true}, 835 }, 836 `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Invalid value: 0: must not be 0 for the In operator`: { 837 job: batch.Job{ 838 ObjectMeta: validJobObjectMeta, 839 Spec: batch.JobSpec{ 840 Selector: validGeneratedSelector, 841 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 842 PodFailurePolicy: &batch.PodFailurePolicy{ 843 Rules: []batch.PodFailurePolicyRule{{ 844 Action: batch.PodFailurePolicyActionIgnore, 845 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 846 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 847 Values: []int32{1, 0, 2}, 848 }, 849 }}, 850 }, 851 }, 852 }, 853 opts: JobValidationOptions{RequirePrefixedLabels: true}, 854 }, 855 `spec.podFailurePolicy.rules[1].onExitCodes.containerName: Invalid value: "xyz": must be one of the container or initContainer names in the pod template`: { 856 job: batch.Job{ 857 ObjectMeta: validJobObjectMeta, 858 Spec: batch.JobSpec{ 859 Selector: validGeneratedSelector, 860 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 861 PodFailurePolicy: &batch.PodFailurePolicy{ 862 Rules: []batch.PodFailurePolicyRule{{ 863 Action: batch.PodFailurePolicyActionIgnore, 864 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 865 ContainerName: pointer.String("abc"), 866 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 867 Values: []int32{1, 2, 3}, 868 }, 869 }, { 870 Action: batch.PodFailurePolicyActionFailJob, 871 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 872 ContainerName: pointer.String("xyz"), 873 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 874 Values: []int32{5, 6, 7}, 875 }, 876 }}, 877 }, 878 }, 879 }, 880 opts: JobValidationOptions{RequirePrefixedLabels: true}, 881 }, 882 `spec.podFailurePolicy.rules[0].action: Unsupported value: "UnknownAction": supported values: "Count", "FailIndex", "FailJob", "Ignore"`: { 883 job: batch.Job{ 884 ObjectMeta: validJobObjectMeta, 885 Spec: batch.JobSpec{ 886 Selector: validGeneratedSelector, 887 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 888 PodFailurePolicy: &batch.PodFailurePolicy{ 889 Rules: []batch.PodFailurePolicyRule{{ 890 Action: "UnknownAction", 891 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 892 ContainerName: pointer.String("abc"), 893 Operator: batch.PodFailurePolicyOnExitCodesOpIn, 894 Values: []int32{1, 2, 3}, 895 }, 896 }}, 897 }, 898 }, 899 }, 900 opts: JobValidationOptions{RequirePrefixedLabels: true}, 901 }, 902 `spec.podFailurePolicy.rules[0].onExitCodes.operator: Unsupported value: "UnknownOperator": supported values: "In", "NotIn"`: { 903 job: batch.Job{ 904 ObjectMeta: validJobObjectMeta, 905 Spec: batch.JobSpec{ 906 Selector: validGeneratedSelector, 907 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 908 PodFailurePolicy: &batch.PodFailurePolicy{ 909 Rules: []batch.PodFailurePolicyRule{{ 910 Action: batch.PodFailurePolicyActionIgnore, 911 OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ 912 Operator: "UnknownOperator", 913 Values: []int32{1, 2, 3}, 914 }, 915 }}, 916 }, 917 }, 918 }, 919 opts: JobValidationOptions{RequirePrefixedLabels: true}, 920 }, 921 `spec.podFailurePolicy.rules[0].onPodConditions[0].status: Required value: valid values: ["False" "True" "Unknown"]`: { 922 job: batch.Job{ 923 ObjectMeta: validJobObjectMeta, 924 Spec: batch.JobSpec{ 925 Selector: validGeneratedSelector, 926 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 927 PodFailurePolicy: &batch.PodFailurePolicy{ 928 Rules: []batch.PodFailurePolicyRule{{ 929 Action: batch.PodFailurePolicyActionIgnore, 930 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 931 Type: api.DisruptionTarget, 932 }}, 933 }}, 934 }, 935 }, 936 }, 937 opts: JobValidationOptions{RequirePrefixedLabels: true}, 938 }, 939 `spec.podFailurePolicy.rules[0].onPodConditions[0].status: Unsupported value: "UnknownStatus": supported values: "False", "True", "Unknown"`: { 940 job: batch.Job{ 941 ObjectMeta: validJobObjectMeta, 942 Spec: batch.JobSpec{ 943 Selector: validGeneratedSelector, 944 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 945 PodFailurePolicy: &batch.PodFailurePolicy{ 946 Rules: []batch.PodFailurePolicyRule{{ 947 Action: batch.PodFailurePolicyActionIgnore, 948 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 949 Type: api.DisruptionTarget, 950 Status: "UnknownStatus", 951 }}, 952 }}, 953 }, 954 }, 955 }, 956 opts: JobValidationOptions{RequirePrefixedLabels: true}, 957 }, 958 `spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "": name part must be non-empty`: { 959 job: batch.Job{ 960 ObjectMeta: validJobObjectMeta, 961 Spec: batch.JobSpec{ 962 Selector: validGeneratedSelector, 963 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 964 PodFailurePolicy: &batch.PodFailurePolicy{ 965 Rules: []batch.PodFailurePolicyRule{{ 966 Action: batch.PodFailurePolicyActionIgnore, 967 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 968 Status: api.ConditionTrue, 969 }}, 970 }}, 971 }, 972 }, 973 }, 974 opts: JobValidationOptions{RequirePrefixedLabels: true}, 975 }, 976 `spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "Invalid Condition Type": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`: { 977 job: batch.Job{ 978 ObjectMeta: validJobObjectMeta, 979 Spec: batch.JobSpec{ 980 Selector: validGeneratedSelector, 981 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 982 PodFailurePolicy: &batch.PodFailurePolicy{ 983 Rules: []batch.PodFailurePolicyRule{{ 984 Action: batch.PodFailurePolicyActionIgnore, 985 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 986 Type: api.PodConditionType("Invalid Condition Type"), 987 Status: api.ConditionTrue, 988 }}, 989 }}, 990 }, 991 }, 992 }, 993 opts: JobValidationOptions{RequirePrefixedLabels: true}, 994 }, 995 `spec.podReplacementPolicy: Unsupported value: "TerminatingOrFailed": supported values: "Failed"`: { 996 job: batch.Job{ 997 ObjectMeta: validJobObjectMeta, 998 Spec: batch.JobSpec{ 999 Selector: validGeneratedSelector, 1000 PodReplacementPolicy: &terminatingOrFailedPodReplacement, 1001 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1002 PodFailurePolicy: &batch.PodFailurePolicy{ 1003 Rules: []batch.PodFailurePolicyRule{{ 1004 Action: batch.PodFailurePolicyActionIgnore, 1005 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 1006 Type: api.DisruptionTarget, 1007 Status: api.ConditionTrue, 1008 }}, 1009 }, 1010 }, 1011 }, 1012 }, 1013 }, 1014 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1015 }, 1016 `spec.podReplacementPolicy: Unsupported value: "": supported values: "Failed", "TerminatingOrFailed"`: { 1017 job: batch.Job{ 1018 ObjectMeta: validJobObjectMeta, 1019 Spec: batch.JobSpec{ 1020 PodReplacementPolicy: (*batch.PodReplacementPolicy)(pointer.String("")), 1021 Selector: validGeneratedSelector, 1022 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1023 }, 1024 }, 1025 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1026 }, 1027 `spec.template.spec.restartPolicy: Invalid value: "OnFailure": only "Never" is supported when podFailurePolicy is specified`: { 1028 job: batch.Job{ 1029 ObjectMeta: validJobObjectMeta, 1030 Spec: batch.JobSpec{ 1031 Selector: validGeneratedSelector, 1032 Template: api.PodTemplateSpec{ 1033 ObjectMeta: metav1.ObjectMeta{ 1034 Labels: validGeneratedSelector.MatchLabels, 1035 }, 1036 Spec: api.PodSpec{ 1037 RestartPolicy: api.RestartPolicyOnFailure, 1038 DNSPolicy: api.DNSClusterFirst, 1039 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1040 }, 1041 }, 1042 PodFailurePolicy: &batch.PodFailurePolicy{ 1043 Rules: []batch.PodFailurePolicyRule{}, 1044 }, 1045 }, 1046 }, 1047 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1048 }, 1049 "spec.parallelism:must be greater than or equal to 0": { 1050 job: batch.Job{ 1051 ObjectMeta: metav1.ObjectMeta{ 1052 Name: "myjob", 1053 Namespace: metav1.NamespaceDefault, 1054 UID: types.UID("1a2b3c"), 1055 }, 1056 Spec: batch.JobSpec{ 1057 Parallelism: &negative, 1058 Selector: validGeneratedSelector, 1059 Template: validPodTemplateSpecForGenerated, 1060 }, 1061 }, 1062 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1063 }, 1064 "spec.backoffLimit:must be greater than or equal to 0": { 1065 job: batch.Job{ 1066 ObjectMeta: metav1.ObjectMeta{ 1067 Name: "myjob", 1068 Namespace: metav1.NamespaceDefault, 1069 UID: types.UID("1a2b3c"), 1070 }, 1071 Spec: batch.JobSpec{ 1072 BackoffLimit: pointer.Int32(-1), 1073 Selector: validGeneratedSelector, 1074 Template: validPodTemplateSpecForGenerated, 1075 }, 1076 }, 1077 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1078 }, 1079 "spec.backoffLimitPerIndex: Invalid value: 1: requires indexed completion mode": { 1080 job: batch.Job{ 1081 ObjectMeta: validJobObjectMeta, 1082 Spec: batch.JobSpec{ 1083 BackoffLimitPerIndex: pointer.Int32(1), 1084 Selector: validGeneratedSelector, 1085 Template: validPodTemplateSpecForGenerated, 1086 }, 1087 }, 1088 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1089 }, 1090 "spec.backoffLimitPerIndex:must be greater than or equal to 0": { 1091 job: batch.Job{ 1092 ObjectMeta: validJobObjectMeta, 1093 Spec: batch.JobSpec{ 1094 BackoffLimitPerIndex: pointer.Int32(-1), 1095 CompletionMode: completionModePtr(batch.IndexedCompletion), 1096 Selector: validGeneratedSelector, 1097 Template: validPodTemplateSpecForGenerated, 1098 }, 1099 }, 1100 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1101 }, 1102 "spec.maxFailedIndexes: Invalid value: 11: must be less than or equal to completions": { 1103 job: batch.Job{ 1104 ObjectMeta: validJobObjectMeta, 1105 Spec: batch.JobSpec{ 1106 Completions: pointer.Int32(10), 1107 MaxFailedIndexes: pointer.Int32(11), 1108 BackoffLimitPerIndex: pointer.Int32(1), 1109 CompletionMode: completionModePtr(batch.IndexedCompletion), 1110 Selector: validGeneratedSelector, 1111 Template: validPodTemplateSpecForGenerated, 1112 }, 1113 }, 1114 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1115 }, 1116 "spec.maxFailedIndexes: Required value: must be specified when completions is above 100000": { 1117 job: batch.Job{ 1118 ObjectMeta: validJobObjectMeta, 1119 Spec: batch.JobSpec{ 1120 Completions: pointer.Int32(100_001), 1121 BackoffLimitPerIndex: pointer.Int32(1), 1122 CompletionMode: completionModePtr(batch.IndexedCompletion), 1123 Selector: validGeneratedSelector, 1124 Template: validPodTemplateSpecForGenerated, 1125 }, 1126 }, 1127 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1128 }, 1129 "spec.parallelism: Invalid value: 50000: must be less than or equal to 10000 when completions are above 100000 and used with backoff limit per index": { 1130 job: batch.Job{ 1131 ObjectMeta: validJobObjectMeta, 1132 Spec: batch.JobSpec{ 1133 Completions: pointer.Int32(100_001), 1134 Parallelism: pointer.Int32(50_000), 1135 BackoffLimitPerIndex: pointer.Int32(1), 1136 MaxFailedIndexes: pointer.Int32(1), 1137 CompletionMode: completionModePtr(batch.IndexedCompletion), 1138 Selector: validGeneratedSelector, 1139 Template: validPodTemplateSpecForGenerated, 1140 }, 1141 }, 1142 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1143 }, 1144 "spec.maxFailedIndexes: Invalid value: 100001: must be less than or equal to 100000": { 1145 job: batch.Job{ 1146 ObjectMeta: validJobObjectMeta, 1147 Spec: batch.JobSpec{ 1148 Completions: pointer.Int32(100_001), 1149 BackoffLimitPerIndex: pointer.Int32(1), 1150 MaxFailedIndexes: pointer.Int32(100_001), 1151 CompletionMode: completionModePtr(batch.IndexedCompletion), 1152 Selector: validGeneratedSelector, 1153 Template: validPodTemplateSpecForGenerated, 1154 }, 1155 }, 1156 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1157 }, 1158 "spec.maxFailedIndexes: Invalid value: 50000: must be less than or equal to 10000 when completions are above 100000 and used with backoff limit per index": { 1159 job: batch.Job{ 1160 ObjectMeta: validJobObjectMeta, 1161 Spec: batch.JobSpec{ 1162 Completions: pointer.Int32(100_001), 1163 BackoffLimitPerIndex: pointer.Int32(1), 1164 MaxFailedIndexes: pointer.Int32(50_000), 1165 CompletionMode: completionModePtr(batch.IndexedCompletion), 1166 Selector: validGeneratedSelector, 1167 Template: validPodTemplateSpecForGenerated, 1168 }, 1169 }, 1170 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1171 }, 1172 "spec.maxFailedIndexes:must be greater than or equal to 0": { 1173 job: batch.Job{ 1174 ObjectMeta: validJobObjectMeta, 1175 Spec: batch.JobSpec{ 1176 BackoffLimitPerIndex: pointer.Int32(1), 1177 MaxFailedIndexes: pointer.Int32(-1), 1178 CompletionMode: completionModePtr(batch.IndexedCompletion), 1179 Selector: validGeneratedSelector, 1180 Template: validPodTemplateSpecForGenerated, 1181 }, 1182 }, 1183 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1184 }, 1185 "spec.backoffLimitPerIndex: Required value: when maxFailedIndexes is specified": { 1186 job: batch.Job{ 1187 ObjectMeta: validJobObjectMeta, 1188 Spec: batch.JobSpec{ 1189 MaxFailedIndexes: pointer.Int32(1), 1190 CompletionMode: completionModePtr(batch.IndexedCompletion), 1191 Selector: validGeneratedSelector, 1192 Template: validPodTemplateSpecForGenerated, 1193 }, 1194 }, 1195 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1196 }, 1197 "spec.completions:must be greater than or equal to 0": { 1198 job: batch.Job{ 1199 ObjectMeta: metav1.ObjectMeta{ 1200 Name: "myjob", 1201 Namespace: metav1.NamespaceDefault, 1202 UID: types.UID("1a2b3c"), 1203 }, 1204 Spec: batch.JobSpec{ 1205 Completions: &negative, 1206 Selector: validGeneratedSelector, 1207 Template: validPodTemplateSpecForGenerated, 1208 }, 1209 }, 1210 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1211 }, 1212 "spec.activeDeadlineSeconds:must be greater than or equal to 0": { 1213 job: batch.Job{ 1214 ObjectMeta: metav1.ObjectMeta{ 1215 Name: "myjob", 1216 Namespace: metav1.NamespaceDefault, 1217 UID: types.UID("1a2b3c"), 1218 }, 1219 Spec: batch.JobSpec{ 1220 ActiveDeadlineSeconds: &negative64, 1221 Selector: validGeneratedSelector, 1222 Template: validPodTemplateSpecForGenerated, 1223 }, 1224 }, 1225 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1226 }, 1227 "spec.selector:Required value": { 1228 job: batch.Job{ 1229 ObjectMeta: metav1.ObjectMeta{ 1230 Name: "myjob", 1231 Namespace: metav1.NamespaceDefault, 1232 UID: types.UID("1a2b3c"), 1233 }, 1234 Spec: batch.JobSpec{ 1235 Template: validPodTemplateSpecForGenerated, 1236 }, 1237 }, 1238 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1239 }, 1240 "spec.template.metadata.labels: Invalid value: map[string]string{\"y\":\"z\"}: `selector` does not match template `labels`": { 1241 job: batch.Job{ 1242 ObjectMeta: metav1.ObjectMeta{ 1243 Name: "myjob", 1244 Namespace: metav1.NamespaceDefault, 1245 UID: types.UID("1a2b3c"), 1246 }, 1247 Spec: batch.JobSpec{ 1248 Selector: validManualSelector, 1249 ManualSelector: pointer.Bool(true), 1250 Template: api.PodTemplateSpec{ 1251 ObjectMeta: metav1.ObjectMeta{ 1252 Labels: map[string]string{"y": "z"}, 1253 }, 1254 Spec: api.PodSpec{ 1255 RestartPolicy: api.RestartPolicyOnFailure, 1256 DNSPolicy: api.DNSClusterFirst, 1257 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1258 }, 1259 }, 1260 }, 1261 }, 1262 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1263 }, 1264 "spec.template.metadata.labels: Invalid value: map[string]string{\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { 1265 job: batch.Job{ 1266 ObjectMeta: metav1.ObjectMeta{ 1267 Name: "myjob", 1268 Namespace: metav1.NamespaceDefault, 1269 UID: types.UID("1a2b3c"), 1270 }, 1271 Spec: batch.JobSpec{ 1272 Selector: validManualSelector, 1273 ManualSelector: pointer.Bool(true), 1274 Template: api.PodTemplateSpec{ 1275 ObjectMeta: metav1.ObjectMeta{ 1276 Labels: map[string]string{"controller-uid": "4d5e6f"}, 1277 }, 1278 Spec: api.PodSpec{ 1279 RestartPolicy: api.RestartPolicyOnFailure, 1280 DNSPolicy: api.DNSClusterFirst, 1281 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1282 }, 1283 }, 1284 }, 1285 }, 1286 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1287 }, 1288 "spec.template.spec.restartPolicy: Required value": { 1289 job: batch.Job{ 1290 ObjectMeta: metav1.ObjectMeta{ 1291 Name: "myjob", 1292 Namespace: metav1.NamespaceDefault, 1293 UID: types.UID("1a2b3c"), 1294 }, 1295 Spec: batch.JobSpec{ 1296 Selector: validManualSelector, 1297 ManualSelector: pointer.Bool(true), 1298 Template: api.PodTemplateSpec{ 1299 ObjectMeta: metav1.ObjectMeta{ 1300 Labels: validManualSelector.MatchLabels, 1301 }, 1302 Spec: api.PodSpec{ 1303 RestartPolicy: api.RestartPolicyAlways, 1304 DNSPolicy: api.DNSClusterFirst, 1305 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1306 }, 1307 }, 1308 }, 1309 }, 1310 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1311 }, 1312 "spec.template.spec.restartPolicy: Unsupported value": { 1313 job: batch.Job{ 1314 ObjectMeta: metav1.ObjectMeta{ 1315 Name: "myjob", 1316 Namespace: metav1.NamespaceDefault, 1317 UID: types.UID("1a2b3c"), 1318 }, 1319 Spec: batch.JobSpec{ 1320 Selector: validManualSelector, 1321 ManualSelector: pointer.Bool(true), 1322 Template: api.PodTemplateSpec{ 1323 ObjectMeta: metav1.ObjectMeta{ 1324 Labels: validManualSelector.MatchLabels, 1325 }, 1326 Spec: api.PodSpec{ 1327 RestartPolicy: "Invalid", 1328 DNSPolicy: api.DNSClusterFirst, 1329 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1330 }, 1331 }, 1332 }, 1333 }, 1334 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1335 }, 1336 "spec.ttlSecondsAfterFinished: must be greater than or equal to 0": { 1337 job: batch.Job{ 1338 ObjectMeta: metav1.ObjectMeta{ 1339 Name: "myjob", 1340 Namespace: metav1.NamespaceDefault, 1341 UID: types.UID("1a2b3c"), 1342 }, 1343 Spec: batch.JobSpec{ 1344 TTLSecondsAfterFinished: &negative, 1345 Selector: validGeneratedSelector, 1346 Template: validPodTemplateSpecForGenerated, 1347 }, 1348 }, 1349 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1350 }, 1351 "spec.completions: Required value: when completion mode is Indexed": { 1352 job: batch.Job{ 1353 ObjectMeta: metav1.ObjectMeta{ 1354 Name: "myjob", 1355 Namespace: metav1.NamespaceDefault, 1356 UID: types.UID("1a2b3c"), 1357 }, 1358 Spec: batch.JobSpec{ 1359 Selector: validGeneratedSelector, 1360 Template: validPodTemplateSpecForGenerated, 1361 CompletionMode: completionModePtr(batch.IndexedCompletion), 1362 }, 1363 }, 1364 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1365 }, 1366 "spec.parallelism: must be less than or equal to 100000 when completion mode is Indexed": { 1367 job: batch.Job{ 1368 ObjectMeta: metav1.ObjectMeta{ 1369 Name: "myjob", 1370 Namespace: metav1.NamespaceDefault, 1371 UID: types.UID("1a2b3c"), 1372 }, 1373 Spec: batch.JobSpec{ 1374 Selector: validGeneratedSelector, 1375 Template: validPodTemplateSpecForGenerated, 1376 CompletionMode: completionModePtr(batch.IndexedCompletion), 1377 Completions: pointer.Int32(2), 1378 Parallelism: pointer.Int32(100001), 1379 }, 1380 }, 1381 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1382 }, 1383 "spec.template.metadata.labels[controller-uid]: Required value: must be '1a2b3c'": { 1384 job: batch.Job{ 1385 ObjectMeta: metav1.ObjectMeta{ 1386 Name: "myjob", 1387 Namespace: metav1.NamespaceDefault, 1388 UID: types.UID("1a2b3c"), 1389 }, 1390 Spec: batch.JobSpec{ 1391 Selector: &metav1.LabelSelector{ 1392 MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c"}, 1393 }, 1394 Template: api.PodTemplateSpec{ 1395 ObjectMeta: metav1.ObjectMeta{ 1396 Labels: map[string]string{batch.LegacyJobNameLabel: "myjob"}, 1397 }, 1398 Spec: api.PodSpec{ 1399 RestartPolicy: api.RestartPolicyOnFailure, 1400 DNSPolicy: api.DNSClusterFirst, 1401 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1402 InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1403 }, 1404 }, 1405 }, 1406 }, 1407 opts: JobValidationOptions{}, 1408 }, 1409 "metadata.uid: Required value": { 1410 job: batch.Job{ 1411 ObjectMeta: metav1.ObjectMeta{ 1412 Name: "myjob", 1413 Namespace: metav1.NamespaceDefault, 1414 }, 1415 Spec: batch.JobSpec{ 1416 Selector: &metav1.LabelSelector{ 1417 MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "test"}, 1418 }, 1419 Template: api.PodTemplateSpec{ 1420 ObjectMeta: metav1.ObjectMeta{ 1421 Labels: map[string]string{batch.LegacyJobNameLabel: "myjob"}, 1422 }, 1423 Spec: api.PodSpec{ 1424 RestartPolicy: api.RestartPolicyOnFailure, 1425 DNSPolicy: api.DNSClusterFirst, 1426 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1427 InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1428 }, 1429 }, 1430 }, 1431 }, 1432 opts: JobValidationOptions{}, 1433 }, 1434 "spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{\"a\":\"b\"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: `selector` not auto-generated": { 1435 job: batch.Job{ 1436 ObjectMeta: metav1.ObjectMeta{ 1437 Name: "myjob", 1438 Namespace: metav1.NamespaceDefault, 1439 UID: types.UID("1a2b3c"), 1440 }, 1441 Spec: batch.JobSpec{ 1442 Selector: &metav1.LabelSelector{ 1443 MatchLabels: map[string]string{"a": "b"}, 1444 }, 1445 Template: validPodTemplateSpecForGenerated, 1446 }, 1447 }, 1448 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1449 }, 1450 "spec.template.metadata.labels[batch.kubernetes.io/controller-uid]: Required value: must be '1a2b3c'": { 1451 job: batch.Job{ 1452 ObjectMeta: metav1.ObjectMeta{ 1453 Name: "myjob", 1454 Namespace: metav1.NamespaceDefault, 1455 UID: types.UID("1a2b3c"), 1456 }, 1457 Spec: batch.JobSpec{ 1458 Selector: &metav1.LabelSelector{ 1459 MatchLabels: map[string]string{batch.ControllerUidLabel: "1a2b3c"}, 1460 }, 1461 Template: api.PodTemplateSpec{ 1462 ObjectMeta: metav1.ObjectMeta{ 1463 Labels: map[string]string{batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: "1a2b3c", batch.LegacyJobNameLabel: "myjob"}, 1464 }, 1465 Spec: api.PodSpec{ 1466 RestartPolicy: api.RestartPolicyOnFailure, 1467 DNSPolicy: api.DNSClusterFirst, 1468 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1469 InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 1470 }, 1471 }, 1472 }, 1473 }, 1474 opts: JobValidationOptions{RequirePrefixedLabels: true}, 1475 }, 1476 } 1477 1478 for k, v := range errorCases { 1479 t.Run(k, func(t *testing.T) { 1480 errs := ValidateJob(&v.job, v.opts) 1481 if len(errs) == 0 { 1482 t.Errorf("expected failure for %s", k) 1483 } else { 1484 s := strings.SplitN(k, ":", 2) 1485 err := errs[0] 1486 if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { 1487 t.Errorf("unexpected error: %v, expected: %s", err, k) 1488 } 1489 } 1490 }) 1491 } 1492 } 1493 1494 func TestValidateJobUpdate(t *testing.T) { 1495 validGeneratedSelector := getValidGeneratedSelector() 1496 validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) 1497 validPodTemplateSpecForGeneratedRestartPolicyNever := getValidPodTemplateSpecForGenerated(validGeneratedSelector) 1498 validPodTemplateSpecForGeneratedRestartPolicyNever.Spec.RestartPolicy = api.RestartPolicyNever 1499 1500 validNodeAffinity := &api.Affinity{ 1501 NodeAffinity: &api.NodeAffinity{ 1502 RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ 1503 NodeSelectorTerms: []api.NodeSelectorTerm{{ 1504 MatchExpressions: []api.NodeSelectorRequirement{{ 1505 Key: "foo", 1506 Operator: api.NodeSelectorOpIn, 1507 Values: []string{"bar", "value2"}, 1508 }}, 1509 }}, 1510 }, 1511 }, 1512 } 1513 validPodTemplateWithAffinity := getValidPodTemplateSpecForGenerated(validGeneratedSelector) 1514 validPodTemplateWithAffinity.Spec.Affinity = &api.Affinity{ 1515 NodeAffinity: &api.NodeAffinity{ 1516 RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ 1517 NodeSelectorTerms: []api.NodeSelectorTerm{{ 1518 MatchExpressions: []api.NodeSelectorRequirement{{ 1519 Key: "foo", 1520 Operator: api.NodeSelectorOpIn, 1521 Values: []string{"bar", "value"}, 1522 }}, 1523 }}, 1524 }, 1525 }, 1526 } 1527 // This is to test immutability of the selector, both the new and old 1528 // selector should match the labels in the template, which is immutable 1529 // on its own; therfore, the only way to test selector immutability is 1530 // when the new selector is changed but still matches the existing labels. 1531 newSelector := getValidGeneratedSelector() 1532 newSelector.MatchLabels["foo"] = "bar" 1533 validTolerations := []api.Toleration{{ 1534 Key: "foo", 1535 Operator: api.TolerationOpEqual, 1536 Value: "bar", 1537 Effect: api.TaintEffectPreferNoSchedule, 1538 }} 1539 cases := map[string]struct { 1540 old batch.Job 1541 update func(*batch.Job) 1542 opts JobValidationOptions 1543 err *field.Error 1544 }{ 1545 "mutable fields": { 1546 old: batch.Job{ 1547 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1548 Spec: batch.JobSpec{ 1549 Selector: validGeneratedSelector, 1550 Template: validPodTemplateSpecForGenerated, 1551 Parallelism: pointer.Int32(5), 1552 ActiveDeadlineSeconds: pointer.Int64(2), 1553 TTLSecondsAfterFinished: pointer.Int32(1), 1554 }, 1555 }, 1556 update: func(job *batch.Job) { 1557 job.Spec.Parallelism = pointer.Int32(2) 1558 job.Spec.ActiveDeadlineSeconds = pointer.Int64(3) 1559 job.Spec.TTLSecondsAfterFinished = pointer.Int32(2) 1560 job.Spec.ManualSelector = pointer.Bool(true) 1561 }, 1562 }, 1563 "invalid attempt to set managedBy field": { 1564 old: batch.Job{ 1565 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1566 Spec: batch.JobSpec{ 1567 Selector: validGeneratedSelector, 1568 Template: validPodTemplateSpecForGenerated, 1569 }, 1570 }, 1571 update: func(job *batch.Job) { 1572 job.Spec.ManagedBy = ptr.To("example.com/custom-controller") 1573 }, 1574 err: &field.Error{ 1575 Type: field.ErrorTypeInvalid, 1576 Field: "spec.managedBy", 1577 }, 1578 }, 1579 "invalid update of the managedBy field": { 1580 old: batch.Job{ 1581 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1582 Spec: batch.JobSpec{ 1583 Selector: validGeneratedSelector, 1584 Template: validPodTemplateSpecForGenerated, 1585 ManagedBy: ptr.To("example.com/custom-controller1"), 1586 }, 1587 }, 1588 update: func(job *batch.Job) { 1589 job.Spec.ManagedBy = ptr.To("example.com/custom-controller2") 1590 }, 1591 err: &field.Error{ 1592 Type: field.ErrorTypeInvalid, 1593 Field: "spec.managedBy", 1594 }, 1595 }, 1596 "immutable completions for non-indexed jobs": { 1597 old: batch.Job{ 1598 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1599 Spec: batch.JobSpec{ 1600 Selector: validGeneratedSelector, 1601 Template: validPodTemplateSpecForGenerated, 1602 }, 1603 }, 1604 update: func(job *batch.Job) { 1605 job.Spec.Completions = pointer.Int32(1) 1606 }, 1607 err: &field.Error{ 1608 Type: field.ErrorTypeInvalid, 1609 Field: "spec.completions", 1610 }, 1611 }, 1612 "immutable completions for indexed job when AllowElasticIndexedJobs is false": { 1613 old: batch.Job{ 1614 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1615 Spec: batch.JobSpec{ 1616 Selector: validGeneratedSelector, 1617 Template: validPodTemplateSpecForGenerated, 1618 }, 1619 }, 1620 update: func(job *batch.Job) { 1621 job.Spec.Completions = pointer.Int32(1) 1622 }, 1623 err: &field.Error{ 1624 Type: field.ErrorTypeInvalid, 1625 Field: "spec.completions", 1626 }, 1627 }, 1628 "immutable selector": { 1629 old: batch.Job{ 1630 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1631 Spec: batch.JobSpec{ 1632 Selector: validGeneratedSelector, 1633 Template: getValidPodTemplateSpecForGenerated(newSelector), 1634 }, 1635 }, 1636 update: func(job *batch.Job) { 1637 job.Spec.Selector = newSelector 1638 }, 1639 err: &field.Error{ 1640 Type: field.ErrorTypeInvalid, 1641 Field: "spec.selector", 1642 }, 1643 }, 1644 "add success policy": { 1645 old: batch.Job{ 1646 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1647 Spec: batch.JobSpec{ 1648 CompletionMode: completionModePtr(batch.IndexedCompletion), 1649 Completions: ptr.To[int32](5), 1650 Selector: validGeneratedSelector, 1651 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1652 }, 1653 }, 1654 update: func(job *batch.Job) { 1655 job.Spec.SuccessPolicy = &batch.SuccessPolicy{ 1656 Rules: []batch.SuccessPolicyRule{{ 1657 SucceededCount: ptr.To[int32](2), 1658 }}, 1659 } 1660 }, 1661 err: &field.Error{ 1662 Type: field.ErrorTypeInvalid, 1663 Field: "spec.successPolicy", 1664 }, 1665 }, 1666 "update success policy": { 1667 old: batch.Job{ 1668 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1669 Spec: batch.JobSpec{ 1670 CompletionMode: completionModePtr(batch.IndexedCompletion), 1671 Completions: ptr.To[int32](5), 1672 Selector: validGeneratedSelector, 1673 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1674 SuccessPolicy: &batch.SuccessPolicy{ 1675 Rules: []batch.SuccessPolicyRule{{ 1676 SucceededIndexes: ptr.To("1-3"), 1677 }}, 1678 }, 1679 }, 1680 }, 1681 update: func(job *batch.Job) { 1682 job.Spec.SuccessPolicy.Rules = append(job.Spec.SuccessPolicy.Rules, batch.SuccessPolicyRule{ 1683 SucceededCount: ptr.To[int32](3), 1684 }) 1685 }, 1686 err: &field.Error{ 1687 Type: field.ErrorTypeInvalid, 1688 Field: "spec.successPolicy", 1689 }, 1690 }, 1691 "remove success policy": { 1692 old: batch.Job{ 1693 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1694 Spec: batch.JobSpec{ 1695 CompletionMode: completionModePtr(batch.IndexedCompletion), 1696 Completions: ptr.To[int32](5), 1697 Selector: validGeneratedSelector, 1698 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1699 SuccessPolicy: &batch.SuccessPolicy{ 1700 Rules: []batch.SuccessPolicyRule{{ 1701 SucceededIndexes: ptr.To("1-3"), 1702 }}, 1703 }, 1704 }, 1705 }, 1706 update: func(job *batch.Job) { 1707 job.Spec.SuccessPolicy = nil 1708 }, 1709 err: &field.Error{ 1710 Type: field.ErrorTypeInvalid, 1711 Field: "spec.successPolicy", 1712 }, 1713 }, 1714 "add pod failure policy": { 1715 old: batch.Job{ 1716 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1717 Spec: batch.JobSpec{ 1718 Selector: validGeneratedSelector, 1719 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1720 }, 1721 }, 1722 update: func(job *batch.Job) { 1723 job.Spec.PodFailurePolicy = &batch.PodFailurePolicy{ 1724 Rules: []batch.PodFailurePolicyRule{{ 1725 Action: batch.PodFailurePolicyActionIgnore, 1726 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 1727 Type: api.DisruptionTarget, 1728 Status: api.ConditionTrue, 1729 }}, 1730 }}, 1731 } 1732 }, 1733 err: &field.Error{ 1734 Type: field.ErrorTypeInvalid, 1735 Field: "spec.podFailurePolicy", 1736 }, 1737 }, 1738 "update pod failure policy": { 1739 old: batch.Job{ 1740 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1741 Spec: batch.JobSpec{ 1742 Selector: validGeneratedSelector, 1743 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1744 PodFailurePolicy: &batch.PodFailurePolicy{ 1745 Rules: []batch.PodFailurePolicyRule{{ 1746 Action: batch.PodFailurePolicyActionIgnore, 1747 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 1748 Type: api.DisruptionTarget, 1749 Status: api.ConditionTrue, 1750 }}, 1751 }}, 1752 }, 1753 }, 1754 }, 1755 update: func(job *batch.Job) { 1756 job.Spec.PodFailurePolicy.Rules = append(job.Spec.PodFailurePolicy.Rules, batch.PodFailurePolicyRule{ 1757 Action: batch.PodFailurePolicyActionCount, 1758 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 1759 Type: api.DisruptionTarget, 1760 Status: api.ConditionTrue, 1761 }}, 1762 }) 1763 }, 1764 err: &field.Error{ 1765 Type: field.ErrorTypeInvalid, 1766 Field: "spec.podFailurePolicy", 1767 }, 1768 }, 1769 "remove pod failure policy": { 1770 old: batch.Job{ 1771 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1772 Spec: batch.JobSpec{ 1773 Selector: validGeneratedSelector, 1774 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1775 PodFailurePolicy: &batch.PodFailurePolicy{ 1776 Rules: []batch.PodFailurePolicyRule{{ 1777 Action: batch.PodFailurePolicyActionIgnore, 1778 OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ 1779 Type: api.DisruptionTarget, 1780 Status: api.ConditionTrue, 1781 }}, 1782 }}, 1783 }, 1784 }, 1785 }, 1786 update: func(job *batch.Job) { 1787 job.Spec.PodFailurePolicy = nil 1788 }, 1789 err: &field.Error{ 1790 Type: field.ErrorTypeInvalid, 1791 Field: "spec.podFailurePolicy", 1792 }, 1793 }, 1794 "set backoff limit per index": { 1795 old: batch.Job{ 1796 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1797 Spec: batch.JobSpec{ 1798 Selector: validGeneratedSelector, 1799 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1800 Completions: pointer.Int32(3), 1801 CompletionMode: completionModePtr(batch.IndexedCompletion), 1802 }, 1803 }, 1804 update: func(job *batch.Job) { 1805 job.Spec.BackoffLimitPerIndex = pointer.Int32(1) 1806 }, 1807 err: &field.Error{ 1808 Type: field.ErrorTypeInvalid, 1809 Field: "spec.backoffLimitPerIndex", 1810 }, 1811 }, 1812 "unset backoff limit per index": { 1813 old: batch.Job{ 1814 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1815 Spec: batch.JobSpec{ 1816 Selector: validGeneratedSelector, 1817 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1818 Completions: pointer.Int32(3), 1819 CompletionMode: completionModePtr(batch.IndexedCompletion), 1820 BackoffLimitPerIndex: pointer.Int32(1), 1821 }, 1822 }, 1823 update: func(job *batch.Job) { 1824 job.Spec.BackoffLimitPerIndex = nil 1825 }, 1826 err: &field.Error{ 1827 Type: field.ErrorTypeInvalid, 1828 Field: "spec.backoffLimitPerIndex", 1829 }, 1830 }, 1831 "update backoff limit per index": { 1832 old: batch.Job{ 1833 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1834 Spec: batch.JobSpec{ 1835 Selector: validGeneratedSelector, 1836 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1837 Completions: pointer.Int32(3), 1838 CompletionMode: completionModePtr(batch.IndexedCompletion), 1839 BackoffLimitPerIndex: pointer.Int32(1), 1840 }, 1841 }, 1842 update: func(job *batch.Job) { 1843 job.Spec.BackoffLimitPerIndex = pointer.Int32(2) 1844 }, 1845 err: &field.Error{ 1846 Type: field.ErrorTypeInvalid, 1847 Field: "spec.backoffLimitPerIndex", 1848 }, 1849 }, 1850 "set max failed indexes": { 1851 old: batch.Job{ 1852 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1853 Spec: batch.JobSpec{ 1854 Selector: validGeneratedSelector, 1855 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1856 Completions: pointer.Int32(3), 1857 CompletionMode: completionModePtr(batch.IndexedCompletion), 1858 BackoffLimitPerIndex: pointer.Int32(1), 1859 }, 1860 }, 1861 update: func(job *batch.Job) { 1862 job.Spec.MaxFailedIndexes = pointer.Int32(1) 1863 }, 1864 }, 1865 "unset max failed indexes": { 1866 old: batch.Job{ 1867 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1868 Spec: batch.JobSpec{ 1869 Selector: validGeneratedSelector, 1870 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1871 Completions: pointer.Int32(3), 1872 CompletionMode: completionModePtr(batch.IndexedCompletion), 1873 BackoffLimitPerIndex: pointer.Int32(1), 1874 MaxFailedIndexes: pointer.Int32(1), 1875 }, 1876 }, 1877 update: func(job *batch.Job) { 1878 job.Spec.MaxFailedIndexes = nil 1879 }, 1880 }, 1881 "update max failed indexes": { 1882 old: batch.Job{ 1883 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1884 Spec: batch.JobSpec{ 1885 Selector: validGeneratedSelector, 1886 Template: validPodTemplateSpecForGeneratedRestartPolicyNever, 1887 Completions: pointer.Int32(3), 1888 CompletionMode: completionModePtr(batch.IndexedCompletion), 1889 BackoffLimitPerIndex: pointer.Int32(1), 1890 MaxFailedIndexes: pointer.Int32(1), 1891 }, 1892 }, 1893 update: func(job *batch.Job) { 1894 job.Spec.MaxFailedIndexes = pointer.Int32(2) 1895 }, 1896 }, 1897 "immutable pod template": { 1898 old: batch.Job{ 1899 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1900 Spec: batch.JobSpec{ 1901 Selector: validGeneratedSelector, 1902 Template: validPodTemplateSpecForGenerated, 1903 Completions: pointer.Int32(3), 1904 CompletionMode: completionModePtr(batch.IndexedCompletion), 1905 }, 1906 }, 1907 update: func(job *batch.Job) { 1908 job.Spec.Template.Spec.DNSPolicy = api.DNSClusterFirstWithHostNet 1909 }, 1910 err: &field.Error{ 1911 Type: field.ErrorTypeInvalid, 1912 Field: "spec.template", 1913 }, 1914 }, 1915 "immutable completion mode": { 1916 old: batch.Job{ 1917 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1918 Spec: batch.JobSpec{ 1919 Selector: validGeneratedSelector, 1920 Template: validPodTemplateSpecForGenerated, 1921 CompletionMode: completionModePtr(batch.IndexedCompletion), 1922 Completions: pointer.Int32(2), 1923 }, 1924 }, 1925 update: func(job *batch.Job) { 1926 job.Spec.CompletionMode = completionModePtr(batch.NonIndexedCompletion) 1927 }, 1928 err: &field.Error{ 1929 Type: field.ErrorTypeInvalid, 1930 Field: "spec.completionMode", 1931 }, 1932 }, 1933 "immutable completions for non-indexed job when AllowElasticIndexedJobs is true": { 1934 old: batch.Job{ 1935 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1936 Spec: batch.JobSpec{ 1937 Selector: validGeneratedSelector, 1938 Template: validPodTemplateSpecForGenerated, 1939 CompletionMode: completionModePtr(batch.NonIndexedCompletion), 1940 Completions: pointer.Int32(2), 1941 }, 1942 }, 1943 update: func(job *batch.Job) { 1944 job.Spec.Completions = pointer.Int32(4) 1945 }, 1946 err: &field.Error{ 1947 Type: field.ErrorTypeInvalid, 1948 Field: "spec.completions", 1949 }, 1950 opts: JobValidationOptions{AllowElasticIndexedJobs: true}, 1951 }, 1952 1953 "immutable node affinity": { 1954 old: batch.Job{ 1955 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1956 Spec: batch.JobSpec{ 1957 Selector: validGeneratedSelector, 1958 Template: validPodTemplateSpecForGenerated, 1959 }, 1960 }, 1961 update: func(job *batch.Job) { 1962 job.Spec.Template.Spec.Affinity = validNodeAffinity 1963 }, 1964 err: &field.Error{ 1965 Type: field.ErrorTypeInvalid, 1966 Field: "spec.template", 1967 }, 1968 }, 1969 "add node affinity": { 1970 old: batch.Job{ 1971 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1972 Spec: batch.JobSpec{ 1973 Selector: validGeneratedSelector, 1974 Template: validPodTemplateSpecForGenerated, 1975 }, 1976 }, 1977 update: func(job *batch.Job) { 1978 job.Spec.Template.Spec.Affinity = validNodeAffinity 1979 }, 1980 opts: JobValidationOptions{ 1981 AllowMutableSchedulingDirectives: true, 1982 }, 1983 }, 1984 "update node affinity": { 1985 old: batch.Job{ 1986 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 1987 Spec: batch.JobSpec{ 1988 Selector: validGeneratedSelector, 1989 Template: validPodTemplateWithAffinity, 1990 }, 1991 }, 1992 update: func(job *batch.Job) { 1993 job.Spec.Template.Spec.Affinity = validNodeAffinity 1994 }, 1995 opts: JobValidationOptions{ 1996 AllowMutableSchedulingDirectives: true, 1997 }, 1998 }, 1999 "remove node affinity": { 2000 old: batch.Job{ 2001 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2002 Spec: batch.JobSpec{ 2003 Selector: validGeneratedSelector, 2004 Template: validPodTemplateWithAffinity, 2005 }, 2006 }, 2007 update: func(job *batch.Job) { 2008 job.Spec.Template.Spec.Affinity.NodeAffinity = nil 2009 }, 2010 opts: JobValidationOptions{ 2011 AllowMutableSchedulingDirectives: true, 2012 }, 2013 }, 2014 "remove affinity": { 2015 old: batch.Job{ 2016 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2017 Spec: batch.JobSpec{ 2018 Selector: validGeneratedSelector, 2019 Template: validPodTemplateWithAffinity, 2020 }, 2021 }, 2022 update: func(job *batch.Job) { 2023 job.Spec.Template.Spec.Affinity = nil 2024 }, 2025 opts: JobValidationOptions{ 2026 AllowMutableSchedulingDirectives: true, 2027 }, 2028 }, 2029 "immutable tolerations": { 2030 old: batch.Job{ 2031 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2032 Spec: batch.JobSpec{ 2033 Selector: validGeneratedSelector, 2034 Template: validPodTemplateSpecForGenerated, 2035 }, 2036 }, 2037 update: func(job *batch.Job) { 2038 job.Spec.Template.Spec.Tolerations = validTolerations 2039 }, 2040 err: &field.Error{ 2041 Type: field.ErrorTypeInvalid, 2042 Field: "spec.template", 2043 }, 2044 }, 2045 "mutable tolerations": { 2046 old: batch.Job{ 2047 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2048 Spec: batch.JobSpec{ 2049 Selector: validGeneratedSelector, 2050 Template: validPodTemplateSpecForGenerated, 2051 }, 2052 }, 2053 update: func(job *batch.Job) { 2054 job.Spec.Template.Spec.Tolerations = validTolerations 2055 }, 2056 opts: JobValidationOptions{ 2057 AllowMutableSchedulingDirectives: true, 2058 }, 2059 }, 2060 "immutable node selector": { 2061 old: batch.Job{ 2062 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2063 Spec: batch.JobSpec{ 2064 Selector: validGeneratedSelector, 2065 Template: validPodTemplateSpecForGenerated, 2066 }, 2067 }, 2068 update: func(job *batch.Job) { 2069 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 2070 }, 2071 err: &field.Error{ 2072 Type: field.ErrorTypeInvalid, 2073 Field: "spec.template", 2074 }, 2075 }, 2076 "mutable node selector": { 2077 old: batch.Job{ 2078 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2079 Spec: batch.JobSpec{ 2080 Selector: validGeneratedSelector, 2081 Template: validPodTemplateSpecForGenerated, 2082 }, 2083 }, 2084 update: func(job *batch.Job) { 2085 job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} 2086 }, 2087 opts: JobValidationOptions{ 2088 AllowMutableSchedulingDirectives: true, 2089 }, 2090 }, 2091 "immutable annotations": { 2092 old: batch.Job{ 2093 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2094 Spec: batch.JobSpec{ 2095 Selector: validGeneratedSelector, 2096 Template: validPodTemplateSpecForGenerated, 2097 }, 2098 }, 2099 update: func(job *batch.Job) { 2100 job.Spec.Template.Annotations = map[string]string{"foo": "baz"} 2101 }, 2102 err: &field.Error{ 2103 Type: field.ErrorTypeInvalid, 2104 Field: "spec.template", 2105 }, 2106 }, 2107 "mutable annotations": { 2108 old: batch.Job{ 2109 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2110 Spec: batch.JobSpec{ 2111 Selector: validGeneratedSelector, 2112 Template: validPodTemplateSpecForGenerated, 2113 }, 2114 }, 2115 update: func(job *batch.Job) { 2116 job.Spec.Template.Annotations = map[string]string{"foo": "baz"} 2117 }, 2118 opts: JobValidationOptions{ 2119 AllowMutableSchedulingDirectives: true, 2120 }, 2121 }, 2122 "immutable labels": { 2123 old: batch.Job{ 2124 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2125 Spec: batch.JobSpec{ 2126 Selector: validGeneratedSelector, 2127 Template: validPodTemplateSpecForGenerated, 2128 }, 2129 }, 2130 update: func(job *batch.Job) { 2131 newLabels := getValidGeneratedSelector().MatchLabels 2132 newLabels["bar"] = "baz" 2133 job.Spec.Template.Labels = newLabels 2134 }, 2135 err: &field.Error{ 2136 Type: field.ErrorTypeInvalid, 2137 Field: "spec.template", 2138 }, 2139 }, 2140 "mutable labels": { 2141 old: batch.Job{ 2142 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2143 Spec: batch.JobSpec{ 2144 Selector: validGeneratedSelector, 2145 Template: validPodTemplateSpecForGenerated, 2146 }, 2147 }, 2148 update: func(job *batch.Job) { 2149 newLabels := getValidGeneratedSelector().MatchLabels 2150 newLabels["bar"] = "baz" 2151 job.Spec.Template.Labels = newLabels 2152 }, 2153 opts: JobValidationOptions{ 2154 AllowMutableSchedulingDirectives: true, 2155 }, 2156 }, 2157 "immutable schedulingGates": { 2158 old: batch.Job{ 2159 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2160 Spec: batch.JobSpec{ 2161 Selector: validGeneratedSelector, 2162 Template: validPodTemplateSpecForGenerated, 2163 }, 2164 }, 2165 update: func(job *batch.Job) { 2166 job.Spec.Template.Spec.SchedulingGates = append(job.Spec.Template.Spec.SchedulingGates, api.PodSchedulingGate{Name: "gate"}) 2167 }, 2168 err: &field.Error{ 2169 Type: field.ErrorTypeInvalid, 2170 Field: "spec.template", 2171 }, 2172 }, 2173 "mutable schedulingGates": { 2174 old: batch.Job{ 2175 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2176 Spec: batch.JobSpec{ 2177 Selector: validGeneratedSelector, 2178 Template: validPodTemplateSpecForGenerated, 2179 }, 2180 }, 2181 update: func(job *batch.Job) { 2182 job.Spec.Template.Spec.SchedulingGates = append(job.Spec.Template.Spec.SchedulingGates, api.PodSchedulingGate{Name: "gate"}) 2183 }, 2184 opts: JobValidationOptions{ 2185 AllowMutableSchedulingDirectives: true, 2186 }, 2187 }, 2188 "update completions and parallelism to same value is valid": { 2189 old: batch.Job{ 2190 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2191 Spec: batch.JobSpec{ 2192 Selector: validGeneratedSelector, 2193 Template: validPodTemplateSpecForGenerated, 2194 Completions: pointer.Int32(1), 2195 Parallelism: pointer.Int32(1), 2196 CompletionMode: completionModePtr(batch.IndexedCompletion), 2197 }, 2198 }, 2199 update: func(job *batch.Job) { 2200 job.Spec.Completions = pointer.Int32(2) 2201 job.Spec.Parallelism = pointer.Int32(2) 2202 }, 2203 opts: JobValidationOptions{ 2204 AllowElasticIndexedJobs: true, 2205 }, 2206 }, 2207 "previous parallelism != previous completions, new parallelism == new completions": { 2208 old: batch.Job{ 2209 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2210 Spec: batch.JobSpec{ 2211 Selector: validGeneratedSelector, 2212 Template: validPodTemplateSpecForGenerated, 2213 Completions: pointer.Int32(1), 2214 Parallelism: pointer.Int32(2), 2215 CompletionMode: completionModePtr(batch.IndexedCompletion), 2216 }, 2217 }, 2218 update: func(job *batch.Job) { 2219 job.Spec.Completions = pointer.Int32(3) 2220 job.Spec.Parallelism = pointer.Int32(3) 2221 }, 2222 opts: JobValidationOptions{ 2223 AllowElasticIndexedJobs: true, 2224 }, 2225 }, 2226 "indexed job updating completions and parallelism to different values is invalid": { 2227 old: batch.Job{ 2228 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2229 Spec: batch.JobSpec{ 2230 Selector: validGeneratedSelector, 2231 Template: validPodTemplateSpecForGenerated, 2232 Completions: pointer.Int32(1), 2233 Parallelism: pointer.Int32(1), 2234 CompletionMode: completionModePtr(batch.IndexedCompletion), 2235 }, 2236 }, 2237 update: func(job *batch.Job) { 2238 job.Spec.Completions = pointer.Int32(2) 2239 job.Spec.Parallelism = pointer.Int32(3) 2240 }, 2241 opts: JobValidationOptions{ 2242 AllowElasticIndexedJobs: true, 2243 }, 2244 err: &field.Error{ 2245 Type: field.ErrorTypeInvalid, 2246 Field: "spec.completions", 2247 }, 2248 }, 2249 "indexed job with completions set updated to nil does not panic": { 2250 old: batch.Job{ 2251 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2252 Spec: batch.JobSpec{ 2253 Selector: validGeneratedSelector, 2254 Template: validPodTemplateSpecForGenerated, 2255 Completions: pointer.Int32(1), 2256 Parallelism: pointer.Int32(1), 2257 CompletionMode: completionModePtr(batch.IndexedCompletion), 2258 }, 2259 }, 2260 update: func(job *batch.Job) { 2261 job.Spec.Completions = nil 2262 job.Spec.Parallelism = pointer.Int32(3) 2263 }, 2264 opts: JobValidationOptions{ 2265 AllowElasticIndexedJobs: true, 2266 }, 2267 err: &field.Error{ 2268 Type: field.ErrorTypeRequired, 2269 Field: "spec.completions", 2270 }, 2271 }, 2272 "indexed job with completions unchanged, parallelism reduced to less than completions": { 2273 old: batch.Job{ 2274 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2275 Spec: batch.JobSpec{ 2276 Selector: validGeneratedSelector, 2277 Template: validPodTemplateSpecForGenerated, 2278 Completions: pointer.Int32(2), 2279 Parallelism: pointer.Int32(2), 2280 CompletionMode: completionModePtr(batch.IndexedCompletion), 2281 }, 2282 }, 2283 update: func(job *batch.Job) { 2284 job.Spec.Completions = pointer.Int32(2) 2285 job.Spec.Parallelism = pointer.Int32(1) 2286 }, 2287 opts: JobValidationOptions{ 2288 AllowElasticIndexedJobs: true, 2289 }, 2290 }, 2291 "indexed job with completions unchanged, parallelism increased higher than completions": { 2292 old: batch.Job{ 2293 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 2294 Spec: batch.JobSpec{ 2295 Selector: validGeneratedSelector, 2296 Template: validPodTemplateSpecForGenerated, 2297 Completions: pointer.Int32(2), 2298 Parallelism: pointer.Int32(2), 2299 CompletionMode: completionModePtr(batch.IndexedCompletion), 2300 }, 2301 }, 2302 update: func(job *batch.Job) { 2303 job.Spec.Completions = pointer.Int32(2) 2304 job.Spec.Parallelism = pointer.Int32(3) 2305 }, 2306 opts: JobValidationOptions{ 2307 AllowElasticIndexedJobs: true, 2308 }, 2309 }, 2310 } 2311 ignoreValueAndDetail := cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") 2312 for k, tc := range cases { 2313 t.Run(k, func(t *testing.T) { 2314 tc.old.ResourceVersion = "1" 2315 update := tc.old.DeepCopy() 2316 tc.update(update) 2317 errs := ValidateJobUpdate(update, &tc.old, tc.opts) 2318 var wantErrs field.ErrorList 2319 if tc.err != nil { 2320 wantErrs = append(wantErrs, tc.err) 2321 } 2322 if diff := cmp.Diff(wantErrs, errs, ignoreValueAndDetail); diff != "" { 2323 t.Errorf("Unexpected validation errors (-want,+got):\n%s", diff) 2324 } 2325 }) 2326 } 2327 } 2328 2329 func TestValidateJobUpdateStatus(t *testing.T) { 2330 cases := map[string]struct { 2331 opts JobStatusValidationOptions 2332 2333 old batch.Job 2334 update batch.Job 2335 wantErrs field.ErrorList 2336 }{ 2337 "valid": { 2338 old: batch.Job{ 2339 ObjectMeta: metav1.ObjectMeta{ 2340 Name: "abc", 2341 Namespace: metav1.NamespaceDefault, 2342 ResourceVersion: "1", 2343 }, 2344 Status: batch.JobStatus{ 2345 Active: 1, 2346 Succeeded: 2, 2347 Failed: 3, 2348 Terminating: pointer.Int32(4), 2349 }, 2350 }, 2351 update: batch.Job{ 2352 ObjectMeta: metav1.ObjectMeta{ 2353 Name: "abc", 2354 Namespace: metav1.NamespaceDefault, 2355 ResourceVersion: "1", 2356 }, 2357 Status: batch.JobStatus{ 2358 Active: 2, 2359 Succeeded: 3, 2360 Failed: 4, 2361 Ready: pointer.Int32(1), 2362 Terminating: pointer.Int32(4), 2363 }, 2364 }, 2365 }, 2366 "nil ready and terminating": { 2367 old: batch.Job{ 2368 ObjectMeta: metav1.ObjectMeta{ 2369 Name: "abc", 2370 Namespace: metav1.NamespaceDefault, 2371 ResourceVersion: "1", 2372 }, 2373 Status: batch.JobStatus{ 2374 Active: 1, 2375 Succeeded: 2, 2376 Failed: 3, 2377 }, 2378 }, 2379 update: batch.Job{ 2380 ObjectMeta: metav1.ObjectMeta{ 2381 Name: "abc", 2382 Namespace: metav1.NamespaceDefault, 2383 ResourceVersion: "1", 2384 }, 2385 Status: batch.JobStatus{ 2386 Active: 2, 2387 Succeeded: 3, 2388 Failed: 4, 2389 }, 2390 }, 2391 }, 2392 "negative counts": { 2393 old: batch.Job{ 2394 ObjectMeta: metav1.ObjectMeta{ 2395 Name: "abc", 2396 Namespace: metav1.NamespaceDefault, 2397 ResourceVersion: "10", 2398 }, 2399 Status: batch.JobStatus{ 2400 Active: 1, 2401 Succeeded: 2, 2402 Failed: 3, 2403 Terminating: pointer.Int32(4), 2404 }, 2405 }, 2406 update: batch.Job{ 2407 ObjectMeta: metav1.ObjectMeta{ 2408 Name: "abc", 2409 Namespace: metav1.NamespaceDefault, 2410 ResourceVersion: "10", 2411 }, 2412 Status: batch.JobStatus{ 2413 Active: -1, 2414 Succeeded: -2, 2415 Failed: -3, 2416 Ready: pointer.Int32(-1), 2417 Terminating: pointer.Int32(-2), 2418 }, 2419 }, 2420 wantErrs: field.ErrorList{ 2421 {Type: field.ErrorTypeInvalid, Field: "status.active"}, 2422 {Type: field.ErrorTypeInvalid, Field: "status.succeeded"}, 2423 {Type: field.ErrorTypeInvalid, Field: "status.failed"}, 2424 {Type: field.ErrorTypeInvalid, Field: "status.ready"}, 2425 {Type: field.ErrorTypeInvalid, Field: "status.terminating"}, 2426 }, 2427 }, 2428 "empty and duplicated uncounted pods": { 2429 old: batch.Job{ 2430 ObjectMeta: metav1.ObjectMeta{ 2431 Name: "abc", 2432 Namespace: metav1.NamespaceDefault, 2433 ResourceVersion: "5", 2434 }, 2435 }, 2436 update: batch.Job{ 2437 ObjectMeta: metav1.ObjectMeta{ 2438 Name: "abc", 2439 Namespace: metav1.NamespaceDefault, 2440 ResourceVersion: "5", 2441 }, 2442 Status: batch.JobStatus{ 2443 UncountedTerminatedPods: &batch.UncountedTerminatedPods{ 2444 Succeeded: []types.UID{"a", "b", "c", "a", ""}, 2445 Failed: []types.UID{"c", "d", "e", "d", ""}, 2446 }, 2447 }, 2448 }, 2449 wantErrs: field.ErrorList{ 2450 {Type: field.ErrorTypeDuplicate, Field: "status.uncountedTerminatedPods.succeeded[3]"}, 2451 {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods.succeeded[4]"}, 2452 {Type: field.ErrorTypeDuplicate, Field: "status.uncountedTerminatedPods.failed[0]"}, 2453 {Type: field.ErrorTypeDuplicate, Field: "status.uncountedTerminatedPods.failed[3]"}, 2454 {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods.failed[4]"}, 2455 }, 2456 }, 2457 } 2458 for name, tc := range cases { 2459 t.Run(name, func(t *testing.T) { 2460 errs := ValidateJobUpdateStatus(&tc.update, &tc.old, tc.opts) 2461 if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" { 2462 t.Errorf("Unexpected errors (-want,+got):\n%s", diff) 2463 } 2464 }) 2465 } 2466 } 2467 2468 func TestValidateCronJob(t *testing.T) { 2469 validManualSelector := getValidManualSelector() 2470 validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) 2471 validPodTemplateSpec.Labels = map[string]string{} 2472 validHostNetPodTemplateSpec := func() api.PodTemplateSpec { 2473 spec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) 2474 spec.Spec.SecurityContext = &api.PodSecurityContext{ 2475 HostNetwork: true, 2476 } 2477 spec.Spec.Containers[0].Ports = []api.ContainerPort{{ 2478 ContainerPort: 12345, 2479 Protocol: api.ProtocolTCP, 2480 }} 2481 return spec 2482 }() 2483 2484 successCases := map[string]batch.CronJob{ 2485 "basic scheduled job": { 2486 ObjectMeta: metav1.ObjectMeta{ 2487 Name: "mycronjob", 2488 Namespace: metav1.NamespaceDefault, 2489 UID: types.UID("1a2b3c"), 2490 }, 2491 Spec: batch.CronJobSpec{ 2492 Schedule: "* * * * ?", 2493 ConcurrencyPolicy: batch.AllowConcurrent, 2494 JobTemplate: batch.JobTemplateSpec{ 2495 Spec: batch.JobSpec{ 2496 Template: validPodTemplateSpec, 2497 }, 2498 }, 2499 }, 2500 }, 2501 "hostnet job": { 2502 ObjectMeta: metav1.ObjectMeta{ 2503 Name: "mycronjob", 2504 Namespace: metav1.NamespaceDefault, 2505 UID: types.UID("1a2b3c"), 2506 }, 2507 Spec: batch.CronJobSpec{ 2508 Schedule: "* * * * ?", 2509 ConcurrencyPolicy: batch.AllowConcurrent, 2510 JobTemplate: batch.JobTemplateSpec{ 2511 Spec: batch.JobSpec{ 2512 Template: validHostNetPodTemplateSpec, 2513 }, 2514 }, 2515 }, 2516 }, 2517 "non-standard scheduled": { 2518 ObjectMeta: metav1.ObjectMeta{ 2519 Name: "mycronjob", 2520 Namespace: metav1.NamespaceDefault, 2521 UID: types.UID("1a2b3c"), 2522 }, 2523 Spec: batch.CronJobSpec{ 2524 Schedule: "@hourly", 2525 ConcurrencyPolicy: batch.AllowConcurrent, 2526 JobTemplate: batch.JobTemplateSpec{ 2527 Spec: batch.JobSpec{ 2528 Template: validPodTemplateSpec, 2529 }, 2530 }, 2531 }, 2532 }, 2533 "correct timeZone value": { 2534 ObjectMeta: metav1.ObjectMeta{ 2535 Name: "mycronjob", 2536 Namespace: metav1.NamespaceDefault, 2537 UID: types.UID("1a2b3c"), 2538 }, 2539 Spec: batch.CronJobSpec{ 2540 Schedule: "0 * * * *", 2541 TimeZone: &timeZoneCorrect, 2542 ConcurrencyPolicy: batch.AllowConcurrent, 2543 JobTemplate: batch.JobTemplateSpec{ 2544 Spec: batch.JobSpec{ 2545 Template: validPodTemplateSpec, 2546 }, 2547 }, 2548 }, 2549 }, 2550 } 2551 for k, v := range successCases { 2552 t.Run(k, func(t *testing.T) { 2553 if errs := ValidateCronJobCreate(&v, corevalidation.PodValidationOptions{}); len(errs) != 0 { 2554 t.Errorf("expected success for %s: %v", k, errs) 2555 } 2556 2557 // Update validation should pass same success cases 2558 // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update 2559 v = *v.DeepCopy() 2560 v.ResourceVersion = "1" 2561 if errs := ValidateCronJobUpdate(&v, &v, corevalidation.PodValidationOptions{}); len(errs) != 0 { 2562 t.Errorf("expected success for %s: %v", k, errs) 2563 } 2564 }) 2565 } 2566 2567 negative := int32(-1) 2568 negative64 := int64(-1) 2569 2570 errorCases := map[string]batch.CronJob{ 2571 "spec.schedule: Invalid value": { 2572 ObjectMeta: metav1.ObjectMeta{ 2573 Name: "mycronjob", 2574 Namespace: metav1.NamespaceDefault, 2575 UID: types.UID("1a2b3c"), 2576 }, 2577 Spec: batch.CronJobSpec{ 2578 Schedule: "error", 2579 ConcurrencyPolicy: batch.AllowConcurrent, 2580 JobTemplate: batch.JobTemplateSpec{ 2581 Spec: batch.JobSpec{ 2582 Template: validPodTemplateSpec, 2583 }, 2584 }, 2585 }, 2586 }, 2587 "spec.schedule: Required value": { 2588 ObjectMeta: metav1.ObjectMeta{ 2589 Name: "mycronjob", 2590 Namespace: metav1.NamespaceDefault, 2591 UID: types.UID("1a2b3c"), 2592 }, 2593 Spec: batch.CronJobSpec{ 2594 Schedule: "", 2595 ConcurrencyPolicy: batch.AllowConcurrent, 2596 JobTemplate: batch.JobTemplateSpec{ 2597 Spec: batch.JobSpec{ 2598 Template: validPodTemplateSpec, 2599 }, 2600 }, 2601 }, 2602 }, 2603 "spec.timeZone: timeZone must be nil or non-empty string": { 2604 ObjectMeta: metav1.ObjectMeta{ 2605 Name: "mycronjob", 2606 Namespace: metav1.NamespaceDefault, 2607 UID: types.UID("1a2b3c"), 2608 }, 2609 Spec: batch.CronJobSpec{ 2610 Schedule: "0 * * * *", 2611 TimeZone: &timeZoneEmpty, 2612 ConcurrencyPolicy: batch.AllowConcurrent, 2613 JobTemplate: batch.JobTemplateSpec{ 2614 Spec: batch.JobSpec{ 2615 Template: validPodTemplateSpec, 2616 }, 2617 }, 2618 }, 2619 }, 2620 "spec.timeZone: timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones": { 2621 ObjectMeta: metav1.ObjectMeta{ 2622 Name: "mycronjob", 2623 Namespace: metav1.NamespaceDefault, 2624 UID: types.UID("1a2b3c"), 2625 }, 2626 Spec: batch.CronJobSpec{ 2627 Schedule: "0 * * * *", 2628 TimeZone: &timeZoneLocal, 2629 ConcurrencyPolicy: batch.AllowConcurrent, 2630 JobTemplate: batch.JobTemplateSpec{ 2631 Spec: batch.JobSpec{ 2632 Template: validPodTemplateSpec, 2633 }, 2634 }, 2635 }, 2636 }, 2637 "spec.timeZone: Invalid value: \" Continent/Zone\": unknown time zone Continent/Zone": { 2638 ObjectMeta: metav1.ObjectMeta{ 2639 Name: "mycronjob", 2640 Namespace: metav1.NamespaceDefault, 2641 UID: types.UID("1a2b3c"), 2642 }, 2643 Spec: batch.CronJobSpec{ 2644 Schedule: "0 * * * *", 2645 TimeZone: &timeZoneBadPrefix, 2646 ConcurrencyPolicy: batch.AllowConcurrent, 2647 JobTemplate: batch.JobTemplateSpec{ 2648 Spec: batch.JobSpec{ 2649 Template: validPodTemplateSpec, 2650 }, 2651 }, 2652 }, 2653 }, 2654 "spec.timeZone: Invalid value: \"Continent/InvalidZone\": unknown time zone Continent/InvalidZone": { 2655 ObjectMeta: metav1.ObjectMeta{ 2656 Name: "mycronjob", 2657 Namespace: metav1.NamespaceDefault, 2658 UID: types.UID("1a2b3c"), 2659 }, 2660 Spec: batch.CronJobSpec{ 2661 Schedule: "0 * * * *", 2662 TimeZone: &timeZoneBadName, 2663 ConcurrencyPolicy: batch.AllowConcurrent, 2664 JobTemplate: batch.JobTemplateSpec{ 2665 Spec: batch.JobSpec{ 2666 Template: validPodTemplateSpec, 2667 }, 2668 }, 2669 }, 2670 }, 2671 "spec.timeZone: Invalid value: \" \": unknown time zone ": { 2672 ObjectMeta: metav1.ObjectMeta{ 2673 Name: "mycronjob", 2674 Namespace: metav1.NamespaceDefault, 2675 UID: types.UID("1a2b3c"), 2676 }, 2677 Spec: batch.CronJobSpec{ 2678 Schedule: "0 * * * *", 2679 TimeZone: &timeZoneEmptySpace, 2680 ConcurrencyPolicy: batch.AllowConcurrent, 2681 JobTemplate: batch.JobTemplateSpec{ 2682 Spec: batch.JobSpec{ 2683 Template: validPodTemplateSpec, 2684 }, 2685 }, 2686 }, 2687 }, 2688 "spec.timeZone: Invalid value: \"Continent/Zone \": unknown time zone Continent/Zone ": { 2689 ObjectMeta: metav1.ObjectMeta{ 2690 Name: "mycronjob", 2691 Namespace: metav1.NamespaceDefault, 2692 UID: types.UID("1a2b3c"), 2693 }, 2694 Spec: batch.CronJobSpec{ 2695 Schedule: "0 * * * *", 2696 TimeZone: &timeZoneBadSuffix, 2697 ConcurrencyPolicy: batch.AllowConcurrent, 2698 JobTemplate: batch.JobTemplateSpec{ 2699 Spec: batch.JobSpec{ 2700 Template: validPodTemplateSpec, 2701 }, 2702 }, 2703 }, 2704 }, 2705 "spec.startingDeadlineSeconds:must be greater than or equal to 0": { 2706 ObjectMeta: metav1.ObjectMeta{ 2707 Name: "mycronjob", 2708 Namespace: metav1.NamespaceDefault, 2709 UID: types.UID("1a2b3c"), 2710 }, 2711 Spec: batch.CronJobSpec{ 2712 Schedule: "* * * * ?", 2713 ConcurrencyPolicy: batch.AllowConcurrent, 2714 StartingDeadlineSeconds: &negative64, 2715 JobTemplate: batch.JobTemplateSpec{ 2716 Spec: batch.JobSpec{ 2717 Template: validPodTemplateSpec, 2718 }, 2719 }, 2720 }, 2721 }, 2722 "spec.successfulJobsHistoryLimit: must be greater than or equal to 0": { 2723 ObjectMeta: metav1.ObjectMeta{ 2724 Name: "mycronjob", 2725 Namespace: metav1.NamespaceDefault, 2726 UID: types.UID("1a2b3c"), 2727 }, 2728 Spec: batch.CronJobSpec{ 2729 Schedule: "* * * * ?", 2730 ConcurrencyPolicy: batch.AllowConcurrent, 2731 SuccessfulJobsHistoryLimit: &negative, 2732 JobTemplate: batch.JobTemplateSpec{ 2733 Spec: batch.JobSpec{ 2734 Template: validPodTemplateSpec, 2735 }, 2736 }, 2737 }, 2738 }, 2739 "spec.failedJobsHistoryLimit: must be greater than or equal to 0": { 2740 ObjectMeta: metav1.ObjectMeta{ 2741 Name: "mycronjob", 2742 Namespace: metav1.NamespaceDefault, 2743 UID: types.UID("1a2b3c"), 2744 }, 2745 Spec: batch.CronJobSpec{ 2746 Schedule: "* * * * ?", 2747 ConcurrencyPolicy: batch.AllowConcurrent, 2748 FailedJobsHistoryLimit: &negative, 2749 JobTemplate: batch.JobTemplateSpec{ 2750 Spec: batch.JobSpec{ 2751 Template: validPodTemplateSpec, 2752 }, 2753 }, 2754 }, 2755 }, 2756 "spec.concurrencyPolicy: Required value": { 2757 ObjectMeta: metav1.ObjectMeta{ 2758 Name: "mycronjob", 2759 Namespace: metav1.NamespaceDefault, 2760 UID: types.UID("1a2b3c"), 2761 }, 2762 Spec: batch.CronJobSpec{ 2763 Schedule: "* * * * ?", 2764 JobTemplate: batch.JobTemplateSpec{ 2765 Spec: batch.JobSpec{ 2766 Template: validPodTemplateSpec, 2767 }, 2768 }, 2769 }, 2770 }, 2771 "spec.jobTemplate.spec.parallelism:must be greater than or equal to 0": { 2772 ObjectMeta: metav1.ObjectMeta{ 2773 Name: "mycronjob", 2774 Namespace: metav1.NamespaceDefault, 2775 UID: types.UID("1a2b3c"), 2776 }, 2777 Spec: batch.CronJobSpec{ 2778 Schedule: "* * * * ?", 2779 ConcurrencyPolicy: batch.AllowConcurrent, 2780 JobTemplate: batch.JobTemplateSpec{ 2781 Spec: batch.JobSpec{ 2782 Parallelism: &negative, 2783 Template: validPodTemplateSpec, 2784 }, 2785 }, 2786 }, 2787 }, 2788 "spec.jobTemplate.spec.completions:must be greater than or equal to 0": { 2789 ObjectMeta: metav1.ObjectMeta{ 2790 Name: "mycronjob", 2791 Namespace: metav1.NamespaceDefault, 2792 UID: types.UID("1a2b3c"), 2793 }, 2794 Spec: batch.CronJobSpec{ 2795 Schedule: "* * * * ?", 2796 ConcurrencyPolicy: batch.AllowConcurrent, 2797 JobTemplate: batch.JobTemplateSpec{ 2798 2799 Spec: batch.JobSpec{ 2800 Completions: &negative, 2801 Template: validPodTemplateSpec, 2802 }, 2803 }, 2804 }, 2805 }, 2806 "spec.jobTemplate.spec.activeDeadlineSeconds:must be greater than or equal to 0": { 2807 ObjectMeta: metav1.ObjectMeta{ 2808 Name: "mycronjob", 2809 Namespace: metav1.NamespaceDefault, 2810 UID: types.UID("1a2b3c"), 2811 }, 2812 Spec: batch.CronJobSpec{ 2813 Schedule: "* * * * ?", 2814 ConcurrencyPolicy: batch.AllowConcurrent, 2815 JobTemplate: batch.JobTemplateSpec{ 2816 Spec: batch.JobSpec{ 2817 ActiveDeadlineSeconds: &negative64, 2818 Template: validPodTemplateSpec, 2819 }, 2820 }, 2821 }, 2822 }, 2823 "spec.jobTemplate.spec.selector: Invalid value: {\"matchLabels\":{\"a\":\"b\"}}: `selector` will be auto-generated": { 2824 ObjectMeta: metav1.ObjectMeta{ 2825 Name: "mycronjob", 2826 Namespace: metav1.NamespaceDefault, 2827 UID: types.UID("1a2b3c"), 2828 }, 2829 Spec: batch.CronJobSpec{ 2830 Schedule: "* * * * ?", 2831 ConcurrencyPolicy: batch.AllowConcurrent, 2832 JobTemplate: batch.JobTemplateSpec{ 2833 Spec: batch.JobSpec{ 2834 Selector: validManualSelector, 2835 Template: validPodTemplateSpec, 2836 }, 2837 }, 2838 }, 2839 }, 2840 "metadata.name: must be no more than 52 characters": { 2841 ObjectMeta: metav1.ObjectMeta{ 2842 Name: "10000000002000000000300000000040000000005000000000123", 2843 Namespace: metav1.NamespaceDefault, 2844 UID: types.UID("1a2b3c"), 2845 }, 2846 Spec: batch.CronJobSpec{ 2847 Schedule: "* * * * ?", 2848 ConcurrencyPolicy: batch.AllowConcurrent, 2849 JobTemplate: batch.JobTemplateSpec{ 2850 Spec: batch.JobSpec{ 2851 Template: validPodTemplateSpec, 2852 }, 2853 }, 2854 }, 2855 }, 2856 "spec.jobTemplate.spec.manualSelector: Unsupported value": { 2857 ObjectMeta: metav1.ObjectMeta{ 2858 Name: "mycronjob", 2859 Namespace: metav1.NamespaceDefault, 2860 UID: types.UID("1a2b3c"), 2861 }, 2862 Spec: batch.CronJobSpec{ 2863 Schedule: "* * * * ?", 2864 ConcurrencyPolicy: batch.AllowConcurrent, 2865 JobTemplate: batch.JobTemplateSpec{ 2866 Spec: batch.JobSpec{ 2867 ManualSelector: pointer.Bool(true), 2868 Template: validPodTemplateSpec, 2869 }, 2870 }, 2871 }, 2872 }, 2873 "spec.jobTemplate.spec.template.spec.restartPolicy: Required value": { 2874 ObjectMeta: metav1.ObjectMeta{ 2875 Name: "mycronjob", 2876 Namespace: metav1.NamespaceDefault, 2877 UID: types.UID("1a2b3c"), 2878 }, 2879 Spec: batch.CronJobSpec{ 2880 Schedule: "* * * * ?", 2881 ConcurrencyPolicy: batch.AllowConcurrent, 2882 JobTemplate: batch.JobTemplateSpec{ 2883 Spec: batch.JobSpec{ 2884 Template: api.PodTemplateSpec{ 2885 Spec: api.PodSpec{ 2886 RestartPolicy: api.RestartPolicyAlways, 2887 DNSPolicy: api.DNSClusterFirst, 2888 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 2889 }, 2890 }, 2891 }, 2892 }, 2893 }, 2894 }, 2895 "spec.jobTemplate.spec.template.spec.restartPolicy: Unsupported value": { 2896 ObjectMeta: metav1.ObjectMeta{ 2897 Name: "mycronjob", 2898 Namespace: metav1.NamespaceDefault, 2899 UID: types.UID("1a2b3c"), 2900 }, 2901 Spec: batch.CronJobSpec{ 2902 Schedule: "* * * * ?", 2903 ConcurrencyPolicy: batch.AllowConcurrent, 2904 JobTemplate: batch.JobTemplateSpec{ 2905 Spec: batch.JobSpec{ 2906 Template: api.PodTemplateSpec{ 2907 Spec: api.PodSpec{ 2908 RestartPolicy: "Invalid", 2909 DNSPolicy: api.DNSClusterFirst, 2910 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, 2911 }, 2912 }, 2913 }, 2914 }, 2915 }, 2916 }, 2917 "spec.jobTemplate.spec.ttlSecondsAfterFinished:must be greater than or equal to 0": { 2918 ObjectMeta: metav1.ObjectMeta{ 2919 Name: "mycronjob", 2920 Namespace: metav1.NamespaceDefault, 2921 UID: types.UID("1a2b3c"), 2922 }, 2923 Spec: batch.CronJobSpec{ 2924 Schedule: "* * * * ?", 2925 ConcurrencyPolicy: batch.AllowConcurrent, 2926 JobTemplate: batch.JobTemplateSpec{ 2927 Spec: batch.JobSpec{ 2928 TTLSecondsAfterFinished: &negative, 2929 Template: validPodTemplateSpec, 2930 }, 2931 }, 2932 }, 2933 }, 2934 } 2935 2936 for k, v := range errorCases { 2937 t.Run(k, func(t *testing.T) { 2938 errs := ValidateCronJobCreate(&v, corevalidation.PodValidationOptions{}) 2939 if len(errs) == 0 { 2940 t.Errorf("expected failure for %s", k) 2941 } else { 2942 s := strings.Split(k, ":") 2943 err := errs[0] 2944 if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { 2945 t.Errorf("unexpected error: %v, expected: %s", err, k) 2946 } 2947 } 2948 2949 // Update validation should fail all failure cases other than the 52 character name limit 2950 // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update 2951 oldSpec := *v.DeepCopy() 2952 oldSpec.ResourceVersion = "1" 2953 oldSpec.Spec.TimeZone = nil 2954 2955 newSpec := *v.DeepCopy() 2956 newSpec.ResourceVersion = "2" 2957 2958 errs = ValidateCronJobUpdate(&newSpec, &oldSpec, corevalidation.PodValidationOptions{}) 2959 if len(errs) == 0 { 2960 if k == "metadata.name: must be no more than 52 characters" { 2961 return 2962 } 2963 t.Errorf("expected failure for %s", k) 2964 } else { 2965 s := strings.Split(k, ":") 2966 err := errs[0] 2967 if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { 2968 t.Errorf("unexpected error: %v, expected: %s", err, k) 2969 } 2970 } 2971 }) 2972 } 2973 } 2974 2975 func TestValidateCronJobScheduleTZ(t *testing.T) { 2976 validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) 2977 validPodTemplateSpec.Labels = map[string]string{} 2978 validSchedule := "0 * * * *" 2979 invalidSchedule := "TZ=UTC 0 * * * *" 2980 invalidCronJob := &batch.CronJob{ 2981 ObjectMeta: metav1.ObjectMeta{ 2982 Name: "mycronjob", 2983 Namespace: metav1.NamespaceDefault, 2984 UID: types.UID("1a2b3c"), 2985 }, 2986 Spec: batch.CronJobSpec{ 2987 Schedule: invalidSchedule, 2988 ConcurrencyPolicy: batch.AllowConcurrent, 2989 JobTemplate: batch.JobTemplateSpec{ 2990 Spec: batch.JobSpec{ 2991 Template: validPodTemplateSpec, 2992 }, 2993 }, 2994 }, 2995 } 2996 validCronJob := &batch.CronJob{ 2997 ObjectMeta: metav1.ObjectMeta{ 2998 Name: "mycronjob", 2999 Namespace: metav1.NamespaceDefault, 3000 UID: types.UID("1a2b3c"), 3001 }, 3002 Spec: batch.CronJobSpec{ 3003 Schedule: validSchedule, 3004 ConcurrencyPolicy: batch.AllowConcurrent, 3005 JobTemplate: batch.JobTemplateSpec{ 3006 Spec: batch.JobSpec{ 3007 Template: validPodTemplateSpec, 3008 }, 3009 }, 3010 }, 3011 } 3012 3013 testCases := map[string]struct { 3014 cronJob *batch.CronJob 3015 createErr string 3016 update func(*batch.CronJob) 3017 updateErr string 3018 }{ 3019 "update removing TZ should work": { 3020 cronJob: invalidCronJob, 3021 createErr: "cannot use TZ or CRON_TZ in schedule", 3022 update: func(cj *batch.CronJob) { 3023 cj.Spec.Schedule = validSchedule 3024 }, 3025 }, 3026 "update not modifying TZ should work": { 3027 cronJob: invalidCronJob, 3028 createErr: "cannot use TZ or CRON_TZ in schedule, use timeZone field instead", 3029 update: func(cj *batch.CronJob) { 3030 cj.Spec.Schedule = invalidSchedule 3031 }, 3032 }, 3033 "update not modifying TZ but adding .spec.timeZone should fail": { 3034 cronJob: invalidCronJob, 3035 createErr: "cannot use TZ or CRON_TZ in schedule, use timeZone field instead", 3036 update: func(cj *batch.CronJob) { 3037 cj.Spec.TimeZone = &timeZoneUTC 3038 }, 3039 updateErr: "cannot use both timeZone field and TZ or CRON_TZ in schedule", 3040 }, 3041 "update adding TZ should fail": { 3042 cronJob: validCronJob, 3043 update: func(cj *batch.CronJob) { 3044 cj.Spec.Schedule = invalidSchedule 3045 }, 3046 updateErr: "cannot use TZ or CRON_TZ in schedule", 3047 }, 3048 } 3049 3050 for k, v := range testCases { 3051 t.Run(k, func(t *testing.T) { 3052 errs := ValidateCronJobCreate(v.cronJob, corevalidation.PodValidationOptions{}) 3053 if len(errs) > 0 { 3054 err := errs[0] 3055 if len(v.createErr) == 0 { 3056 t.Errorf("unexpected error: %#v, none expected", err) 3057 return 3058 } 3059 if !strings.Contains(err.Error(), v.createErr) { 3060 t.Errorf("unexpected error: %v, expected: %s", err, v.createErr) 3061 } 3062 } else if len(v.createErr) != 0 { 3063 t.Errorf("no error, expected %v", v.createErr) 3064 return 3065 } 3066 3067 oldSpec := v.cronJob.DeepCopy() 3068 oldSpec.ResourceVersion = "1" 3069 3070 newSpec := v.cronJob.DeepCopy() 3071 newSpec.ResourceVersion = "2" 3072 if v.update != nil { 3073 v.update(newSpec) 3074 } 3075 3076 errs = ValidateCronJobUpdate(newSpec, oldSpec, corevalidation.PodValidationOptions{}) 3077 if len(errs) > 0 { 3078 err := errs[0] 3079 if len(v.updateErr) == 0 { 3080 t.Errorf("unexpected error: %#v, none expected", err) 3081 return 3082 } 3083 if !strings.Contains(err.Error(), v.updateErr) { 3084 t.Errorf("unexpected error: %v, expected: %s", err, v.updateErr) 3085 } 3086 } else if len(v.updateErr) != 0 { 3087 t.Errorf("no error, expected %v", v.updateErr) 3088 return 3089 } 3090 }) 3091 } 3092 } 3093 3094 func TestValidateCronJobSpec(t *testing.T) { 3095 validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) 3096 validPodTemplateSpec.Labels = map[string]string{} 3097 3098 type testCase struct { 3099 old *batch.CronJobSpec 3100 new *batch.CronJobSpec 3101 expectErr bool 3102 } 3103 3104 cases := map[string]testCase{ 3105 "no validation because timeZone is nil for old and new": { 3106 old: &batch.CronJobSpec{ 3107 Schedule: "0 * * * *", 3108 TimeZone: nil, 3109 ConcurrencyPolicy: batch.AllowConcurrent, 3110 JobTemplate: batch.JobTemplateSpec{ 3111 Spec: batch.JobSpec{ 3112 Template: validPodTemplateSpec, 3113 }, 3114 }, 3115 }, 3116 new: &batch.CronJobSpec{ 3117 Schedule: "0 * * * *", 3118 TimeZone: nil, 3119 ConcurrencyPolicy: batch.AllowConcurrent, 3120 JobTemplate: batch.JobTemplateSpec{ 3121 Spec: batch.JobSpec{ 3122 Template: validPodTemplateSpec, 3123 }, 3124 }, 3125 }, 3126 }, 3127 "check validation because timeZone is different for new": { 3128 old: &batch.CronJobSpec{ 3129 Schedule: "0 * * * *", 3130 TimeZone: nil, 3131 ConcurrencyPolicy: batch.AllowConcurrent, 3132 JobTemplate: batch.JobTemplateSpec{ 3133 Spec: batch.JobSpec{ 3134 Template: validPodTemplateSpec, 3135 }, 3136 }, 3137 }, 3138 new: &batch.CronJobSpec{ 3139 Schedule: "0 * * * *", 3140 TimeZone: pointer.String("America/New_York"), 3141 ConcurrencyPolicy: batch.AllowConcurrent, 3142 JobTemplate: batch.JobTemplateSpec{ 3143 Spec: batch.JobSpec{ 3144 Template: validPodTemplateSpec, 3145 }, 3146 }, 3147 }, 3148 }, 3149 "check validation because timeZone is different for new and invalid": { 3150 old: &batch.CronJobSpec{ 3151 Schedule: "0 * * * *", 3152 TimeZone: nil, 3153 ConcurrencyPolicy: batch.AllowConcurrent, 3154 JobTemplate: batch.JobTemplateSpec{ 3155 Spec: batch.JobSpec{ 3156 Template: validPodTemplateSpec, 3157 }, 3158 }, 3159 }, 3160 new: &batch.CronJobSpec{ 3161 Schedule: "0 * * * *", 3162 TimeZone: pointer.String("broken"), 3163 ConcurrencyPolicy: batch.AllowConcurrent, 3164 JobTemplate: batch.JobTemplateSpec{ 3165 Spec: batch.JobSpec{ 3166 Template: validPodTemplateSpec, 3167 }, 3168 }, 3169 }, 3170 expectErr: true, 3171 }, 3172 "old timeZone and new timeZone are valid": { 3173 old: &batch.CronJobSpec{ 3174 Schedule: "0 * * * *", 3175 TimeZone: pointer.String("America/New_York"), 3176 ConcurrencyPolicy: batch.AllowConcurrent, 3177 JobTemplate: batch.JobTemplateSpec{ 3178 Spec: batch.JobSpec{ 3179 Template: validPodTemplateSpec, 3180 }, 3181 }, 3182 }, 3183 new: &batch.CronJobSpec{ 3184 Schedule: "0 * * * *", 3185 TimeZone: pointer.String("America/Chicago"), 3186 ConcurrencyPolicy: batch.AllowConcurrent, 3187 JobTemplate: batch.JobTemplateSpec{ 3188 Spec: batch.JobSpec{ 3189 Template: validPodTemplateSpec, 3190 }, 3191 }, 3192 }, 3193 }, 3194 "old timeZone is valid, but new timeZone is invalid": { 3195 old: &batch.CronJobSpec{ 3196 Schedule: "0 * * * *", 3197 TimeZone: pointer.String("America/New_York"), 3198 ConcurrencyPolicy: batch.AllowConcurrent, 3199 JobTemplate: batch.JobTemplateSpec{ 3200 Spec: batch.JobSpec{ 3201 Template: validPodTemplateSpec, 3202 }, 3203 }, 3204 }, 3205 new: &batch.CronJobSpec{ 3206 Schedule: "0 * * * *", 3207 TimeZone: pointer.String("broken"), 3208 ConcurrencyPolicy: batch.AllowConcurrent, 3209 JobTemplate: batch.JobTemplateSpec{ 3210 Spec: batch.JobSpec{ 3211 Template: validPodTemplateSpec, 3212 }, 3213 }, 3214 }, 3215 expectErr: true, 3216 }, 3217 "old timeZone and new timeZone are invalid, but unchanged": { 3218 old: &batch.CronJobSpec{ 3219 Schedule: "0 * * * *", 3220 TimeZone: pointer.String("broken"), 3221 ConcurrencyPolicy: batch.AllowConcurrent, 3222 JobTemplate: batch.JobTemplateSpec{ 3223 Spec: batch.JobSpec{ 3224 Template: validPodTemplateSpec, 3225 }, 3226 }, 3227 }, 3228 new: &batch.CronJobSpec{ 3229 Schedule: "0 * * * *", 3230 TimeZone: pointer.String("broken"), 3231 ConcurrencyPolicy: batch.AllowConcurrent, 3232 JobTemplate: batch.JobTemplateSpec{ 3233 Spec: batch.JobSpec{ 3234 Template: validPodTemplateSpec, 3235 }, 3236 }, 3237 }, 3238 }, 3239 "old timeZone and new timeZone are invalid, but different": { 3240 old: &batch.CronJobSpec{ 3241 Schedule: "0 * * * *", 3242 TimeZone: pointer.String("broken"), 3243 ConcurrencyPolicy: batch.AllowConcurrent, 3244 JobTemplate: batch.JobTemplateSpec{ 3245 Spec: batch.JobSpec{ 3246 Template: validPodTemplateSpec, 3247 }, 3248 }, 3249 }, 3250 new: &batch.CronJobSpec{ 3251 Schedule: "0 * * * *", 3252 TimeZone: pointer.String("still broken"), 3253 ConcurrencyPolicy: batch.AllowConcurrent, 3254 JobTemplate: batch.JobTemplateSpec{ 3255 Spec: batch.JobSpec{ 3256 Template: validPodTemplateSpec, 3257 }, 3258 }, 3259 }, 3260 expectErr: true, 3261 }, 3262 "old timeZone is invalid, but new timeZone is valid": { 3263 old: &batch.CronJobSpec{ 3264 Schedule: "0 * * * *", 3265 TimeZone: pointer.String("broken"), 3266 ConcurrencyPolicy: batch.AllowConcurrent, 3267 JobTemplate: batch.JobTemplateSpec{ 3268 Spec: batch.JobSpec{ 3269 Template: validPodTemplateSpec, 3270 }, 3271 }, 3272 }, 3273 new: &batch.CronJobSpec{ 3274 Schedule: "0 * * * *", 3275 TimeZone: pointer.String("America/New_York"), 3276 ConcurrencyPolicy: batch.AllowConcurrent, 3277 JobTemplate: batch.JobTemplateSpec{ 3278 Spec: batch.JobSpec{ 3279 Template: validPodTemplateSpec, 3280 }, 3281 }, 3282 }, 3283 }, 3284 } 3285 3286 for k, v := range cases { 3287 errs := validateCronJobSpec(v.new, v.old, field.NewPath("spec"), corevalidation.PodValidationOptions{}) 3288 if len(errs) > 0 && !v.expectErr { 3289 t.Errorf("unexpected error for %s: %v", k, errs) 3290 } else if len(errs) == 0 && v.expectErr { 3291 t.Errorf("expected error for %s but got nil", k) 3292 } 3293 } 3294 } 3295 3296 func completionModePtr(m batch.CompletionMode) *batch.CompletionMode { 3297 return &m 3298 } 3299 3300 func TestTimeZones(t *testing.T) { 3301 // all valid time zones as of go1.19 release on 2022-08-02 3302 data := []string{ 3303 `Africa/Abidjan`, 3304 `Africa/Accra`, 3305 `Africa/Addis_Ababa`, 3306 `Africa/Algiers`, 3307 `Africa/Asmara`, 3308 `Africa/Asmera`, 3309 `Africa/Bamako`, 3310 `Africa/Bangui`, 3311 `Africa/Banjul`, 3312 `Africa/Bissau`, 3313 `Africa/Blantyre`, 3314 `Africa/Brazzaville`, 3315 `Africa/Bujumbura`, 3316 `Africa/Cairo`, 3317 `Africa/Casablanca`, 3318 `Africa/Ceuta`, 3319 `Africa/Conakry`, 3320 `Africa/Dakar`, 3321 `Africa/Dar_es_Salaam`, 3322 `Africa/Djibouti`, 3323 `Africa/Douala`, 3324 `Africa/El_Aaiun`, 3325 `Africa/Freetown`, 3326 `Africa/Gaborone`, 3327 `Africa/Harare`, 3328 `Africa/Johannesburg`, 3329 `Africa/Juba`, 3330 `Africa/Kampala`, 3331 `Africa/Khartoum`, 3332 `Africa/Kigali`, 3333 `Africa/Kinshasa`, 3334 `Africa/Lagos`, 3335 `Africa/Libreville`, 3336 `Africa/Lome`, 3337 `Africa/Luanda`, 3338 `Africa/Lubumbashi`, 3339 `Africa/Lusaka`, 3340 `Africa/Malabo`, 3341 `Africa/Maputo`, 3342 `Africa/Maseru`, 3343 `Africa/Mbabane`, 3344 `Africa/Mogadishu`, 3345 `Africa/Monrovia`, 3346 `Africa/Nairobi`, 3347 `Africa/Ndjamena`, 3348 `Africa/Niamey`, 3349 `Africa/Nouakchott`, 3350 `Africa/Ouagadougou`, 3351 `Africa/Porto-Novo`, 3352 `Africa/Sao_Tome`, 3353 `Africa/Timbuktu`, 3354 `Africa/Tripoli`, 3355 `Africa/Tunis`, 3356 `Africa/Windhoek`, 3357 `America/Adak`, 3358 `America/Anchorage`, 3359 `America/Anguilla`, 3360 `America/Antigua`, 3361 `America/Araguaina`, 3362 `America/Argentina/Buenos_Aires`, 3363 `America/Argentina/Catamarca`, 3364 `America/Argentina/ComodRivadavia`, 3365 `America/Argentina/Cordoba`, 3366 `America/Argentina/Jujuy`, 3367 `America/Argentina/La_Rioja`, 3368 `America/Argentina/Mendoza`, 3369 `America/Argentina/Rio_Gallegos`, 3370 `America/Argentina/Salta`, 3371 `America/Argentina/San_Juan`, 3372 `America/Argentina/San_Luis`, 3373 `America/Argentina/Tucuman`, 3374 `America/Argentina/Ushuaia`, 3375 `America/Aruba`, 3376 `America/Asuncion`, 3377 `America/Atikokan`, 3378 `America/Atka`, 3379 `America/Bahia`, 3380 `America/Bahia_Banderas`, 3381 `America/Barbados`, 3382 `America/Belem`, 3383 `America/Belize`, 3384 `America/Blanc-Sablon`, 3385 `America/Boa_Vista`, 3386 `America/Bogota`, 3387 `America/Boise`, 3388 `America/Buenos_Aires`, 3389 `America/Cambridge_Bay`, 3390 `America/Campo_Grande`, 3391 `America/Cancun`, 3392 `America/Caracas`, 3393 `America/Catamarca`, 3394 `America/Cayenne`, 3395 `America/Cayman`, 3396 `America/Chicago`, 3397 `America/Chihuahua`, 3398 `America/Coral_Harbour`, 3399 `America/Cordoba`, 3400 `America/Costa_Rica`, 3401 `America/Creston`, 3402 `America/Cuiaba`, 3403 `America/Curacao`, 3404 `America/Danmarkshavn`, 3405 `America/Dawson`, 3406 `America/Dawson_Creek`, 3407 `America/Denver`, 3408 `America/Detroit`, 3409 `America/Dominica`, 3410 `America/Edmonton`, 3411 `America/Eirunepe`, 3412 `America/El_Salvador`, 3413 `America/Ensenada`, 3414 `America/Fort_Nelson`, 3415 `America/Fort_Wayne`, 3416 `America/Fortaleza`, 3417 `America/Glace_Bay`, 3418 `America/Godthab`, 3419 `America/Goose_Bay`, 3420 `America/Grand_Turk`, 3421 `America/Grenada`, 3422 `America/Guadeloupe`, 3423 `America/Guatemala`, 3424 `America/Guayaquil`, 3425 `America/Guyana`, 3426 `America/Halifax`, 3427 `America/Havana`, 3428 `America/Hermosillo`, 3429 `America/Indiana/Indianapolis`, 3430 `America/Indiana/Knox`, 3431 `America/Indiana/Marengo`, 3432 `America/Indiana/Petersburg`, 3433 `America/Indiana/Tell_City`, 3434 `America/Indiana/Vevay`, 3435 `America/Indiana/Vincennes`, 3436 `America/Indiana/Winamac`, 3437 `America/Indianapolis`, 3438 `America/Inuvik`, 3439 `America/Iqaluit`, 3440 `America/Jamaica`, 3441 `America/Jujuy`, 3442 `America/Juneau`, 3443 `America/Kentucky/Louisville`, 3444 `America/Kentucky/Monticello`, 3445 `America/Knox_IN`, 3446 `America/Kralendijk`, 3447 `America/La_Paz`, 3448 `America/Lima`, 3449 `America/Los_Angeles`, 3450 `America/Louisville`, 3451 `America/Lower_Princes`, 3452 `America/Maceio`, 3453 `America/Managua`, 3454 `America/Manaus`, 3455 `America/Marigot`, 3456 `America/Martinique`, 3457 `America/Matamoros`, 3458 `America/Mazatlan`, 3459 `America/Mendoza`, 3460 `America/Menominee`, 3461 `America/Merida`, 3462 `America/Metlakatla`, 3463 `America/Mexico_City`, 3464 `America/Miquelon`, 3465 `America/Moncton`, 3466 `America/Monterrey`, 3467 `America/Montevideo`, 3468 `America/Montreal`, 3469 `America/Montserrat`, 3470 `America/Nassau`, 3471 `America/New_York`, 3472 `America/Nipigon`, 3473 `America/Nome`, 3474 `America/Noronha`, 3475 `America/North_Dakota/Beulah`, 3476 `America/North_Dakota/Center`, 3477 `America/North_Dakota/New_Salem`, 3478 `America/Nuuk`, 3479 `America/Ojinaga`, 3480 `America/Panama`, 3481 `America/Pangnirtung`, 3482 `America/Paramaribo`, 3483 `America/Phoenix`, 3484 `America/Port-au-Prince`, 3485 `America/Port_of_Spain`, 3486 `America/Porto_Acre`, 3487 `America/Porto_Velho`, 3488 `America/Puerto_Rico`, 3489 `America/Punta_Arenas`, 3490 `America/Rainy_River`, 3491 `America/Rankin_Inlet`, 3492 `America/Recife`, 3493 `America/Regina`, 3494 `America/Resolute`, 3495 `America/Rio_Branco`, 3496 `America/Rosario`, 3497 `America/Santa_Isabel`, 3498 `America/Santarem`, 3499 `America/Santiago`, 3500 `America/Santo_Domingo`, 3501 `America/Sao_Paulo`, 3502 `America/Scoresbysund`, 3503 `America/Shiprock`, 3504 `America/Sitka`, 3505 `America/St_Barthelemy`, 3506 `America/St_Johns`, 3507 `America/St_Kitts`, 3508 `America/St_Lucia`, 3509 `America/St_Thomas`, 3510 `America/St_Vincent`, 3511 `America/Swift_Current`, 3512 `America/Tegucigalpa`, 3513 `America/Thule`, 3514 `America/Thunder_Bay`, 3515 `America/Tijuana`, 3516 `America/Toronto`, 3517 `America/Tortola`, 3518 `America/Vancouver`, 3519 `America/Virgin`, 3520 `America/Whitehorse`, 3521 `America/Winnipeg`, 3522 `America/Yakutat`, 3523 `America/Yellowknife`, 3524 `Antarctica/Casey`, 3525 `Antarctica/Davis`, 3526 `Antarctica/DumontDUrville`, 3527 `Antarctica/Macquarie`, 3528 `Antarctica/Mawson`, 3529 `Antarctica/McMurdo`, 3530 `Antarctica/Palmer`, 3531 `Antarctica/Rothera`, 3532 `Antarctica/South_Pole`, 3533 `Antarctica/Syowa`, 3534 `Antarctica/Troll`, 3535 `Antarctica/Vostok`, 3536 `Arctic/Longyearbyen`, 3537 `Asia/Aden`, 3538 `Asia/Almaty`, 3539 `Asia/Amman`, 3540 `Asia/Anadyr`, 3541 `Asia/Aqtau`, 3542 `Asia/Aqtobe`, 3543 `Asia/Ashgabat`, 3544 `Asia/Ashkhabad`, 3545 `Asia/Atyrau`, 3546 `Asia/Baghdad`, 3547 `Asia/Bahrain`, 3548 `Asia/Baku`, 3549 `Asia/Bangkok`, 3550 `Asia/Barnaul`, 3551 `Asia/Beirut`, 3552 `Asia/Bishkek`, 3553 `Asia/Brunei`, 3554 `Asia/Calcutta`, 3555 `Asia/Chita`, 3556 `Asia/Choibalsan`, 3557 `Asia/Chongqing`, 3558 `Asia/Chungking`, 3559 `Asia/Colombo`, 3560 `Asia/Dacca`, 3561 `Asia/Damascus`, 3562 `Asia/Dhaka`, 3563 `Asia/Dili`, 3564 `Asia/Dubai`, 3565 `Asia/Dushanbe`, 3566 `Asia/Famagusta`, 3567 `Asia/Gaza`, 3568 `Asia/Harbin`, 3569 `Asia/Hebron`, 3570 `Asia/Ho_Chi_Minh`, 3571 `Asia/Hong_Kong`, 3572 `Asia/Hovd`, 3573 `Asia/Irkutsk`, 3574 `Asia/Istanbul`, 3575 `Asia/Jakarta`, 3576 `Asia/Jayapura`, 3577 `Asia/Jerusalem`, 3578 `Asia/Kabul`, 3579 `Asia/Kamchatka`, 3580 `Asia/Karachi`, 3581 `Asia/Kashgar`, 3582 `Asia/Kathmandu`, 3583 `Asia/Katmandu`, 3584 `Asia/Khandyga`, 3585 `Asia/Kolkata`, 3586 `Asia/Krasnoyarsk`, 3587 `Asia/Kuala_Lumpur`, 3588 `Asia/Kuching`, 3589 `Asia/Kuwait`, 3590 `Asia/Macao`, 3591 `Asia/Macau`, 3592 `Asia/Magadan`, 3593 `Asia/Makassar`, 3594 `Asia/Manila`, 3595 `Asia/Muscat`, 3596 `Asia/Nicosia`, 3597 `Asia/Novokuznetsk`, 3598 `Asia/Novosibirsk`, 3599 `Asia/Omsk`, 3600 `Asia/Oral`, 3601 `Asia/Phnom_Penh`, 3602 `Asia/Pontianak`, 3603 `Asia/Pyongyang`, 3604 `Asia/Qatar`, 3605 `Asia/Qostanay`, 3606 `Asia/Qyzylorda`, 3607 `Asia/Rangoon`, 3608 `Asia/Riyadh`, 3609 `Asia/Saigon`, 3610 `Asia/Sakhalin`, 3611 `Asia/Samarkand`, 3612 `Asia/Seoul`, 3613 `Asia/Shanghai`, 3614 `Asia/Singapore`, 3615 `Asia/Srednekolymsk`, 3616 `Asia/Taipei`, 3617 `Asia/Tashkent`, 3618 `Asia/Tbilisi`, 3619 `Asia/Tehran`, 3620 `Asia/Tel_Aviv`, 3621 `Asia/Thimbu`, 3622 `Asia/Thimphu`, 3623 `Asia/Tokyo`, 3624 `Asia/Tomsk`, 3625 `Asia/Ujung_Pandang`, 3626 `Asia/Ulaanbaatar`, 3627 `Asia/Ulan_Bator`, 3628 `Asia/Urumqi`, 3629 `Asia/Ust-Nera`, 3630 `Asia/Vientiane`, 3631 `Asia/Vladivostok`, 3632 `Asia/Yakutsk`, 3633 `Asia/Yangon`, 3634 `Asia/Yekaterinburg`, 3635 `Asia/Yerevan`, 3636 `Atlantic/Azores`, 3637 `Atlantic/Bermuda`, 3638 `Atlantic/Canary`, 3639 `Atlantic/Cape_Verde`, 3640 `Atlantic/Faeroe`, 3641 `Atlantic/Faroe`, 3642 `Atlantic/Jan_Mayen`, 3643 `Atlantic/Madeira`, 3644 `Atlantic/Reykjavik`, 3645 `Atlantic/South_Georgia`, 3646 `Atlantic/St_Helena`, 3647 `Atlantic/Stanley`, 3648 `Australia/ACT`, 3649 `Australia/Adelaide`, 3650 `Australia/Brisbane`, 3651 `Australia/Broken_Hill`, 3652 `Australia/Canberra`, 3653 `Australia/Currie`, 3654 `Australia/Darwin`, 3655 `Australia/Eucla`, 3656 `Australia/Hobart`, 3657 `Australia/LHI`, 3658 `Australia/Lindeman`, 3659 `Australia/Lord_Howe`, 3660 `Australia/Melbourne`, 3661 `Australia/North`, 3662 `Australia/NSW`, 3663 `Australia/Perth`, 3664 `Australia/Queensland`, 3665 `Australia/South`, 3666 `Australia/Sydney`, 3667 `Australia/Tasmania`, 3668 `Australia/Victoria`, 3669 `Australia/West`, 3670 `Australia/Yancowinna`, 3671 `Brazil/Acre`, 3672 `Brazil/DeNoronha`, 3673 `Brazil/East`, 3674 `Brazil/West`, 3675 `Canada/Atlantic`, 3676 `Canada/Central`, 3677 `Canada/Eastern`, 3678 `Canada/Mountain`, 3679 `Canada/Newfoundland`, 3680 `Canada/Pacific`, 3681 `Canada/Saskatchewan`, 3682 `Canada/Yukon`, 3683 `CET`, 3684 `Chile/Continental`, 3685 `Chile/EasterIsland`, 3686 `CST6CDT`, 3687 `Cuba`, 3688 `EET`, 3689 `Egypt`, 3690 `Eire`, 3691 `EST`, 3692 `EST5EDT`, 3693 `Etc/GMT`, 3694 `Etc/GMT+0`, 3695 `Etc/GMT+1`, 3696 `Etc/GMT+10`, 3697 `Etc/GMT+11`, 3698 `Etc/GMT+12`, 3699 `Etc/GMT+2`, 3700 `Etc/GMT+3`, 3701 `Etc/GMT+4`, 3702 `Etc/GMT+5`, 3703 `Etc/GMT+6`, 3704 `Etc/GMT+7`, 3705 `Etc/GMT+8`, 3706 `Etc/GMT+9`, 3707 `Etc/GMT-0`, 3708 `Etc/GMT-1`, 3709 `Etc/GMT-10`, 3710 `Etc/GMT-11`, 3711 `Etc/GMT-12`, 3712 `Etc/GMT-13`, 3713 `Etc/GMT-14`, 3714 `Etc/GMT-2`, 3715 `Etc/GMT-3`, 3716 `Etc/GMT-4`, 3717 `Etc/GMT-5`, 3718 `Etc/GMT-6`, 3719 `Etc/GMT-7`, 3720 `Etc/GMT-8`, 3721 `Etc/GMT-9`, 3722 `Etc/GMT0`, 3723 `Etc/Greenwich`, 3724 `Etc/UCT`, 3725 `Etc/Universal`, 3726 `Etc/UTC`, 3727 `Etc/Zulu`, 3728 `Europe/Amsterdam`, 3729 `Europe/Andorra`, 3730 `Europe/Astrakhan`, 3731 `Europe/Athens`, 3732 `Europe/Belfast`, 3733 `Europe/Belgrade`, 3734 `Europe/Berlin`, 3735 `Europe/Bratislava`, 3736 `Europe/Brussels`, 3737 `Europe/Bucharest`, 3738 `Europe/Budapest`, 3739 `Europe/Busingen`, 3740 `Europe/Chisinau`, 3741 `Europe/Copenhagen`, 3742 `Europe/Dublin`, 3743 `Europe/Gibraltar`, 3744 `Europe/Guernsey`, 3745 `Europe/Helsinki`, 3746 `Europe/Isle_of_Man`, 3747 `Europe/Istanbul`, 3748 `Europe/Jersey`, 3749 `Europe/Kaliningrad`, 3750 `Europe/Kiev`, 3751 `Europe/Kirov`, 3752 `Europe/Lisbon`, 3753 `Europe/Ljubljana`, 3754 `Europe/London`, 3755 `Europe/Luxembourg`, 3756 `Europe/Madrid`, 3757 `Europe/Malta`, 3758 `Europe/Mariehamn`, 3759 `Europe/Minsk`, 3760 `Europe/Monaco`, 3761 `Europe/Moscow`, 3762 `Europe/Nicosia`, 3763 `Europe/Oslo`, 3764 `Europe/Paris`, 3765 `Europe/Podgorica`, 3766 `Europe/Prague`, 3767 `Europe/Riga`, 3768 `Europe/Rome`, 3769 `Europe/Samara`, 3770 `Europe/San_Marino`, 3771 `Europe/Sarajevo`, 3772 `Europe/Saratov`, 3773 `Europe/Simferopol`, 3774 `Europe/Skopje`, 3775 `Europe/Sofia`, 3776 `Europe/Stockholm`, 3777 `Europe/Tallinn`, 3778 `Europe/Tirane`, 3779 `Europe/Tiraspol`, 3780 `Europe/Ulyanovsk`, 3781 `Europe/Uzhgorod`, 3782 `Europe/Vaduz`, 3783 `Europe/Vatican`, 3784 `Europe/Vienna`, 3785 `Europe/Vilnius`, 3786 `Europe/Volgograd`, 3787 `Europe/Warsaw`, 3788 `Europe/Zagreb`, 3789 `Europe/Zaporozhye`, 3790 `Europe/Zurich`, 3791 `Factory`, 3792 `GB`, 3793 `GB-Eire`, 3794 `GMT`, 3795 `GMT+0`, 3796 `GMT-0`, 3797 `GMT0`, 3798 `Greenwich`, 3799 `Hongkong`, 3800 `HST`, 3801 `Iceland`, 3802 `Indian/Antananarivo`, 3803 `Indian/Chagos`, 3804 `Indian/Christmas`, 3805 `Indian/Cocos`, 3806 `Indian/Comoro`, 3807 `Indian/Kerguelen`, 3808 `Indian/Mahe`, 3809 `Indian/Maldives`, 3810 `Indian/Mauritius`, 3811 `Indian/Mayotte`, 3812 `Indian/Reunion`, 3813 `Iran`, 3814 `Israel`, 3815 `Jamaica`, 3816 `Japan`, 3817 `Kwajalein`, 3818 `Libya`, 3819 `MET`, 3820 `Mexico/BajaNorte`, 3821 `Mexico/BajaSur`, 3822 `Mexico/General`, 3823 `MST`, 3824 `MST7MDT`, 3825 `Navajo`, 3826 `NZ`, 3827 `NZ-CHAT`, 3828 `Pacific/Apia`, 3829 `Pacific/Auckland`, 3830 `Pacific/Bougainville`, 3831 `Pacific/Chatham`, 3832 `Pacific/Chuuk`, 3833 `Pacific/Easter`, 3834 `Pacific/Efate`, 3835 `Pacific/Enderbury`, 3836 `Pacific/Fakaofo`, 3837 `Pacific/Fiji`, 3838 `Pacific/Funafuti`, 3839 `Pacific/Galapagos`, 3840 `Pacific/Gambier`, 3841 `Pacific/Guadalcanal`, 3842 `Pacific/Guam`, 3843 `Pacific/Honolulu`, 3844 `Pacific/Johnston`, 3845 `Pacific/Kanton`, 3846 `Pacific/Kiritimati`, 3847 `Pacific/Kosrae`, 3848 `Pacific/Kwajalein`, 3849 `Pacific/Majuro`, 3850 `Pacific/Marquesas`, 3851 `Pacific/Midway`, 3852 `Pacific/Nauru`, 3853 `Pacific/Niue`, 3854 `Pacific/Norfolk`, 3855 `Pacific/Noumea`, 3856 `Pacific/Pago_Pago`, 3857 `Pacific/Palau`, 3858 `Pacific/Pitcairn`, 3859 `Pacific/Pohnpei`, 3860 `Pacific/Ponape`, 3861 `Pacific/Port_Moresby`, 3862 `Pacific/Rarotonga`, 3863 `Pacific/Saipan`, 3864 `Pacific/Samoa`, 3865 `Pacific/Tahiti`, 3866 `Pacific/Tarawa`, 3867 `Pacific/Tongatapu`, 3868 `Pacific/Truk`, 3869 `Pacific/Wake`, 3870 `Pacific/Wallis`, 3871 `Pacific/Yap`, 3872 `Poland`, 3873 `Portugal`, 3874 `PRC`, 3875 `PST8PDT`, 3876 `ROC`, 3877 `ROK`, 3878 `Singapore`, 3879 `Turkey`, 3880 `UCT`, 3881 `Universal`, 3882 `US/Alaska`, 3883 `US/Aleutian`, 3884 `US/Arizona`, 3885 `US/Central`, 3886 `US/East-Indiana`, 3887 `US/Eastern`, 3888 `US/Hawaii`, 3889 `US/Indiana-Starke`, 3890 `US/Michigan`, 3891 `US/Mountain`, 3892 `US/Pacific`, 3893 `US/Samoa`, 3894 `UTC`, 3895 `W-SU`, 3896 `WET`, 3897 `Zulu`, 3898 } 3899 for _, tz := range data { 3900 errs := validateTimeZone(&tz, nil) 3901 if len(errs) > 0 { 3902 t.Errorf("%s failed: %v", tz, errs) 3903 } 3904 } 3905 } 3906 3907 func TestValidateIndexesString(t *testing.T) { 3908 testCases := map[string]struct { 3909 indexesString string 3910 completions int32 3911 wantTotal int32 3912 wantError error 3913 }{ 3914 "empty is valid": { 3915 indexesString: "", 3916 completions: 6, 3917 wantTotal: 0, 3918 }, 3919 "single number is valid": { 3920 indexesString: "1", 3921 completions: 6, 3922 wantTotal: 1, 3923 }, 3924 "single interval is valid": { 3925 indexesString: "1-3", 3926 completions: 6, 3927 wantTotal: 3, 3928 }, 3929 "mixed intervals valid": { 3930 indexesString: "0,1-3,5,7-10", 3931 completions: 12, 3932 wantTotal: 9, 3933 }, 3934 "invalid due to extra space": { 3935 indexesString: "0,1-3, 5", 3936 completions: 6, 3937 wantTotal: 0, 3938 wantError: errors.New(`cannot convert string to integer for index: " 5"`), 3939 }, 3940 "invalid due to too large index": { 3941 indexesString: "0,1-3,5", 3942 completions: 5, 3943 wantTotal: 0, 3944 wantError: errors.New(`too large index: "5"`), 3945 }, 3946 "invalid due to non-increasing order of intervals": { 3947 indexesString: "1-3,0,5", 3948 completions: 6, 3949 wantTotal: 0, 3950 wantError: errors.New(`non-increasing order, previous: 3, current: 0`), 3951 }, 3952 "invalid due to non-increasing order between intervals": { 3953 indexesString: "0,0,5", 3954 completions: 6, 3955 wantTotal: 0, 3956 wantError: errors.New(`non-increasing order, previous: 0, current: 0`), 3957 }, 3958 "invalid due to non-increasing order within interval": { 3959 indexesString: "0,1-1,5", 3960 completions: 6, 3961 wantTotal: 0, 3962 wantError: errors.New(`non-increasing order, previous: 1, current: 1`), 3963 }, 3964 "invalid due to starting with '-'": { 3965 indexesString: "-1,0", 3966 completions: 6, 3967 wantTotal: 0, 3968 wantError: errors.New(`cannot convert string to integer for index: ""`), 3969 }, 3970 "invalid due to ending with '-'": { 3971 indexesString: "0,1-", 3972 completions: 6, 3973 wantTotal: 0, 3974 wantError: errors.New(`cannot convert string to integer for index: ""`), 3975 }, 3976 "invalid due to repeated '-'": { 3977 indexesString: "0,1--3", 3978 completions: 6, 3979 wantTotal: 0, 3980 wantError: errors.New(`the fragment "1--3" violates the requirement that an index interval can have at most two parts separated by '-'`), 3981 }, 3982 "invalid due to repeated ','": { 3983 indexesString: "0,,1,3", 3984 completions: 6, 3985 wantTotal: 0, 3986 wantError: errors.New(`cannot convert string to integer for index: ""`), 3987 }, 3988 } 3989 3990 for name, tc := range testCases { 3991 t.Run(name, func(t *testing.T) { 3992 gotTotal, gotErr := validateIndexesFormat(tc.indexesString, tc.completions) 3993 if tc.wantError == nil && gotErr != nil { 3994 t.Errorf("unexpected error: %s", gotErr) 3995 } else if tc.wantError != nil && gotErr == nil { 3996 t.Errorf("missing error: %s", tc.wantError) 3997 } else if tc.wantError != nil && gotErr != nil { 3998 if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" { 3999 t.Errorf("unexpected error, diff: %s", diff) 4000 } 4001 } 4002 if tc.wantTotal != gotTotal { 4003 t.Errorf("unexpected total want:%d, got:%d", tc.wantTotal, gotTotal) 4004 } 4005 }) 4006 } 4007 } 4008 4009 func TestValidateFailedIndexesNotOverlapCompleted(t *testing.T) { 4010 testCases := map[string]struct { 4011 completedIndexesStr string 4012 failedIndexesStr string 4013 completions int32 4014 wantError error 4015 }{ 4016 "empty intervals": { 4017 completedIndexesStr: "", 4018 failedIndexesStr: "", 4019 completions: 6, 4020 }, 4021 "empty completed intervals": { 4022 completedIndexesStr: "", 4023 failedIndexesStr: "1-3", 4024 completions: 6, 4025 }, 4026 "empty failed intervals": { 4027 completedIndexesStr: "1-2", 4028 failedIndexesStr: "", 4029 completions: 6, 4030 }, 4031 "non-overlapping intervals": { 4032 completedIndexesStr: "0,2-4,6-8,12-19", 4033 failedIndexesStr: "1,9-10", 4034 completions: 20, 4035 }, 4036 "overlapping intervals": { 4037 completedIndexesStr: "0,2-4,6-8,12-19", 4038 failedIndexesStr: "1,8,9-10", 4039 completions: 20, 4040 wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), 4041 }, 4042 "overlapping intervals, corrupted completed interval skipped": { 4043 completedIndexesStr: "0,2-4,x,6-8,12-19", 4044 failedIndexesStr: "1,8,9-10", 4045 completions: 20, 4046 wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), 4047 }, 4048 "overlapping intervals, corrupted failed interval skipped": { 4049 completedIndexesStr: "0,2-4,6-8,12-19", 4050 failedIndexesStr: "1,y,8,9-10", 4051 completions: 20, 4052 wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), 4053 }, 4054 "overlapping intervals, first corrupted intervals skipped": { 4055 completedIndexesStr: "x,0,2-4,6-8,12-19", 4056 failedIndexesStr: "y,1,8,9-10", 4057 completions: 20, 4058 wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), 4059 }, 4060 "non-overlapping intervals, last intervals corrupted": { 4061 completedIndexesStr: "0,2-4,6-8,12-19,x", 4062 failedIndexesStr: "1,9-10,y", 4063 completions: 20, 4064 }, 4065 } 4066 for name, tc := range testCases { 4067 t.Run(name, func(t *testing.T) { 4068 gotErr := validateFailedIndexesNotOverlapCompleted(tc.completedIndexesStr, tc.failedIndexesStr, tc.completions) 4069 if tc.wantError == nil && gotErr != nil { 4070 t.Errorf("unexpected error: %s", gotErr) 4071 } else if tc.wantError != nil && gotErr == nil { 4072 t.Errorf("missing error: %s", tc.wantError) 4073 } else if tc.wantError != nil && gotErr != nil { 4074 if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" { 4075 t.Errorf("unexpected error, diff: %s", diff) 4076 } 4077 } 4078 }) 4079 } 4080 }