k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/registry/batch/job/strategy.go (about) 1 /* 2 Copyright 2015 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 job 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 24 batchv1 "k8s.io/api/batch/v1" 25 apiequality "k8s.io/apimachinery/pkg/api/equality" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 28 "k8s.io/apimachinery/pkg/fields" 29 "k8s.io/apimachinery/pkg/labels" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 utilvalidation "k8s.io/apimachinery/pkg/util/validation" 33 "k8s.io/apimachinery/pkg/util/validation/field" 34 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 35 "k8s.io/apiserver/pkg/registry/generic" 36 "k8s.io/apiserver/pkg/registry/rest" 37 "k8s.io/apiserver/pkg/storage" 38 "k8s.io/apiserver/pkg/storage/names" 39 utilfeature "k8s.io/apiserver/pkg/util/feature" 40 "k8s.io/kubernetes/pkg/api/job" 41 "k8s.io/kubernetes/pkg/api/legacyscheme" 42 "k8s.io/kubernetes/pkg/api/pod" 43 "k8s.io/kubernetes/pkg/apis/batch" 44 batchvalidation "k8s.io/kubernetes/pkg/apis/batch/validation" 45 "k8s.io/kubernetes/pkg/apis/core" 46 "k8s.io/kubernetes/pkg/features" 47 "k8s.io/utils/ptr" 48 "sigs.k8s.io/structured-merge-diff/v4/fieldpath" 49 ) 50 51 // jobStrategy implements verification logic for Replication Controllers. 52 type jobStrategy struct { 53 runtime.ObjectTyper 54 names.NameGenerator 55 } 56 57 // Strategy is the default logic that applies when creating and updating Replication Controller objects. 58 var Strategy = jobStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} 59 60 // DefaultGarbageCollectionPolicy returns OrphanDependents for batch/v1 for backwards compatibility, 61 // and DeleteDependents for all other versions. 62 func (jobStrategy) DefaultGarbageCollectionPolicy(ctx context.Context) rest.GarbageCollectionPolicy { 63 var groupVersion schema.GroupVersion 64 if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found { 65 groupVersion = schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} 66 } 67 switch groupVersion { 68 case batchv1.SchemeGroupVersion: 69 // for back compatibility 70 return rest.OrphanDependents 71 default: 72 return rest.DeleteDependents 73 } 74 } 75 76 // NamespaceScoped returns true because all jobs need to be within a namespace. 77 func (jobStrategy) NamespaceScoped() bool { 78 return true 79 } 80 81 // GetResetFields returns the set of fields that get reset by the strategy 82 // and should not be modified by the user. 83 func (jobStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { 84 fields := map[fieldpath.APIVersion]*fieldpath.Set{ 85 "batch/v1": fieldpath.NewSet( 86 fieldpath.MakePathOrDie("status"), 87 ), 88 } 89 90 return fields 91 } 92 93 // PrepareForCreate clears the status of a job before creation. 94 func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { 95 job := obj.(*batch.Job) 96 generateSelectorIfNeeded(job) 97 job.Status = batch.JobStatus{} 98 99 job.Generation = 1 100 101 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) { 102 job.Spec.PodFailurePolicy = nil 103 } 104 if !utilfeature.DefaultFeatureGate.Enabled(features.JobManagedBy) { 105 job.Spec.ManagedBy = nil 106 } 107 if !utilfeature.DefaultFeatureGate.Enabled(features.JobSuccessPolicy) { 108 job.Spec.SuccessPolicy = nil 109 } 110 111 if !utilfeature.DefaultFeatureGate.Enabled(features.JobBackoffLimitPerIndex) { 112 job.Spec.BackoffLimitPerIndex = nil 113 job.Spec.MaxFailedIndexes = nil 114 if job.Spec.PodFailurePolicy != nil { 115 // We drop the FailIndex pod failure policy rules because 116 // JobBackoffLimitPerIndex is disabled. 117 index := 0 118 for _, rule := range job.Spec.PodFailurePolicy.Rules { 119 if rule.Action != batch.PodFailurePolicyActionFailIndex { 120 job.Spec.PodFailurePolicy.Rules[index] = rule 121 index++ 122 } 123 } 124 job.Spec.PodFailurePolicy.Rules = job.Spec.PodFailurePolicy.Rules[:index] 125 } 126 } 127 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodReplacementPolicy) { 128 job.Spec.PodReplacementPolicy = nil 129 } 130 131 pod.DropDisabledTemplateFields(&job.Spec.Template, nil) 132 } 133 134 // PrepareForUpdate clears fields that are not allowed to be set by end users on update. 135 func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { 136 newJob := obj.(*batch.Job) 137 oldJob := old.(*batch.Job) 138 newJob.Status = oldJob.Status 139 140 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) && oldJob.Spec.PodFailurePolicy == nil { 141 newJob.Spec.PodFailurePolicy = nil 142 } 143 if !utilfeature.DefaultFeatureGate.Enabled(features.JobSuccessPolicy) && oldJob.Spec.SuccessPolicy == nil { 144 newJob.Spec.SuccessPolicy = nil 145 } 146 147 if !utilfeature.DefaultFeatureGate.Enabled(features.JobBackoffLimitPerIndex) { 148 if oldJob.Spec.BackoffLimitPerIndex == nil { 149 newJob.Spec.BackoffLimitPerIndex = nil 150 } 151 if oldJob.Spec.MaxFailedIndexes == nil { 152 newJob.Spec.MaxFailedIndexes = nil 153 } 154 // We keep pod failure policy rules with FailIndex actions (is any), 155 // since the pod failure policy is immutable. Note that, if the old job 156 // had BackoffLimitPerIndex set, the new Job will also have it, so the 157 // validation of the pod failure policy with FailIndex rules will 158 // continue to pass. 159 } 160 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodReplacementPolicy) && oldJob.Spec.PodReplacementPolicy == nil { 161 newJob.Spec.PodReplacementPolicy = nil 162 } 163 164 pod.DropDisabledTemplateFields(&newJob.Spec.Template, &oldJob.Spec.Template) 165 166 // Any changes to the spec increment the generation number. 167 // See metav1.ObjectMeta description for more information on Generation. 168 if !apiequality.Semantic.DeepEqual(newJob.Spec, oldJob.Spec) { 169 newJob.Generation = oldJob.Generation + 1 170 } 171 172 } 173 174 // Validate validates a new job. 175 func (jobStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { 176 job := obj.(*batch.Job) 177 opts := validationOptionsForJob(job, nil) 178 return batchvalidation.ValidateJob(job, opts) 179 } 180 181 func validationOptionsForJob(newJob, oldJob *batch.Job) batchvalidation.JobValidationOptions { 182 var newPodTemplate, oldPodTemplate *core.PodTemplateSpec 183 if newJob != nil { 184 newPodTemplate = &newJob.Spec.Template 185 } 186 if oldJob != nil { 187 oldPodTemplate = &oldJob.Spec.Template 188 } 189 opts := batchvalidation.JobValidationOptions{ 190 PodValidationOptions: pod.GetValidationOptionsFromPodTemplate(newPodTemplate, oldPodTemplate), 191 AllowElasticIndexedJobs: utilfeature.DefaultFeatureGate.Enabled(features.ElasticIndexedJob), 192 RequirePrefixedLabels: true, 193 } 194 if oldJob != nil { 195 opts.AllowInvalidLabelValueInSelector = opts.AllowInvalidLabelValueInSelector || metav1validation.LabelSelectorHasInvalidLabelValue(oldJob.Spec.Selector) 196 197 // Updating node affinity, node selector and tolerations is allowed 198 // only for suspended jobs that never started before. 199 suspended := oldJob.Spec.Suspend != nil && *oldJob.Spec.Suspend 200 notStarted := oldJob.Status.StartTime == nil 201 opts.AllowMutableSchedulingDirectives = suspended && notStarted 202 203 // Validation should not fail jobs if they don't have the new labels. 204 // This can be removed once we have high confidence that both labels exist (1.30 at least) 205 _, hadJobName := oldJob.Spec.Template.Labels[batch.JobNameLabel] 206 _, hadControllerUid := oldJob.Spec.Template.Labels[batch.ControllerUidLabel] 207 opts.RequirePrefixedLabels = hadJobName && hadControllerUid 208 } 209 return opts 210 } 211 212 // WarningsOnCreate returns warnings for the creation of the given object. 213 func (jobStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { 214 newJob := obj.(*batch.Job) 215 var warnings []string 216 if msgs := utilvalidation.IsDNS1123Label(newJob.Name); len(msgs) != 0 { 217 warnings = append(warnings, fmt.Sprintf("metadata.name: this is used in Pod names and hostnames, which can result in surprising behavior; a DNS label is recommended: %v", msgs)) 218 } 219 warnings = append(warnings, job.WarningsForJobSpec(ctx, field.NewPath("spec"), &newJob.Spec, nil)...) 220 return warnings 221 } 222 223 // generateSelectorIfNeeded checks the job's manual selector flag and generates selector labels if the flag is true. 224 func generateSelectorIfNeeded(obj *batch.Job) { 225 if !*obj.Spec.ManualSelector { 226 generateSelector(obj) 227 } 228 } 229 230 // generateSelector adds a selector to a job and labels to its template 231 // which can be used to uniquely identify the pods created by that job, 232 // if the user has requested this behavior. 233 func generateSelector(obj *batch.Job) { 234 if obj.Spec.Template.Labels == nil { 235 obj.Spec.Template.Labels = make(map[string]string) 236 } 237 // The job-name label is unique except in cases that are expected to be 238 // quite uncommon, and is more user friendly than uid. So, we add it as 239 // a label. 240 jobNameLabels := []string{batch.LegacyJobNameLabel, batch.JobNameLabel} 241 for _, value := range jobNameLabels { 242 _, found := obj.Spec.Template.Labels[value] 243 if found { 244 // User asked us to automatically generate a selector, but set manual labels. 245 // If there is a conflict, we will reject in validation. 246 } else { 247 obj.Spec.Template.Labels[value] = string(obj.ObjectMeta.Name) 248 } 249 } 250 251 // The controller-uid label makes the pods that belong to this job 252 // only match this job. 253 controllerUidLabels := []string{batch.LegacyControllerUidLabel, batch.ControllerUidLabel} 254 for _, value := range controllerUidLabels { 255 _, found := obj.Spec.Template.Labels[value] 256 if found { 257 // User asked us to automatically generate a selector, but set manual labels. 258 // If there is a conflict, we will reject in validation. 259 } else { 260 obj.Spec.Template.Labels[value] = string(obj.ObjectMeta.UID) 261 } 262 } 263 // Select the controller-uid label. This is sufficient for uniqueness. 264 if obj.Spec.Selector == nil { 265 obj.Spec.Selector = &metav1.LabelSelector{} 266 } 267 if obj.Spec.Selector.MatchLabels == nil { 268 obj.Spec.Selector.MatchLabels = make(map[string]string) 269 } 270 271 if _, found := obj.Spec.Selector.MatchLabels[batch.ControllerUidLabel]; !found { 272 obj.Spec.Selector.MatchLabels[batch.ControllerUidLabel] = string(obj.ObjectMeta.UID) 273 } 274 // If the user specified matchLabel controller-uid=$WRONGUID, then it should fail 275 // in validation, either because the selector does not match the pod template 276 // (controller-uid=$WRONGUID does not match controller-uid=$UID, which we applied 277 // above, or we will reject in validation because the template has the wrong 278 // labels. 279 } 280 281 // TODO: generalize generateSelector so it can work for other controller 282 // objects such as ReplicaSet. Can use pkg/api/meta to generically get the 283 // UID, but need some way to generically access the selector and pod labels 284 // fields. 285 286 // Canonicalize normalizes the object after validation. 287 func (jobStrategy) Canonicalize(obj runtime.Object) { 288 } 289 290 func (jobStrategy) AllowUnconditionalUpdate() bool { 291 return true 292 } 293 294 // AllowCreateOnUpdate is false for jobs; this means a POST is needed to create one. 295 func (jobStrategy) AllowCreateOnUpdate() bool { 296 return false 297 } 298 299 // ValidateUpdate is the default update validation for an end user. 300 func (jobStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { 301 job := obj.(*batch.Job) 302 oldJob := old.(*batch.Job) 303 304 opts := validationOptionsForJob(job, oldJob) 305 validationErrorList := batchvalidation.ValidateJob(job, opts) 306 updateErrorList := batchvalidation.ValidateJobUpdate(job, oldJob, opts) 307 return append(validationErrorList, updateErrorList...) 308 } 309 310 // WarningsOnUpdate returns warnings for the given update. 311 func (jobStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { 312 var warnings []string 313 newJob := obj.(*batch.Job) 314 oldJob := old.(*batch.Job) 315 if newJob.Generation != oldJob.Generation { 316 warnings = job.WarningsForJobSpec(ctx, field.NewPath("spec"), &newJob.Spec, &oldJob.Spec) 317 } 318 return warnings 319 } 320 321 type jobStatusStrategy struct { 322 jobStrategy 323 } 324 325 var StatusStrategy = jobStatusStrategy{Strategy} 326 327 // GetResetFields returns the set of fields that get reset by the strategy 328 // and should not be modified by the user. 329 func (jobStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { 330 return map[fieldpath.APIVersion]*fieldpath.Set{ 331 "batch/v1": fieldpath.NewSet( 332 fieldpath.MakePathOrDie("spec"), 333 ), 334 } 335 } 336 337 func (jobStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { 338 newJob := obj.(*batch.Job) 339 oldJob := old.(*batch.Job) 340 newJob.Spec = oldJob.Spec 341 } 342 343 func (jobStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { 344 newJob := obj.(*batch.Job) 345 oldJob := old.(*batch.Job) 346 347 opts := getStatusValidationOptions(newJob, oldJob) 348 return batchvalidation.ValidateJobUpdateStatus(newJob, oldJob, opts) 349 } 350 351 // getStatusValidationOptions returns validation options for Job status 352 func getStatusValidationOptions(newJob, oldJob *batch.Job) batchvalidation.JobStatusValidationOptions { 353 if utilfeature.DefaultFeatureGate.Enabled(features.JobManagedBy) { 354 // A strengthened validation of the Job status transitions is needed since the 355 // Job managedBy field let's the Job object be controlled by external 356 // controllers. We want to make sure the transitions done by the external 357 // controllers meet the expectations of the clients of the Job API. 358 // For example, we verify that a Job in terminal state (Failed or Complete) 359 // does not flip to a non-terminal state. 360 // 361 // In the checks below we fail validation for Job status fields (or conditions) only if they change their values 362 // (compared to the oldJob). This allows proceeding with status updates unrelated to the fields violating the 363 // checks, while blocking bad status updates for jobs with correct status. 364 // 365 // Also note, there is another reason we run the validation rules only 366 // if the associated status fields changed. We do it also because some of 367 // the validation rules might be temporarily violated just after a user 368 // updating the spec. In that case we want to give time to the Job 369 // controller to "fix" the status in the following sync. For example, the 370 // rule for checking the format of completedIndexes expects them to be 371 // below .spec.completions, however, this it is ok if the 372 // status.completedIndexes go beyond completions just after a user scales 373 // down a Job. 374 isIndexed := ptr.Deref(newJob.Spec.CompletionMode, batch.NonIndexedCompletion) == batch.IndexedCompletion 375 376 isJobFinishedChanged := batchvalidation.IsJobFinished(oldJob) != batchvalidation.IsJobFinished(newJob) 377 isJobCompleteChanged := batchvalidation.IsJobComplete(oldJob) != batchvalidation.IsJobComplete(newJob) 378 isJobFailedChanged := batchvalidation.IsJobFailed(oldJob) != batchvalidation.IsJobFailed(newJob) 379 isJobFailureTargetChanged := batchvalidation.IsConditionTrue(oldJob.Status.Conditions, batch.JobFailureTarget) != batchvalidation.IsConditionTrue(newJob.Status.Conditions, batch.JobFailureTarget) 380 isCompletedIndexesChanged := oldJob.Status.CompletedIndexes != newJob.Status.CompletedIndexes 381 isFailedIndexesChanged := !ptr.Equal(oldJob.Status.FailedIndexes, newJob.Status.FailedIndexes) 382 isActiveChanged := oldJob.Status.Active != newJob.Status.Active 383 isStartTimeChanged := !ptr.Equal(oldJob.Status.StartTime, newJob.Status.StartTime) 384 isCompletionTimeChanged := !ptr.Equal(oldJob.Status.CompletionTime, newJob.Status.CompletionTime) 385 isUncountedTerminatedPodsChanged := !apiequality.Semantic.DeepEqual(oldJob.Status.UncountedTerminatedPods, newJob.Status.UncountedTerminatedPods) 386 387 return batchvalidation.JobStatusValidationOptions{ 388 // We allow to decrease the counter for succeeded pods for jobs which 389 // have equal parallelism and completions, as they can be scaled-down. 390 RejectDecreasingSucceededCounter: !isIndexed || !ptr.Equal(newJob.Spec.Completions, newJob.Spec.Parallelism), 391 RejectDecreasingFailedCounter: true, 392 RejectDisablingTerminalCondition: true, 393 RejectInvalidCompletedIndexes: isCompletedIndexesChanged, 394 RejectInvalidFailedIndexes: isFailedIndexesChanged, 395 RejectCompletedIndexesForNonIndexedJob: isCompletedIndexesChanged, 396 RejectFailedIndexesForNoBackoffLimitPerIndex: isFailedIndexesChanged, 397 RejectFailedIndexesOverlappingCompleted: isFailedIndexesChanged || isCompletedIndexesChanged, 398 RejectFinishedJobWithActivePods: isJobFinishedChanged || isActiveChanged, 399 RejectFinishedJobWithoutStartTime: isJobFinishedChanged || isStartTimeChanged, 400 RejectFinishedJobWithUncountedTerminatedPods: isJobFinishedChanged || isUncountedTerminatedPodsChanged, 401 RejectStartTimeUpdateForUnsuspendedJob: isStartTimeChanged, 402 RejectCompletionTimeBeforeStartTime: isStartTimeChanged || isCompletionTimeChanged, 403 RejectMutatingCompletionTime: true, 404 RejectNotCompleteJobWithCompletionTime: isJobCompleteChanged || isCompletionTimeChanged, 405 RejectCompleteJobWithoutCompletionTime: isJobCompleteChanged || isCompletionTimeChanged, 406 RejectCompleteJobWithFailedCondition: isJobCompleteChanged || isJobFailedChanged, 407 RejectCompleteJobWithFailureTargetCondition: isJobCompleteChanged || isJobFailureTargetChanged, 408 } 409 } 410 return batchvalidation.JobStatusValidationOptions{} 411 } 412 413 // WarningsOnUpdate returns warnings for the given update. 414 func (jobStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { 415 return nil 416 } 417 418 // JobSelectableFields returns a field set that represents the object for matching purposes. 419 func JobToSelectableFields(job *batch.Job) fields.Set { 420 objectMetaFieldsSet := generic.ObjectMetaFieldsSet(&job.ObjectMeta, true) 421 specificFieldsSet := fields.Set{ 422 "status.successful": strconv.Itoa(int(job.Status.Succeeded)), 423 } 424 return generic.MergeFieldsSets(objectMetaFieldsSet, specificFieldsSet) 425 } 426 427 // GetAttrs returns labels and fields of a given object for filtering purposes. 428 func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { 429 job, ok := obj.(*batch.Job) 430 if !ok { 431 return nil, nil, fmt.Errorf("given object is not a job.") 432 } 433 return labels.Set(job.ObjectMeta.Labels), JobToSelectableFields(job), nil 434 } 435 436 // MatchJob is the filter used by the generic etcd backend to route 437 // watch events from etcd to clients of the apiserver only interested in specific 438 // labels/fields. 439 func MatchJob(label labels.Selector, field fields.Selector) storage.SelectionPredicate { 440 return storage.SelectionPredicate{ 441 Label: label, 442 Field: field, 443 GetAttrs: GetAttrs, 444 } 445 }