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