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