2948 lines
84 KiB
Go
2948 lines
84 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.backoffLimit:must be greater than or equal to 0": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "myjob",
|
|
Namespace: metav1.NamespaceDefault,
|
|
UID: types.UID("1a2b3c"),
|
|
},
|
|
Spec: batch.JobSpec{
|
|
BackoffLimit: &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",
|
|
},
|
|
},
|
|
"Completions but not indexed jobs": {
|
|
old: batch.Job{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
|
Spec: batch.JobSpec{
|
|
Selector: validGeneratedSelector,
|
|
Template: validPodTemplateSpecForGenerated,
|
|
CompletionMode: completionModePtr(batch.NonIndexedCompletion),
|
|
Completions: pointer.Int32Ptr(2),
|
|
},
|
|
},
|
|
update: func(job *batch.Job) {
|
|
job.Spec.Completions = pointer.Int32Ptr(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.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)
|
|
}
|
|
}
|
|
}
|