kubernetes/pkg/apis/batch/validation/validation_test.go

2884 lines
82 KiB
Go

/*
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 (
_ "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"
)
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{"controller-uid": "1a2b3c", "job-name": "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()
validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector)
validGeneratedSelector := getValidGeneratedSelector()
validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector)
validPodTemplateSpecForGeneratedRestartPolicyNever := getValidPodTemplateSpecForGenerated(validGeneratedSelector)
validPodTemplateSpecForGeneratedRestartPolicyNever.Spec.RestartPolicy = api.RestartPolicyNever
successCases := map[string]struct {
opts JobValidationOptions
job batch.Job
}{
"valid pod failure policy": {
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 manual selector": {
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.BoolPtr(true),
Template: validPodTemplateSpecForManual,
},
},
},
"valid generated selector": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
},
"valid NonIndexed completion mode": {
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": {
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.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(100000),
},
},
},
"valid job tracking annotation": {
opts: JobValidationOptions{
AllowTrackingAnnotation: true,
},
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
Annotations: map[string]string{
batch.JobTrackingFinalizer: "",
},
},
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
},
}
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]batch.Job{
`spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: {
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGeneratedRestartPolicyNever,
PodFailurePolicy: &batch.PodFailurePolicy{
Rules: []batch.PodFailurePolicyRule{
{
Action: batch.PodFailurePolicyActionFailJob,
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Duplicate value: 11`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values: Too many: 256: must have at most 255 items`: {
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
}(),
},
},
},
},
},
},
`spec.podFailurePolicy.rules: Too many: 21: must have at most 20 items`: {
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
}(),
},
},
},
`spec.podFailurePolicy.rules[0].onPodConditions: Too many: 21: must have at most 20 items`: {
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
}(),
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values[2]: Duplicate value: 13`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{19, 11}: must be ordered`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{}: at least one value is required`: {
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{},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].action: Required value: valid values: ["Count" "FailJob" "Ignore"]`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.operator: Required value: valid values: ["In" "NotIn"]`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0]: Invalid value: specifying both OnExitCodes and OnPodConditions is not supported`: {
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,
},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Invalid value: 0: must not be 0 for the In operator`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[1].onExitCodes.containerName: Invalid value: "xyz": must be one of the container or initContainer names in the pod template`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].action: Unsupported value: "UnknownAction": supported values: "Count", "FailJob", "Ignore"`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onExitCodes.operator: Unsupported value: "UnknownOperator": supported values: "In", "NotIn"`: {
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},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onPodConditions[0].status: Required value: valid values: ["False" "True" "Unknown"]`: {
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGeneratedRestartPolicyNever,
PodFailurePolicy: &batch.PodFailurePolicy{
Rules: []batch.PodFailurePolicyRule{
{
Action: batch.PodFailurePolicyActionIgnore,
OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{
{
Type: api.DisruptionTarget,
},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onPodConditions[0].status: Unsupported value: "UnknownStatus": supported values: "False", "True", "Unknown"`: {
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",
},
},
},
},
},
},
},
`spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "": name part must be non-empty`: {
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGeneratedRestartPolicyNever,
PodFailurePolicy: &batch.PodFailurePolicy{
Rules: []batch.PodFailurePolicyRule{
{
Action: batch.PodFailurePolicyActionIgnore,
OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{
{
Status: api.ConditionTrue,
},
},
},
},
},
},
},
`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]')`: {
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,
},
},
},
},
},
},
},
`spec.template.spec.restartPolicy: Invalid value: "OnFailure": only "Never" is supported when podFailurePolicy is specified`: {
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{},
},
},
},
"spec.parallelism:must be greater than or equal to 0": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Parallelism: &negative,
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
"spec.completions:must be greater than or equal to 0": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Completions: &negative,
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
"spec.activeDeadlineSeconds:must be greater than or equal to 0": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
ActiveDeadlineSeconds: &negative64,
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
"spec.selector:Required value": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Template: validPodTemplateSpecForGenerated,
},
},
"spec.template.metadata.labels: Invalid value: map[string]string{\"y\":\"z\"}: `selector` does not match template `labels`": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.BoolPtr(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}},
},
},
},
},
"spec.template.metadata.labels: Invalid value: map[string]string{\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.BoolPtr(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}},
},
},
},
},
"spec.template.spec.restartPolicy: Required value": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.BoolPtr(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}},
},
},
},
},
"spec.template.spec.restartPolicy: Unsupported value": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.BoolPtr(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}},
},
},
},
},
"spec.ttlSecondsAfterFinished: must be greater than or equal to 0": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
TTLSecondsAfterFinished: &negative,
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
"spec.completions: Required value: when completion mode is Indexed": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
"spec.parallelism: must be less than or equal to 100000 when completion mode is Indexed": {
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.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(100001),
},
},
"metadata.annotations[batch.kubernetes.io/job-tracking]: cannot add this annotation": {
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
Annotations: map[string]string{
batch.JobTrackingFinalizer: "",
},
},
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
}
for k, v := range errorCases {
t.Run(k, func(t *testing.T) {
errs := ValidateJob(&v, JobValidationOptions{})
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.Int32Ptr(5),
ActiveDeadlineSeconds: pointer.Int64Ptr(2),
TTLSecondsAfterFinished: pointer.Int32Ptr(1),
},
},
update: func(job *batch.Job) {
job.Spec.Parallelism = pointer.Int32Ptr(2)
job.Spec.ActiveDeadlineSeconds = pointer.Int64Ptr(3)
job.Spec.TTLSecondsAfterFinished = pointer.Int32Ptr(2)
job.Spec.ManualSelector = pointer.BoolPtr(true)
},
},
"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.Int32Ptr(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.Int32Ptr(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 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",
},
},
"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.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",
},
},
"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 = nil
},
err: &field.Error{
Type: field.ErrorTypeInvalid,
Field: "spec.podFailurePolicy",
},
},
"immutable pod template": {
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.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.Int32Ptr(2),
},
},
update: func(job *batch.Job) {
job.Spec.CompletionMode = completionModePtr(batch.NonIndexedCompletion)
},
err: &field.Error{
Type: field.ErrorTypeInvalid,
Field: "spec.completionMode",
},
},
"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,
},
},
"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.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
update: func(job *batch.Job) {
job.Spec.Completions = pointer.Int32Ptr(2)
job.Spec.Parallelism = pointer.Int32Ptr(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.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(2),
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
update: func(job *batch.Job) {
job.Spec.Completions = pointer.Int32Ptr(3)
job.Spec.Parallelism = pointer.Int32Ptr(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.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
update: func(job *batch.Job) {
job.Spec.Completions = pointer.Int32Ptr(2)
job.Spec.Parallelism = pointer.Int32Ptr(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.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
update: func(job *batch.Job) {
job.Spec.Completions = nil
job.Spec.Parallelism = pointer.Int32Ptr(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.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(2),
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
update: func(job *batch.Job) {
job.Spec.Completions = pointer.Int32Ptr(2)
job.Spec.Parallelism = pointer.Int32Ptr(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.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(2),
CompletionMode: completionModePtr(batch.IndexedCompletion),
},
},
update: func(job *batch.Job) {
job.Spec.Completions = pointer.Int32Ptr(2)
job.Spec.Parallelism = pointer.Int32Ptr(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 {
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,
},
},
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),
},
},
},
"nil ready": {
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,
},
},
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),
},
},
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"},
},
},
"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)
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{}
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,
},
},
},
},
"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.schedule: cannot use both timeZone field and TZ or CRON_TZ in schedule": {
ObjectMeta: metav1.ObjectMeta{
Name: "mycronjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.CronJobSpec{
Schedule: "TZ=UTC 0 * * * *",
TimeZone: &timeZoneUTC,
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.BoolPtr(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 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)
}
}
}