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  }