diff --git a/pkg/apis/batch/validation/validation.go b/pkg/apis/batch/validation/validation.go index e8612e022d4..21ecefdf96d 100644 --- a/pkg/apis/batch/validation/validation.go +++ b/pkg/apis/batch/validation/validation.go @@ -838,13 +838,7 @@ func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path, o } func validateCompletions(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts JobValidationOptions) field.ErrorList { - if !opts.AllowElasticIndexedJobs { - return apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath) - } - - // Completions is immutable for non-indexed jobs. - // For Indexed Jobs, if ElasticIndexedJob feature gate is not enabled, - // fall back to validating that spec.Completions is always immutable. + // Completions is immutable for non-indexed jobs, but mutable for Indexed Jobs. isIndexedJob := spec.CompletionMode != nil && *spec.CompletionMode == batch.IndexedCompletion if !isIndexedJob { return apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath) @@ -998,8 +992,6 @@ type JobValidationOptions struct { apivalidation.PodValidationOptions // Allow mutable node affinity, selector and tolerations of the template AllowMutableSchedulingDirectives bool - // Allow elastic indexed jobs - AllowElasticIndexedJobs bool // Require Job to have the label on batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid RequirePrefixedLabels bool } diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index 9a4166c3861..aded56c5ff4 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -1609,22 +1609,6 @@ func TestValidateJobUpdate(t *testing.T) { Field: "spec.completions", }, }, - "immutable completions for indexed job when AllowElasticIndexedJobs is false": { - old: batch.Job{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, - }, - }, - update: func(job *batch.Job) { - job.Spec.Completions = pointer.Int32(1) - }, - err: &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "spec.completions", - }, - }, "immutable selector": { old: batch.Job{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, @@ -1930,7 +1914,7 @@ func TestValidateJobUpdate(t *testing.T) { Field: "spec.completionMode", }, }, - "immutable completions for non-indexed job when AllowElasticIndexedJobs is true": { + "immutable completions for non-indexed job": { old: batch.Job{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: batch.JobSpec{ @@ -1947,7 +1931,6 @@ func TestValidateJobUpdate(t *testing.T) { Type: field.ErrorTypeInvalid, Field: "spec.completions", }, - opts: JobValidationOptions{AllowElasticIndexedJobs: true}, }, "immutable node affinity": { @@ -2200,9 +2183,6 @@ func TestValidateJobUpdate(t *testing.T) { job.Spec.Completions = pointer.Int32(2) job.Spec.Parallelism = pointer.Int32(2) }, - opts: JobValidationOptions{ - AllowElasticIndexedJobs: true, - }, }, "previous parallelism != previous completions, new parallelism == new completions": { old: batch.Job{ @@ -2219,9 +2199,6 @@ func TestValidateJobUpdate(t *testing.T) { job.Spec.Completions = pointer.Int32(3) job.Spec.Parallelism = pointer.Int32(3) }, - opts: JobValidationOptions{ - AllowElasticIndexedJobs: true, - }, }, "indexed job updating completions and parallelism to different values is invalid": { old: batch.Job{ @@ -2238,9 +2215,6 @@ func TestValidateJobUpdate(t *testing.T) { job.Spec.Completions = pointer.Int32(2) job.Spec.Parallelism = pointer.Int32(3) }, - opts: JobValidationOptions{ - AllowElasticIndexedJobs: true, - }, err: &field.Error{ Type: field.ErrorTypeInvalid, Field: "spec.completions", @@ -2261,9 +2235,6 @@ func TestValidateJobUpdate(t *testing.T) { job.Spec.Completions = nil job.Spec.Parallelism = pointer.Int32(3) }, - opts: JobValidationOptions{ - AllowElasticIndexedJobs: true, - }, err: &field.Error{ Type: field.ErrorTypeRequired, Field: "spec.completions", @@ -2284,9 +2255,6 @@ func TestValidateJobUpdate(t *testing.T) { job.Spec.Completions = pointer.Int32(2) job.Spec.Parallelism = pointer.Int32(1) }, - opts: JobValidationOptions{ - AllowElasticIndexedJobs: true, - }, }, "indexed job with completions unchanged, parallelism increased higher than completions": { old: batch.Job{ @@ -2303,9 +2271,6 @@ func TestValidateJobUpdate(t *testing.T) { job.Spec.Completions = pointer.Int32(2) job.Spec.Parallelism = pointer.Int32(3) }, - opts: JobValidationOptions{ - AllowElasticIndexedJobs: true, - }, }, } ignoreValueAndDetail := cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index b19102c297a..9e443b91524 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -1131,7 +1131,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS RuntimeClassInImageCriAPI: {Default: false, PreRelease: featuregate.Alpha}, - ElasticIndexedJob: {Default: true, PreRelease: featuregate.Beta}, + ElasticIndexedJob: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.31, remove in 1.32 SchedulerQueueingHints: {Default: false, PreRelease: featuregate.Beta}, diff --git a/pkg/registry/batch/job/strategy.go b/pkg/registry/batch/job/strategy.go index 498c4d998f3..e39679a545a 100644 --- a/pkg/registry/batch/job/strategy.go +++ b/pkg/registry/batch/job/strategy.go @@ -187,9 +187,8 @@ func validationOptionsForJob(newJob, oldJob *batch.Job) batchvalidation.JobValid oldPodTemplate = &oldJob.Spec.Template } opts := batchvalidation.JobValidationOptions{ - PodValidationOptions: pod.GetValidationOptionsFromPodTemplate(newPodTemplate, oldPodTemplate), - AllowElasticIndexedJobs: utilfeature.DefaultFeatureGate.Enabled(features.ElasticIndexedJob), - RequirePrefixedLabels: true, + PodValidationOptions: pod.GetValidationOptionsFromPodTemplate(newPodTemplate, oldPodTemplate), + RequirePrefixedLabels: true, } if oldJob != nil { opts.AllowInvalidLabelValueInSelector = opts.AllowInvalidLabelValueInSelector || metav1validation.LabelSelectorHasInvalidLabelValue(oldJob.Spec.Selector) diff --git a/test/integration/job/job_test.go b/test/integration/job/job_test.go index 427e62b466c..bb692c577bc 100644 --- a/test/integration/job/job_test.go +++ b/test/integration/job/job_test.go @@ -34,10 +34,8 @@ import ( eventsv1 "k8s.io/api/events/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/util/feature" @@ -2756,25 +2754,10 @@ func TestElasticIndexedJob(t *testing.T) { wantTerminating *int32 } cases := map[string]struct { - featureGate bool - jobUpdates []jobUpdate - wantErr *apierrors.StatusError + jobUpdates []jobUpdate + wantErr *apierrors.StatusError }{ - "feature flag off, mutation not allowed": { - jobUpdates: []jobUpdate{ - { - completions: ptr.To[int32](4), - wantTerminating: ptr.To[int32](0), - }, - }, - wantErr: apierrors.NewInvalid( - schema.GroupKind{Group: "batch", Kind: "Job"}, - "test-job", - field.ErrorList{field.Invalid(field.NewPath("spec", "completions"), 4, "field is immutable")}, - ), - }, "scale up": { - featureGate: true, jobUpdates: []jobUpdate{ { // Scale up completions 3->4 then succeed indexes 0-3 @@ -2786,7 +2769,6 @@ func TestElasticIndexedJob(t *testing.T) { }, }, "scale down": { - featureGate: true, jobUpdates: []jobUpdate{ // First succeed index 1 and fail index 2 while completions is still original value (3). { @@ -2810,7 +2792,6 @@ func TestElasticIndexedJob(t *testing.T) { }, }, "index finishes successfully, scale down, scale up": { - featureGate: true, jobUpdates: []jobUpdate{ // First succeed index 2 while completions is still original value (3). { @@ -2837,7 +2818,6 @@ func TestElasticIndexedJob(t *testing.T) { }, }, "scale down to 0, verify that the job succeeds": { - featureGate: true, jobUpdates: []jobUpdate{ { completions: ptr.To[int32](0), @@ -2850,7 +2830,6 @@ func TestElasticIndexedJob(t *testing.T) { for name, tc := range cases { tc := tc t.Run(name, func(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ElasticIndexedJob, tc.featureGate) closeFn, restConfig, clientSet, ns := setup(t, "indexed") defer closeFn() ctx, cancel := startJobControllerAndWaitForCaches(t, restConfig)