From c21cb83c74c98f18443f32f146768b3938c4a43b Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Wed, 4 Feb 2026 15:25:03 +0200 Subject: [PATCH 1/4] test: modify integration test helpers Add helper functions to use for testing DRA extended resources: - Add createTestClassWithSpec to support custom DeviceClassSpec - Add createPodWithExtendedResource to create pods requesting extended resources --- test/integration/dra/helpers_test.go | 36 +++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/integration/dra/helpers_test.go b/test/integration/dra/helpers_test.go index d83689851e5..02d84299692 100644 --- a/test/integration/dra/helpers_test.go +++ b/test/integration/dra/helpers_test.go @@ -29,6 +29,7 @@ import ( v1 "k8s.io/api/core/v1" resourceapi "k8s.io/api/resource/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/kubernetes/test/utils/ktesting" "k8s.io/utils/ptr" @@ -88,6 +89,10 @@ func createSlice(tCtx ktesting.TContext, slice *resourceapi.ResourceSlice) *reso // createTestClass creates a DeviceClass with a driver name derived from the test namespace func createTestClass(tCtx ktesting.TContext, namespace string) (*resourceapi.DeviceClass, string) { + return createTestClassWithSpec(tCtx, namespace, nil) +} + +func createTestClassWithSpec(tCtx ktesting.TContext, namespace string, spec *resourceapi.DeviceClassSpec) (*resourceapi.DeviceClass, string) { tCtx.Helper() driverName := namespace + driverNameSuffix class := class.DeepCopy() @@ -97,7 +102,10 @@ func createTestClass(tCtx ktesting.TContext, namespace string) (*resourceapi.Dev Expression: fmt.Sprintf("device.driver == %q", driverName), }, }} - _, err := tCtx.Client().ResourceV1().DeviceClasses().Create(tCtx, class, metav1.CreateOptions{}) + if spec != nil { + class.Spec = *spec + } + class, err := tCtx.Client().ResourceV1().DeviceClasses().Create(tCtx, class, metav1.CreateOptions{}) tCtx.ExpectNoError(err, "create class") tCtx.CleanupCtx(func(tCtx ktesting.TContext) { tCtx.Log("Cleaning up DeviceClass...") @@ -138,6 +146,19 @@ func createClaim(tCtx ktesting.TContext, namespace string, suffix string, class // createPod create a pod in the namespace, referencing the given claim. func createPod(tCtx ktesting.TContext, namespace string, suffix string, pod *v1.Pod, claims ...*resourceapi.ResourceClaim) *v1.Pod { + return createPodInternal(tCtx, namespace, suffix, pod, nil, claims...) +} + +// createPodWithExtendedResource creates a pod in the namespace, requesting the given extended resource. +func createPodWithExtendedResource(tCtx ktesting.TContext, namespace, resourceName, resourceQuantity string, pod *v1.Pod) *v1.Pod { + return createPodInternal(tCtx, namespace, "", pod, map[v1.ResourceName]string{ + v1.ResourceName(resourceName): resourceQuantity, + }) +} + +// createPodInternal contains the common logic for createPod and createPodWithExtendedResource. +// It shouldn't be called directly from tests. +var createPodInternal = func(tCtx ktesting.TContext, namespace string, suffix string, pod *v1.Pod, resources map[v1.ResourceName]string, claims ...*resourceapi.ResourceClaim) *v1.Pod { tCtx.Helper() pod = pod.DeepCopy() pod.Name += suffix @@ -150,6 +171,19 @@ func createPod(tCtx ktesting.TContext, namespace string, suffix string, pod *v1. ResourceClaimName: &claim.Name, }) } + for res, qty := range resources { + for i := range pod.Spec.Containers { + container := &pod.Spec.Containers[i] + if container.Resources.Requests == nil { + container.Resources.Requests = v1.ResourceList{} + } + if container.Resources.Limits == nil { + container.Resources.Limits = v1.ResourceList{} + } + container.Resources.Requests[res] = resource.MustParse(qty) + container.Resources.Limits[res] = resource.MustParse(qty) + } + } pod.Spec.ResourceClaims = resourceClaims pod, err := tCtx.Client().CoreV1().Pods(namespace).Create(tCtx, pod, metav1.CreateOptions{}) tCtx.ExpectNoError(err, "create pod "+podName) From ef68c5f91c4453e02a33a6d00c12b7eaa96ad4a1 Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Wed, 4 Feb 2026 15:43:32 +0200 Subject: [PATCH 2/4] test: integration: refactor DRA extended resource test Simplified testExtendedResource by using the helper functions added in the previous commit: - Removed unused global test data (podWithExtendedResource, classWithExtendedResource, etc.) - Used createTestClassWithSpec to create DeviceClass with extended resources - Used createPodWithExtendedResource to create test pods - Removed manual class and pod creation boilerplate --- test/integration/dra/dra_test.go | 77 ++++++++++++-------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/test/integration/dra/dra_test.go b/test/integration/dra/dra_test.go index caeff63e6e5..7c742c06052 100644 --- a/test/integration/dra/dra_test.go +++ b/test/integration/dra/dra_test.go @@ -77,37 +77,24 @@ import ( var ( // For more test data see pkg/scheduler/framework/plugin/dynamicresources/dynamicresources_test.go. - podName = "my-pod" - podWithExtendedResourceName = "my-pod-with-extended-resource" - namespace = "default" - resourceName = "my-resource" - extendedResourceName = "my-example.com/my-extended-resource" - claimName = podName + "-" + resourceName - className = "my-resource-class" - extendedClassName = "my-extended-resource-class" - device1 = "device-1" - device2 = "device-2" - podWithClaimName = st.MakePod().Name(podName).Namespace(namespace). - Container("my-container"). - PodResourceClaims(v1.PodResourceClaim{Name: resourceName, ResourceClaimName: &claimName}). - Obj() - podWithExtendedResource = st.MakePod().Name(podWithExtendedResourceName).Namespace(namespace). + podName = "my-pod" + namespace = "default" + resourceName = "my-resource" + extendedResourceName = "my-example.com/my-extended-resource" + claimName = podName + "-" + resourceName + className = "my-resource-class" + device1 = "device-1" + device2 = "device-2" + + podWithClaimName = st.MakePod().Name(podName).Namespace(namespace). Container("my-container"). - Res(map[v1.ResourceName]string{v1.ResourceName(extendedResourceName): "1"}). + PodResourceClaims(v1.PodResourceClaim{Name: resourceName, ResourceClaimName: &claimName}). Obj() class = &resourceapi.DeviceClass{ ObjectMeta: metav1.ObjectMeta{ Name: className, }, } - classWithExtendedResource = &resourceapi.DeviceClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: extendedClassName, - }, - Spec: resourceapi.DeviceClassSpec{ - ExtendedResourceName: &extendedResourceName, - }, - } claim = st.MakeResourceClaim(). Name(claimName). Namespace(namespace). @@ -798,38 +785,21 @@ func testExtendedResource(tCtx ktesting.TContext, enabled bool) { tCtx.Parallel() namespace := createTestNamespace(tCtx, nil) - driverName := namespace + driverNameSuffix - class := classWithExtendedResource.DeepCopy() - class.Spec.Selectors = []resourceapi.DeviceSelector{{ - CEL: &resourceapi.CELDeviceSelector{ - Expression: fmt.Sprintf("device.driver == %q", driverName), - }, - }} - c, err := tCtx.Client().ResourceV1().DeviceClasses().Create(tCtx, class, metav1.CreateOptions{FieldValidation: "Strict"}) - tCtx.ExpectNoError(err, "create class") + spec := &resourceapi.DeviceClassSpec{ + ExtendedResourceName: &extendedResourceName, + } + _, driverName := createTestClassWithSpec(tCtx, namespace, spec) slice := st.MakeResourceSlice("worker-0", driverName).Devices(device1) createSlice(tCtx, slice.Obj()) - if enabled { - require.NotEmpty(tCtx, c.Spec.ExtendedResourceName, "should store ExtendedResourceName") - } - tCtx.Run("scheduler", func(tCtx ktesting.TContext) { startScheduler(tCtx) - pod := podWithExtendedResource.DeepCopy() - pod.Namespace = namespace - _, err := tCtx.Client().CoreV1().Pods(namespace).Create(tCtx, pod, metav1.CreateOptions{FieldValidation: "Strict"}) - tCtx.ExpectNoError(err, "create pod") - schedulingAttempted := gomega.HaveField("Status.Conditions", gomega.ContainElement( - gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Type": gomega.Equal(v1.PodScheduled), - "Status": gomega.Equal(v1.ConditionFalse), - "Reason": gomega.Equal("Unschedulable"), - "Message": gomega.Equal("0/8 nodes are available: 8 Insufficient my-example.com/my-extended-resource. no new claims to deallocate, preemption: 0/8 nodes are available: 8 Preemption is not helpful for scheduling."), - }), - )) + podWithOneContainer := st.MakePod().Name("test-pod").Namespace(namespace).Container("test-container").Obj() + pod := createPodWithExtendedResource(tCtx, namespace, extendedResourceName, "1", podWithOneContainer) + + var schedulingAttempted gtypes.GomegaMatcher if enabled { // pod can be scheduled as the drivers in testPublishResourceSlices provide the devices. schedulingAttempted = gomega.HaveField("Status.Conditions", gomega.ContainElement( @@ -838,6 +808,15 @@ func testExtendedResource(tCtx ktesting.TContext, enabled bool) { "Status": gomega.Equal(v1.ConditionTrue), }), )) + } else { + schedulingAttempted = gomega.HaveField("Status.Conditions", gomega.ContainElement( + gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Type": gomega.Equal(v1.PodScheduled), + "Status": gomega.Equal(v1.ConditionFalse), + "Reason": gomega.Equal("Unschedulable"), + "Message": gomega.Equal(fmt.Sprintf("0/8 nodes are available: 8 Insufficient %s. no new claims to deallocate, preemption: 0/8 nodes are available: 8 Preemption is not helpful for scheduling.", extendedResourceName)), + }), + )) } tCtx.Eventually(func(tCtx ktesting.TContext) (*v1.Pod, error) { return tCtx.Client().CoreV1().Pods(namespace).Get(tCtx, pod.Name, metav1.GetOptions{}) From f56e2545f09d52c68b6933f652c66c819ce5ccab Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Wed, 4 Feb 2026 16:20:07 +0200 Subject: [PATCH 3/4] DRA: integration: test implicit resources Modified DRA integration test to test implicit extended resources. --- test/integration/dra/dra_test.go | 49 +++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/test/integration/dra/dra_test.go b/test/integration/dra/dra_test.go index 7c742c06052..dc24b9cc47f 100644 --- a/test/integration/dra/dra_test.go +++ b/test/integration/dra/dra_test.go @@ -77,14 +77,13 @@ import ( var ( // For more test data see pkg/scheduler/framework/plugin/dynamicresources/dynamicresources_test.go. - podName = "my-pod" - namespace = "default" - resourceName = "my-resource" - extendedResourceName = "my-example.com/my-extended-resource" - claimName = podName + "-" + resourceName - className = "my-resource-class" - device1 = "device-1" - device2 = "device-2" + podName = "my-pod" + namespace = "default" + resourceName = "my-resource" + claimName = podName + "-" + resourceName + className = "my-resource-class" + device1 = "device-1" + device2 = "device-2" podWithClaimName = st.MakePod().Name(podName).Namespace(namespace). Container("my-container"). @@ -163,7 +162,8 @@ func TestDRA(t *testing.T) { tCtx.Run("PublishResourceSlices", func(tCtx ktesting.TContext) { testPublishResourceSlices(tCtx, true, features.DRADeviceTaints, features.DRAPartitionableDevices, features.DRADeviceBindingConditions) }) - tCtx.Run("ExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, false) }) + tCtx.Run("ExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, false, true) }) + tCtx.Run("ImplicitExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, false, false) }) tCtx.Run("ResourceClaimDeviceStatus", func(tCtx ktesting.TContext) { testResourceClaimDeviceStatus(tCtx, false) }) tCtx.Run("DeviceBindingConditions", func(tCtx ktesting.TContext) { testDeviceBindingConditions(tCtx, false) }) tCtx.Run("ResourceSliceController", func(tCtx ktesting.TContext) { @@ -237,7 +237,8 @@ func TestDRA(t *testing.T) { tCtx.Run("PrioritizedListScoring", func(tCtx ktesting.TContext) { testPrioritizedListScoring(tCtx) }) tCtx.Run("PublishResourceSlices", func(tCtx ktesting.TContext) { testPublishResourceSlices(tCtx, true) }) // note testExtendedResource depends on testPublishResourceSlices to provide the devices - tCtx.Run("ExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, true) }) + tCtx.Run("ExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, true, true) }) + tCtx.Run("ImplicitExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, true, false) }) tCtx.Run("ResourceClaimDeviceStatus", func(tCtx ktesting.TContext) { testResourceClaimDeviceStatus(tCtx, true) }) tCtx.Run("MaxResourceSlice", testMaxResourceSlice) tCtx.Run("EvictClusterWithRule", func(tCtx ktesting.TContext) { testEvictCluster(tCtx, true) }) @@ -781,14 +782,28 @@ func expectedAllocatedClaim(request string, nodeInfo nodeInfo) gtypes.GomegaMatc }))) } -func testExtendedResource(tCtx ktesting.TContext, enabled bool) { +func testExtendedResource(tCtx ktesting.TContext, enabled, explicit bool) { tCtx.Parallel() namespace := createTestNamespace(tCtx, nil) - spec := &resourceapi.DeviceClassSpec{ - ExtendedResourceName: &extendedResourceName, + + // Create a DeviceClass with or without ExtendedResourceName based on whether we're testing explicit or implicit extended resources. + var resourceName string + var spec *resourceapi.DeviceClassSpec + if explicit { + resourceName = "example.com/" + namespace + // Set extended resource name in the DeviceClass spec only for explicit resources. + // It's not required for implicit extended resources. + spec = &resourceapi.DeviceClassSpec{ + ExtendedResourceName: &resourceName, + } + } + class, driverName := createTestClassWithSpec(tCtx, namespace, spec) + + if !explicit { + // For implicit extended resources, derive the resource name from the class. + resourceName = resourceapi.ResourceDeviceClassPrefix + class.Name } - _, driverName := createTestClassWithSpec(tCtx, namespace, spec) slice := st.MakeResourceSlice("worker-0", driverName).Devices(device1) createSlice(tCtx, slice.Obj()) @@ -796,8 +811,8 @@ func testExtendedResource(tCtx ktesting.TContext, enabled bool) { tCtx.Run("scheduler", func(tCtx ktesting.TContext) { startScheduler(tCtx) - podWithOneContainer := st.MakePod().Name("test-pod").Namespace(namespace).Container("test-container").Obj() - pod := createPodWithExtendedResource(tCtx, namespace, extendedResourceName, "1", podWithOneContainer) + podWithOneContainer := st.MakePod().Name(podName).Namespace(namespace).Container("test-container").Obj() + pod := createPodWithExtendedResource(tCtx, namespace, resourceName, "1", podWithOneContainer) var schedulingAttempted gtypes.GomegaMatcher if enabled { @@ -814,7 +829,7 @@ func testExtendedResource(tCtx ktesting.TContext, enabled bool) { "Type": gomega.Equal(v1.PodScheduled), "Status": gomega.Equal(v1.ConditionFalse), "Reason": gomega.Equal("Unschedulable"), - "Message": gomega.Equal(fmt.Sprintf("0/8 nodes are available: 8 Insufficient %s. no new claims to deallocate, preemption: 0/8 nodes are available: 8 Preemption is not helpful for scheduling.", extendedResourceName)), + "Message": gomega.Equal(fmt.Sprintf("0/8 nodes are available: 8 Insufficient %s. no new claims to deallocate, preemption: 0/8 nodes are available: 8 Preemption is not helpful for scheduling.", resourceName)), }), )) } From 299471da26d39d099852fbc3da1b10f788236e55 Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Thu, 5 Feb 2026 12:59:55 +0200 Subject: [PATCH 4/4] test: refactor extended resource test Simplify testExtendedResource structure and add proper field validation: - Rename tests to distinguish explicit vs implicit modes clearly - Remove unnecessary nested sub-test to flatten test hierarchy - Validate ExtendedResourceName is correctly stored or stripped based on feature gate state to ensure proper API behavior - Remove outdated comment that no longer applies Co-Authored-By: Patrick Ohly --- test/integration/dra/dra_test.go | 66 ++++++++++++++++---------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/test/integration/dra/dra_test.go b/test/integration/dra/dra_test.go index dc24b9cc47f..7414bc317ef 100644 --- a/test/integration/dra/dra_test.go +++ b/test/integration/dra/dra_test.go @@ -162,7 +162,7 @@ func TestDRA(t *testing.T) { tCtx.Run("PublishResourceSlices", func(tCtx ktesting.TContext) { testPublishResourceSlices(tCtx, true, features.DRADeviceTaints, features.DRAPartitionableDevices, features.DRADeviceBindingConditions) }) - tCtx.Run("ExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, false, true) }) + tCtx.Run("ExplicitExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, false, true) }) tCtx.Run("ImplicitExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, false, false) }) tCtx.Run("ResourceClaimDeviceStatus", func(tCtx ktesting.TContext) { testResourceClaimDeviceStatus(tCtx, false) }) tCtx.Run("DeviceBindingConditions", func(tCtx ktesting.TContext) { testDeviceBindingConditions(tCtx, false) }) @@ -236,8 +236,7 @@ func TestDRA(t *testing.T) { tCtx.Run("PrioritizedList", func(tCtx ktesting.TContext) { testPrioritizedList(tCtx, true) }) tCtx.Run("PrioritizedListScoring", func(tCtx ktesting.TContext) { testPrioritizedListScoring(tCtx) }) tCtx.Run("PublishResourceSlices", func(tCtx ktesting.TContext) { testPublishResourceSlices(tCtx, true) }) - // note testExtendedResource depends on testPublishResourceSlices to provide the devices - tCtx.Run("ExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, true, true) }) + tCtx.Run("ExplicitExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, true, true) }) tCtx.Run("ImplicitExtendedResource", func(tCtx ktesting.TContext) { testExtendedResource(tCtx, true, false) }) tCtx.Run("ResourceClaimDeviceStatus", func(tCtx ktesting.TContext) { testResourceClaimDeviceStatus(tCtx, true) }) tCtx.Run("MaxResourceSlice", testMaxResourceSlice) @@ -799,8 +798,13 @@ func testExtendedResource(tCtx ktesting.TContext, enabled, explicit bool) { } } class, driverName := createTestClassWithSpec(tCtx, namespace, spec) - - if !explicit { + if explicit { + if enabled { + require.NotEmpty(tCtx, class.Spec.ExtendedResourceName, "should store ExtendedResourceName") + } else { + require.Empty(tCtx, class.Spec.ExtendedResourceName, "should strip ExtendedResourceName") + } + } else { // For implicit extended resources, derive the resource name from the class. resourceName = resourceapi.ResourceDeviceClassPrefix + class.Name } @@ -808,35 +812,33 @@ func testExtendedResource(tCtx ktesting.TContext, enabled, explicit bool) { slice := st.MakeResourceSlice("worker-0", driverName).Devices(device1) createSlice(tCtx, slice.Obj()) - tCtx.Run("scheduler", func(tCtx ktesting.TContext) { - startScheduler(tCtx) + startScheduler(tCtx) - podWithOneContainer := st.MakePod().Name(podName).Namespace(namespace).Container("test-container").Obj() - pod := createPodWithExtendedResource(tCtx, namespace, resourceName, "1", podWithOneContainer) + podWithOneContainer := st.MakePod().Name(podName).Namespace(namespace).Container("test-container").Obj() + pod := createPodWithExtendedResource(tCtx, namespace, resourceName, "1", podWithOneContainer) - var schedulingAttempted gtypes.GomegaMatcher - if enabled { - // pod can be scheduled as the drivers in testPublishResourceSlices provide the devices. - schedulingAttempted = gomega.HaveField("Status.Conditions", gomega.ContainElement( - gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Type": gomega.Equal(v1.PodScheduled), - "Status": gomega.Equal(v1.ConditionTrue), - }), - )) - } else { - schedulingAttempted = gomega.HaveField("Status.Conditions", gomega.ContainElement( - gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Type": gomega.Equal(v1.PodScheduled), - "Status": gomega.Equal(v1.ConditionFalse), - "Reason": gomega.Equal("Unschedulable"), - "Message": gomega.Equal(fmt.Sprintf("0/8 nodes are available: 8 Insufficient %s. no new claims to deallocate, preemption: 0/8 nodes are available: 8 Preemption is not helpful for scheduling.", resourceName)), - }), - )) - } - tCtx.Eventually(func(tCtx ktesting.TContext) (*v1.Pod, error) { - return tCtx.Client().CoreV1().Pods(namespace).Get(tCtx, pod.Name, metav1.GetOptions{}) - }).WithTimeout(time.Minute).WithPolling(time.Second).Should(schedulingAttempted) - }) + var schedulingAttempted gtypes.GomegaMatcher + if enabled { + // Scheduled using device1 in the slice above. + schedulingAttempted = gomega.HaveField("Status.Conditions", gomega.ContainElement( + gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Type": gomega.Equal(v1.PodScheduled), + "Status": gomega.Equal(v1.ConditionTrue), + }), + )) + } else { + schedulingAttempted = gomega.HaveField("Status.Conditions", gomega.ContainElement( + gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Type": gomega.Equal(v1.PodScheduled), + "Status": gomega.Equal(v1.ConditionFalse), + "Reason": gomega.Equal("Unschedulable"), + "Message": gomega.Equal(fmt.Sprintf("0/8 nodes are available: 8 Insufficient %s. no new claims to deallocate, preemption: 0/8 nodes are available: 8 Preemption is not helpful for scheduling.", resourceName)), + }), + )) + } + tCtx.Eventually(func(tCtx ktesting.TContext) (*v1.Pod, error) { + return tCtx.Client().CoreV1().Pods(namespace).Get(tCtx, pod.Name, metav1.GetOptions{}) + }).WithTimeout(time.Minute).WithPolling(time.Second).Should(schedulingAttempted) } func testPublishResourceSlices(tCtx ktesting.TContext, haveLatestAPI bool, disabledFeatures ...featuregate.Feature) {