k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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 "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 }