...

Source file src/k8s.io/kubernetes/pkg/apis/batch/validation/validation.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  	"fmt"
    21  	"regexp"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/robfig/cron/v3"
    27  
    28  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
    31  	"k8s.io/apimachinery/pkg/labels"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  	apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation"
    35  	"k8s.io/apimachinery/pkg/util/validation/field"
    36  	"k8s.io/kubernetes/pkg/apis/batch"
    37  	api "k8s.io/kubernetes/pkg/apis/core"
    38  	apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
    39  	"k8s.io/utils/pointer"
    40  	"k8s.io/utils/ptr"
    41  )
    42  
    43  // maxParallelismForIndexJob is the maximum parallelism that an Indexed Job
    44  // is allowed to have. This threshold allows to cap the length of
    45  // .status.completedIndexes.
    46  const maxParallelismForIndexedJob = 100000
    47  
    48  // maxFailedIndexesForIndexedJob is the maximum number of failed indexes that
    49  // an Indexed Job is allowed to have. This threshold allows to cap the length of
    50  // .status.completedIndexes and .status.failedIndexes.
    51  const maxFailedIndexesForIndexedJob = 100_000
    52  
    53  const (
    54  	completionsSoftLimit                    = 100_000
    55  	parallelismLimitForHighCompletions      = 10_000
    56  	maxFailedIndexesLimitForHighCompletions = 10_000
    57  
    58  	// maximum number of rules in pod failure policy
    59  	maxPodFailurePolicyRules = 20
    60  
    61  	// maximum number of values for a OnExitCodes requirement in pod failure policy
    62  	maxPodFailurePolicyOnExitCodesValues = 255
    63  
    64  	// maximum number of patterns for a OnPodConditions requirement in pod failure policy
    65  	maxPodFailurePolicyOnPodConditionsPatterns = 20
    66  
    67  	// maximum length of the value of the managedBy field
    68  	maxManagedByLength = 63
    69  
    70  	// maximum length of succeededIndexes in JobSuccessPolicy.
    71  	maxJobSuccessPolicySucceededIndexesLimit = 64 * 1024
    72  	// maximum number of rules in successPolicy.
    73  	maxSuccessPolicyRule = 20
    74  )
    75  
    76  var (
    77  	supportedPodFailurePolicyActions = sets.New(
    78  		batch.PodFailurePolicyActionCount,
    79  		batch.PodFailurePolicyActionFailIndex,
    80  		batch.PodFailurePolicyActionFailJob,
    81  		batch.PodFailurePolicyActionIgnore)
    82  
    83  	supportedPodFailurePolicyOnExitCodesOperator = sets.New(
    84  		batch.PodFailurePolicyOnExitCodesOpIn,
    85  		batch.PodFailurePolicyOnExitCodesOpNotIn)
    86  
    87  	supportedPodFailurePolicyOnPodConditionsStatus = sets.New(
    88  		api.ConditionFalse,
    89  		api.ConditionTrue,
    90  		api.ConditionUnknown)
    91  
    92  	supportedPodReplacementPolicy = sets.New(
    93  		batch.Failed,
    94  		batch.TerminatingOrFailed)
    95  )
    96  
    97  // validateGeneratedSelector validates that the generated selector on a controller object match the controller object
    98  // metadata, and the labels on the pod template are as generated.
    99  //
   100  // TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and
   101  // move to new location.  Replace batch.Job with an interface.
   102  func validateGeneratedSelector(obj *batch.Job, validateBatchLabels bool) field.ErrorList {
   103  	allErrs := field.ErrorList{}
   104  	if obj.Spec.ManualSelector != nil && *obj.Spec.ManualSelector {
   105  		return allErrs
   106  	}
   107  
   108  	if obj.Spec.Selector == nil {
   109  		return allErrs // This case should already have been checked in caller.  No need for more errors.
   110  	}
   111  
   112  	// If somehow uid was unset then we would get "controller-uid=" as the selector
   113  	// which is bad.
   114  	if obj.ObjectMeta.UID == "" {
   115  		allErrs = append(allErrs, field.Required(field.NewPath("metadata").Child("uid"), ""))
   116  	}
   117  
   118  	// If selector generation was requested, then expected labels must be
   119  	// present on pod template, and must match job's uid and name.  The
   120  	// generated (not-manual) selectors/labels ensure no overlap with other
   121  	// controllers.  The manual mode allows orphaning, adoption,
   122  	// backward-compatibility, and experimentation with new
   123  	// labeling/selection schemes.  Automatic selector generation should
   124  	// have placed certain labels on the pod, but this could have failed if
   125  	// the user added conflicting labels.  Validate that the expected
   126  	// generated ones are there.
   127  	allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.LegacyControllerUidLabel, string(obj.UID))...)
   128  	allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.LegacyJobNameLabel, string(obj.Name))...)
   129  	expectedLabels := make(map[string]string)
   130  	if validateBatchLabels {
   131  		allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.ControllerUidLabel, string(obj.UID))...)
   132  		allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.JobNameLabel, string(obj.Name))...)
   133  		expectedLabels[batch.ControllerUidLabel] = string(obj.UID)
   134  		expectedLabels[batch.JobNameLabel] = string(obj.Name)
   135  	}
   136  	// Labels created by the Kubernetes project should have a Kubernetes prefix.
   137  	// These labels are set due to legacy reasons.
   138  
   139  	expectedLabels[batch.LegacyControllerUidLabel] = string(obj.UID)
   140  	expectedLabels[batch.LegacyJobNameLabel] = string(obj.Name)
   141  	// Whether manually or automatically generated, the selector of the job must match the pods it will produce.
   142  	if selector, err := metav1.LabelSelectorAsSelector(obj.Spec.Selector); err == nil {
   143  		if !selector.Matches(labels.Set(expectedLabels)) {
   144  			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("selector"), obj.Spec.Selector, "`selector` not auto-generated"))
   145  		}
   146  	}
   147  
   148  	return allErrs
   149  }
   150  
   151  // ValidateJob validates a Job and returns an ErrorList with any errors.
   152  func ValidateJob(job *batch.Job, opts JobValidationOptions) field.ErrorList {
   153  	// Jobs and rcs have the same name validation
   154  	allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
   155  	allErrs = append(allErrs, validateGeneratedSelector(job, opts.RequirePrefixedLabels)...)
   156  	allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"), opts.PodValidationOptions)...)
   157  	if job.Spec.CompletionMode != nil && *job.Spec.CompletionMode == batch.IndexedCompletion && job.Spec.Completions != nil && *job.Spec.Completions > 0 {
   158  		// For indexed job, the job controller appends a suffix (`-$INDEX`)
   159  		// to the pod hostname when indexed job create pods.
   160  		// The index could be maximum `.spec.completions-1`
   161  		// If we don't validate this here, the indexed job will fail to create pods later.
   162  		maximumPodHostname := fmt.Sprintf("%s-%d", job.ObjectMeta.Name, *job.Spec.Completions-1)
   163  		if errs := apimachineryvalidation.IsDNS1123Label(maximumPodHostname); len(errs) > 0 {
   164  			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)))
   165  		}
   166  	}
   167  	return allErrs
   168  }
   169  
   170  // ValidateJobSpec validates a JobSpec and returns an ErrorList with any errors.
   171  func ValidateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   172  	allErrs := validateJobSpec(spec, fldPath, opts)
   173  	if spec.Selector == nil {
   174  		allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
   175  	} else {
   176  		labelSelectorValidationOpts := unversionedvalidation.LabelSelectorValidationOptions{
   177  			AllowInvalidLabelValueInSelector: opts.AllowInvalidLabelValueInSelector,
   178  		}
   179  		allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, labelSelectorValidationOpts, fldPath.Child("selector"))...)
   180  	}
   181  
   182  	// Whether manually or automatically generated, the selector of the job must match the pods it will produce.
   183  	if selector, err := metav1.LabelSelectorAsSelector(spec.Selector); err == nil {
   184  		labels := labels.Set(spec.Template.Labels)
   185  		if !selector.Matches(labels) {
   186  			allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "`selector` does not match template `labels`"))
   187  		}
   188  	}
   189  	return allErrs
   190  }
   191  
   192  func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   193  	allErrs := field.ErrorList{}
   194  
   195  	if spec.Parallelism != nil {
   196  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.Parallelism), fldPath.Child("parallelism"))...)
   197  	}
   198  	if spec.Completions != nil {
   199  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.Completions), fldPath.Child("completions"))...)
   200  	}
   201  	if spec.ActiveDeadlineSeconds != nil {
   202  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.ActiveDeadlineSeconds), fldPath.Child("activeDeadlineSeconds"))...)
   203  	}
   204  	if spec.BackoffLimit != nil {
   205  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimit), fldPath.Child("backoffLimit"))...)
   206  	}
   207  	if spec.TTLSecondsAfterFinished != nil {
   208  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.TTLSecondsAfterFinished), fldPath.Child("ttlSecondsAfterFinished"))...)
   209  	}
   210  	if spec.BackoffLimitPerIndex != nil {
   211  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimitPerIndex), fldPath.Child("backoffLimitPerIndex"))...)
   212  	}
   213  	if spec.MaxFailedIndexes != nil {
   214  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.MaxFailedIndexes), fldPath.Child("maxFailedIndexes"))...)
   215  		if spec.BackoffLimitPerIndex == nil {
   216  			allErrs = append(allErrs, field.Required(fldPath.Child("backoffLimitPerIndex"), fmt.Sprintf("when maxFailedIndexes is specified")))
   217  		}
   218  	}
   219  	if spec.ManagedBy != nil {
   220  		allErrs = append(allErrs, apimachineryvalidation.IsDomainPrefixedPath(fldPath.Child("managedBy"), *spec.ManagedBy)...)
   221  		if len(*spec.ManagedBy) > maxManagedByLength {
   222  			allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("managedBy"), *spec.ManagedBy, maxManagedByLength))
   223  		}
   224  	}
   225  	if spec.CompletionMode != nil {
   226  		if *spec.CompletionMode != batch.NonIndexedCompletion && *spec.CompletionMode != batch.IndexedCompletion {
   227  			allErrs = append(allErrs, field.NotSupported(fldPath.Child("completionMode"), spec.CompletionMode, []batch.CompletionMode{batch.NonIndexedCompletion, batch.IndexedCompletion}))
   228  		}
   229  		if *spec.CompletionMode == batch.IndexedCompletion {
   230  			if spec.Completions == nil {
   231  				allErrs = append(allErrs, field.Required(fldPath.Child("completions"), fmt.Sprintf("when completion mode is %s", batch.IndexedCompletion)))
   232  			}
   233  			if spec.Parallelism != nil && *spec.Parallelism > maxParallelismForIndexedJob {
   234  				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)))
   235  			}
   236  			if spec.Completions != nil && spec.MaxFailedIndexes != nil && *spec.MaxFailedIndexes > *spec.Completions {
   237  				allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, "must be less than or equal to completions"))
   238  			}
   239  			if spec.MaxFailedIndexes != nil && *spec.MaxFailedIndexes > maxFailedIndexesForIndexedJob {
   240  				allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, fmt.Sprintf("must be less than or equal to %d", maxFailedIndexesForIndexedJob)))
   241  			}
   242  			if spec.Completions != nil && *spec.Completions > completionsSoftLimit && spec.BackoffLimitPerIndex != nil {
   243  				if spec.MaxFailedIndexes == nil {
   244  					allErrs = append(allErrs, field.Required(fldPath.Child("maxFailedIndexes"), fmt.Sprintf("must be specified when completions is above %d", completionsSoftLimit)))
   245  				}
   246  				if spec.Parallelism != nil && *spec.Parallelism > parallelismLimitForHighCompletions {
   247  					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)))
   248  				}
   249  				if spec.MaxFailedIndexes != nil && *spec.MaxFailedIndexes > maxFailedIndexesLimitForHighCompletions {
   250  					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)))
   251  				}
   252  			}
   253  		}
   254  	}
   255  	if spec.CompletionMode == nil || *spec.CompletionMode == batch.NonIndexedCompletion {
   256  		if spec.BackoffLimitPerIndex != nil {
   257  			allErrs = append(allErrs, field.Invalid(fldPath.Child("backoffLimitPerIndex"), *spec.BackoffLimitPerIndex, "requires indexed completion mode"))
   258  		}
   259  		if spec.MaxFailedIndexes != nil {
   260  			allErrs = append(allErrs, field.Invalid(fldPath.Child("maxFailedIndexes"), *spec.MaxFailedIndexes, "requires indexed completion mode"))
   261  		}
   262  	}
   263  
   264  	if spec.PodFailurePolicy != nil {
   265  		allErrs = append(allErrs, validatePodFailurePolicy(spec, fldPath.Child("podFailurePolicy"))...)
   266  	}
   267  	if spec.SuccessPolicy != nil {
   268  		if ptr.Deref(spec.CompletionMode, batch.NonIndexedCompletion) != batch.IndexedCompletion {
   269  			allErrs = append(allErrs, field.Invalid(fldPath.Child("successPolicy"), *spec.SuccessPolicy, "requires indexed completion mode"))
   270  		} else {
   271  			allErrs = append(allErrs, validateSuccessPolicy(spec, fldPath.Child("successPolicy"))...)
   272  		}
   273  	}
   274  
   275  	allErrs = append(allErrs, validatePodReplacementPolicy(spec, fldPath.Child("podReplacementPolicy"))...)
   276  
   277  	allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"), opts)...)
   278  
   279  	// spec.Template.Spec.RestartPolicy can be defaulted as RestartPolicyAlways
   280  	// by SetDefaults_PodSpec function when the user does not explicitly specify a value for it,
   281  	// so we check both empty and RestartPolicyAlways cases here
   282  	if spec.Template.Spec.RestartPolicy == api.RestartPolicyAlways || spec.Template.Spec.RestartPolicy == "" {
   283  		allErrs = append(allErrs, field.Required(fldPath.Child("template", "spec", "restartPolicy"),
   284  			fmt.Sprintf("valid values: %q, %q", api.RestartPolicyOnFailure, api.RestartPolicyNever)))
   285  	} else if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever {
   286  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("template", "spec", "restartPolicy"),
   287  			spec.Template.Spec.RestartPolicy, []api.RestartPolicy{api.RestartPolicyOnFailure, api.RestartPolicyNever}))
   288  	} else if spec.PodFailurePolicy != nil && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever {
   289  		allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "spec", "restartPolicy"),
   290  			spec.Template.Spec.RestartPolicy, fmt.Sprintf("only %q is supported when podFailurePolicy is specified", api.RestartPolicyNever)))
   291  	}
   292  	return allErrs
   293  }
   294  
   295  func validatePodFailurePolicy(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList {
   296  	var allErrs field.ErrorList
   297  	rulesPath := fldPath.Child("rules")
   298  	if len(spec.PodFailurePolicy.Rules) > maxPodFailurePolicyRules {
   299  		allErrs = append(allErrs, field.TooMany(rulesPath, len(spec.PodFailurePolicy.Rules), maxPodFailurePolicyRules))
   300  	}
   301  	containerNames := sets.NewString()
   302  	for _, containerSpec := range spec.Template.Spec.Containers {
   303  		containerNames.Insert(containerSpec.Name)
   304  	}
   305  	for _, containerSpec := range spec.Template.Spec.InitContainers {
   306  		containerNames.Insert(containerSpec.Name)
   307  	}
   308  	for i, rule := range spec.PodFailurePolicy.Rules {
   309  		allErrs = append(allErrs, validatePodFailurePolicyRule(spec, &rule, rulesPath.Index(i), containerNames)...)
   310  	}
   311  	return allErrs
   312  }
   313  
   314  func validatePodReplacementPolicy(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList {
   315  	var allErrs field.ErrorList
   316  	if spec.PodReplacementPolicy != nil {
   317  		// If PodFailurePolicy is specified then we only allow Failed.
   318  		if spec.PodFailurePolicy != nil {
   319  			if *spec.PodReplacementPolicy != batch.Failed {
   320  				allErrs = append(allErrs, field.NotSupported(fldPath, *spec.PodReplacementPolicy, []batch.PodReplacementPolicy{batch.Failed}))
   321  			}
   322  			// If PodFailurePolicy not specified we allow values in supportedPodReplacementPolicy.
   323  		} else if !supportedPodReplacementPolicy.Has(*spec.PodReplacementPolicy) {
   324  			allErrs = append(allErrs, field.NotSupported(fldPath, *spec.PodReplacementPolicy, sets.List(supportedPodReplacementPolicy)))
   325  		}
   326  	}
   327  	return allErrs
   328  }
   329  
   330  func validatePodFailurePolicyRule(spec *batch.JobSpec, rule *batch.PodFailurePolicyRule, rulePath *field.Path, containerNames sets.String) field.ErrorList {
   331  	var allErrs field.ErrorList
   332  	actionPath := rulePath.Child("action")
   333  	if rule.Action == "" {
   334  		allErrs = append(allErrs, field.Required(actionPath, fmt.Sprintf("valid values: %q", sets.List(supportedPodFailurePolicyActions))))
   335  	} else if rule.Action == batch.PodFailurePolicyActionFailIndex {
   336  		if spec.BackoffLimitPerIndex == nil {
   337  			allErrs = append(allErrs, field.Invalid(actionPath, rule.Action, "requires the backoffLimitPerIndex to be set"))
   338  		}
   339  	} else if !supportedPodFailurePolicyActions.Has(rule.Action) {
   340  		allErrs = append(allErrs, field.NotSupported(actionPath, rule.Action, sets.List(supportedPodFailurePolicyActions)))
   341  	}
   342  	if rule.OnExitCodes != nil {
   343  		allErrs = append(allErrs, validatePodFailurePolicyRuleOnExitCodes(rule.OnExitCodes, rulePath.Child("onExitCodes"), containerNames)...)
   344  	}
   345  	if len(rule.OnPodConditions) > 0 {
   346  		allErrs = append(allErrs, validatePodFailurePolicyRuleOnPodConditions(rule.OnPodConditions, rulePath.Child("onPodConditions"))...)
   347  	}
   348  	if rule.OnExitCodes != nil && len(rule.OnPodConditions) > 0 {
   349  		allErrs = append(allErrs, field.Invalid(rulePath, field.OmitValueType{}, "specifying both OnExitCodes and OnPodConditions is not supported"))
   350  	}
   351  	if rule.OnExitCodes == nil && len(rule.OnPodConditions) == 0 {
   352  		allErrs = append(allErrs, field.Invalid(rulePath, field.OmitValueType{}, "specifying one of OnExitCodes and OnPodConditions is required"))
   353  	}
   354  	return allErrs
   355  }
   356  
   357  func validatePodFailurePolicyRuleOnPodConditions(onPodConditions []batch.PodFailurePolicyOnPodConditionsPattern, onPodConditionsPath *field.Path) field.ErrorList {
   358  	var allErrs field.ErrorList
   359  	if len(onPodConditions) > maxPodFailurePolicyOnPodConditionsPatterns {
   360  		allErrs = append(allErrs, field.TooMany(onPodConditionsPath, len(onPodConditions), maxPodFailurePolicyOnPodConditionsPatterns))
   361  	}
   362  	for j, pattern := range onPodConditions {
   363  		patternPath := onPodConditionsPath.Index(j)
   364  		statusPath := patternPath.Child("status")
   365  		allErrs = append(allErrs, apivalidation.ValidateQualifiedName(string(pattern.Type), patternPath.Child("type"))...)
   366  		if pattern.Status == "" {
   367  			allErrs = append(allErrs, field.Required(statusPath, fmt.Sprintf("valid values: %q", sets.List(supportedPodFailurePolicyOnPodConditionsStatus))))
   368  		} else if !supportedPodFailurePolicyOnPodConditionsStatus.Has(pattern.Status) {
   369  			allErrs = append(allErrs, field.NotSupported(statusPath, pattern.Status, sets.List(supportedPodFailurePolicyOnPodConditionsStatus)))
   370  		}
   371  	}
   372  	return allErrs
   373  }
   374  
   375  func validatePodFailurePolicyRuleOnExitCodes(onExitCode *batch.PodFailurePolicyOnExitCodesRequirement, onExitCodesPath *field.Path, containerNames sets.String) field.ErrorList {
   376  	var allErrs field.ErrorList
   377  	operatorPath := onExitCodesPath.Child("operator")
   378  	if onExitCode.Operator == "" {
   379  		allErrs = append(allErrs, field.Required(operatorPath, fmt.Sprintf("valid values: %q", sets.List(supportedPodFailurePolicyOnExitCodesOperator))))
   380  	} else if !supportedPodFailurePolicyOnExitCodesOperator.Has(onExitCode.Operator) {
   381  		allErrs = append(allErrs, field.NotSupported(operatorPath, onExitCode.Operator, sets.List(supportedPodFailurePolicyOnExitCodesOperator)))
   382  	}
   383  	if onExitCode.ContainerName != nil && !containerNames.Has(*onExitCode.ContainerName) {
   384  		allErrs = append(allErrs, field.Invalid(onExitCodesPath.Child("containerName"), *onExitCode.ContainerName, "must be one of the container or initContainer names in the pod template"))
   385  	}
   386  	valuesPath := onExitCodesPath.Child("values")
   387  	if len(onExitCode.Values) == 0 {
   388  		allErrs = append(allErrs, field.Invalid(valuesPath, onExitCode.Values, "at least one value is required"))
   389  	} else if len(onExitCode.Values) > maxPodFailurePolicyOnExitCodesValues {
   390  		allErrs = append(allErrs, field.TooMany(valuesPath, len(onExitCode.Values), maxPodFailurePolicyOnExitCodesValues))
   391  	}
   392  	isOrdered := true
   393  	uniqueValues := sets.NewInt32()
   394  	for j, exitCodeValue := range onExitCode.Values {
   395  		valuePath := valuesPath.Index(j)
   396  		if onExitCode.Operator == batch.PodFailurePolicyOnExitCodesOpIn && exitCodeValue == 0 {
   397  			allErrs = append(allErrs, field.Invalid(valuePath, exitCodeValue, "must not be 0 for the In operator"))
   398  		}
   399  		if uniqueValues.Has(exitCodeValue) {
   400  			allErrs = append(allErrs, field.Duplicate(valuePath, exitCodeValue))
   401  		} else {
   402  			uniqueValues.Insert(exitCodeValue)
   403  		}
   404  		if j > 0 && onExitCode.Values[j-1] > exitCodeValue {
   405  			isOrdered = false
   406  		}
   407  	}
   408  	if !isOrdered {
   409  		allErrs = append(allErrs, field.Invalid(valuesPath, onExitCode.Values, "must be ordered"))
   410  	}
   411  
   412  	return allErrs
   413  }
   414  
   415  func validateSuccessPolicy(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList {
   416  	var allErrs field.ErrorList
   417  	rulesPath := fldPath.Child("rules")
   418  	if len(spec.SuccessPolicy.Rules) == 0 {
   419  		allErrs = append(allErrs, field.Required(rulesPath, "at least one rules must be specified when the successPolicy is specified"))
   420  	}
   421  	if len(spec.SuccessPolicy.Rules) > maxSuccessPolicyRule {
   422  		allErrs = append(allErrs, field.TooMany(rulesPath, len(spec.SuccessPolicy.Rules), maxSuccessPolicyRule))
   423  	}
   424  	for i, rule := range spec.SuccessPolicy.Rules {
   425  		allErrs = append(allErrs, validateSuccessPolicyRule(spec, &rule, rulesPath.Index(i))...)
   426  	}
   427  	return allErrs
   428  }
   429  
   430  func validateSuccessPolicyRule(spec *batch.JobSpec, rule *batch.SuccessPolicyRule, rulePath *field.Path) field.ErrorList {
   431  	var allErrs field.ErrorList
   432  	if rule.SucceededCount == nil && rule.SucceededIndexes == nil {
   433  		allErrs = append(allErrs, field.Required(rulePath, "at least one of succeededCount or succeededIndexes must be specified"))
   434  	}
   435  	var totalIndexes int32
   436  	if rule.SucceededIndexes != nil {
   437  		succeededIndexes := rulePath.Child("succeededIndexes")
   438  		if len(*rule.SucceededIndexes) > maxJobSuccessPolicySucceededIndexesLimit {
   439  			allErrs = append(allErrs, field.TooLong(succeededIndexes, *rule.SucceededIndexes, maxJobSuccessPolicySucceededIndexesLimit))
   440  		}
   441  		var err error
   442  		if totalIndexes, err = validateIndexesFormat(*rule.SucceededIndexes, *spec.Completions); err != nil {
   443  			allErrs = append(allErrs, field.Invalid(succeededIndexes, *rule.SucceededIndexes, fmt.Sprintf("error parsing succeededIndexes: %s", err.Error())))
   444  		}
   445  	}
   446  	if rule.SucceededCount != nil {
   447  		succeededCountPath := rulePath.Child("succeededCount")
   448  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*rule.SucceededCount), succeededCountPath)...)
   449  		if *rule.SucceededCount > *spec.Completions {
   450  			allErrs = append(allErrs, field.Invalid(succeededCountPath, *rule.SucceededCount, fmt.Sprintf("must be less than or equal to %d (the number of specified completions)", *spec.Completions)))
   451  		}
   452  		if rule.SucceededIndexes != nil && *rule.SucceededCount > totalIndexes {
   453  			allErrs = append(allErrs, field.Invalid(succeededCountPath, *rule.SucceededCount, fmt.Sprintf("must be less than or equal to %d (the number of indexes in the specified succeededIndexes field)", totalIndexes)))
   454  		}
   455  	}
   456  	return allErrs
   457  }
   458  
   459  // validateJobStatus validates a JobStatus and returns an ErrorList with any errors.
   460  func validateJobStatus(job *batch.Job, fldPath *field.Path, opts JobStatusValidationOptions) field.ErrorList {
   461  	allErrs := field.ErrorList{}
   462  	status := job.Status
   463  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Active), fldPath.Child("active"))...)
   464  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Succeeded), fldPath.Child("succeeded"))...)
   465  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Failed), fldPath.Child("failed"))...)
   466  	if status.Ready != nil {
   467  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.Ready), fldPath.Child("ready"))...)
   468  	}
   469  	if status.Terminating != nil {
   470  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.Terminating), fldPath.Child("terminating"))...)
   471  	}
   472  	if status.UncountedTerminatedPods != nil {
   473  		path := fldPath.Child("uncountedTerminatedPods")
   474  		seen := sets.New[types.UID]()
   475  		for i, k := range status.UncountedTerminatedPods.Succeeded {
   476  			p := path.Child("succeeded").Index(i)
   477  			if k == "" {
   478  				allErrs = append(allErrs, field.Invalid(p, k, "must not be empty"))
   479  			} else if seen.Has(k) {
   480  				allErrs = append(allErrs, field.Duplicate(p, k))
   481  			} else {
   482  				seen.Insert(k)
   483  			}
   484  		}
   485  		for i, k := range status.UncountedTerminatedPods.Failed {
   486  			p := path.Child("failed").Index(i)
   487  			if k == "" {
   488  				allErrs = append(allErrs, field.Invalid(p, k, "must not be empty"))
   489  			} else if seen.Has(k) {
   490  				allErrs = append(allErrs, field.Duplicate(p, k))
   491  			} else {
   492  				seen.Insert(k)
   493  			}
   494  		}
   495  	}
   496  	if opts.RejectCompleteJobWithFailedCondition {
   497  		if IsJobComplete(job) && IsJobFailed(job) {
   498  			allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set Complete=True and Failed=true conditions"))
   499  		}
   500  	}
   501  	if opts.RejectCompleteJobWithFailureTargetCondition {
   502  		if IsJobComplete(job) && IsConditionTrue(status.Conditions, batch.JobFailureTarget) {
   503  			allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set Complete=True and FailureTarget=true conditions"))
   504  		}
   505  	}
   506  	if opts.RejectNotCompleteJobWithCompletionTime {
   507  		if status.CompletionTime != nil && !IsJobComplete(job) {
   508  			allErrs = append(allErrs, field.Invalid(fldPath.Child("completionTime"), status.CompletionTime, "cannot set completionTime when there is no Complete=True condition"))
   509  		}
   510  	}
   511  	if opts.RejectCompleteJobWithoutCompletionTime {
   512  		if status.CompletionTime == nil && IsJobComplete(job) {
   513  			allErrs = append(allErrs, field.Required(fldPath.Child("completionTime"), "completionTime is required for Complete jobs"))
   514  		}
   515  	}
   516  	if opts.RejectCompletionTimeBeforeStartTime {
   517  		if status.StartTime != nil && status.CompletionTime != nil && status.CompletionTime.Before(status.StartTime) {
   518  			allErrs = append(allErrs, field.Invalid(fldPath.Child("completionTime"), status.CompletionTime, "completionTime cannot be set before startTime"))
   519  		}
   520  	}
   521  	isJobFinished := IsJobFinished(job)
   522  	if opts.RejectFinishedJobWithActivePods {
   523  		if status.Active > 0 && isJobFinished {
   524  			allErrs = append(allErrs, field.Invalid(fldPath.Child("active"), status.Active, "active>0 is invalid for finished job"))
   525  		}
   526  	}
   527  	if opts.RejectFinishedJobWithoutStartTime {
   528  		if status.StartTime == nil && isJobFinished {
   529  			allErrs = append(allErrs, field.Required(fldPath.Child("startTime"), "startTime is required for finished job"))
   530  		}
   531  	}
   532  	if opts.RejectFinishedJobWithUncountedTerminatedPods {
   533  		if isJobFinished && status.UncountedTerminatedPods != nil && len(status.UncountedTerminatedPods.Failed)+len(status.UncountedTerminatedPods.Succeeded) > 0 {
   534  			allErrs = append(allErrs, field.Invalid(fldPath.Child("uncountedTerminatedPods"), status.UncountedTerminatedPods, "uncountedTerminatedPods needs to be empty for finished job"))
   535  		}
   536  	}
   537  	if opts.RejectInvalidCompletedIndexes {
   538  		if job.Spec.Completions != nil {
   539  			if _, err := validateIndexesFormat(status.CompletedIndexes, int32(*job.Spec.Completions)); err != nil {
   540  				allErrs = append(allErrs, field.Invalid(fldPath.Child("completedIndexes"), status.CompletedIndexes, fmt.Sprintf("error parsing completedIndexes: %s", err.Error())))
   541  			}
   542  		}
   543  	}
   544  	if opts.RejectInvalidFailedIndexes {
   545  		if job.Spec.Completions != nil && job.Spec.BackoffLimitPerIndex != nil && status.FailedIndexes != nil {
   546  			if _, err := validateIndexesFormat(*status.FailedIndexes, int32(*job.Spec.Completions)); err != nil {
   547  				allErrs = append(allErrs, field.Invalid(fldPath.Child("failedIndexes"), status.FailedIndexes, fmt.Sprintf("error parsing failedIndexes: %s", err.Error())))
   548  			}
   549  		}
   550  	}
   551  	isIndexed := ptr.Deref(job.Spec.CompletionMode, batch.NonIndexedCompletion) == batch.IndexedCompletion
   552  	if opts.RejectCompletedIndexesForNonIndexedJob {
   553  		if len(status.CompletedIndexes) != 0 && !isIndexed {
   554  			allErrs = append(allErrs, field.Invalid(fldPath.Child("completedIndexes"), status.CompletedIndexes, "cannot set non-empty completedIndexes when non-indexed completion mode"))
   555  		}
   556  	}
   557  	if opts.RejectFailedIndexesForNoBackoffLimitPerIndex {
   558  		// Note that this check also verifies that FailedIndexes are not used for
   559  		// regular (non-indexed) jobs, because regular jobs have backoffLimitPerIndex = nil.
   560  		if job.Spec.BackoffLimitPerIndex == nil && status.FailedIndexes != nil {
   561  			allErrs = append(allErrs, field.Invalid(fldPath.Child("failedIndexes"), *status.FailedIndexes, "cannot set non-null failedIndexes when backoffLimitPerIndex is null"))
   562  		}
   563  	}
   564  	if opts.RejectFailedIndexesOverlappingCompleted {
   565  		if job.Spec.Completions != nil && status.FailedIndexes != nil {
   566  			if err := validateFailedIndexesNotOverlapCompleted(status.CompletedIndexes, *status.FailedIndexes, int32(*job.Spec.Completions)); err != nil {
   567  				allErrs = append(allErrs, field.Invalid(fldPath.Child("failedIndexes"), *status.FailedIndexes, err.Error()))
   568  			}
   569  		}
   570  	}
   571  	if ptr.Deref(job.Spec.CompletionMode, batch.NonIndexedCompletion) != batch.IndexedCompletion && isJobSuccessCriteriaMet(job) {
   572  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set SuccessCriteriaMet to NonIndexed Job"))
   573  	}
   574  	if isJobSuccessCriteriaMet(job) && IsJobFailed(job) {
   575  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set SuccessCriteriaMet=True and Failed=true conditions"))
   576  	}
   577  	if isJobSuccessCriteriaMet(job) && isJobFailureTarget(job) {
   578  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set SuccessCriteriaMet=True and FailureTarget=true conditions"))
   579  	}
   580  	if job.Spec.SuccessPolicy == nil && isJobSuccessCriteriaMet(job) {
   581  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set SuccessCriteriaMet=True for Job without SuccessPolicy"))
   582  	}
   583  	if job.Spec.SuccessPolicy != nil && !isJobSuccessCriteriaMet(job) && IsJobComplete(job) {
   584  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set Complete=True for Job with SuccessPolicy unless SuccessCriteriaMet=True"))
   585  	}
   586  	return allErrs
   587  }
   588  
   589  // ValidateJobUpdate validates an update to a Job and returns an ErrorList with any errors.
   590  func ValidateJobUpdate(job, oldJob *batch.Job, opts JobValidationOptions) field.ErrorList {
   591  	allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
   592  	allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec, field.NewPath("spec"), opts)...)
   593  	return allErrs
   594  }
   595  
   596  // ValidateJobUpdateStatus validates an update to the status of a Job and returns an ErrorList with any errors.
   597  func ValidateJobUpdateStatus(job, oldJob *batch.Job, opts JobStatusValidationOptions) field.ErrorList {
   598  	allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
   599  	allErrs = append(allErrs, ValidateJobStatusUpdate(job, oldJob, opts)...)
   600  	return allErrs
   601  }
   602  
   603  // ValidateJobSpecUpdate validates an update to a JobSpec and returns an ErrorList with any errors.
   604  func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList {
   605  	allErrs := field.ErrorList{}
   606  	allErrs = append(allErrs, ValidateJobSpec(&spec, fldPath, opts.PodValidationOptions)...)
   607  	allErrs = append(allErrs, validateCompletions(spec, oldSpec, fldPath.Child("completions"), opts)...)
   608  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, fldPath.Child("selector"))...)
   609  	allErrs = append(allErrs, validatePodTemplateUpdate(spec, oldSpec, fldPath, opts)...)
   610  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.CompletionMode, oldSpec.CompletionMode, fldPath.Child("completionMode"))...)
   611  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.PodFailurePolicy, oldSpec.PodFailurePolicy, fldPath.Child("podFailurePolicy"))...)
   612  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.BackoffLimitPerIndex, oldSpec.BackoffLimitPerIndex, fldPath.Child("backoffLimitPerIndex"))...)
   613  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.ManagedBy, oldSpec.ManagedBy, fldPath.Child("managedBy"))...)
   614  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.SuccessPolicy, oldSpec.SuccessPolicy, fldPath.Child("successPolicy"))...)
   615  	return allErrs
   616  }
   617  
   618  func validatePodTemplateUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList {
   619  	allErrs := field.ErrorList{}
   620  	template := &spec.Template
   621  	oldTemplate := &oldSpec.Template
   622  	if opts.AllowMutableSchedulingDirectives {
   623  		oldTemplate = oldSpec.Template.DeepCopy() // +k8s:verify-mutation:reason=clone
   624  		switch {
   625  		case template.Spec.Affinity == nil && oldTemplate.Spec.Affinity != nil:
   626  			// allow the Affinity field to be cleared if the old template had no affinity directives other than NodeAffinity
   627  			oldTemplate.Spec.Affinity.NodeAffinity = nil // +k8s:verify-mutation:reason=clone
   628  			if (*oldTemplate.Spec.Affinity) == (api.Affinity{}) {
   629  				oldTemplate.Spec.Affinity = nil // +k8s:verify-mutation:reason=clone
   630  			}
   631  		case template.Spec.Affinity != nil && oldTemplate.Spec.Affinity == nil:
   632  			// allow the NodeAffinity field to skip immutability checking
   633  			oldTemplate.Spec.Affinity = &api.Affinity{NodeAffinity: template.Spec.Affinity.NodeAffinity} // +k8s:verify-mutation:reason=clone
   634  		case template.Spec.Affinity != nil && oldTemplate.Spec.Affinity != nil:
   635  			// allow the NodeAffinity field to skip immutability checking
   636  			oldTemplate.Spec.Affinity.NodeAffinity = template.Spec.Affinity.NodeAffinity // +k8s:verify-mutation:reason=clone
   637  		}
   638  		oldTemplate.Spec.NodeSelector = template.Spec.NodeSelector       // +k8s:verify-mutation:reason=clone
   639  		oldTemplate.Spec.Tolerations = template.Spec.Tolerations         // +k8s:verify-mutation:reason=clone
   640  		oldTemplate.Annotations = template.Annotations                   // +k8s:verify-mutation:reason=clone
   641  		oldTemplate.Labels = template.Labels                             // +k8s:verify-mutation:reason=clone
   642  		oldTemplate.Spec.SchedulingGates = template.Spec.SchedulingGates // +k8s:verify-mutation:reason=clone
   643  	}
   644  	allErrs = append(allErrs, apivalidation.ValidateImmutableField(template, oldTemplate, fldPath.Child("template"))...)
   645  	return allErrs
   646  }
   647  
   648  // ValidateJobStatusUpdate validates an update to a JobStatus and returns an ErrorList with any errors.
   649  func ValidateJobStatusUpdate(job, oldJob *batch.Job, opts JobStatusValidationOptions) field.ErrorList {
   650  	allErrs := field.ErrorList{}
   651  	statusFld := field.NewPath("status")
   652  	allErrs = append(allErrs, validateJobStatus(job, statusFld, opts)...)
   653  
   654  	if opts.RejectDisablingTerminalCondition {
   655  		for _, cType := range []batch.JobConditionType{batch.JobFailed, batch.JobComplete, batch.JobFailureTarget} {
   656  			if IsConditionTrue(oldJob.Status.Conditions, cType) && !IsConditionTrue(job.Status.Conditions, cType) {
   657  				allErrs = append(allErrs, field.Invalid(statusFld.Child("conditions"), field.OmitValueType{}, fmt.Sprintf("cannot disable the terminal %s=True condition", string(cType))))
   658  			}
   659  		}
   660  	}
   661  	if opts.RejectDecreasingFailedCounter {
   662  		if job.Status.Failed < oldJob.Status.Failed {
   663  			allErrs = append(allErrs, field.Invalid(statusFld.Child("failed"), job.Status.Failed, "cannot decrease the failed counter"))
   664  		}
   665  	}
   666  	if opts.RejectDecreasingSucceededCounter {
   667  		if job.Status.Succeeded < oldJob.Status.Succeeded {
   668  			allErrs = append(allErrs, field.Invalid(statusFld.Child("succeeded"), job.Status.Succeeded, "cannot decrease the succeeded counter"))
   669  		}
   670  	}
   671  	if opts.RejectMutatingCompletionTime {
   672  		// Note that we check the condition only when `job.Status.CompletionTime != nil`, this is because
   673  		// we don't want to block transitions to completionTime = nil when the job is not finished yet.
   674  		// Setting completionTime = nil for finished jobs is prevented in RejectCompleteJobWithoutCompletionTime.
   675  		if job.Status.CompletionTime != nil && oldJob.Status.CompletionTime != nil && !ptr.Equal(job.Status.CompletionTime, oldJob.Status.CompletionTime) {
   676  			allErrs = append(allErrs, field.Invalid(statusFld.Child("completionTime"), job.Status.CompletionTime, "completionTime cannot be mutated"))
   677  		}
   678  	}
   679  	if opts.RejectStartTimeUpdateForUnsuspendedJob {
   680  		// Note that we check `oldJob.Status.StartTime != nil` to allow transitioning from
   681  		// startTime = nil to startTime != nil for unsuspended jobs, which is a desired transition.
   682  		if oldJob.Status.StartTime != nil && !ptr.Equal(oldJob.Status.StartTime, job.Status.StartTime) && !ptr.Deref(job.Spec.Suspend, false) {
   683  			allErrs = append(allErrs, field.Required(statusFld.Child("startTime"), "startTime cannot be removed for unsuspended job"))
   684  		}
   685  	}
   686  	if isJobSuccessCriteriaMet(oldJob) && !isJobSuccessCriteriaMet(job) {
   687  		allErrs = append(allErrs, field.Invalid(statusFld.Child("conditions"), field.OmitValueType{}, "cannot disable the SuccessCriteriaMet=True condition"))
   688  	}
   689  	if IsJobComplete(oldJob) && !isJobSuccessCriteriaMet(oldJob) && isJobSuccessCriteriaMet(job) {
   690  		allErrs = append(allErrs, field.Invalid(statusFld.Child("conditions"), field.OmitValueType{}, "cannot set SuccessCriteriaMet=True for Job already has Complete=true conditions"))
   691  	}
   692  	return allErrs
   693  }
   694  
   695  // ValidateCronJobCreate validates a CronJob on creation and returns an ErrorList with any errors.
   696  func ValidateCronJobCreate(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
   697  	// CronJobs and rcs have the same name validation
   698  	allErrs := apivalidation.ValidateObjectMeta(&cronJob.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
   699  	allErrs = append(allErrs, validateCronJobSpec(&cronJob.Spec, nil, field.NewPath("spec"), opts)...)
   700  	if len(cronJob.ObjectMeta.Name) > apimachineryvalidation.DNS1035LabelMaxLength-11 {
   701  		// The cronjob controller appends a 11-character suffix to the cronjob (`-$TIMESTAMP`) when
   702  		// creating a job. The job name length limit is 63 characters.
   703  		// Therefore cronjob names must have length <= 63-11=52. If we don't validate this here,
   704  		// then job creation will fail later.
   705  		allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), cronJob.ObjectMeta.Name, "must be no more than 52 characters"))
   706  	}
   707  	return allErrs
   708  }
   709  
   710  // ValidateCronJobUpdate validates an update to a CronJob and returns an ErrorList with any errors.
   711  func ValidateCronJobUpdate(job, oldJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
   712  	allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
   713  	allErrs = append(allErrs, validateCronJobSpec(&job.Spec, &oldJob.Spec, field.NewPath("spec"), opts)...)
   714  
   715  	// skip the 52-character name validation limit on update validation
   716  	// to allow old cronjobs with names > 52 chars to be updated/deleted
   717  	return allErrs
   718  }
   719  
   720  // validateCronJobSpec validates a CronJobSpec and returns an ErrorList with any errors.
   721  func validateCronJobSpec(spec, oldSpec *batch.CronJobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   722  	allErrs := field.ErrorList{}
   723  
   724  	if len(spec.Schedule) == 0 {
   725  		allErrs = append(allErrs, field.Required(fldPath.Child("schedule"), ""))
   726  	} else {
   727  		allowTZInSchedule := false
   728  		if oldSpec != nil {
   729  			allowTZInSchedule = strings.Contains(oldSpec.Schedule, "TZ")
   730  		}
   731  		allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, allowTZInSchedule, spec.TimeZone, fldPath.Child("schedule"))...)
   732  	}
   733  
   734  	if spec.StartingDeadlineSeconds != nil {
   735  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...)
   736  	}
   737  
   738  	if oldSpec == nil || !pointer.StringEqual(oldSpec.TimeZone, spec.TimeZone) {
   739  		allErrs = append(allErrs, validateTimeZone(spec.TimeZone, fldPath.Child("timeZone"))...)
   740  	}
   741  
   742  	allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...)
   743  	allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"), opts)...)
   744  
   745  	if spec.SuccessfulJobsHistoryLimit != nil {
   746  		// zero is a valid SuccessfulJobsHistoryLimit
   747  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.SuccessfulJobsHistoryLimit), fldPath.Child("successfulJobsHistoryLimit"))...)
   748  	}
   749  	if spec.FailedJobsHistoryLimit != nil {
   750  		// zero is a valid SuccessfulJobsHistoryLimit
   751  		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.FailedJobsHistoryLimit), fldPath.Child("failedJobsHistoryLimit"))...)
   752  	}
   753  
   754  	return allErrs
   755  }
   756  
   757  func validateConcurrencyPolicy(concurrencyPolicy *batch.ConcurrencyPolicy, fldPath *field.Path) field.ErrorList {
   758  	allErrs := field.ErrorList{}
   759  	switch *concurrencyPolicy {
   760  	case batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent:
   761  		break
   762  	case "":
   763  		allErrs = append(allErrs, field.Required(fldPath, ""))
   764  	default:
   765  		validValues := []batch.ConcurrencyPolicy{batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent}
   766  		allErrs = append(allErrs, field.NotSupported(fldPath, *concurrencyPolicy, validValues))
   767  	}
   768  
   769  	return allErrs
   770  }
   771  
   772  func validateScheduleFormat(schedule string, allowTZInSchedule bool, timeZone *string, fldPath *field.Path) field.ErrorList {
   773  	allErrs := field.ErrorList{}
   774  	if _, err := cron.ParseStandard(schedule); err != nil {
   775  		allErrs = append(allErrs, field.Invalid(fldPath, schedule, err.Error()))
   776  	}
   777  	switch {
   778  	case allowTZInSchedule && strings.Contains(schedule, "TZ") && timeZone != nil:
   779  		allErrs = append(allErrs, field.Invalid(fldPath, schedule, "cannot use both timeZone field and TZ or CRON_TZ in schedule"))
   780  	case !allowTZInSchedule && strings.Contains(schedule, "TZ"):
   781  		allErrs = append(allErrs, field.Invalid(fldPath, schedule, "cannot use TZ or CRON_TZ in schedule, use timeZone field instead"))
   782  	}
   783  
   784  	return allErrs
   785  }
   786  
   787  // https://data.iana.org/time-zones/theory.html#naming
   788  // * A name must not be empty, or contain '//', or start or end with '/'.
   789  // * Do not use the file name components '.' and '..'.
   790  // * Within a file name component, use only ASCII letters, '.', '-' and '_'.
   791  // * Do not use digits, as that might create an ambiguity with POSIX TZ strings.
   792  // * A file name component must not exceed 14 characters or start with '-'
   793  //
   794  // 0-9 and + characters are tolerated to accommodate legacy compatibility names
   795  var validTimeZoneCharacters = regexp.MustCompile(`^[A-Za-z\.\-_0-9+]{1,14}$`)
   796  
   797  func validateTimeZone(timeZone *string, fldPath *field.Path) field.ErrorList {
   798  	allErrs := field.ErrorList{}
   799  	if timeZone == nil {
   800  		return allErrs
   801  	}
   802  
   803  	if len(*timeZone) == 0 {
   804  		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be nil or non-empty string"))
   805  		return allErrs
   806  	}
   807  
   808  	for _, part := range strings.Split(*timeZone, "/") {
   809  		if part == "." || part == ".." || strings.HasPrefix(part, "-") || !validTimeZoneCharacters.MatchString(part) {
   810  			allErrs = append(allErrs, field.Invalid(fldPath, timeZone, fmt.Sprintf("unknown time zone %s", *timeZone)))
   811  			return allErrs
   812  		}
   813  	}
   814  
   815  	if strings.EqualFold(*timeZone, "Local") {
   816  		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones"))
   817  	}
   818  
   819  	if _, err := time.LoadLocation(*timeZone); err != nil {
   820  		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, err.Error()))
   821  	}
   822  
   823  	return allErrs
   824  }
   825  
   826  // ValidateJobTemplateSpec validates a JobTemplateSpec and returns an ErrorList with any errors.
   827  func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
   828  	allErrs := validateJobSpec(&spec.Spec, fldPath.Child("spec"), opts)
   829  
   830  	// jobtemplate will always have the selector automatically generated
   831  	if spec.Spec.Selector != nil {
   832  		allErrs = append(allErrs, field.Invalid(fldPath.Child("spec", "selector"), spec.Spec.Selector, "`selector` will be auto-generated"))
   833  	}
   834  	if spec.Spec.ManualSelector != nil && *spec.Spec.ManualSelector {
   835  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("spec", "manualSelector"), spec.Spec.ManualSelector, []string{"nil", "false"}))
   836  	}
   837  	return allErrs
   838  }
   839  
   840  func validateCompletions(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList {
   841  	if !opts.AllowElasticIndexedJobs {
   842  		return apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath)
   843  	}
   844  
   845  	// Completions is immutable for non-indexed jobs.
   846  	// For Indexed Jobs, if ElasticIndexedJob feature gate is not enabled,
   847  	// fall back to validating that spec.Completions is always immutable.
   848  	isIndexedJob := spec.CompletionMode != nil && *spec.CompletionMode == batch.IndexedCompletion
   849  	if !isIndexedJob {
   850  		return apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath)
   851  	}
   852  
   853  	var allErrs field.ErrorList
   854  	if apiequality.Semantic.DeepEqual(spec.Completions, oldSpec.Completions) {
   855  		return allErrs
   856  	}
   857  	// Indexed Jobs cannot set completions to nil. The nil check
   858  	// is already performed in validateJobSpec, no need to add another error.
   859  	if spec.Completions == nil {
   860  		return allErrs
   861  	}
   862  
   863  	if *spec.Completions != *spec.Parallelism {
   864  		allErrs = append(allErrs, field.Invalid(fldPath, spec.Completions, fmt.Sprintf("can only be modified in tandem with %s", fldPath.Root().Child("parallelism").String())))
   865  	}
   866  	return allErrs
   867  }
   868  
   869  func IsJobFinished(job *batch.Job) bool {
   870  	for _, c := range job.Status.Conditions {
   871  		if (c.Type == batch.JobComplete || c.Type == batch.JobFailed) && c.Status == api.ConditionTrue {
   872  			return true
   873  		}
   874  	}
   875  	return false
   876  }
   877  
   878  func IsJobComplete(job *batch.Job) bool {
   879  	return IsConditionTrue(job.Status.Conditions, batch.JobComplete)
   880  }
   881  
   882  func IsJobFailed(job *batch.Job) bool {
   883  	return IsConditionTrue(job.Status.Conditions, batch.JobFailed)
   884  }
   885  
   886  func isJobSuccessCriteriaMet(job *batch.Job) bool {
   887  	return IsConditionTrue(job.Status.Conditions, batch.JobSuccessCriteriaMet)
   888  }
   889  
   890  func isJobFailureTarget(job *batch.Job) bool {
   891  	return IsConditionTrue(job.Status.Conditions, batch.JobFailureTarget)
   892  }
   893  
   894  func IsConditionTrue(list []batch.JobCondition, cType batch.JobConditionType) bool {
   895  	for _, c := range list {
   896  		if c.Type == cType && c.Status == api.ConditionTrue {
   897  			return true
   898  		}
   899  	}
   900  	return false
   901  }
   902  
   903  func validateFailedIndexesNotOverlapCompleted(completedIndexesStr string, failedIndexesStr string, completions int32) error {
   904  	if len(completedIndexesStr) == 0 || len(failedIndexesStr) == 0 {
   905  		return nil
   906  	}
   907  	completedIndexesIntervals := strings.Split(completedIndexesStr, ",")
   908  	failedIndexesIntervals := strings.Split(failedIndexesStr, ",")
   909  	var completedPos, failedPos int
   910  	cX, cY, cErr := parseIndexInterval(completedIndexesIntervals[completedPos], completions)
   911  	fX, fY, fErr := parseIndexInterval(failedIndexesIntervals[failedPos], completions)
   912  	for completedPos < len(completedIndexesIntervals) && failedPos < len(failedIndexesIntervals) {
   913  		if cErr != nil {
   914  			// Failure to parse "completed" interval. We go to the next interval,
   915  			// the error will be reported to the user when validating the format.
   916  			completedPos++
   917  			if completedPos < len(completedIndexesIntervals) {
   918  				cX, cY, cErr = parseIndexInterval(completedIndexesIntervals[completedPos], completions)
   919  			}
   920  		} else if fErr != nil {
   921  			// Failure to parse "failed" interval. We go to the next interval,
   922  			// the error will be reported to the user when validating the format.
   923  			failedPos++
   924  			if failedPos < len(failedIndexesIntervals) {
   925  				fX, fY, fErr = parseIndexInterval(failedIndexesIntervals[failedPos], completions)
   926  			}
   927  		} else {
   928  			// We have one failed and one completed interval parsed.
   929  			if cX <= fY && fX <= cY {
   930  				return fmt.Errorf("failedIndexes and completedIndexes overlap at index: %d", max(cX, fX))
   931  			}
   932  			// No overlap, let's move to the next one.
   933  			if cX <= fX {
   934  				completedPos++
   935  				if completedPos < len(completedIndexesIntervals) {
   936  					cX, cY, cErr = parseIndexInterval(completedIndexesIntervals[completedPos], completions)
   937  				}
   938  			} else {
   939  				failedPos++
   940  				if failedPos < len(failedIndexesIntervals) {
   941  					fX, fY, fErr = parseIndexInterval(failedIndexesIntervals[failedPos], completions)
   942  				}
   943  			}
   944  		}
   945  	}
   946  	return nil
   947  }
   948  
   949  func validateIndexesFormat(indexesStr string, completions int32) (int32, error) {
   950  	if len(indexesStr) == 0 {
   951  		return 0, nil
   952  	}
   953  	var lastIndex *int32
   954  	var total int32
   955  	for _, intervalStr := range strings.Split(indexesStr, ",") {
   956  		x, y, err := parseIndexInterval(intervalStr, completions)
   957  		if err != nil {
   958  			return 0, err
   959  		}
   960  		if lastIndex != nil && *lastIndex >= x {
   961  			return 0, fmt.Errorf("non-increasing order, previous: %d, current: %d", *lastIndex, x)
   962  		}
   963  		total += y - x + 1
   964  		lastIndex = &y
   965  	}
   966  	return total, nil
   967  }
   968  
   969  func parseIndexInterval(intervalStr string, completions int32) (int32, int32, error) {
   970  	limitsStr := strings.Split(intervalStr, "-")
   971  	if len(limitsStr) > 2 {
   972  		return 0, 0, fmt.Errorf("the fragment %q violates the requirement that an index interval can have at most two parts separated by '-'", intervalStr)
   973  	}
   974  	x, err := strconv.Atoi(limitsStr[0])
   975  	if err != nil {
   976  		return 0, 0, fmt.Errorf("cannot convert string to integer for index: %q", limitsStr[0])
   977  	}
   978  	if x >= int(completions) {
   979  		return 0, 0, fmt.Errorf("too large index: %q", limitsStr[0])
   980  	}
   981  	if len(limitsStr) > 1 {
   982  		y, err := strconv.Atoi(limitsStr[1])
   983  		if err != nil {
   984  			return 0, 0, fmt.Errorf("cannot convert string to integer for index: %q", limitsStr[1])
   985  		}
   986  		if y >= int(completions) {
   987  			return 0, 0, fmt.Errorf("too large index: %q", limitsStr[1])
   988  		}
   989  		if x >= y {
   990  			return 0, 0, fmt.Errorf("non-increasing order, previous: %d, current: %d", x, y)
   991  		}
   992  		return int32(x), int32(y), nil
   993  	}
   994  	return int32(x), int32(x), nil
   995  }
   996  
   997  type JobValidationOptions struct {
   998  	apivalidation.PodValidationOptions
   999  	// Allow mutable node affinity, selector and tolerations of the template
  1000  	AllowMutableSchedulingDirectives bool
  1001  	// Allow elastic indexed jobs
  1002  	AllowElasticIndexedJobs bool
  1003  	// Require Job to have the label on batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid
  1004  	RequirePrefixedLabels bool
  1005  }
  1006  
  1007  type JobStatusValidationOptions struct {
  1008  	RejectDecreasingSucceededCounter             bool
  1009  	RejectDecreasingFailedCounter                bool
  1010  	RejectDisablingTerminalCondition             bool
  1011  	RejectInvalidCompletedIndexes                bool
  1012  	RejectInvalidFailedIndexes                   bool
  1013  	RejectFailedIndexesOverlappingCompleted      bool
  1014  	RejectCompletedIndexesForNonIndexedJob       bool
  1015  	RejectFailedIndexesForNoBackoffLimitPerIndex bool
  1016  	RejectFinishedJobWithActivePods              bool
  1017  	RejectFinishedJobWithoutStartTime            bool
  1018  	RejectFinishedJobWithUncountedTerminatedPods bool
  1019  	RejectStartTimeUpdateForUnsuspendedJob       bool
  1020  	RejectCompletionTimeBeforeStartTime          bool
  1021  	RejectMutatingCompletionTime                 bool
  1022  	RejectCompleteJobWithoutCompletionTime       bool
  1023  	RejectNotCompleteJobWithCompletionTime       bool
  1024  	RejectCompleteJobWithFailedCondition         bool
  1025  	RejectCompleteJobWithFailureTargetCondition  bool
  1026  }
  1027  

View as plain text