Merge pull request #135965 from kannon92/kep-5440-feature-gate-enable-e2e-test

set KEP-5440 to enabled by default
This commit is contained in:
Kubernetes Prow Robot 2026-02-18 18:13:38 +05:30 committed by GitHub
commit 0ca5cba140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 218 additions and 3 deletions

View file

@ -1506,10 +1506,12 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
MutablePodResourcesForSuspendedJobs: {
{Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta},
},
MutableSchedulingDirectivesForSuspendedJobs: {
{Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta},
},
NFTablesProxyMode: {

View file

@ -121,8 +121,8 @@
| MultiCIDRServiceAllocator | :ballot_box_with_check: 1.33+ | :closed_lock_with_key: 1.34+ | 1.271.30 | 1.311.32 | 1.33 | | | [code](https://cs.k8s.io/?q=%5CbMultiCIDRServiceAllocator%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMultiCIDRServiceAllocator%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutableCSINodeAllocatableCount | :ballot_box_with_check: 1.35+ | | 1.33 | 1.34 | | | | [code](https://cs.k8s.io/?q=%5CbMutableCSINodeAllocatableCount%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutableCSINodeAllocatableCount%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutablePVNodeAffinity | | | 1.35 | | | | | [code](https://cs.k8s.io/?q=%5CbMutablePVNodeAffinity%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutablePVNodeAffinity%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutablePodResourcesForSuspendedJobs | | | 1.35 | | | | | [code](https://cs.k8s.io/?q=%5CbMutablePodResourcesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutablePodResourcesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutableSchedulingDirectivesForSuspendedJobs | | | 1.35 | | | | | [code](https://cs.k8s.io/?q=%5CbMutableSchedulingDirectivesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutableSchedulingDirectivesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutablePodResourcesForSuspendedJobs | :ballot_box_with_check: 1.36+ | | 1.35 | 1.36 | | | | [code](https://cs.k8s.io/?q=%5CbMutablePodResourcesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutablePodResourcesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutableSchedulingDirectivesForSuspendedJobs | :ballot_box_with_check: 1.36+ | | 1.35 | 1.36 | | | | [code](https://cs.k8s.io/?q=%5CbMutableSchedulingDirectivesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutableSchedulingDirectivesForSuspendedJobs%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| MutatingAdmissionPolicy | | | 1.321.33 | 1.34 | | | | [code](https://cs.k8s.io/?q=%5CbMutatingAdmissionPolicy%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbMutatingAdmissionPolicy%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| NFTablesProxyMode | :ballot_box_with_check: 1.31+ | :closed_lock_with_key: 1.33+ | 1.291.30 | 1.311.32 | 1.33 | | | [code](https://cs.k8s.io/?q=%5CbNFTablesProxyMode%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbNFTablesProxyMode%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| NodeDeclaredFeatures | | | 1.35 | | | | | [code](https://cs.k8s.io/?q=%5CbNodeDeclaredFeatures%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbNodeDeclaredFeatures%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |

View file

@ -1111,6 +1111,10 @@
lockToDefault: false
preRelease: Alpha
version: "1.35"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.36"
- name: MutablePVNodeAffinity
versionedSpecs:
- default: false
@ -1123,6 +1127,10 @@
lockToDefault: false
preRelease: Alpha
version: "1.35"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.36"
- name: MutatingAdmissionPolicy
versionedSpecs:
- default: false

View file

@ -1377,6 +1377,203 @@ done`}
gomega.Expect(job.Status.Ready).Should(gomega.Equal(ptr.To[int32](0)))
gomega.Expect(job.Status.Terminating).Should(gomega.Equal(ptr.To[int32](0)))
})
/*
Testname: Allow updating pod resources for suspended Jobs
Description: Create a suspended Job with initial container resources.
Update the container resources while the job is suspended.
Unsuspend the job and verify that pods are created with the updated resources.
This verifies KEP-5440: Mutable Job Pod Resource Updates.
*/
framework.It("should allow updating pod resources for a suspended job", framework.WithFeatureGate(features.MutablePodResourcesForSuspendedJobs), func(ctx context.Context) {
jobName := "e2e-mutable-resources" + utilrand.String(5)
parallelism := int32(1)
completions := int32(1)
backoffLimit := int32(6)
initialCPU := resource.MustParse("100m")
initialMemory := resource.MustParse("128Mi")
updatedCPU := resource.MustParse("200m")
updatedMemory := resource.MustParse("256Mi")
ginkgo.By("Creating a suspended job with initial resources")
job := e2ejob.NewTestJob("succeed", jobName, v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
job.Spec.Suspend = ptr.To(true)
for i := range job.Spec.Template.Spec.Containers {
job.Spec.Template.Spec.Containers[i].Resources = v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: initialCPU,
v1.ResourceMemory: initialMemory,
},
}
}
job, err := e2ejob.CreateJob(ctx, f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to create job in namespace: %s", f.Namespace.Name)
ginkgo.By("Verifying job is suspended and no pods are created")
err = e2ejob.WaitForJobSuspend(ctx, f.ClientSet, f.Namespace.Name, jobName)
framework.ExpectNoError(err, "failed to verify job is suspended")
pods, err := e2ejob.GetJobPods(ctx, f.ClientSet, f.Namespace.Name, jobName)
framework.ExpectNoError(err, "failed to get pods for job")
gomega.Expect(pods.Items).To(gomega.BeEmpty(), "expected no pods while job is suspended")
ginkgo.By("Updating container resources while job is suspended")
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
job, err = e2ejob.GetJob(ctx, f.ClientSet, f.Namespace.Name, jobName)
if err != nil {
return err
}
for i := range job.Spec.Template.Spec.Containers {
job.Spec.Template.Spec.Containers[i].Resources = v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: updatedCPU,
v1.ResourceMemory: updatedMemory,
},
}
}
job, err = e2ejob.UpdateJob(ctx, f.ClientSet, f.Namespace.Name, job)
return err
})
framework.ExpectNoError(err, "failed to update job resources")
ginkgo.By("Unsuspending the job")
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
job, err = e2ejob.GetJob(ctx, f.ClientSet, f.Namespace.Name, jobName)
if err != nil {
return err
}
job.Spec.Suspend = ptr.To(false)
job, err = e2ejob.UpdateJob(ctx, f.ClientSet, f.Namespace.Name, job)
return err
})
framework.ExpectNoError(err, "failed to unsuspend job")
ginkgo.By("Waiting for job to complete")
err = e2ejob.WaitForJobComplete(ctx, f.ClientSet, f.Namespace.Name, jobName, batchv1.JobReasonCompletionsReached, completions)
framework.ExpectNoError(err, "failed to wait for job completion")
ginkgo.By("Verifying pods were created with updated resources")
pods, err = e2ejob.GetJobPods(ctx, f.ClientSet, f.Namespace.Name, jobName)
framework.ExpectNoError(err, "failed to get pods for job")
gomega.Expect(pods.Items).NotTo(gomega.BeEmpty(), "expected at least one pod")
for _, pod := range pods.Items {
for _, container := range pod.Spec.Containers {
cpuRequest := container.Resources.Requests[v1.ResourceCPU]
memoryRequest := container.Resources.Requests[v1.ResourceMemory]
gomega.Expect(cpuRequest.Equal(updatedCPU)).To(gomega.BeTrueBecause(
"expected CPU request %v, got %v", updatedCPU.String(), cpuRequest.String()))
gomega.Expect(memoryRequest.Equal(updatedMemory)).To(gomega.BeTrueBecause(
"expected memory request %v, got %v", updatedMemory.String(), memoryRequest.String()))
}
}
})
/*
Testname: Allow updating pod resources for a job that started and was suspended
Description: Create a job that starts running, suspend it, update the
container resources while suspended, then unsuspend and verify that newly
created pods have the updated resources. This verifies that resource updates
are allowed even for jobs that have previously started.
*/
framework.It("should allow updating pod resources for a job that started and then was suspended", framework.WithFeatureGate(features.MutablePodResourcesForSuspendedJobs), func(ctx context.Context) {
jobName := "e2e-start-suspend" + utilrand.String(5)
parallelism := int32(2)
completions := int32(4)
backoffLimit := int32(6)
initialCPU := resource.MustParse("100m")
initialMemory := resource.MustParse("128Mi")
updatedCPU := resource.MustParse("200m")
updatedMemory := resource.MustParse("256Mi")
ginkgo.By("Creating a running job with initial resources")
job := e2ejob.NewTestJob("notTerminate", jobName, v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
job.Spec.Suspend = ptr.To(false)
for i := range job.Spec.Template.Spec.Containers {
job.Spec.Template.Spec.Containers[i].Resources = v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: initialCPU,
v1.ResourceMemory: initialMemory,
},
}
}
job, err := e2ejob.CreateJob(ctx, f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to create job in namespace: %s", f.Namespace.Name)
ginkgo.By("Waiting for pods to be running")
err = e2ejob.WaitForJobPodsRunning(ctx, f.ClientSet, f.Namespace.Name, jobName, parallelism)
framework.ExpectNoError(err, "failed to wait for job pods to be running")
ginkgo.By("Suspending the running job")
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
job, err = e2ejob.GetJob(ctx, f.ClientSet, f.Namespace.Name, jobName)
if err != nil {
return err
}
job.Spec.Suspend = ptr.To(true)
job, err = e2ejob.UpdateJob(ctx, f.ClientSet, f.Namespace.Name, job)
return err
})
framework.ExpectNoError(err, "failed to suspend job")
ginkgo.By("Waiting for all pods to be deleted after suspension")
err = e2ejob.WaitForAllJobPodsGone(ctx, f.ClientSet, f.Namespace.Name, jobName)
framework.ExpectNoError(err, "failed to wait for pods to be deleted after suspension")
ginkgo.By("Updating container resources while job is suspended")
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
job, err = e2ejob.GetJob(ctx, f.ClientSet, f.Namespace.Name, jobName)
if err != nil {
return err
}
for i := range job.Spec.Template.Spec.Containers {
job.Spec.Template.Spec.Containers[i].Resources = v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: updatedCPU,
v1.ResourceMemory: updatedMemory,
},
}
}
job, err = e2ejob.UpdateJob(ctx, f.ClientSet, f.Namespace.Name, job)
return err
})
framework.ExpectNoError(err, "failed to update job resources")
ginkgo.By("Unsuspending the job")
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
job, err = e2ejob.GetJob(ctx, f.ClientSet, f.Namespace.Name, jobName)
if err != nil {
return err
}
job.Spec.Suspend = ptr.To(false)
job, err = e2ejob.UpdateJob(ctx, f.ClientSet, f.Namespace.Name, job)
return err
})
framework.ExpectNoError(err, "failed to unsuspend job")
ginkgo.By("Waiting for new pods to be running with updated resources")
err = e2ejob.WaitForJobPodsRunning(ctx, f.ClientSet, f.Namespace.Name, jobName, parallelism)
framework.ExpectNoError(err, "failed to wait for job pods to be running after unsuspending")
ginkgo.By("Verifying newly created pods have updated resources")
pods, err := e2ejob.GetJobPods(ctx, f.ClientSet, f.Namespace.Name, jobName)
framework.ExpectNoError(err, "failed to get pods for job")
gomega.Expect(pods.Items).NotTo(gomega.BeEmpty(), "expected at least one pod")
for _, pod := range pods.Items {
for _, container := range pod.Spec.Containers {
cpuRequest := container.Resources.Requests[v1.ResourceCPU]
memoryRequest := container.Resources.Requests[v1.ResourceMemory]
gomega.Expect(cpuRequest.Equal(updatedCPU)).To(gomega.BeTrueBecause(
"expected CPU request %v, got %v", updatedCPU.String(), cpuRequest.String()))
gomega.Expect(memoryRequest.Equal(updatedMemory)).To(gomega.BeTrueBecause(
"expected memory request %v, got %v", updatedMemory.String(), memoryRequest.String()))
}
}
})
})
func updateJobSuspendWithRetries(ctx context.Context, f *framework.Framework, job *batchv1.Job, suspend *bool) error {

View file

@ -4228,6 +4228,7 @@ func TestSuspendJobControllerRestart(t *testing.T) {
func TestNodeSelectorUpdate(t *testing.T) {
closeFn, restConfig, clientSet, ns := setup(t, "suspend")
t.Cleanup(closeFn)
ctx, cancel := startJobControllerAndWaitForCaches(t, restConfig)
t.Cleanup(cancel)
@ -4243,7 +4244,14 @@ func TestNodeSelectorUpdate(t *testing.T) {
jobNamespace := job.Namespace
jobClient := clientSet.BatchV1().Jobs(jobNamespace)
// (1) Unsuspend and set node selector in the same update.
// Since MutableSchedulingDirectives is set to true, one needs
// to wait for the suspend condition to be set reflecting that the
// job is actually suspended.
waitForPodsToBeActive(ctx, t, jobClient, 0, job)
validateJobCondition(ctx, t, clientSet, job, batchv1.JobSuspended)
// (1) set node selector in the same update.
nodeSelector := map[string]string{"foo": "bar"}
if _, err := updateJob(ctx, jobClient, jobName, func(j *batchv1.Job) {
j.Spec.Template.Spec.NodeSelector = nodeSelector