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  }