k8s.io/kubernetes@v1.29.3/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 "sigs.k8s.io/structured-merge-diff/v4/fieldpath" 48 ) 49 50 // jobStrategy implements verification logic for Replication Controllers. 51 type jobStrategy struct { 52 runtime.ObjectTyper 53 names.NameGenerator 54 } 55 56 // Strategy is the default logic that applies when creating and updating Replication Controller objects. 57 var Strategy = jobStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} 58 59 // DefaultGarbageCollectionPolicy returns OrphanDependents for batch/v1 for backwards compatibility, 60 // and DeleteDependents for all other versions. 61 func (jobStrategy) DefaultGarbageCollectionPolicy(ctx context.Context) rest.GarbageCollectionPolicy { 62 var groupVersion schema.GroupVersion 63 if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found { 64 groupVersion = schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} 65 } 66 switch groupVersion { 67 case batchv1.SchemeGroupVersion: 68 // for back compatibility 69 return rest.OrphanDependents 70 default: 71 return rest.DeleteDependents 72 } 73 } 74 75 // NamespaceScoped returns true because all jobs need to be within a namespace. 76 func (jobStrategy) NamespaceScoped() bool { 77 return true 78 } 79 80 // GetResetFields returns the set of fields that get reset by the strategy 81 // and should not be modified by the user. 82 func (jobStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { 83 fields := map[fieldpath.APIVersion]*fieldpath.Set{ 84 "batch/v1": fieldpath.NewSet( 85 fieldpath.MakePathOrDie("status"), 86 ), 87 } 88 89 return fields 90 } 91 92 // PrepareForCreate clears the status of a job before creation. 93 func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { 94 job := obj.(*batch.Job) 95 generateSelectorIfNeeded(job) 96 job.Status = batch.JobStatus{} 97 98 job.Generation = 1 99 100 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) { 101 job.Spec.PodFailurePolicy = nil 102 } 103 104 if !utilfeature.DefaultFeatureGate.Enabled(features.JobBackoffLimitPerIndex) { 105 job.Spec.BackoffLimitPerIndex = nil 106 job.Spec.MaxFailedIndexes = nil 107 if job.Spec.PodFailurePolicy != nil { 108 // We drop the FailIndex pod failure policy rules because 109 // JobBackoffLimitPerIndex is disabled. 110 index := 0 111 for _, rule := range job.Spec.PodFailurePolicy.Rules { 112 if rule.Action != batch.PodFailurePolicyActionFailIndex { 113 job.Spec.PodFailurePolicy.Rules[index] = rule 114 index++ 115 } 116 } 117 job.Spec.PodFailurePolicy.Rules = job.Spec.PodFailurePolicy.Rules[:index] 118 } 119 } 120 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodReplacementPolicy) { 121 job.Spec.PodReplacementPolicy = nil 122 } 123 124 pod.DropDisabledTemplateFields(&job.Spec.Template, nil) 125 } 126 127 // PrepareForUpdate clears fields that are not allowed to be set by end users on update. 128 func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { 129 newJob := obj.(*batch.Job) 130 oldJob := old.(*batch.Job) 131 newJob.Status = oldJob.Status 132 133 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) && oldJob.Spec.PodFailurePolicy == nil { 134 newJob.Spec.PodFailurePolicy = nil 135 } 136 137 if !utilfeature.DefaultFeatureGate.Enabled(features.JobBackoffLimitPerIndex) { 138 if oldJob.Spec.BackoffLimitPerIndex == nil { 139 newJob.Spec.BackoffLimitPerIndex = nil 140 } 141 if oldJob.Spec.MaxFailedIndexes == nil { 142 newJob.Spec.MaxFailedIndexes = nil 143 } 144 // We keep pod failure policy rules with FailIndex actions (is any), 145 // since the pod failure policy is immutable. Note that, if the old job 146 // had BackoffLimitPerIndex set, the new Job will also have it, so the 147 // validation of the pod failure policy with FailIndex rules will 148 // continue to pass. 149 } 150 if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodReplacementPolicy) && oldJob.Spec.PodReplacementPolicy == nil { 151 newJob.Spec.PodReplacementPolicy = nil 152 } 153 154 pod.DropDisabledTemplateFields(&newJob.Spec.Template, &oldJob.Spec.Template) 155 156 // Any changes to the spec increment the generation number. 157 // See metav1.ObjectMeta description for more information on Generation. 158 if !apiequality.Semantic.DeepEqual(newJob.Spec, oldJob.Spec) { 159 newJob.Generation = oldJob.Generation + 1 160 } 161 162 } 163 164 // Validate validates a new job. 165 func (jobStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { 166 job := obj.(*batch.Job) 167 opts := validationOptionsForJob(job, nil) 168 return batchvalidation.ValidateJob(job, opts) 169 } 170 171 func validationOptionsForJob(newJob, oldJob *batch.Job) batchvalidation.JobValidationOptions { 172 var newPodTemplate, oldPodTemplate *core.PodTemplateSpec 173 if newJob != nil { 174 newPodTemplate = &newJob.Spec.Template 175 } 176 if oldJob != nil { 177 oldPodTemplate = &oldJob.Spec.Template 178 } 179 opts := batchvalidation.JobValidationOptions{ 180 PodValidationOptions: pod.GetValidationOptionsFromPodTemplate(newPodTemplate, oldPodTemplate), 181 AllowElasticIndexedJobs: utilfeature.DefaultFeatureGate.Enabled(features.ElasticIndexedJob), 182 RequirePrefixedLabels: true, 183 } 184 if oldJob != nil { 185 opts.AllowInvalidLabelValueInSelector = opts.AllowInvalidLabelValueInSelector || metav1validation.LabelSelectorHasInvalidLabelValue(oldJob.Spec.Selector) 186 187 // Updating node affinity, node selector and tolerations is allowed 188 // only for suspended jobs that never started before. 189 suspended := oldJob.Spec.Suspend != nil && *oldJob.Spec.Suspend 190 notStarted := oldJob.Status.StartTime == nil 191 opts.AllowMutableSchedulingDirectives = suspended && notStarted 192 193 // Validation should not fail jobs if they don't have the new labels. 194 // This can be removed once we have high confidence that both labels exist (1.30 at least) 195 _, hadJobName := oldJob.Spec.Template.Labels[batch.JobNameLabel] 196 _, hadControllerUid := oldJob.Spec.Template.Labels[batch.ControllerUidLabel] 197 opts.RequirePrefixedLabels = hadJobName && hadControllerUid 198 } 199 return opts 200 } 201 202 // WarningsOnCreate returns warnings for the creation of the given object. 203 func (jobStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { 204 newJob := obj.(*batch.Job) 205 var warnings []string 206 if msgs := utilvalidation.IsDNS1123Label(newJob.Name); len(msgs) != 0 { 207 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)) 208 } 209 warnings = append(warnings, job.WarningsForJobSpec(ctx, field.NewPath("spec"), &newJob.Spec, nil)...) 210 return warnings 211 } 212 213 // generateSelectorIfNeeded checks the job's manual selector flag and generates selector labels if the flag is true. 214 func generateSelectorIfNeeded(obj *batch.Job) { 215 if !*obj.Spec.ManualSelector { 216 generateSelector(obj) 217 } 218 } 219 220 // generateSelector adds a selector to a job and labels to its template 221 // which can be used to uniquely identify the pods created by that job, 222 // if the user has requested this behavior. 223 func generateSelector(obj *batch.Job) { 224 if obj.Spec.Template.Labels == nil { 225 obj.Spec.Template.Labels = make(map[string]string) 226 } 227 // The job-name label is unique except in cases that are expected to be 228 // quite uncommon, and is more user friendly than uid. So, we add it as 229 // a label. 230 jobNameLabels := []string{batch.LegacyJobNameLabel, batch.JobNameLabel} 231 for _, value := range jobNameLabels { 232 _, found := obj.Spec.Template.Labels[value] 233 if found { 234 // User asked us to automatically generate a selector, but set manual labels. 235 // If there is a conflict, we will reject in validation. 236 } else { 237 obj.Spec.Template.Labels[value] = string(obj.ObjectMeta.Name) 238 } 239 } 240 241 // The controller-uid label makes the pods that belong to this job 242 // only match this job. 243 controllerUidLabels := []string{batch.LegacyControllerUidLabel, batch.ControllerUidLabel} 244 for _, value := range controllerUidLabels { 245 _, found := obj.Spec.Template.Labels[value] 246 if found { 247 // User asked us to automatically generate a selector, but set manual labels. 248 // If there is a conflict, we will reject in validation. 249 } else { 250 obj.Spec.Template.Labels[value] = string(obj.ObjectMeta.UID) 251 } 252 } 253 // Select the controller-uid label. This is sufficient for uniqueness. 254 if obj.Spec.Selector == nil { 255 obj.Spec.Selector = &metav1.LabelSelector{} 256 } 257 if obj.Spec.Selector.MatchLabels == nil { 258 obj.Spec.Selector.MatchLabels = make(map[string]string) 259 } 260 261 if _, found := obj.Spec.Selector.MatchLabels[batch.ControllerUidLabel]; !found { 262 obj.Spec.Selector.MatchLabels[batch.ControllerUidLabel] = string(obj.ObjectMeta.UID) 263 } 264 // If the user specified matchLabel controller-uid=$WRONGUID, then it should fail 265 // in validation, either because the selector does not match the pod template 266 // (controller-uid=$WRONGUID does not match controller-uid=$UID, which we applied 267 // above, or we will reject in validation because the template has the wrong 268 // labels. 269 } 270 271 // TODO: generalize generateSelector so it can work for other controller 272 // objects such as ReplicaSet. Can use pkg/api/meta to generically get the 273 // UID, but need some way to generically access the selector and pod labels 274 // fields. 275 276 // Canonicalize normalizes the object after validation. 277 func (jobStrategy) Canonicalize(obj runtime.Object) { 278 } 279 280 func (jobStrategy) AllowUnconditionalUpdate() bool { 281 return true 282 } 283 284 // AllowCreateOnUpdate is false for jobs; this means a POST is needed to create one. 285 func (jobStrategy) AllowCreateOnUpdate() bool { 286 return false 287 } 288 289 // ValidateUpdate is the default update validation for an end user. 290 func (jobStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { 291 job := obj.(*batch.Job) 292 oldJob := old.(*batch.Job) 293 294 opts := validationOptionsForJob(job, oldJob) 295 validationErrorList := batchvalidation.ValidateJob(job, opts) 296 updateErrorList := batchvalidation.ValidateJobUpdate(job, oldJob, opts) 297 return append(validationErrorList, updateErrorList...) 298 } 299 300 // WarningsOnUpdate returns warnings for the given update. 301 func (jobStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { 302 var warnings []string 303 newJob := obj.(*batch.Job) 304 oldJob := old.(*batch.Job) 305 if newJob.Generation != oldJob.Generation { 306 warnings = job.WarningsForJobSpec(ctx, field.NewPath("spec"), &newJob.Spec, &oldJob.Spec) 307 } 308 return warnings 309 } 310 311 type jobStatusStrategy struct { 312 jobStrategy 313 } 314 315 var StatusStrategy = jobStatusStrategy{Strategy} 316 317 // GetResetFields returns the set of fields that get reset by the strategy 318 // and should not be modified by the user. 319 func (jobStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { 320 return map[fieldpath.APIVersion]*fieldpath.Set{ 321 "batch/v1": fieldpath.NewSet( 322 fieldpath.MakePathOrDie("spec"), 323 ), 324 } 325 } 326 327 func (jobStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { 328 newJob := obj.(*batch.Job) 329 oldJob := old.(*batch.Job) 330 newJob.Spec = oldJob.Spec 331 } 332 333 func (jobStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { 334 return batchvalidation.ValidateJobUpdateStatus(obj.(*batch.Job), old.(*batch.Job)) 335 } 336 337 // WarningsOnUpdate returns warnings for the given update. 338 func (jobStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { 339 return nil 340 } 341 342 // JobSelectableFields returns a field set that represents the object for matching purposes. 343 func JobToSelectableFields(job *batch.Job) fields.Set { 344 objectMetaFieldsSet := generic.ObjectMetaFieldsSet(&job.ObjectMeta, true) 345 specificFieldsSet := fields.Set{ 346 "status.successful": strconv.Itoa(int(job.Status.Succeeded)), 347 } 348 return generic.MergeFieldsSets(objectMetaFieldsSet, specificFieldsSet) 349 } 350 351 // GetAttrs returns labels and fields of a given object for filtering purposes. 352 func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { 353 job, ok := obj.(*batch.Job) 354 if !ok { 355 return nil, nil, fmt.Errorf("given object is not a job.") 356 } 357 return labels.Set(job.ObjectMeta.Labels), JobToSelectableFields(job), nil 358 } 359 360 // MatchJob is the filter used by the generic etcd backend to route 361 // watch events from etcd to clients of the apiserver only interested in specific 362 // labels/fields. 363 func MatchJob(label labels.Selector, field fields.Selector) storage.SelectionPredicate { 364 return storage.SelectionPredicate{ 365 Label: label, 366 Field: field, 367 GetAttrs: GetAttrs, 368 } 369 }