...

Source file src/k8s.io/kubernetes/pkg/apis/batch/validation/validation_test.go

Documentation: k8s.io/kubernetes/pkg/apis/batch/validation

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

View as plain text