| /* |
| Copyright 2016 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package validation |
| |
| import ( |
| "errors" |
| _ "time/tzdata" |
| |
| "fmt" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/types" |
| "k8s.io/apimachinery/pkg/util/validation/field" |
| "k8s.io/kubernetes/pkg/apis/batch" |
| api "k8s.io/kubernetes/pkg/apis/core" |
| corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" |
| "k8s.io/utils/pointer" |
| "k8s.io/utils/ptr" |
| ) |
| |
| var ( |
| timeZoneEmpty = "" |
| timeZoneLocal = "LOCAL" |
| timeZoneUTC = "UTC" |
| timeZoneCorrect = "Europe/Rome" |
| timeZoneBadPrefix = " Europe/Rome" |
| timeZoneBadSuffix = "Europe/Rome " |
| timeZoneBadName = "Europe/InvalidRome" |
| timeZoneEmptySpace = " " |
| ) |
| |
| var ignoreErrValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") |
| |
| func getValidManualSelector() *metav1.LabelSelector { |
| return &metav1.LabelSelector{ |
| MatchLabels: map[string]string{"a": "b"}, |
| } |
| } |
| |
| func getValidPodTemplateSpecForManual(selector *metav1.LabelSelector) api.PodTemplateSpec { |
| return api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: selector.MatchLabels, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| } |
| } |
| |
| func getValidGeneratedSelector() *metav1.LabelSelector { |
| return &metav1.LabelSelector{ |
| MatchLabels: map[string]string{batch.ControllerUidLabel: "1a2b3c", batch.LegacyControllerUidLabel: "1a2b3c", batch.JobNameLabel: "myjob", batch.LegacyJobNameLabel: "myjob"}, |
| } |
| } |
| |
| func getValidPodTemplateSpecForGenerated(selector *metav1.LabelSelector) api.PodTemplateSpec { |
| return api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: selector.MatchLabels, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| } |
| } |
| |
| func TestValidateJob(t *testing.T) { |
| validJobObjectMeta := metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| } |
| validManualSelector := getValidManualSelector() |
| failedPodReplacement := batch.Failed |
| terminatingOrFailedPodReplacement := batch.TerminatingOrFailed |
| validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector) |
| validGeneratedSelector := getValidGeneratedSelector() |
| validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) |
| validPodTemplateSpecForGeneratedRestartPolicyNever := getValidPodTemplateSpecForGenerated(validGeneratedSelector) |
| validPodTemplateSpecForGeneratedRestartPolicyNever.Spec.RestartPolicy = api.RestartPolicyNever |
| validHostNetPodTemplateSpec := func() api.PodTemplateSpec { |
| spec := getValidPodTemplateSpecForGenerated(validGeneratedSelector) |
| spec.Spec.SecurityContext = &api.PodSecurityContext{ |
| HostNetwork: true, |
| } |
| spec.Spec.Containers[0].Ports = []api.ContainerPort{{ |
| ContainerPort: 12345, |
| Protocol: api.ProtocolTCP, |
| }} |
| return spec |
| }() |
| |
| successCases := map[string]struct { |
| opts JobValidationOptions |
| job batch.Job |
| }{ |
| "valid success policy": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](10), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{ |
| { |
| SucceededCount: ptr.To[int32](1), |
| SucceededIndexes: ptr.To("0,2,4"), |
| }, |
| { |
| SucceededIndexes: ptr.To("1,3,5-9"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "valid pod failure policy": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }, { |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.PodConditionType("CustomConditionType"), |
| Status: api.ConditionFalse, |
| }}, |
| }, { |
| Action: batch.PodFailurePolicyActionCount, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| ContainerName: pointer.String("abc"), |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{1, 2, 3}, |
| }, |
| }, { |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| ContainerName: pointer.String("def"), |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{4}, |
| }, |
| }, { |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpNotIn, |
| Values: []int32{5, 6, 7}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| }, |
| "valid pod failure policy with FailIndex": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: pointer.Int32(2), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| Selector: validGeneratedSelector, |
| ManualSelector: pointer.Bool(true), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailIndex, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{10}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| }, |
| "valid manual selector": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| Annotations: map[string]string{"foo": "bar"}, |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validManualSelector, |
| ManualSelector: pointer.Bool(true), |
| Template: validPodTemplateSpecForManual, |
| }, |
| }, |
| }, |
| "valid generated selector": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| }, |
| "valid pod replacement": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| PodReplacementPolicy: &terminatingOrFailedPodReplacement, |
| }, |
| }, |
| }, |
| "valid pod replacement with failed": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| PodReplacementPolicy: &failedPodReplacement, |
| }, |
| }, |
| }, |
| "valid hostnet": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validHostNetPodTemplateSpec, |
| }, |
| }, |
| }, |
| "valid NonIndexed completion mode": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| CompletionMode: completionModePtr(batch.NonIndexedCompletion), |
| }, |
| }, |
| }, |
| "valid Indexed completion mode": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: pointer.Int32(2), |
| Parallelism: pointer.Int32(100000), |
| }, |
| }, |
| }, |
| "valid parallelism and maxFailedIndexes for high completions when backoffLimitPerIndex is used": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(100_000), |
| Parallelism: pointer.Int32(100_000), |
| MaxFailedIndexes: pointer.Int32(100_000), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "valid parallelism and maxFailedIndexes for unlimited completions when backoffLimitPerIndex is used": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(1_000_000_000), |
| Parallelism: pointer.Int32(10_000), |
| MaxFailedIndexes: pointer.Int32(10_000), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "valid job tracking annotation": { |
| opts: JobValidationOptions{ |
| RequirePrefixedLabels: true, |
| }, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| }, |
| "valid batch labels": { |
| opts: JobValidationOptions{ |
| RequirePrefixedLabels: true, |
| }, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| }, |
| "do not allow new batch labels": { |
| opts: JobValidationOptions{ |
| RequirePrefixedLabels: false, |
| }, |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: &metav1.LabelSelector{ |
| MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c"}, |
| }, |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c", batch.LegacyJobNameLabel: "myjob"}, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "valid managedBy field": { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| ManagedBy: ptr.To("example.com/foo"), |
| }, |
| }, |
| }, |
| } |
| for k, v := range successCases { |
| t.Run(k, func(t *testing.T) { |
| if errs := ValidateJob(&v.job, v.opts); len(errs) != 0 { |
| t.Errorf("Got unexpected validation errors: %v", errs) |
| } |
| }) |
| } |
| negative := int32(-1) |
| negative64 := int64(-1) |
| errorCases := map[string]struct { |
| opts JobValidationOptions |
| job batch.Job |
| }{ |
| `spec.managedBy: Too long: may not be longer than 63`: { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| ManagedBy: ptr.To("example.com/" + strings.Repeat("x", 60)), |
| }, |
| }, |
| }, |
| `spec.managedBy: Invalid value: "invalid custom controller name": must be a domain-prefixed path (such as "acme.io/foo")`: { |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| ManagedBy: ptr.To("invalid custom controller name"), |
| }, |
| }, |
| }, |
| `spec.successPolicy: Invalid value: batch.SuccessPolicy{Rules:[]batch.SuccessPolicyRule{}}: requires indexed completion mode`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules: Required value: at least one rules must be specified when the successPolicy is specified`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{}, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules[0]: Required value: at least one of succeededCount or succeededIndexes must be specified`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededCount: nil, |
| SucceededIndexes: nil, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules[0].succeededIndexes: Invalid value: "invalid-format": error parsing succeededIndexes: cannot convert string to integer for index: "invalid"`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededIndexes: ptr.To("invalid-format"), |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules[0].succeededIndexes: Too long: must have at most 65536 bytes`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededIndexes: ptr.To(strings.Repeat("1", maxJobSuccessPolicySucceededIndexesLimit+1)), |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules[0].succeededCount: must be greater than or equal to 0`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededCount: ptr.To[int32](-1), |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules[0].succeededCount: Invalid value: 6: must be less than or equal to 5 (the number of specified completions)`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededCount: ptr.To[int32](6), |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `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)`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededCount: ptr.To[int32](4), |
| SucceededIndexes: ptr.To("0-2"), |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.successPolicy.rules: Too many: 21: must have at most 20 items`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: func() []batch.SuccessPolicyRule { |
| var rules []batch.SuccessPolicyRule |
| for i := 0; i < 21; i++ { |
| rules = append(rules, batch.SuccessPolicyRule{ |
| SucceededCount: ptr.To[int32](5), |
| }) |
| } |
| return rules |
| }(), |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Duplicate value: 11`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{11, 11}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.values: Too many: 256: must have at most 255 items`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: func() (values []int32) { |
| tooManyValues := make([]int32, maxPodFailurePolicyOnExitCodesValues+1) |
| for i := range tooManyValues { |
| tooManyValues[i] = int32(i) |
| } |
| return tooManyValues |
| }(), |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules: Too many: 21: must have at most 20 items`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: func() []batch.PodFailurePolicyRule { |
| tooManyRules := make([]batch.PodFailurePolicyRule, maxPodFailurePolicyRules+1) |
| for i := range tooManyRules { |
| tooManyRules[i] = batch.PodFailurePolicyRule{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{int32(i + 1)}, |
| }, |
| } |
| } |
| return tooManyRules |
| }(), |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onPodConditions: Too many: 21: must have at most 20 items`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnPodConditions: func() []batch.PodFailurePolicyOnPodConditionsPattern { |
| tooManyPatterns := make([]batch.PodFailurePolicyOnPodConditionsPattern, maxPodFailurePolicyOnPodConditionsPatterns+1) |
| for i := range tooManyPatterns { |
| tooManyPatterns[i] = batch.PodFailurePolicyOnPodConditionsPattern{ |
| Type: api.PodConditionType(fmt.Sprintf("CustomType_%d", i)), |
| Status: api.ConditionTrue, |
| } |
| } |
| return tooManyPatterns |
| }(), |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.values[2]: Duplicate value: 13`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{12, 13, 13, 13}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{19, 11}: must be ordered`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{19, 11}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{}: at least one value is required`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].action: Required value: valid values: ["Count" "FailIndex" "FailJob" "Ignore"]`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: "", |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{1, 2, 3}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.operator: Required value: valid values: ["In" "NotIn"]`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: "", |
| Values: []int32{1, 2, 3}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0]: Invalid value: specifying both OnExitCodes and OnPodConditions is not supported`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| ContainerName: pointer.String("abc"), |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{1, 2, 3}, |
| }, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Invalid value: 0: must not be 0 for the In operator`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{1, 0, 2}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[1].onExitCodes.containerName: Invalid value: "xyz": must be one of the container or initContainer names in the pod template`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| ContainerName: pointer.String("abc"), |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{1, 2, 3}, |
| }, |
| }, { |
| Action: batch.PodFailurePolicyActionFailJob, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| ContainerName: pointer.String("xyz"), |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{5, 6, 7}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].action: Unsupported value: "UnknownAction": supported values: "Count", "FailIndex", "FailJob", "Ignore"`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: "UnknownAction", |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| ContainerName: pointer.String("abc"), |
| Operator: batch.PodFailurePolicyOnExitCodesOpIn, |
| Values: []int32{1, 2, 3}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onExitCodes.operator: Unsupported value: "UnknownOperator": supported values: "In", "NotIn"`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ |
| Operator: "UnknownOperator", |
| Values: []int32{1, 2, 3}, |
| }, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onPodConditions[0].status: Required value: valid values: ["False" "True" "Unknown"]`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onPodConditions[0].status: Unsupported value: "UnknownStatus": supported values: "False", "True", "Unknown"`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: "UnknownStatus", |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "": name part must be non-empty`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Status: api.ConditionTrue, |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `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]')`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.PodConditionType("Invalid Condition Type"), |
| Status: api.ConditionTrue, |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podReplacementPolicy: Unsupported value: "TerminatingOrFailed": supported values: "Failed"`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| PodReplacementPolicy: &terminatingOrFailedPodReplacement, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.podReplacementPolicy: Unsupported value: "": supported values: "Failed", "TerminatingOrFailed"`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| PodReplacementPolicy: (*batch.PodReplacementPolicy)(pointer.String("")), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| `spec.template.spec.restartPolicy: Invalid value: "OnFailure": only "Never" is supported when podFailurePolicy is specified`: { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: validGeneratedSelector.MatchLabels, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{}, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.parallelism:must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Parallelism: &negative, |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.backoffLimit:must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| BackoffLimit: pointer.Int32(-1), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.backoffLimitPerIndex: Invalid value: 1: requires indexed completion mode": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| BackoffLimitPerIndex: pointer.Int32(1), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.backoffLimitPerIndex:must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| BackoffLimitPerIndex: pointer.Int32(-1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.maxFailedIndexes: Invalid value: 11: must be less than or equal to completions": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(10), |
| MaxFailedIndexes: pointer.Int32(11), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.maxFailedIndexes: Required value: must be specified when completions is above 100000": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(100_001), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "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": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(100_001), |
| Parallelism: pointer.Int32(50_000), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| MaxFailedIndexes: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.maxFailedIndexes: Invalid value: 100001: must be less than or equal to 100000": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(100_001), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| MaxFailedIndexes: pointer.Int32(100_001), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "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": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| Completions: pointer.Int32(100_001), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| MaxFailedIndexes: pointer.Int32(50_000), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.maxFailedIndexes:must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| BackoffLimitPerIndex: pointer.Int32(1), |
| MaxFailedIndexes: pointer.Int32(-1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.backoffLimitPerIndex: Required value: when maxFailedIndexes is specified": { |
| job: batch.Job{ |
| ObjectMeta: validJobObjectMeta, |
| Spec: batch.JobSpec{ |
| MaxFailedIndexes: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.completions:must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Completions: &negative, |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.activeDeadlineSeconds:must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| ActiveDeadlineSeconds: &negative64, |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.selector:Required value": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.template.metadata.labels: Invalid value: map[string]string{\"y\":\"z\"}: `selector` does not match template `labels`": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validManualSelector, |
| ManualSelector: pointer.Bool(true), |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: map[string]string{"y": "z"}, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.template.metadata.labels: Invalid value: map[string]string{\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validManualSelector, |
| ManualSelector: pointer.Bool(true), |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: map[string]string{"controller-uid": "4d5e6f"}, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.template.spec.restartPolicy: Required value": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validManualSelector, |
| ManualSelector: pointer.Bool(true), |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: validManualSelector.MatchLabels, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyAlways, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.template.spec.restartPolicy: Unsupported value": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validManualSelector, |
| ManualSelector: pointer.Bool(true), |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: validManualSelector.MatchLabels, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: "Invalid", |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.ttlSecondsAfterFinished: must be greater than or equal to 0": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| TTLSecondsAfterFinished: &negative, |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.completions: Required value: when completion mode is Indexed": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.parallelism: must be less than or equal to 100000 when completion mode is Indexed": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: pointer.Int32(2), |
| Parallelism: pointer.Int32(100001), |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.template.metadata.labels[controller-uid]: Required value: must be '1a2b3c'": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: &metav1.LabelSelector{ |
| MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c"}, |
| }, |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: map[string]string{batch.LegacyJobNameLabel: "myjob"}, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{}, |
| }, |
| "metadata.uid: Required value": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| }, |
| Spec: batch.JobSpec{ |
| Selector: &metav1.LabelSelector{ |
| MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "test"}, |
| }, |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: map[string]string{batch.LegacyJobNameLabel: "myjob"}, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{}, |
| }, |
| "spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{\"a\":\"b\"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: `selector` not auto-generated": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: &metav1.LabelSelector{ |
| MatchLabels: map[string]string{"a": "b"}, |
| }, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| "spec.template.metadata.labels[batch.kubernetes.io/controller-uid]: Required value: must be '1a2b3c'": { |
| job: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "myjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.JobSpec{ |
| Selector: &metav1.LabelSelector{ |
| MatchLabels: map[string]string{batch.ControllerUidLabel: "1a2b3c"}, |
| }, |
| Template: api.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: map[string]string{batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: "1a2b3c", batch.LegacyJobNameLabel: "myjob"}, |
| }, |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyOnFailure, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| opts: JobValidationOptions{RequirePrefixedLabels: true}, |
| }, |
| } |
| |
| for k, v := range errorCases { |
| t.Run(k, func(t *testing.T) { |
| errs := ValidateJob(&v.job, v.opts) |
| if len(errs) == 0 { |
| t.Errorf("expected failure for %s", k) |
| } else { |
| s := strings.SplitN(k, ":", 2) |
| err := errs[0] |
| if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { |
| t.Errorf("unexpected error: %v, expected: %s", err, k) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestValidateJobUpdate(t *testing.T) { |
| validGeneratedSelector := getValidGeneratedSelector() |
| validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) |
| validPodTemplateSpecForGeneratedRestartPolicyNever := getValidPodTemplateSpecForGenerated(validGeneratedSelector) |
| validPodTemplateSpecForGeneratedRestartPolicyNever.Spec.RestartPolicy = api.RestartPolicyNever |
| |
| validNodeAffinity := &api.Affinity{ |
| NodeAffinity: &api.NodeAffinity{ |
| RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ |
| NodeSelectorTerms: []api.NodeSelectorTerm{{ |
| MatchExpressions: []api.NodeSelectorRequirement{{ |
| Key: "foo", |
| Operator: api.NodeSelectorOpIn, |
| Values: []string{"bar", "value2"}, |
| }}, |
| }}, |
| }, |
| }, |
| } |
| validPodTemplateWithAffinity := getValidPodTemplateSpecForGenerated(validGeneratedSelector) |
| validPodTemplateWithAffinity.Spec.Affinity = &api.Affinity{ |
| NodeAffinity: &api.NodeAffinity{ |
| RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ |
| NodeSelectorTerms: []api.NodeSelectorTerm{{ |
| MatchExpressions: []api.NodeSelectorRequirement{{ |
| Key: "foo", |
| Operator: api.NodeSelectorOpIn, |
| Values: []string{"bar", "value"}, |
| }}, |
| }}, |
| }, |
| }, |
| } |
| // This is to test immutability of the selector, both the new and old |
| // selector should match the labels in the template, which is immutable |
| // on its own; therfore, the only way to test selector immutability is |
| // when the new selector is changed but still matches the existing labels. |
| newSelector := getValidGeneratedSelector() |
| newSelector.MatchLabels["foo"] = "bar" |
| validTolerations := []api.Toleration{{ |
| Key: "foo", |
| Operator: api.TolerationOpEqual, |
| Value: "bar", |
| Effect: api.TaintEffectPreferNoSchedule, |
| }} |
| cases := map[string]struct { |
| old batch.Job |
| update func(*batch.Job) |
| opts JobValidationOptions |
| err *field.Error |
| }{ |
| "mutable fields": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Parallelism: pointer.Int32(5), |
| ActiveDeadlineSeconds: pointer.Int64(2), |
| TTLSecondsAfterFinished: pointer.Int32(1), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Parallelism = pointer.Int32(2) |
| job.Spec.ActiveDeadlineSeconds = pointer.Int64(3) |
| job.Spec.TTLSecondsAfterFinished = pointer.Int32(2) |
| job.Spec.ManualSelector = pointer.Bool(true) |
| }, |
| }, |
| "invalid attempt to set managedBy field": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.ManagedBy = ptr.To("example.com/custom-controller") |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.managedBy", |
| }, |
| }, |
| "invalid update of the managedBy field": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| ManagedBy: ptr.To("example.com/custom-controller1"), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.ManagedBy = ptr.To("example.com/custom-controller2") |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.managedBy", |
| }, |
| }, |
| "immutable completions for non-indexed jobs": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(1) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.completions", |
| }, |
| }, |
| "immutable completions for indexed job when AllowElasticIndexedJobs is false": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(1) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.completions", |
| }, |
| }, |
| "immutable selector": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: getValidPodTemplateSpecForGenerated(newSelector), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Selector = newSelector |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.selector", |
| }, |
| }, |
| "add success policy": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.SuccessPolicy = &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededCount: ptr.To[int32](2), |
| }}, |
| } |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.successPolicy", |
| }, |
| }, |
| "update success policy": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededIndexes: ptr.To("1-3"), |
| }}, |
| }, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.SuccessPolicy.Rules = append(job.Spec.SuccessPolicy.Rules, batch.SuccessPolicyRule{ |
| SucceededCount: ptr.To[int32](3), |
| }) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.successPolicy", |
| }, |
| }, |
| "remove success policy": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: ptr.To[int32](5), |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| SuccessPolicy: &batch.SuccessPolicy{ |
| Rules: []batch.SuccessPolicyRule{{ |
| SucceededIndexes: ptr.To("1-3"), |
| }}, |
| }, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.SuccessPolicy = nil |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.successPolicy", |
| }, |
| }, |
| "add pod failure policy": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.PodFailurePolicy = &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }}, |
| } |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.podFailurePolicy", |
| }, |
| }, |
| "update pod failure policy": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.PodFailurePolicy.Rules = append(job.Spec.PodFailurePolicy.Rules, batch.PodFailurePolicyRule{ |
| Action: batch.PodFailurePolicyActionCount, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.podFailurePolicy", |
| }, |
| }, |
| "remove pod failure policy": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| PodFailurePolicy: &batch.PodFailurePolicy{ |
| Rules: []batch.PodFailurePolicyRule{{ |
| Action: batch.PodFailurePolicyActionIgnore, |
| OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{{ |
| Type: api.DisruptionTarget, |
| Status: api.ConditionTrue, |
| }}, |
| }}, |
| }, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.PodFailurePolicy = nil |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.podFailurePolicy", |
| }, |
| }, |
| "set backoff limit per index": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.BackoffLimitPerIndex = pointer.Int32(1) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.backoffLimitPerIndex", |
| }, |
| }, |
| "unset backoff limit per index": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.BackoffLimitPerIndex = nil |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.backoffLimitPerIndex", |
| }, |
| }, |
| "update backoff limit per index": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.BackoffLimitPerIndex = pointer.Int32(2) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.backoffLimitPerIndex", |
| }, |
| }, |
| "set max failed indexes": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.MaxFailedIndexes = pointer.Int32(1) |
| }, |
| }, |
| "unset max failed indexes": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| MaxFailedIndexes: pointer.Int32(1), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.MaxFailedIndexes = nil |
| }, |
| }, |
| "update max failed indexes": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGeneratedRestartPolicyNever, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| BackoffLimitPerIndex: pointer.Int32(1), |
| MaxFailedIndexes: pointer.Int32(1), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.MaxFailedIndexes = pointer.Int32(2) |
| }, |
| }, |
| "immutable pod template": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(3), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.DNSPolicy = api.DNSClusterFirstWithHostNet |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "immutable completion mode": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| Completions: pointer.Int32(2), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.CompletionMode = completionModePtr(batch.NonIndexedCompletion) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.completionMode", |
| }, |
| }, |
| "immutable completions for non-indexed job when AllowElasticIndexedJobs is true": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| CompletionMode: completionModePtr(batch.NonIndexedCompletion), |
| Completions: pointer.Int32(2), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(4) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.completions", |
| }, |
| opts: JobValidationOptions{AllowElasticIndexedJobs: true}, |
| }, |
| |
| "immutable node affinity": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Affinity = validNodeAffinity |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "add node affinity": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Affinity = validNodeAffinity |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "update node affinity": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateWithAffinity, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Affinity = validNodeAffinity |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "remove node affinity": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateWithAffinity, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Affinity.NodeAffinity = nil |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "remove affinity": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateWithAffinity, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Affinity = nil |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "immutable tolerations": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Tolerations = validTolerations |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "mutable tolerations": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.Tolerations = validTolerations |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "immutable node selector": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "mutable node selector": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.NodeSelector = map[string]string{"foo": "bar"} |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "immutable annotations": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Annotations = map[string]string{"foo": "baz"} |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "mutable annotations": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Annotations = map[string]string{"foo": "baz"} |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "immutable labels": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| newLabels := getValidGeneratedSelector().MatchLabels |
| newLabels["bar"] = "baz" |
| job.Spec.Template.Labels = newLabels |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "mutable labels": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| newLabels := getValidGeneratedSelector().MatchLabels |
| newLabels["bar"] = "baz" |
| job.Spec.Template.Labels = newLabels |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "immutable schedulingGates": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.SchedulingGates = append(job.Spec.Template.Spec.SchedulingGates, api.PodSchedulingGate{Name: "gate"}) |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.template", |
| }, |
| }, |
| "mutable schedulingGates": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Template.Spec.SchedulingGates = append(job.Spec.Template.Spec.SchedulingGates, api.PodSchedulingGate{Name: "gate"}) |
| }, |
| opts: JobValidationOptions{ |
| AllowMutableSchedulingDirectives: true, |
| }, |
| }, |
| "update completions and parallelism to same value is valid": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(1), |
| Parallelism: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(2) |
| job.Spec.Parallelism = pointer.Int32(2) |
| }, |
| opts: JobValidationOptions{ |
| AllowElasticIndexedJobs: true, |
| }, |
| }, |
| "previous parallelism != previous completions, new parallelism == new completions": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(1), |
| Parallelism: pointer.Int32(2), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(3) |
| job.Spec.Parallelism = pointer.Int32(3) |
| }, |
| opts: JobValidationOptions{ |
| AllowElasticIndexedJobs: true, |
| }, |
| }, |
| "indexed job updating completions and parallelism to different values is invalid": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(1), |
| Parallelism: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(2) |
| job.Spec.Parallelism = pointer.Int32(3) |
| }, |
| opts: JobValidationOptions{ |
| AllowElasticIndexedJobs: true, |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeInvalid, |
| Field: "spec.completions", |
| }, |
| }, |
| "indexed job with completions set updated to nil does not panic": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(1), |
| Parallelism: pointer.Int32(1), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = nil |
| job.Spec.Parallelism = pointer.Int32(3) |
| }, |
| opts: JobValidationOptions{ |
| AllowElasticIndexedJobs: true, |
| }, |
| err: &field.Error{ |
| Type: field.ErrorTypeRequired, |
| Field: "spec.completions", |
| }, |
| }, |
| "indexed job with completions unchanged, parallelism reduced to less than completions": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(2), |
| Parallelism: pointer.Int32(2), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(2) |
| job.Spec.Parallelism = pointer.Int32(1) |
| }, |
| opts: JobValidationOptions{ |
| AllowElasticIndexedJobs: true, |
| }, |
| }, |
| "indexed job with completions unchanged, parallelism increased higher than completions": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, |
| Spec: batch.JobSpec{ |
| Selector: validGeneratedSelector, |
| Template: validPodTemplateSpecForGenerated, |
| Completions: pointer.Int32(2), |
| Parallelism: pointer.Int32(2), |
| CompletionMode: completionModePtr(batch.IndexedCompletion), |
| }, |
| }, |
| update: func(job *batch.Job) { |
| job.Spec.Completions = pointer.Int32(2) |
| job.Spec.Parallelism = pointer.Int32(3) |
| }, |
| opts: JobValidationOptions{ |
| AllowElasticIndexedJobs: true, |
| }, |
| }, |
| } |
| ignoreValueAndDetail := cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") |
| for k, tc := range cases { |
| t.Run(k, func(t *testing.T) { |
| tc.old.ResourceVersion = "1" |
| update := tc.old.DeepCopy() |
| tc.update(update) |
| errs := ValidateJobUpdate(update, &tc.old, tc.opts) |
| var wantErrs field.ErrorList |
| if tc.err != nil { |
| wantErrs = append(wantErrs, tc.err) |
| } |
| if diff := cmp.Diff(wantErrs, errs, ignoreValueAndDetail); diff != "" { |
| t.Errorf("Unexpected validation errors (-want,+got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestValidateJobUpdateStatus(t *testing.T) { |
| cases := map[string]struct { |
| opts JobStatusValidationOptions |
| |
| old batch.Job |
| update batch.Job |
| wantErrs field.ErrorList |
| }{ |
| "valid": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "1", |
| }, |
| Status: batch.JobStatus{ |
| Active: 1, |
| Succeeded: 2, |
| Failed: 3, |
| Terminating: pointer.Int32(4), |
| }, |
| }, |
| update: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "1", |
| }, |
| Status: batch.JobStatus{ |
| Active: 2, |
| Succeeded: 3, |
| Failed: 4, |
| Ready: pointer.Int32(1), |
| Terminating: pointer.Int32(4), |
| }, |
| }, |
| }, |
| "nil ready and terminating": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "1", |
| }, |
| Status: batch.JobStatus{ |
| Active: 1, |
| Succeeded: 2, |
| Failed: 3, |
| }, |
| }, |
| update: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "1", |
| }, |
| Status: batch.JobStatus{ |
| Active: 2, |
| Succeeded: 3, |
| Failed: 4, |
| }, |
| }, |
| }, |
| "negative counts": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "10", |
| }, |
| Status: batch.JobStatus{ |
| Active: 1, |
| Succeeded: 2, |
| Failed: 3, |
| Terminating: pointer.Int32(4), |
| }, |
| }, |
| update: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "10", |
| }, |
| Status: batch.JobStatus{ |
| Active: -1, |
| Succeeded: -2, |
| Failed: -3, |
| Ready: pointer.Int32(-1), |
| Terminating: pointer.Int32(-2), |
| }, |
| }, |
| wantErrs: field.ErrorList{ |
| {Type: field.ErrorTypeInvalid, Field: "status.active"}, |
| {Type: field.ErrorTypeInvalid, Field: "status.succeeded"}, |
| {Type: field.ErrorTypeInvalid, Field: "status.failed"}, |
| {Type: field.ErrorTypeInvalid, Field: "status.ready"}, |
| {Type: field.ErrorTypeInvalid, Field: "status.terminating"}, |
| }, |
| }, |
| "empty and duplicated uncounted pods": { |
| old: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "5", |
| }, |
| }, |
| update: batch.Job{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "abc", |
| Namespace: metav1.NamespaceDefault, |
| ResourceVersion: "5", |
| }, |
| Status: batch.JobStatus{ |
| UncountedTerminatedPods: &batch.UncountedTerminatedPods{ |
| Succeeded: []types.UID{"a", "b", "c", "a", ""}, |
| Failed: []types.UID{"c", "d", "e", "d", ""}, |
| }, |
| }, |
| }, |
| wantErrs: field.ErrorList{ |
| {Type: field.ErrorTypeDuplicate, Field: "status.uncountedTerminatedPods.succeeded[3]"}, |
| {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods.succeeded[4]"}, |
| {Type: field.ErrorTypeDuplicate, Field: "status.uncountedTerminatedPods.failed[0]"}, |
| {Type: field.ErrorTypeDuplicate, Field: "status.uncountedTerminatedPods.failed[3]"}, |
| {Type: field.ErrorTypeInvalid, Field: "status.uncountedTerminatedPods.failed[4]"}, |
| }, |
| }, |
| } |
| for name, tc := range cases { |
| t.Run(name, func(t *testing.T) { |
| errs := ValidateJobUpdateStatus(&tc.update, &tc.old, tc.opts) |
| if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" { |
| t.Errorf("Unexpected errors (-want,+got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestValidateCronJob(t *testing.T) { |
| validManualSelector := getValidManualSelector() |
| validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) |
| validPodTemplateSpec.Labels = map[string]string{} |
| validHostNetPodTemplateSpec := func() api.PodTemplateSpec { |
| spec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) |
| spec.Spec.SecurityContext = &api.PodSecurityContext{ |
| HostNetwork: true, |
| } |
| spec.Spec.Containers[0].Ports = []api.ContainerPort{{ |
| ContainerPort: 12345, |
| Protocol: api.ProtocolTCP, |
| }} |
| return spec |
| }() |
| |
| successCases := map[string]batch.CronJob{ |
| "basic scheduled job": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "hostnet job": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validHostNetPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "non-standard scheduled": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "@hourly", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "correct timeZone value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneCorrect, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| } |
| for k, v := range successCases { |
| t.Run(k, func(t *testing.T) { |
| if errs := ValidateCronJobCreate(&v, corevalidation.PodValidationOptions{}); len(errs) != 0 { |
| t.Errorf("expected success for %s: %v", k, errs) |
| } |
| |
| // Update validation should pass same success cases |
| // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update |
| v = *v.DeepCopy() |
| v.ResourceVersion = "1" |
| if errs := ValidateCronJobUpdate(&v, &v, corevalidation.PodValidationOptions{}); len(errs) != 0 { |
| t.Errorf("expected success for %s: %v", k, errs) |
| } |
| }) |
| } |
| |
| negative := int32(-1) |
| negative64 := int64(-1) |
| |
| errorCases := map[string]batch.CronJob{ |
| "spec.schedule: Invalid value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "error", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.schedule: Required value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.timeZone: timeZone must be nil or non-empty string": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneEmpty, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.timeZone: timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneLocal, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.timeZone: Invalid value: \" Continent/Zone\": unknown time zone Continent/Zone": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneBadPrefix, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.timeZone: Invalid value: \"Continent/InvalidZone\": unknown time zone Continent/InvalidZone": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneBadName, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.timeZone: Invalid value: \" \": unknown time zone ": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneEmptySpace, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.timeZone: Invalid value: \"Continent/Zone \": unknown time zone Continent/Zone ": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: &timeZoneBadSuffix, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.startingDeadlineSeconds:must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| StartingDeadlineSeconds: &negative64, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.successfulJobsHistoryLimit: must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| SuccessfulJobsHistoryLimit: &negative, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.failedJobsHistoryLimit: must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| FailedJobsHistoryLimit: &negative, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.concurrencyPolicy: Required value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.parallelism:must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Parallelism: &negative, |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.completions:must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| |
| Spec: batch.JobSpec{ |
| Completions: &negative, |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.activeDeadlineSeconds:must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| ActiveDeadlineSeconds: &negative64, |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.selector: Invalid value: {\"matchLabels\":{\"a\":\"b\"}}: `selector` will be auto-generated": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Selector: validManualSelector, |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "metadata.name: must be no more than 52 characters": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "10000000002000000000300000000040000000005000000000123", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.manualSelector: Unsupported value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| ManualSelector: pointer.Bool(true), |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.template.spec.restartPolicy: Required value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: api.PodTemplateSpec{ |
| Spec: api.PodSpec{ |
| RestartPolicy: api.RestartPolicyAlways, |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.template.spec.restartPolicy: Unsupported value": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: api.PodTemplateSpec{ |
| Spec: api.PodSpec{ |
| RestartPolicy: "Invalid", |
| DNSPolicy: api.DNSClusterFirst, |
| Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "spec.jobTemplate.spec.ttlSecondsAfterFinished:must be greater than or equal to 0": { |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: "* * * * ?", |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| TTLSecondsAfterFinished: &negative, |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| for k, v := range errorCases { |
| t.Run(k, func(t *testing.T) { |
| errs := ValidateCronJobCreate(&v, corevalidation.PodValidationOptions{}) |
| if len(errs) == 0 { |
| t.Errorf("expected failure for %s", k) |
| } else { |
| s := strings.Split(k, ":") |
| err := errs[0] |
| if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { |
| t.Errorf("unexpected error: %v, expected: %s", err, k) |
| } |
| } |
| |
| // Update validation should fail all failure cases other than the 52 character name limit |
| // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update |
| oldSpec := *v.DeepCopy() |
| oldSpec.ResourceVersion = "1" |
| oldSpec.Spec.TimeZone = nil |
| |
| newSpec := *v.DeepCopy() |
| newSpec.ResourceVersion = "2" |
| |
| errs = ValidateCronJobUpdate(&newSpec, &oldSpec, corevalidation.PodValidationOptions{}) |
| if len(errs) == 0 { |
| if k == "metadata.name: must be no more than 52 characters" { |
| return |
| } |
| t.Errorf("expected failure for %s", k) |
| } else { |
| s := strings.Split(k, ":") |
| err := errs[0] |
| if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { |
| t.Errorf("unexpected error: %v, expected: %s", err, k) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestValidateCronJobScheduleTZ(t *testing.T) { |
| validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) |
| validPodTemplateSpec.Labels = map[string]string{} |
| validSchedule := "0 * * * *" |
| invalidSchedule := "TZ=UTC 0 * * * *" |
| invalidCronJob := &batch.CronJob{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: invalidSchedule, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| } |
| validCronJob := &batch.CronJob{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "mycronjob", |
| Namespace: metav1.NamespaceDefault, |
| UID: types.UID("1a2b3c"), |
| }, |
| Spec: batch.CronJobSpec{ |
| Schedule: validSchedule, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| } |
| |
| testCases := map[string]struct { |
| cronJob *batch.CronJob |
| createErr string |
| update func(*batch.CronJob) |
| updateErr string |
| }{ |
| "update removing TZ should work": { |
| cronJob: invalidCronJob, |
| createErr: "cannot use TZ or CRON_TZ in schedule", |
| update: func(cj *batch.CronJob) { |
| cj.Spec.Schedule = validSchedule |
| }, |
| }, |
| "update not modifying TZ should work": { |
| cronJob: invalidCronJob, |
| createErr: "cannot use TZ or CRON_TZ in schedule, use timeZone field instead", |
| update: func(cj *batch.CronJob) { |
| cj.Spec.Schedule = invalidSchedule |
| }, |
| }, |
| "update not modifying TZ but adding .spec.timeZone should fail": { |
| cronJob: invalidCronJob, |
| createErr: "cannot use TZ or CRON_TZ in schedule, use timeZone field instead", |
| update: func(cj *batch.CronJob) { |
| cj.Spec.TimeZone = &timeZoneUTC |
| }, |
| updateErr: "cannot use both timeZone field and TZ or CRON_TZ in schedule", |
| }, |
| "update adding TZ should fail": { |
| cronJob: validCronJob, |
| update: func(cj *batch.CronJob) { |
| cj.Spec.Schedule = invalidSchedule |
| }, |
| updateErr: "cannot use TZ or CRON_TZ in schedule", |
| }, |
| } |
| |
| for k, v := range testCases { |
| t.Run(k, func(t *testing.T) { |
| errs := ValidateCronJobCreate(v.cronJob, corevalidation.PodValidationOptions{}) |
| if len(errs) > 0 { |
| err := errs[0] |
| if len(v.createErr) == 0 { |
| t.Errorf("unexpected error: %#v, none expected", err) |
| return |
| } |
| if !strings.Contains(err.Error(), v.createErr) { |
| t.Errorf("unexpected error: %v, expected: %s", err, v.createErr) |
| } |
| } else if len(v.createErr) != 0 { |
| t.Errorf("no error, expected %v", v.createErr) |
| return |
| } |
| |
| oldSpec := v.cronJob.DeepCopy() |
| oldSpec.ResourceVersion = "1" |
| |
| newSpec := v.cronJob.DeepCopy() |
| newSpec.ResourceVersion = "2" |
| if v.update != nil { |
| v.update(newSpec) |
| } |
| |
| errs = ValidateCronJobUpdate(newSpec, oldSpec, corevalidation.PodValidationOptions{}) |
| if len(errs) > 0 { |
| err := errs[0] |
| if len(v.updateErr) == 0 { |
| t.Errorf("unexpected error: %#v, none expected", err) |
| return |
| } |
| if !strings.Contains(err.Error(), v.updateErr) { |
| t.Errorf("unexpected error: %v, expected: %s", err, v.updateErr) |
| } |
| } else if len(v.updateErr) != 0 { |
| t.Errorf("no error, expected %v", v.updateErr) |
| return |
| } |
| }) |
| } |
| } |
| |
| func TestValidateCronJobSpec(t *testing.T) { |
| validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) |
| validPodTemplateSpec.Labels = map[string]string{} |
| |
| type testCase struct { |
| old *batch.CronJobSpec |
| new *batch.CronJobSpec |
| expectErr bool |
| } |
| |
| cases := map[string]testCase{ |
| "no validation because timeZone is nil for old and new": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: nil, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: nil, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "check validation because timeZone is different for new": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: nil, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("America/New_York"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "check validation because timeZone is different for new and invalid": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: nil, |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| expectErr: true, |
| }, |
| "old timeZone and new timeZone are valid": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("America/New_York"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("America/Chicago"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "old timeZone is valid, but new timeZone is invalid": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("America/New_York"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| expectErr: true, |
| }, |
| "old timeZone and new timeZone are invalid, but unchanged": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| "old timeZone and new timeZone are invalid, but different": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("still broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| expectErr: true, |
| }, |
| "old timeZone is invalid, but new timeZone is valid": { |
| old: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("broken"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| new: &batch.CronJobSpec{ |
| Schedule: "0 * * * *", |
| TimeZone: pointer.String("America/New_York"), |
| ConcurrencyPolicy: batch.AllowConcurrent, |
| JobTemplate: batch.JobTemplateSpec{ |
| Spec: batch.JobSpec{ |
| Template: validPodTemplateSpec, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| for k, v := range cases { |
| errs := validateCronJobSpec(v.new, v.old, field.NewPath("spec"), corevalidation.PodValidationOptions{}) |
| if len(errs) > 0 && !v.expectErr { |
| t.Errorf("unexpected error for %s: %v", k, errs) |
| } else if len(errs) == 0 && v.expectErr { |
| t.Errorf("expected error for %s but got nil", k) |
| } |
| } |
| } |
| |
| func completionModePtr(m batch.CompletionMode) *batch.CompletionMode { |
| return &m |
| } |
| |
| func TestTimeZones(t *testing.T) { |
| // all valid time zones as of go1.19 release on 2022-08-02 |
| data := []string{ |
| `Africa/Abidjan`, |
| `Africa/Accra`, |
| `Africa/Addis_Ababa`, |
| `Africa/Algiers`, |
| `Africa/Asmara`, |
| `Africa/Asmera`, |
| `Africa/Bamako`, |
| `Africa/Bangui`, |
| `Africa/Banjul`, |
| `Africa/Bissau`, |
| `Africa/Blantyre`, |
| `Africa/Brazzaville`, |
| `Africa/Bujumbura`, |
| `Africa/Cairo`, |
| `Africa/Casablanca`, |
| `Africa/Ceuta`, |
| `Africa/Conakry`, |
| `Africa/Dakar`, |
| `Africa/Dar_es_Salaam`, |
| `Africa/Djibouti`, |
| `Africa/Douala`, |
| `Africa/El_Aaiun`, |
| `Africa/Freetown`, |
| `Africa/Gaborone`, |
| `Africa/Harare`, |
| `Africa/Johannesburg`, |
| `Africa/Juba`, |
| `Africa/Kampala`, |
| `Africa/Khartoum`, |
| `Africa/Kigali`, |
| `Africa/Kinshasa`, |
| `Africa/Lagos`, |
| `Africa/Libreville`, |
| `Africa/Lome`, |
| `Africa/Luanda`, |
| `Africa/Lubumbashi`, |
| `Africa/Lusaka`, |
| `Africa/Malabo`, |
| `Africa/Maputo`, |
| `Africa/Maseru`, |
| `Africa/Mbabane`, |
| `Africa/Mogadishu`, |
| `Africa/Monrovia`, |
| `Africa/Nairobi`, |
| `Africa/Ndjamena`, |
| `Africa/Niamey`, |
| `Africa/Nouakchott`, |
| `Africa/Ouagadougou`, |
| `Africa/Porto-Novo`, |
| `Africa/Sao_Tome`, |
| `Africa/Timbuktu`, |
| `Africa/Tripoli`, |
| `Africa/Tunis`, |
| `Africa/Windhoek`, |
| `America/Adak`, |
| `America/Anchorage`, |
| `America/Anguilla`, |
| `America/Antigua`, |
| `America/Araguaina`, |
| `America/Argentina/Buenos_Aires`, |
| `America/Argentina/Catamarca`, |
| `America/Argentina/ComodRivadavia`, |
| `America/Argentina/Cordoba`, |
| `America/Argentina/Jujuy`, |
| `America/Argentina/La_Rioja`, |
| `America/Argentina/Mendoza`, |
| `America/Argentina/Rio_Gallegos`, |
| `America/Argentina/Salta`, |
| `America/Argentina/San_Juan`, |
| `America/Argentina/San_Luis`, |
| `America/Argentina/Tucuman`, |
| `America/Argentina/Ushuaia`, |
| `America/Aruba`, |
| `America/Asuncion`, |
| `America/Atikokan`, |
| `America/Atka`, |
| `America/Bahia`, |
| `America/Bahia_Banderas`, |
| `America/Barbados`, |
| `America/Belem`, |
| `America/Belize`, |
| `America/Blanc-Sablon`, |
| `America/Boa_Vista`, |
| `America/Bogota`, |
| `America/Boise`, |
| `America/Buenos_Aires`, |
| `America/Cambridge_Bay`, |
| `America/Campo_Grande`, |
| `America/Cancun`, |
| `America/Caracas`, |
| `America/Catamarca`, |
| `America/Cayenne`, |
| `America/Cayman`, |
| `America/Chicago`, |
| `America/Chihuahua`, |
| `America/Coral_Harbour`, |
| `America/Cordoba`, |
| `America/Costa_Rica`, |
| `America/Creston`, |
| `America/Cuiaba`, |
| `America/Curacao`, |
| `America/Danmarkshavn`, |
| `America/Dawson`, |
| `America/Dawson_Creek`, |
| `America/Denver`, |
| `America/Detroit`, |
| `America/Dominica`, |
| `America/Edmonton`, |
| `America/Eirunepe`, |
| `America/El_Salvador`, |
| `America/Ensenada`, |
| `America/Fort_Nelson`, |
| `America/Fort_Wayne`, |
| `America/Fortaleza`, |
| `America/Glace_Bay`, |
| `America/Godthab`, |
| `America/Goose_Bay`, |
| `America/Grand_Turk`, |
| `America/Grenada`, |
| `America/Guadeloupe`, |
| `America/Guatemala`, |
| `America/Guayaquil`, |
| `America/Guyana`, |
| `America/Halifax`, |
| `America/Havana`, |
| `America/Hermosillo`, |
| `America/Indiana/Indianapolis`, |
| `America/Indiana/Knox`, |
| `America/Indiana/Marengo`, |
| `America/Indiana/Petersburg`, |
| `America/Indiana/Tell_City`, |
| `America/Indiana/Vevay`, |
| `America/Indiana/Vincennes`, |
| `America/Indiana/Winamac`, |
| `America/Indianapolis`, |
| `America/Inuvik`, |
| `America/Iqaluit`, |
| `America/Jamaica`, |
| `America/Jujuy`, |
| `America/Juneau`, |
| `America/Kentucky/Louisville`, |
| `America/Kentucky/Monticello`, |
| `America/Knox_IN`, |
| `America/Kralendijk`, |
| `America/La_Paz`, |
| `America/Lima`, |
| `America/Los_Angeles`, |
| `America/Louisville`, |
| `America/Lower_Princes`, |
| `America/Maceio`, |
| `America/Managua`, |
| `America/Manaus`, |
| `America/Marigot`, |
| `America/Martinique`, |
| `America/Matamoros`, |
| `America/Mazatlan`, |
| `America/Mendoza`, |
| `America/Menominee`, |
| `America/Merida`, |
| `America/Metlakatla`, |
| `America/Mexico_City`, |
| `America/Miquelon`, |
| `America/Moncton`, |
| `America/Monterrey`, |
| `America/Montevideo`, |
| `America/Montreal`, |
| `America/Montserrat`, |
| `America/Nassau`, |
| `America/New_York`, |
| `America/Nipigon`, |
| `America/Nome`, |
| `America/Noronha`, |
| `America/North_Dakota/Beulah`, |
| `America/North_Dakota/Center`, |
| `America/North_Dakota/New_Salem`, |
| `America/Nuuk`, |
| `America/Ojinaga`, |
| `America/Panama`, |
| `America/Pangnirtung`, |
| `America/Paramaribo`, |
| `America/Phoenix`, |
| `America/Port-au-Prince`, |
| `America/Port_of_Spain`, |
| `America/Porto_Acre`, |
| `America/Porto_Velho`, |
| `America/Puerto_Rico`, |
| `America/Punta_Arenas`, |
| `America/Rainy_River`, |
| `America/Rankin_Inlet`, |
| `America/Recife`, |
| `America/Regina`, |
| `America/Resolute`, |
| `America/Rio_Branco`, |
| `America/Rosario`, |
| `America/Santa_Isabel`, |
| `America/Santarem`, |
| `America/Santiago`, |
| `America/Santo_Domingo`, |
| `America/Sao_Paulo`, |
| `America/Scoresbysund`, |
| `America/Shiprock`, |
| `America/Sitka`, |
| `America/St_Barthelemy`, |
| `America/St_Johns`, |
| `America/St_Kitts`, |
| `America/St_Lucia`, |
| `America/St_Thomas`, |
| `America/St_Vincent`, |
| `America/Swift_Current`, |
| `America/Tegucigalpa`, |
| `America/Thule`, |
| `America/Thunder_Bay`, |
| `America/Tijuana`, |
| `America/Toronto`, |
| `America/Tortola`, |
| `America/Vancouver`, |
| `America/Virgin`, |
| `America/Whitehorse`, |
| `America/Winnipeg`, |
| `America/Yakutat`, |
| `America/Yellowknife`, |
| `Antarctica/Casey`, |
| `Antarctica/Davis`, |
| `Antarctica/DumontDUrville`, |
| `Antarctica/Macquarie`, |
| `Antarctica/Mawson`, |
| `Antarctica/McMurdo`, |
| `Antarctica/Palmer`, |
| `Antarctica/Rothera`, |
| `Antarctica/South_Pole`, |
| `Antarctica/Syowa`, |
| `Antarctica/Troll`, |
| `Antarctica/Vostok`, |
| `Arctic/Longyearbyen`, |
| `Asia/Aden`, |
| `Asia/Almaty`, |
| `Asia/Amman`, |
| `Asia/Anadyr`, |
| `Asia/Aqtau`, |
| `Asia/Aqtobe`, |
| `Asia/Ashgabat`, |
| `Asia/Ashkhabad`, |
| `Asia/Atyrau`, |
| `Asia/Baghdad`, |
| `Asia/Bahrain`, |
| `Asia/Baku`, |
| `Asia/Bangkok`, |
| `Asia/Barnaul`, |
| `Asia/Beirut`, |
| `Asia/Bishkek`, |
| `Asia/Brunei`, |
| `Asia/Calcutta`, |
| `Asia/Chita`, |
| `Asia/Choibalsan`, |
| `Asia/Chongqing`, |
| `Asia/Chungking`, |
| `Asia/Colombo`, |
| `Asia/Dacca`, |
| `Asia/Damascus`, |
| `Asia/Dhaka`, |
| `Asia/Dili`, |
| `Asia/Dubai`, |
| `Asia/Dushanbe`, |
| `Asia/Famagusta`, |
| `Asia/Gaza`, |
| `Asia/Harbin`, |
| `Asia/Hebron`, |
| `Asia/Ho_Chi_Minh`, |
| `Asia/Hong_Kong`, |
| `Asia/Hovd`, |
| `Asia/Irkutsk`, |
| `Asia/Istanbul`, |
| `Asia/Jakarta`, |
| `Asia/Jayapura`, |
| `Asia/Jerusalem`, |
| `Asia/Kabul`, |
| `Asia/Kamchatka`, |
| `Asia/Karachi`, |
| `Asia/Kashgar`, |
| `Asia/Kathmandu`, |
| `Asia/Katmandu`, |
| `Asia/Khandyga`, |
| `Asia/Kolkata`, |
| `Asia/Krasnoyarsk`, |
| `Asia/Kuala_Lumpur`, |
| `Asia/Kuching`, |
| `Asia/Kuwait`, |
| `Asia/Macao`, |
| `Asia/Macau`, |
| `Asia/Magadan`, |
| `Asia/Makassar`, |
| `Asia/Manila`, |
| `Asia/Muscat`, |
| `Asia/Nicosia`, |
| `Asia/Novokuznetsk`, |
| `Asia/Novosibirsk`, |
| `Asia/Omsk`, |
| `Asia/Oral`, |
| `Asia/Phnom_Penh`, |
| `Asia/Pontianak`, |
| `Asia/Pyongyang`, |
| `Asia/Qatar`, |
| `Asia/Qostanay`, |
| `Asia/Qyzylorda`, |
| `Asia/Rangoon`, |
| `Asia/Riyadh`, |
| `Asia/Saigon`, |
| `Asia/Sakhalin`, |
| `Asia/Samarkand`, |
| `Asia/Seoul`, |
| `Asia/Shanghai`, |
| `Asia/Singapore`, |
| `Asia/Srednekolymsk`, |
| `Asia/Taipei`, |
| `Asia/Tashkent`, |
| `Asia/Tbilisi`, |
| `Asia/Tehran`, |
| `Asia/Tel_Aviv`, |
| `Asia/Thimbu`, |
| `Asia/Thimphu`, |
| `Asia/Tokyo`, |
| `Asia/Tomsk`, |
| `Asia/Ujung_Pandang`, |
| `Asia/Ulaanbaatar`, |
| `Asia/Ulan_Bator`, |
| `Asia/Urumqi`, |
| `Asia/Ust-Nera`, |
| `Asia/Vientiane`, |
| `Asia/Vladivostok`, |
| `Asia/Yakutsk`, |
| `Asia/Yangon`, |
| `Asia/Yekaterinburg`, |
| `Asia/Yerevan`, |
| `Atlantic/Azores`, |
| `Atlantic/Bermuda`, |
| `Atlantic/Canary`, |
| `Atlantic/Cape_Verde`, |
| `Atlantic/Faeroe`, |
| `Atlantic/Faroe`, |
| `Atlantic/Jan_Mayen`, |
| `Atlantic/Madeira`, |
| `Atlantic/Reykjavik`, |
| `Atlantic/South_Georgia`, |
| `Atlantic/St_Helena`, |
| `Atlantic/Stanley`, |
| `Australia/ACT`, |
| `Australia/Adelaide`, |
| `Australia/Brisbane`, |
| `Australia/Broken_Hill`, |
| `Australia/Canberra`, |
| `Australia/Currie`, |
| `Australia/Darwin`, |
| `Australia/Eucla`, |
| `Australia/Hobart`, |
| `Australia/LHI`, |
| `Australia/Lindeman`, |
| `Australia/Lord_Howe`, |
| `Australia/Melbourne`, |
| `Australia/North`, |
| `Australia/NSW`, |
| `Australia/Perth`, |
| `Australia/Queensland`, |
| `Australia/South`, |
| `Australia/Sydney`, |
| `Australia/Tasmania`, |
| `Australia/Victoria`, |
| `Australia/West`, |
| `Australia/Yancowinna`, |
| `Brazil/Acre`, |
| `Brazil/DeNoronha`, |
| `Brazil/East`, |
| `Brazil/West`, |
| `Canada/Atlantic`, |
| `Canada/Central`, |
| `Canada/Eastern`, |
| `Canada/Mountain`, |
| `Canada/Newfoundland`, |
| `Canada/Pacific`, |
| `Canada/Saskatchewan`, |
| `Canada/Yukon`, |
| `CET`, |
| `Chile/Continental`, |
| `Chile/EasterIsland`, |
| `CST6CDT`, |
| `Cuba`, |
| `EET`, |
| `Egypt`, |
| `Eire`, |
| `EST`, |
| `EST5EDT`, |
| `Etc/GMT`, |
| `Etc/GMT+0`, |
| `Etc/GMT+1`, |
| `Etc/GMT+10`, |
| `Etc/GMT+11`, |
| `Etc/GMT+12`, |
| `Etc/GMT+2`, |
| `Etc/GMT+3`, |
| `Etc/GMT+4`, |
| `Etc/GMT+5`, |
| `Etc/GMT+6`, |
| `Etc/GMT+7`, |
| `Etc/GMT+8`, |
| `Etc/GMT+9`, |
| `Etc/GMT-0`, |
| `Etc/GMT-1`, |
| `Etc/GMT-10`, |
| `Etc/GMT-11`, |
| `Etc/GMT-12`, |
| `Etc/GMT-13`, |
| `Etc/GMT-14`, |
| `Etc/GMT-2`, |
| `Etc/GMT-3`, |
| `Etc/GMT-4`, |
| `Etc/GMT-5`, |
| `Etc/GMT-6`, |
| `Etc/GMT-7`, |
| `Etc/GMT-8`, |
| `Etc/GMT-9`, |
| `Etc/GMT0`, |
| `Etc/Greenwich`, |
| `Etc/UCT`, |
| `Etc/Universal`, |
| `Etc/UTC`, |
| `Etc/Zulu`, |
| `Europe/Amsterdam`, |
| `Europe/Andorra`, |
| `Europe/Astrakhan`, |
| `Europe/Athens`, |
| `Europe/Belfast`, |
| `Europe/Belgrade`, |
| `Europe/Berlin`, |
| `Europe/Bratislava`, |
| `Europe/Brussels`, |
| `Europe/Bucharest`, |
| `Europe/Budapest`, |
| `Europe/Busingen`, |
| `Europe/Chisinau`, |
| `Europe/Copenhagen`, |
| `Europe/Dublin`, |
| `Europe/Gibraltar`, |
| `Europe/Guernsey`, |
| `Europe/Helsinki`, |
| `Europe/Isle_of_Man`, |
| `Europe/Istanbul`, |
| `Europe/Jersey`, |
| `Europe/Kaliningrad`, |
| `Europe/Kiev`, |
| `Europe/Kirov`, |
| `Europe/Lisbon`, |
| `Europe/Ljubljana`, |
| `Europe/London`, |
| `Europe/Luxembourg`, |
| `Europe/Madrid`, |
| `Europe/Malta`, |
| `Europe/Mariehamn`, |
| `Europe/Minsk`, |
| `Europe/Monaco`, |
| `Europe/Moscow`, |
| `Europe/Nicosia`, |
| `Europe/Oslo`, |
| `Europe/Paris`, |
| `Europe/Podgorica`, |
| `Europe/Prague`, |
| `Europe/Riga`, |
| `Europe/Rome`, |
| `Europe/Samara`, |
| `Europe/San_Marino`, |
| `Europe/Sarajevo`, |
| `Europe/Saratov`, |
| `Europe/Simferopol`, |
| `Europe/Skopje`, |
| `Europe/Sofia`, |
| `Europe/Stockholm`, |
| `Europe/Tallinn`, |
| `Europe/Tirane`, |
| `Europe/Tiraspol`, |
| `Europe/Ulyanovsk`, |
| `Europe/Uzhgorod`, |
| `Europe/Vaduz`, |
| `Europe/Vatican`, |
| `Europe/Vienna`, |
| `Europe/Vilnius`, |
| `Europe/Volgograd`, |
| `Europe/Warsaw`, |
| `Europe/Zagreb`, |
| `Europe/Zaporozhye`, |
| `Europe/Zurich`, |
| `Factory`, |
| `GB`, |
| `GB-Eire`, |
| `GMT`, |
| `GMT+0`, |
| `GMT-0`, |
| `GMT0`, |
| `Greenwich`, |
| `Hongkong`, |
| `HST`, |
| `Iceland`, |
| `Indian/Antananarivo`, |
| `Indian/Chagos`, |
| `Indian/Christmas`, |
| `Indian/Cocos`, |
| `Indian/Comoro`, |
| `Indian/Kerguelen`, |
| `Indian/Mahe`, |
| `Indian/Maldives`, |
| `Indian/Mauritius`, |
| `Indian/Mayotte`, |
| `Indian/Reunion`, |
| `Iran`, |
| `Israel`, |
| `Jamaica`, |
| `Japan`, |
| `Kwajalein`, |
| `Libya`, |
| `MET`, |
| `Mexico/BajaNorte`, |
| `Mexico/BajaSur`, |
| `Mexico/General`, |
| `MST`, |
| `MST7MDT`, |
| `Navajo`, |
| `NZ`, |
| `NZ-CHAT`, |
| `Pacific/Apia`, |
| `Pacific/Auckland`, |
| `Pacific/Bougainville`, |
| `Pacific/Chatham`, |
| `Pacific/Chuuk`, |
| `Pacific/Easter`, |
| `Pacific/Efate`, |
| `Pacific/Enderbury`, |
| `Pacific/Fakaofo`, |
| `Pacific/Fiji`, |
| `Pacific/Funafuti`, |
| `Pacific/Galapagos`, |
| `Pacific/Gambier`, |
| `Pacific/Guadalcanal`, |
| `Pacific/Guam`, |
| `Pacific/Honolulu`, |
| `Pacific/Johnston`, |
| `Pacific/Kanton`, |
| `Pacific/Kiritimati`, |
| `Pacific/Kosrae`, |
| `Pacific/Kwajalein`, |
| `Pacific/Majuro`, |
| `Pacific/Marquesas`, |
| `Pacific/Midway`, |
| `Pacific/Nauru`, |
| `Pacific/Niue`, |
| `Pacific/Norfolk`, |
| `Pacific/Noumea`, |
| `Pacific/Pago_Pago`, |
| `Pacific/Palau`, |
| `Pacific/Pitcairn`, |
| `Pacific/Pohnpei`, |
| `Pacific/Ponape`, |
| `Pacific/Port_Moresby`, |
| `Pacific/Rarotonga`, |
| `Pacific/Saipan`, |
| `Pacific/Samoa`, |
| `Pacific/Tahiti`, |
| `Pacific/Tarawa`, |
| `Pacific/Tongatapu`, |
| `Pacific/Truk`, |
| `Pacific/Wake`, |
| `Pacific/Wallis`, |
| `Pacific/Yap`, |
| `Poland`, |
| `Portugal`, |
| `PRC`, |
| `PST8PDT`, |
| `ROC`, |
| `ROK`, |
| `Singapore`, |
| `Turkey`, |
| `UCT`, |
| `Universal`, |
| `US/Alaska`, |
| `US/Aleutian`, |
| `US/Arizona`, |
| `US/Central`, |
| `US/East-Indiana`, |
| `US/Eastern`, |
| `US/Hawaii`, |
| `US/Indiana-Starke`, |
| `US/Michigan`, |
| `US/Mountain`, |
| `US/Pacific`, |
| `US/Samoa`, |
| `UTC`, |
| `W-SU`, |
| `WET`, |
| `Zulu`, |
| } |
| for _, tz := range data { |
| errs := validateTimeZone(&tz, nil) |
| if len(errs) > 0 { |
| t.Errorf("%s failed: %v", tz, errs) |
| } |
| } |
| } |
| |
| func TestValidateIndexesString(t *testing.T) { |
| testCases := map[string]struct { |
| indexesString string |
| completions int32 |
| wantTotal int32 |
| wantError error |
| }{ |
| "empty is valid": { |
| indexesString: "", |
| completions: 6, |
| wantTotal: 0, |
| }, |
| "single number is valid": { |
| indexesString: "1", |
| completions: 6, |
| wantTotal: 1, |
| }, |
| "single interval is valid": { |
| indexesString: "1-3", |
| completions: 6, |
| wantTotal: 3, |
| }, |
| "mixed intervals valid": { |
| indexesString: "0,1-3,5,7-10", |
| completions: 12, |
| wantTotal: 9, |
| }, |
| "invalid due to extra space": { |
| indexesString: "0,1-3, 5", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`cannot convert string to integer for index: " 5"`), |
| }, |
| "invalid due to too large index": { |
| indexesString: "0,1-3,5", |
| completions: 5, |
| wantTotal: 0, |
| wantError: errors.New(`too large index: "5"`), |
| }, |
| "invalid due to non-increasing order of intervals": { |
| indexesString: "1-3,0,5", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`non-increasing order, previous: 3, current: 0`), |
| }, |
| "invalid due to non-increasing order between intervals": { |
| indexesString: "0,0,5", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`non-increasing order, previous: 0, current: 0`), |
| }, |
| "invalid due to non-increasing order within interval": { |
| indexesString: "0,1-1,5", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`non-increasing order, previous: 1, current: 1`), |
| }, |
| "invalid due to starting with '-'": { |
| indexesString: "-1,0", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`cannot convert string to integer for index: ""`), |
| }, |
| "invalid due to ending with '-'": { |
| indexesString: "0,1-", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`cannot convert string to integer for index: ""`), |
| }, |
| "invalid due to repeated '-'": { |
| indexesString: "0,1--3", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`the fragment "1--3" violates the requirement that an index interval can have at most two parts separated by '-'`), |
| }, |
| "invalid due to repeated ','": { |
| indexesString: "0,,1,3", |
| completions: 6, |
| wantTotal: 0, |
| wantError: errors.New(`cannot convert string to integer for index: ""`), |
| }, |
| } |
| |
| for name, tc := range testCases { |
| t.Run(name, func(t *testing.T) { |
| gotTotal, gotErr := validateIndexesFormat(tc.indexesString, tc.completions) |
| if tc.wantError == nil && gotErr != nil { |
| t.Errorf("unexpected error: %s", gotErr) |
| } else if tc.wantError != nil && gotErr == nil { |
| t.Errorf("missing error: %s", tc.wantError) |
| } else if tc.wantError != nil && gotErr != nil { |
| if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" { |
| t.Errorf("unexpected error, diff: %s", diff) |
| } |
| } |
| if tc.wantTotal != gotTotal { |
| t.Errorf("unexpected total want:%d, got:%d", tc.wantTotal, gotTotal) |
| } |
| }) |
| } |
| } |
| |
| func TestValidateFailedIndexesNotOverlapCompleted(t *testing.T) { |
| testCases := map[string]struct { |
| completedIndexesStr string |
| failedIndexesStr string |
| completions int32 |
| wantError error |
| }{ |
| "empty intervals": { |
| completedIndexesStr: "", |
| failedIndexesStr: "", |
| completions: 6, |
| }, |
| "empty completed intervals": { |
| completedIndexesStr: "", |
| failedIndexesStr: "1-3", |
| completions: 6, |
| }, |
| "empty failed intervals": { |
| completedIndexesStr: "1-2", |
| failedIndexesStr: "", |
| completions: 6, |
| }, |
| "non-overlapping intervals": { |
| completedIndexesStr: "0,2-4,6-8,12-19", |
| failedIndexesStr: "1,9-10", |
| completions: 20, |
| }, |
| "overlapping intervals": { |
| completedIndexesStr: "0,2-4,6-8,12-19", |
| failedIndexesStr: "1,8,9-10", |
| completions: 20, |
| wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), |
| }, |
| "overlapping intervals, corrupted completed interval skipped": { |
| completedIndexesStr: "0,2-4,x,6-8,12-19", |
| failedIndexesStr: "1,8,9-10", |
| completions: 20, |
| wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), |
| }, |
| "overlapping intervals, corrupted failed interval skipped": { |
| completedIndexesStr: "0,2-4,6-8,12-19", |
| failedIndexesStr: "1,y,8,9-10", |
| completions: 20, |
| wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), |
| }, |
| "overlapping intervals, first corrupted intervals skipped": { |
| completedIndexesStr: "x,0,2-4,6-8,12-19", |
| failedIndexesStr: "y,1,8,9-10", |
| completions: 20, |
| wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"), |
| }, |
| "non-overlapping intervals, last intervals corrupted": { |
| completedIndexesStr: "0,2-4,6-8,12-19,x", |
| failedIndexesStr: "1,9-10,y", |
| completions: 20, |
| }, |
| } |
| for name, tc := range testCases { |
| t.Run(name, func(t *testing.T) { |
| gotErr := validateFailedIndexesNotOverlapCompleted(tc.completedIndexesStr, tc.failedIndexesStr, tc.completions) |
| if tc.wantError == nil && gotErr != nil { |
| t.Errorf("unexpected error: %s", gotErr) |
| } else if tc.wantError != nil && gotErr == nil { |
| t.Errorf("missing error: %s", tc.wantError) |
| } else if tc.wantError != nil && gotErr != nil { |
| if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" { |
| t.Errorf("unexpected error, diff: %s", diff) |
| } |
| } |
| }) |
| } |
| } |