k8s.io/kubernetes@v1.29.3/pkg/apis/batch/validation/validation.go (about)

     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  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/robfig/cron/v3"
    26  
    27  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation"
    33  	"k8s.io/apimachinery/pkg/util/validation/field"
    34  	"k8s.io/kubernetes/pkg/apis/batch"
    35  	api "k8s.io/kubernetes/pkg/apis/core"
    36  	apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
    37  	"k8s.io/utils/pointer"
    38  )
    39  
    40  // maxParallelismForIndexJob is the maximum parallelism that an Indexed Job
    41  // is allowed to have. This threshold allows to cap the length of
    42  // .status.completedIndexes.
    43  const maxParallelismForIndexedJob = 100000
    44  
    45  // maxFailedIndexesForIndexedJob is the maximum number of failed indexes that
    46  // an Indexed Job is allowed to have. This threshold allows to cap the length of
    47  // .status.completedIndexes and .status.failedIndexes.
    48  const maxFailedIndexesForIndexedJob = 100_000
    49  
    50  const (
    51  	completionsSoftLimit                    = 100_000
    52  	parallelismLimitForHighCompletions      = 10_000
    53  	maxFailedIndexesLimitForHighCompletions = 10_000
    54  
    55  	// maximum number of rules in pod failure policy
    56  	maxPodFailurePolicyRules = 20
    57  
    58  	// maximum number of values for a OnExitCodes requirement in pod failure policy
    59  	maxPodFailurePolicyOnExitCodesValues = 255
    60  
    61  	// maximum number of patterns for a OnPodConditions requirement in pod failure policy
    62  	maxPodFailurePolicyOnPodConditionsPatterns = 20
    63  )
    64  
    65  var (
    66  	supportedPodFailurePolicyActions = sets.New(
    67  		string(batch.PodFailurePolicyActionCount),
    68  		string(batch.PodFailurePolicyActionFailIndex),
    69  		string(batch.PodFailurePolicyActionFailJob),
    70  		string(batch.PodFailurePolicyActionIgnore))
    71  
    72  	supportedPodFailurePolicyOnExitCodesOperator = sets.New(
    73  		string(batch.PodFailurePolicyOnExitCodesOpIn),
    74  		string(batch.PodFailurePolicyOnExitCodesOpNotIn))
    75  
    76  	supportedPodFailurePolicyOnPodConditionsStatus = sets.New(
    77  		string(api.ConditionFalse),
    78  		string(api.ConditionTrue),
    79  		string(api.ConditionUnknown))
    80  
    81  	supportedPodReplacementPolicy = sets.New(
    82  		string(batch.Failed),
    83  		string(batch.TerminatingOrFailed))
    84  )
    85  
    86  // validateGeneratedSelector validates that the generated selector on a controller object match the controller object
    87  // metadata, and the labels on the pod template are as generated.
    88  //
    89  // TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and
    90  // move to new location.  Replace batch.Job with an interface.
    91  func validateGeneratedSelector(obj *batch.Job, validateBatchLabels bool) field.ErrorList {
    92  	allErrs := field.ErrorList{}
    93  	if obj.Spec.ManualSelector != nil && *obj.Spec.ManualSelector {
    94  		return allErrs
    95  	}
    96  
    97  	if obj.Spec.Selector == nil {
    98  		return allErrs // This case should already have been checked in caller.  No need for more errors.
    99  	}
   100  
   101  	// If somehow uid was unset then we would get "controller-uid=" as the selector
   102  	// which is bad.
   103  	if obj.ObjectMeta.UID == "" {
   104  		allErrs = append(allErrs, field.Required(field.NewPath("metadata").Child("uid"), ""))
   105  	}
   106  
   107  	// If selector generation was requested, then expected labels must be
   108  	// present on pod template, and must match job's uid and name.  The
   109  	// generated (not-manual) selectors/labels ensure no overlap with other
   110  	// controllers.  The manual mode allows orphaning, adoption,
   111  	// backward-compatibility, and experimentation with new
   112  	// labeling/selection schemes.  Automatic selector generation should
   113  	// have placed certain labels on the pod, but this could have failed if
   114  	// the user added conflicting labels.  Validate that the expected
   115  	// generated ones are there.
   116  	allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.LegacyControllerUidLabel, string(obj.UID))...)
   117  	allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.LegacyJobNameLabel, string(obj.Name))...)
   118  	expectedLabels := make(map[string]string)
   119  	if validateBatchLabels {
   120  		allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.ControllerUidLabel, string(obj.UID))...)
   121  		allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.JobNameLabel, string(obj.Name))...)
   122  		expectedLabels[batch.ControllerUidLabel] = string(obj.UID)
   123  		expectedLabels[batch.JobNameLabel] = string(obj.Name)
   124  	}
   125  	// Labels created by the Kubernetes project should have a Kubernetes prefix.
   126  	// These labels are set due to legacy reasons.
   127  
   128  	expectedLabels[batch.LegacyControllerUidLabel] = string(obj.UID)
   129  	expectedLabels[batch.LegacyJobNameLabel] = string(obj.Name)
   130  	// Whether manually or automatically generated, the selector of the job must match the pods it will produce.
   131  	if selector, err := metav1.LabelSelectorAsSelector(obj.Spec.Selector); err == nil {
   132  		if !selector.Matches(labels.Set(expectedLabels)) {
   133  			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("selector"), obj.Spec.Selector, "`selector` not auto-generated"))
   134  		}
   135  	}
   136  
   137  	return allErrs
   138  }
   139  
   140  // ValidateJob validates a Job and returns an ErrorList with any errors.
   141  func ValidateJob(job *batch.Job, opts JobValidationOptions) field.ErrorList {
   142  	// Jobs and rcs have the same name validation
   143  	allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
   144  	allErrs = append(allErrs, validateGeneratedSelector(job, opts.RequirePrefixedLabels)...)
   145  	allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"), opts.PodValidationOptions)...)
   146  	if job.Spec.CompletionMode != nil && *job.Spec.CompletionMode == batch.IndexedCompletion && job.Spec.Completions != nil && *job.Spec.Completions > 0 {
   147  		// For indexed job, the job controller appends a suffix (`-$INDEX`)
   148  		// to the pod hostname when indexed job create pods.
   149  		// The index could be maximum `.spec.completions-1`
   150  		// If we don't validate this here, the indexed job will fail to create pods later.
   151  		maximumPodHostname := fmt.Sprintf("%s-%d", job.ObjectMeta.Name, *job.Spec.Completions-1)
   152  		if errs := apimachineryvalidation.IsDNS1123Label(maximumPodHostname); len(errs) > 0 {
   153  			allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), job.ObjectMeta.Name, fmt.Sprintf("will not able to create pod with invalid DNS label: %s", maximumPodHostname)))
   154  		}
   155  	}
   156  	return allErrs
   157  }
   158  
   159  // ValidateJobSpec validates a JobSpec and returns an ErrorList with any errors.
   160  func ValidateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   161  	allErrs := validateJobSpec(spec, fldPath, opts)
   162  	if spec.Selector == nil {
   163  		allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
   164  	} else {
   165  		labelSelectorValidationOpts := unversionedvalidation.LabelSelectorValidationOptions{
   166  			AllowInvalidLabelValueInSelector: opts.AllowInvalidLabelValueInSelector,
   167  		}
   168  		allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, labelSelectorValidationOpts, fldPath.Child("selector"))...)
   169  	}
   170  
   171  	// Whether manually or automatically generated, the selector of the job must match the pods it will produce.
   172  	if selector, err := metav1.LabelSelectorAsSelector(spec.Selector); err == nil {
   173  		labels := labels.Set(spec.Template.Labels)
   174  		if !selector.Matches(labels) {
   175  			allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "`selector` does not match template `labels`"))
   176  		}
   177  	}
   178  	return allErrs
   179  }
   180  
   181  func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   182  	allErrs := field.ErrorList{}
   183  
   184  	if spec.Parallelism != nil {
   185  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.Parallelism), fldPath.Child("parallelism"))...)
   186  	}
   187  	if spec.Completions != nil {
   188  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.Completions), fldPath.Child("completions"))...)
   189  	}
   190  	if spec.ActiveDeadlineSeconds != nil {
   191  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.ActiveDeadlineSeconds), fldPath.Child("activeDeadlineSeconds"))...)
   192  	}
   193  	if spec.BackoffLimit != nil {
   194  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimit), fldPath.Child("backoffLimit"))...)
   195  	}
   196  	if spec.TTLSecondsAfterFinished != nil {
   197  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.TTLSecondsAfterFinished), fldPath.Child("ttlSecondsAfterFinished"))...)
   198  	}
   199  	if spec.BackoffLimitPerIndex != nil {
   200  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimitPerIndex), fldPath.Child("backoffLimitPerIndex"))...)
   201  	}
   202  	if spec.MaxFailedIndexes != nil {
   203  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.MaxFailedIndexes), fldPath.Child("maxFailedIndexes"))...)
   204  		if spec.BackoffLimitPerIndex == nil {
   205  			allErrs = append(allErrs, field.Required(fldPath.Child("backoffLimitPerIndex"), fmt.Sprintf("when maxFailedIndexes is specified")))
   206  		}
   207  	}
   208  	if spec.CompletionMode != nil {
   209  		if *spec.CompletionMode != batch.NonIndexedCompletion && *spec.CompletionMode != batch.IndexedCompletion {
   210  			allErrs = append(allErrs, field.NotSupported(fldPath.Child("completionMode"), spec.CompletionMode, []string{string(batch.NonIndexedCompletion), string(batch.IndexedCompletion)}))
   211  		}
   212  		if *spec.CompletionMode == batch.IndexedCompletion {
   213  			if spec.Completions == nil {
   214  				allErrs = append(allErrs, field.Required(fldPath.Child("completions"), fmt.Sprintf("when completion mode is %s", batch.IndexedCompletion)))
   215  			}
   216  			if spec.Parallelism != nil && *spec.Parallelism > maxParallelismForIndexedJob {
   217  				allErrs = append(allErrs, field.Invalid(fldPath.Child("parallelism"), *spec.Parallelism, fmt.Sprintf("must be less than or equal to %d when completion mode is %s", maxParallelismForIndexedJob, batch.IndexedCompletion)))
   218  			}
   219  			if spec.Completions != nil && spec.MaxFailedIndexes != nil && *spec.MaxFailedIndexes > *spec.Completions {
   220  				allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, "must be less than or equal to completions"))
   221  			}
   222  			if spec.MaxFailedIndexes != nil && *spec.MaxFailedIndexes > maxFailedIndexesForIndexedJob {
   223  				allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, fmt.Sprintf("must be less than or equal to %d", maxFailedIndexesForIndexedJob)))
   224  			}
   225  			if spec.Completions != nil && *spec.Completions > completionsSoftLimit && spec.BackoffLimitPerIndex != nil {
   226  				if spec.MaxFailedIndexes == nil {
   227  					allErrs = append(allErrs, field.Required(fldPath.Child("maxFailedIndexes"), fmt.Sprintf("must be specified when completions is above %d", completionsSoftLimit)))
   228  				}
   229  				if spec.Parallelism != nil && *spec.Parallelism > parallelismLimitForHighCompletions {
   230  					allErrs = append(allErrs, field.Invalid(fldPath.Child("parallelism"), *spec.Parallelism, fmt.Sprintf("must be less than or equal to %d when completions are above %d and used with backoff limit per index", parallelismLimitForHighCompletions, completionsSoftLimit)))
   231  				}
   232  				if spec.MaxFailedIndexes != nil && *spec.MaxFailedIndexes > maxFailedIndexesLimitForHighCompletions {
   233  					allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, fmt.Sprintf("must be less than or equal to %d when completions are above %d and used with backoff limit per index", maxFailedIndexesLimitForHighCompletions, completionsSoftLimit)))
   234  				}
   235  			}
   236  		}
   237  	}
   238  	if spec.CompletionMode == nil || *spec.CompletionMode == batch.NonIndexedCompletion {
   239  		if spec.BackoffLimitPerIndex != nil {
   240  			allErrs = append(allErrs, field.Invalid(fldPath.Child("backoffLimitPerIndex"), *spec.BackoffLimitPerIndex, "requires indexed completion mode"))
   241  		}
   242  		if spec.MaxFailedIndexes != nil {
   243  			allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, "requires indexed completion mode"))
   244  		}
   245  	}
   246  
   247  	if spec.PodFailurePolicy != nil {
   248  		allErrs = append(allErrs, validatePodFailurePolicy(spec, fldPath.Child("podFailurePolicy"))...)
   249  	}
   250  
   251  	allErrs = append(allErrs, validatePodReplacementPolicy(spec, fldPath.Child("podReplacementPolicy"))...)
   252  
   253  	allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"), opts)...)
   254  
   255  	// spec.Template.Spec.RestartPolicy can be defaulted as RestartPolicyAlways
   256  	// by SetDefaults_PodSpec function when the user does not explicitly specify a value for it,
   257  	// so we check both empty and RestartPolicyAlways cases here
   258  	if spec.Template.Spec.RestartPolicy == api.RestartPolicyAlways || spec.Template.Spec.RestartPolicy == "" {
   259  		allErrs = append(allErrs, field.Required(fldPath.Child("template", "spec", "restartPolicy"),
   260  			fmt.Sprintf("valid values: %q, %q", api.RestartPolicyOnFailure, api.RestartPolicyNever)))
   261  	} else if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever {
   262  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("template", "spec", "restartPolicy"),
   263  			spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyOnFailure), string(api.RestartPolicyNever)}))
   264  	} else if spec.PodFailurePolicy != nil && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever {
   265  		allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "spec", "restartPolicy"),
   266  			spec.Template.Spec.RestartPolicy, fmt.Sprintf("only %q is supported when podFailurePolicy is specified", api.RestartPolicyNever)))
   267  	}
   268  	return allErrs
   269  }
   270  
   271  func validatePodFailurePolicy(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList {
   272  	var allErrs field.ErrorList
   273  	rulesPath := fldPath.Child("rules")
   274  	if len(spec.PodFailurePolicy.Rules) > maxPodFailurePolicyRules {
   275  		allErrs = append(allErrs, field.TooMany(rulesPath, len(spec.PodFailurePolicy.Rules), maxPodFailurePolicyRules))
   276  	}
   277  	containerNames := sets.NewString()
   278  	for _, containerSpec := range spec.Template.Spec.Containers {
   279  		containerNames.Insert(containerSpec.Name)
   280  	}
   281  	for _, containerSpec := range spec.Template.Spec.InitContainers {
   282  		containerNames.Insert(containerSpec.Name)
   283  	}
   284  	for i, rule := range spec.PodFailurePolicy.Rules {
   285  		allErrs = append(allErrs, validatePodFailurePolicyRule(spec, &rule, rulesPath.Index(i), containerNames)...)
   286  	}
   287  	return allErrs
   288  }
   289  
   290  func validatePodReplacementPolicy(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList {
   291  	var allErrs field.ErrorList
   292  	if spec.PodReplacementPolicy != nil {
   293  		// If PodFailurePolicy is specified then we only allow Failed.
   294  		if spec.PodFailurePolicy != nil {
   295  			if *spec.PodReplacementPolicy != batch.Failed {
   296  				allErrs = append(allErrs, field.NotSupported(fldPath, *spec.PodReplacementPolicy, []string{string(batch.Failed)}))
   297  			}
   298  			// If PodFailurePolicy not specified we allow values in supportedPodReplacementPolicy.
   299  		} else if !supportedPodReplacementPolicy.Has(string(*spec.PodReplacementPolicy)) {
   300  			allErrs = append(allErrs, field.NotSupported(fldPath, *spec.PodReplacementPolicy, sets.List(supportedPodReplacementPolicy)))
   301  		}
   302  	}
   303  	return allErrs
   304  }
   305  
   306  func validatePodFailurePolicyRule(spec *batch.JobSpec, rule *batch.PodFailurePolicyRule, rulePath *field.Path, containerNames sets.String) field.ErrorList {
   307  	var allErrs field.ErrorList
   308  	actionPath := rulePath.Child("action")
   309  	if rule.Action == "" {
   310  		allErrs = append(allErrs, field.Required(actionPath, fmt.Sprintf("valid values: %q", sets.List(supportedPodFailurePolicyActions))))
   311  	} else if rule.Action == batch.PodFailurePolicyActionFailIndex {
   312  		if spec.BackoffLimitPerIndex == nil {
   313  			allErrs = append(allErrs, field.Invalid(actionPath, rule.Action, "requires the backoffLimitPerIndex to be set"))
   314  		}
   315  	} else if !supportedPodFailurePolicyActions.Has(string(rule.Action)) {
   316  		allErrs = append(allErrs, field.NotSupported(actionPath, rule.Action, sets.List(supportedPodFailurePolicyActions)))
   317  	}
   318  	if rule.OnExitCodes != nil {
   319  		allErrs = append(allErrs, validatePodFailurePolicyRuleOnExitCodes(rule.OnExitCodes, rulePath.Child("onExitCodes"), containerNames)...)
   320  	}
   321  	if len(rule.OnPodConditions) > 0 {
   322  		allErrs = append(allErrs, validatePodFailurePolicyRuleOnPodConditions(rule.OnPodConditions, rulePath.Child("onPodConditions"))...)
   323  	}
   324  	if rule.OnExitCodes != nil && len(rule.OnPodConditions) > 0 {
   325  		allErrs = append(allErrs, field.Invalid(rulePath, field.OmitValueType{}, "specifying both OnExitCodes and OnPodConditions is not supported"))
   326  	}
   327  	if rule.OnExitCodes == nil && len(rule.OnPodConditions) == 0 {
   328  		allErrs = append(allErrs, field.Invalid(rulePath, field.OmitValueType{}, "specifying one of OnExitCodes and OnPodConditions is required"))
   329  	}
   330  	return allErrs
   331  }
   332  
   333  func validatePodFailurePolicyRuleOnPodConditions(onPodConditions []batch.PodFailurePolicyOnPodConditionsPattern, onPodConditionsPath *field.Path) field.ErrorList {
   334  	var allErrs field.ErrorList
   335  	if len(onPodConditions) > maxPodFailurePolicyOnPodConditionsPatterns {
   336  		allErrs = append(allErrs, field.TooMany(onPodConditionsPath, len(onPodConditions), maxPodFailurePolicyOnPodConditionsPatterns))
   337  	}
   338  	for j, pattern := range onPodConditions {
   339  		patternPath := onPodConditionsPath.Index(j)
   340  		statusPath := patternPath.Child("status")
   341  		allErrs = append(allErrs, apivalidation.ValidateQualifiedName(string(pattern.Type), patternPath.Child("type"))...)
   342  		if pattern.Status == "" {
   343  			allErrs = append(allErrs, field.Required(statusPath, fmt.Sprintf("valid values: %q", sets.List(supportedPodFailurePolicyOnPodConditionsStatus))))
   344  		} else if !supportedPodFailurePolicyOnPodConditionsStatus.Has(string(pattern.Status)) {
   345  			allErrs = append(allErrs, field.NotSupported(statusPath, pattern.Status, sets.List(supportedPodFailurePolicyOnPodConditionsStatus)))
   346  		}
   347  	}
   348  	return allErrs
   349  }
   350  
   351  func validatePodFailurePolicyRuleOnExitCodes(onExitCode *batch.PodFailurePolicyOnExitCodesRequirement, onExitCodesPath *field.Path, containerNames sets.String) field.ErrorList {
   352  	var allErrs field.ErrorList
   353  	operatorPath := onExitCodesPath.Child("operator")
   354  	if onExitCode.Operator == "" {
   355  		allErrs = append(allErrs, field.Required(operatorPath, fmt.Sprintf("valid values: %q", sets.List(supportedPodFailurePolicyOnExitCodesOperator))))
   356  	} else if !supportedPodFailurePolicyOnExitCodesOperator.Has(string(onExitCode.Operator)) {
   357  		allErrs = append(allErrs, field.NotSupported(operatorPath, onExitCode.Operator, sets.List(supportedPodFailurePolicyOnExitCodesOperator)))
   358  	}
   359  	if onExitCode.ContainerName != nil && !containerNames.Has(*onExitCode.ContainerName) {
   360  		allErrs = append(allErrs, field.Invalid(onExitCodesPath.Child("containerName"), *onExitCode.ContainerName, "must be one of the container or initContainer names in the pod template"))
   361  	}
   362  	valuesPath := onExitCodesPath.Child("values")
   363  	if len(onExitCode.Values) == 0 {
   364  		allErrs = append(allErrs, field.Invalid(valuesPath, onExitCode.Values, "at least one value is required"))
   365  	} else if len(onExitCode.Values) > maxPodFailurePolicyOnExitCodesValues {
   366  		allErrs = append(allErrs, field.TooMany(valuesPath, len(onExitCode.Values), maxPodFailurePolicyOnExitCodesValues))
   367  	}
   368  	isOrdered := true
   369  	uniqueValues := sets.NewInt32()
   370  	for j, exitCodeValue := range onExitCode.Values {
   371  		valuePath := valuesPath.Index(j)
   372  		if onExitCode.Operator == batch.PodFailurePolicyOnExitCodesOpIn && exitCodeValue == 0 {
   373  			allErrs = append(allErrs, field.Invalid(valuePath, exitCodeValue, "must not be 0 for the In operator"))
   374  		}
   375  		if uniqueValues.Has(exitCodeValue) {
   376  			allErrs = append(allErrs, field.Duplicate(valuePath, exitCodeValue))
   377  		} else {
   378  			uniqueValues.Insert(exitCodeValue)
   379  		}
   380  		if j > 0 && onExitCode.Values[j-1] > exitCodeValue {
   381  			isOrdered = false
   382  		}
   383  	}
   384  	if !isOrdered {
   385  		allErrs = append(allErrs, field.Invalid(valuesPath, onExitCode.Values, "must be ordered"))
   386  	}
   387  
   388  	return allErrs
   389  }
   390  
   391  // validateJobStatus validates a JobStatus and returns an ErrorList with any errors.
   392  func validateJobStatus(status *batch.JobStatus, fldPath *field.Path) field.ErrorList {
   393  	allErrs := field.ErrorList{}
   394  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Active), fldPath.Child("active"))...)
   395  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Succeeded), fldPath.Child("succeeded"))...)
   396  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Failed), fldPath.Child("failed"))...)
   397  	if status.Ready != nil {
   398  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.Ready), fldPath.Child("ready"))...)
   399  	}
   400  	if status.Terminating != nil {
   401  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.Terminating), fldPath.Child("terminating"))...)
   402  	}
   403  	if status.UncountedTerminatedPods != nil {
   404  		path := fldPath.Child("uncountedTerminatedPods")
   405  		seen := sets.NewString()
   406  		for i, k := range status.UncountedTerminatedPods.Succeeded {
   407  			p := path.Child("succeeded").Index(i)
   408  			if k == "" {
   409  				allErrs = append(allErrs, field.Invalid(p, k, "must not be empty"))
   410  			} else if seen.Has(string(k)) {
   411  				allErrs = append(allErrs, field.Duplicate(p, k))
   412  			} else {
   413  				seen.Insert(string(k))
   414  			}
   415  		}
   416  		for i, k := range status.UncountedTerminatedPods.Failed {
   417  			p := path.Child("failed").Index(i)
   418  			if k == "" {
   419  				allErrs = append(allErrs, field.Invalid(p, k, "must not be empty"))
   420  			} else if seen.Has(string(k)) {
   421  				allErrs = append(allErrs, field.Duplicate(p, k))
   422  			} else {
   423  				seen.Insert(string(k))
   424  			}
   425  		}
   426  	}
   427  	return allErrs
   428  }
   429  
   430  // ValidateJobUpdate validates an update to a Job and returns an ErrorList with any errors.
   431  func ValidateJobUpdate(job, oldJob *batch.Job, opts JobValidationOptions) field.ErrorList {
   432  	allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
   433  	allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec, field.NewPath("spec"), opts)...)
   434  	return allErrs
   435  }
   436  
   437  // ValidateJobUpdateStatus validates an update to the status of a Job and returns an ErrorList with any errors.
   438  func ValidateJobUpdateStatus(job, oldJob *batch.Job) field.ErrorList {
   439  	allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
   440  	allErrs = append(allErrs, ValidateJobStatusUpdate(job.Status, oldJob.Status)...)
   441  	return allErrs
   442  }
   443  
   444  // ValidateJobSpecUpdate validates an update to a JobSpec and returns an ErrorList with any errors.
   445  func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList {
   446  	allErrs := field.ErrorList{}
   447  	allErrs = append(allErrs, ValidateJobSpec(&spec, fldPath, opts.PodValidationOptions)...)
   448  	allErrs = append(allErrs, validateCompletions(spec, oldSpec, fldPath.Child("completions"), opts)...)
   449  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, fldPath.Child("selector"))...)
   450  	allErrs = append(allErrs, validatePodTemplateUpdate(spec, oldSpec, fldPath, opts)...)
   451  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.CompletionMode, oldSpec.CompletionMode, fldPath.Child("completionMode"))...)
   452  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.PodFailurePolicy, oldSpec.PodFailurePolicy, fldPath.Child("podFailurePolicy"))...)
   453  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.BackoffLimitPerIndex, oldSpec.BackoffLimitPerIndex, fldPath.Child("backoffLimitPerIndex"))...)
   454  	return allErrs
   455  }
   456  
   457  func validatePodTemplateUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList {
   458  	allErrs := field.ErrorList{}
   459  	template := &spec.Template
   460  	oldTemplate := &oldSpec.Template
   461  	if opts.AllowMutableSchedulingDirectives {
   462  		oldTemplate = oldSpec.Template.DeepCopy() // +k8s:verify-mutation:reason=clone
   463  		switch {
   464  		case template.Spec.Affinity == nil && oldTemplate.Spec.Affinity != nil:
   465  			// allow the Affinity field to be cleared if the old template had no affinity directives other than NodeAffinity
   466  			oldTemplate.Spec.Affinity.NodeAffinity = nil // +k8s:verify-mutation:reason=clone
   467  			if (*oldTemplate.Spec.Affinity) == (api.Affinity{}) {
   468  				oldTemplate.Spec.Affinity = nil // +k8s:verify-mutation:reason=clone
   469  			}
   470  		case template.Spec.Affinity != nil && oldTemplate.Spec.Affinity == nil:
   471  			// allow the NodeAffinity field to skip immutability checking
   472  			oldTemplate.Spec.Affinity = &api.Affinity{NodeAffinity: template.Spec.Affinity.NodeAffinity} // +k8s:verify-mutation:reason=clone
   473  		case template.Spec.Affinity != nil && oldTemplate.Spec.Affinity != nil:
   474  			// allow the NodeAffinity field to skip immutability checking
   475  			oldTemplate.Spec.Affinity.NodeAffinity = template.Spec.Affinity.NodeAffinity // +k8s:verify-mutation:reason=clone
   476  		}
   477  		oldTemplate.Spec.NodeSelector = template.Spec.NodeSelector       // +k8s:verify-mutation:reason=clone
   478  		oldTemplate.Spec.Tolerations = template.Spec.Tolerations         // +k8s:verify-mutation:reason=clone
   479  		oldTemplate.Annotations = template.Annotations                   // +k8s:verify-mutation:reason=clone
   480  		oldTemplate.Labels = template.Labels                             // +k8s:verify-mutation:reason=clone
   481  		oldTemplate.Spec.SchedulingGates = template.Spec.SchedulingGates // +k8s:verify-mutation:reason=clone
   482  	}
   483  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(template, oldTemplate, fldPath.Child("template"))...)
   484  	return allErrs
   485  }
   486  
   487  // ValidateJobStatusUpdate validates an update to a JobStatus and returns an ErrorList with any errors.
   488  func ValidateJobStatusUpdate(status, oldStatus batch.JobStatus) field.ErrorList {
   489  	allErrs := field.ErrorList{}
   490  	allErrs = append(allErrs, validateJobStatus(&status, field.NewPath("status"))...)
   491  	return allErrs
   492  }
   493  
   494  // ValidateCronJobCreate validates a CronJob on creation and returns an ErrorList with any errors.
   495  func ValidateCronJobCreate(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
   496  	// CronJobs and rcs have the same name validation
   497  	allErrs := apivalidation.ValidateObjectMeta(&cronJob.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
   498  	allErrs = append(allErrs, validateCronJobSpec(&cronJob.Spec, nil, field.NewPath("spec"), opts)...)
   499  	if len(cronJob.ObjectMeta.Name) > apimachineryvalidation.DNS1035LabelMaxLength-11 {
   500  		// The cronjob controller appends a 11-character suffix to the cronjob (`-$TIMESTAMP`) when
   501  		// creating a job. The job name length limit is 63 characters.
   502  		// Therefore cronjob names must have length <= 63-11=52. If we don't validate this here,
   503  		// then job creation will fail later.
   504  		allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), cronJob.ObjectMeta.Name, "must be no more than 52 characters"))
   505  	}
   506  	return allErrs
   507  }
   508  
   509  // ValidateCronJobUpdate validates an update to a CronJob and returns an ErrorList with any errors.
   510  func ValidateCronJobUpdate(job, oldJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
   511  	allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
   512  	allErrs = append(allErrs, validateCronJobSpec(&job.Spec, &oldJob.Spec, field.NewPath("spec"), opts)...)
   513  
   514  	// skip the 52-character name validation limit on update validation
   515  	// to allow old cronjobs with names > 52 chars to be updated/deleted
   516  	return allErrs
   517  }
   518  
   519  // validateCronJobSpec validates a CronJobSpec and returns an ErrorList with any errors.
   520  func validateCronJobSpec(spec, oldSpec *batch.CronJobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   521  	allErrs := field.ErrorList{}
   522  
   523  	if len(spec.Schedule) == 0 {
   524  		allErrs = append(allErrs, field.Required(fldPath.Child("schedule"), ""))
   525  	} else {
   526  		allowTZInSchedule := false
   527  		if oldSpec != nil {
   528  			allowTZInSchedule = strings.Contains(oldSpec.Schedule, "TZ")
   529  		}
   530  		allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, allowTZInSchedule, spec.TimeZone, fldPath.Child("schedule"))...)
   531  	}
   532  
   533  	if spec.StartingDeadlineSeconds != nil {
   534  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...)
   535  	}
   536  
   537  	if oldSpec == nil || !pointer.StringEqual(oldSpec.TimeZone, spec.TimeZone) {
   538  		allErrs = append(allErrs, validateTimeZone(spec.TimeZone, fldPath.Child("timeZone"))...)
   539  	}
   540  
   541  	allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...)
   542  	allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"), opts)...)
   543  
   544  	if spec.SuccessfulJobsHistoryLimit != nil {
   545  		// zero is a valid SuccessfulJobsHistoryLimit
   546  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.SuccessfulJobsHistoryLimit), fldPath.Child("successfulJobsHistoryLimit"))...)
   547  	}
   548  	if spec.FailedJobsHistoryLimit != nil {
   549  		// zero is a valid SuccessfulJobsHistoryLimit
   550  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.FailedJobsHistoryLimit), fldPath.Child("failedJobsHistoryLimit"))...)
   551  	}
   552  
   553  	return allErrs
   554  }
   555  
   556  func validateConcurrencyPolicy(concurrencyPolicy *batch.ConcurrencyPolicy, fldPath *field.Path) field.ErrorList {
   557  	allErrs := field.ErrorList{}
   558  	switch *concurrencyPolicy {
   559  	case batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent:
   560  		break
   561  	case "":
   562  		allErrs = append(allErrs, field.Required(fldPath, ""))
   563  	default:
   564  		validValues := []string{string(batch.AllowConcurrent), string(batch.ForbidConcurrent), string(batch.ReplaceConcurrent)}
   565  		allErrs = append(allErrs, field.NotSupported(fldPath, *concurrencyPolicy, validValues))
   566  	}
   567  
   568  	return allErrs
   569  }
   570  
   571  func validateScheduleFormat(schedule string, allowTZInSchedule bool, timeZone *string, fldPath *field.Path) field.ErrorList {
   572  	allErrs := field.ErrorList{}
   573  	if _, err := cron.ParseStandard(schedule); err != nil {
   574  		allErrs = append(allErrs, field.Invalid(fldPath, schedule, err.Error()))
   575  	}
   576  	switch {
   577  	case allowTZInSchedule && strings.Contains(schedule, "TZ") && timeZone != nil:
   578  		allErrs = append(allErrs, field.Invalid(fldPath, schedule, "cannot use both timeZone field and TZ or CRON_TZ in schedule"))
   579  	case !allowTZInSchedule && strings.Contains(schedule, "TZ"):
   580  		allErrs = append(allErrs, field.Invalid(fldPath, schedule, "cannot use TZ or CRON_TZ in schedule, use timeZone field instead"))
   581  	}
   582  
   583  	return allErrs
   584  }
   585  
   586  // https://data.iana.org/time-zones/theory.html#naming
   587  // * A name must not be empty, or contain '//', or start or end with '/'.
   588  // * Do not use the file name components '.' and '..'.
   589  // * Within a file name component, use only ASCII letters, '.', '-' and '_'.
   590  // * Do not use digits, as that might create an ambiguity with POSIX TZ strings.
   591  // * A file name component must not exceed 14 characters or start with '-'
   592  //
   593  // 0-9 and + characters are tolerated to accommodate legacy compatibility names
   594  var validTimeZoneCharacters = regexp.MustCompile(`^[A-Za-z\.\-_0-9+]{1,14}$`)
   595  
   596  func validateTimeZone(timeZone *string, fldPath *field.Path) field.ErrorList {
   597  	allErrs := field.ErrorList{}
   598  	if timeZone == nil {
   599  		return allErrs
   600  	}
   601  
   602  	if len(*timeZone) == 0 {
   603  		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be nil or non-empty string"))
   604  		return allErrs
   605  	}
   606  
   607  	for _, part := range strings.Split(*timeZone, "/") {
   608  		if part == "." || part == ".." || strings.HasPrefix(part, "-") || !validTimeZoneCharacters.MatchString(part) {
   609  			allErrs = append(allErrs, field.Invalid(fldPath, timeZone, fmt.Sprintf("unknown time zone %s", *timeZone)))
   610  			return allErrs
   611  		}
   612  	}
   613  
   614  	if strings.EqualFold(*timeZone, "Local") {
   615  		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones"))
   616  	}
   617  
   618  	if _, err := time.LoadLocation(*timeZone); err != nil {
   619  		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, err.Error()))
   620  	}
   621  
   622  	return allErrs
   623  }
   624  
   625  // ValidateJobTemplateSpec validates a JobTemplateSpec and returns an ErrorList with any errors.
   626  func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   627  	allErrs := validateJobSpec(&spec.Spec, fldPath.Child("spec"), opts)
   628  
   629  	// jobtemplate will always have the selector automatically generated
   630  	if spec.Spec.Selector != nil {
   631  		allErrs = append(allErrs, field.Invalid(fldPath.Child("spec", "selector"), spec.Spec.Selector, "`selector` will be auto-generated"))
   632  	}
   633  	if spec.Spec.ManualSelector != nil && *spec.Spec.ManualSelector {
   634  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("spec", "manualSelector"), spec.Spec.ManualSelector, []string{"nil", "false"}))
   635  	}
   636  	return allErrs
   637  }
   638  
   639  func validateCompletions(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList {
   640  	if !opts.AllowElasticIndexedJobs {
   641  		return apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath)
   642  	}
   643  
   644  	// Completions is immutable for non-indexed jobs.
   645  	// For Indexed Jobs, if ElasticIndexedJob feature gate is not enabled,
   646  	// fall back to validating that spec.Completions is always immutable.
   647  	isIndexedJob := spec.CompletionMode != nil && *spec.CompletionMode == batch.IndexedCompletion
   648  	if !isIndexedJob {
   649  		return apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath)
   650  	}
   651  
   652  	var allErrs field.ErrorList
   653  	if apiequality.Semantic.DeepEqual(spec.Completions, oldSpec.Completions) {
   654  		return allErrs
   655  	}
   656  	// Indexed Jobs cannot set completions to nil. The nil check
   657  	// is already performed in validateJobSpec, no need to add another error.
   658  	if spec.Completions == nil {
   659  		return allErrs
   660  	}
   661  
   662  	if *spec.Completions != *spec.Parallelism {
   663  		allErrs = append(allErrs, field.Invalid(fldPath, spec.Completions, fmt.Sprintf("can only be modified in tandem with %s", fldPath.Root().Child("parallelism").String())))
   664  	}
   665  	return allErrs
   666  }
   667  
   668  type JobValidationOptions struct {
   669  	apivalidation.PodValidationOptions
   670  	// Allow mutable node affinity, selector and tolerations of the template
   671  	AllowMutableSchedulingDirectives bool
   672  	// Allow elastic indexed jobs
   673  	AllowElasticIndexedJobs bool
   674  	// Require Job to have the label on batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid
   675  	RequirePrefixedLabels bool
   676  }