1
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
44
45
46 const maxParallelismForIndexedJob = 100000
47
48
49
50
51 const maxFailedIndexesForIndexedJob = 100_000
52
53 const (
54 completionsSoftLimit = 100_000
55 parallelismLimitForHighCompletions = 10_000
56 maxFailedIndexesLimitForHighCompletions = 10_000
57
58
59 maxPodFailurePolicyRules = 20
60
61
62 maxPodFailurePolicyOnExitCodesValues = 255
63
64
65 maxPodFailurePolicyOnPodConditionsPatterns = 20
66
67
68 maxManagedByLength = 63
69
70
71 maxJobSuccessPolicySucceededIndexesLimit = 64 * 1024
72
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
98
99
100
101
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
110 }
111
112
113
114 if obj.ObjectMeta.UID == "" {
115 allErrs = append(allErrs, field.Required(field.NewPath("metadata").Child("uid"), ""))
116 }
117
118
119
120
121
122
123
124
125
126
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
137
138
139 expectedLabels[batch.LegacyControllerUidLabel] = string(obj.UID)
140 expectedLabels[batch.LegacyJobNameLabel] = string(obj.Name)
141
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
152 func ValidateJob(job *batch.Job, opts JobValidationOptions) field.ErrorList {
153
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
159
160
161
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
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
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
280
281
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
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
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
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
559
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
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
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
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()
624 switch {
625 case template.Spec.Affinity == nil && oldTemplate.Spec.Affinity != nil:
626
627 oldTemplate.Spec.Affinity.NodeAffinity = nil
628 if (*oldTemplate.Spec.Affinity) == (api.Affinity{}) {
629 oldTemplate.Spec.Affinity = nil
630 }
631 case template.Spec.Affinity != nil && oldTemplate.Spec.Affinity == nil:
632
633 oldTemplate.Spec.Affinity = &api.Affinity{NodeAffinity: template.Spec.Affinity.NodeAffinity}
634 case template.Spec.Affinity != nil && oldTemplate.Spec.Affinity != nil:
635
636 oldTemplate.Spec.Affinity.NodeAffinity = template.Spec.Affinity.NodeAffinity
637 }
638 oldTemplate.Spec.NodeSelector = template.Spec.NodeSelector
639 oldTemplate.Spec.Tolerations = template.Spec.Tolerations
640 oldTemplate.Annotations = template.Annotations
641 oldTemplate.Labels = template.Labels
642 oldTemplate.Spec.SchedulingGates = template.Spec.SchedulingGates
643 }
644 allErrs = append(allErrs, apivalidation.ValidateImmutableField(template, oldTemplate, fldPath.Child("template"))...)
645 return allErrs
646 }
647
648
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
673
674
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
681
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
696 func ValidateCronJobCreate(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
697
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
702
703
704
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
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
716
717 return allErrs
718 }
719
720
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
747 allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.SuccessfulJobsHistoryLimit), fldPath.Child("successfulJobsHistoryLimit"))...)
748 }
749 if spec.FailedJobsHistoryLimit != nil {
750
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
788
789
790
791
792
793
794
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
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
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
846
847
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
858
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
915
916 completedPos++
917 if completedPos < len(completedIndexesIntervals) {
918 cX, cY, cErr = parseIndexInterval(completedIndexesIntervals[completedPos], completions)
919 }
920 } else if fErr != nil {
921
922
923 failedPos++
924 if failedPos < len(failedIndexesIntervals) {
925 fX, fY, fErr = parseIndexInterval(failedIndexesIntervals[failedPos], completions)
926 }
927 } else {
928
929 if cX <= fY && fX <= cY {
930 return fmt.Errorf("failedIndexes and completedIndexes overlap at index: %d", max(cX, fX))
931 }
932
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
1000 AllowMutableSchedulingDirectives bool
1001
1002 AllowElasticIndexedJobs bool
1003
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