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