Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pkg/testsuites/standard_suites.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,19 @@ var staticSuites = []ginkgo.TestSuite{
TestTimeout: 90 * time.Minute,
ClusterStabilityDuringTest: ginkgo.Disruptive,
},
{
Name: "openshift/dra-example",
Description: templates.LongDesc(`
Tests that exercise Dynamic Resource Allocation (DRA) functionality
using the upstream dra-example-driver. Requires the driver to be
pre-installed on the cluster.
`),
Qualifiers: []string{
withStandardEarlyOrLateTests(`name.contains("[Suite:openshift/dra-example]") || name.contains("[Feature:DynamicResourceAllocation]")`),
},
Parallelism: 1,
TestTimeout: 60 * time.Minute,
},
}

func withExcludedTestsFilter(baseExpr string) string {
Expand Down
1 change: 1 addition & 0 deletions test/extended/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
_ "github.com/openshift/origin/test/extended/node"
_ "github.com/openshift/origin/test/extended/node/dra/example"
_ "github.com/openshift/origin/test/extended/node/dra/nvidia"
_ "github.com/openshift/origin/test/extended/node/dra/partitionable"
_ "github.com/openshift/origin/test/extended/node/node_e2e"
_ "github.com/openshift/origin/test/extended/node_tuning"
_ "github.com/openshift/origin/test/extended/oauth"
Expand Down
191 changes: 191 additions & 0 deletions test/extended/node/dra/common/counter_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package common

import (
"context"
"fmt"
"strings"

corev1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/test/e2e/framework"
)

func (cv *CounterValidator) listOptions() metav1.ListOptions {
return metav1.ListOptions{
FieldSelector: resourceapi.ResourceSliceSelectorDriver + "=" + cv.driverName,
}
}

// CounterValidator provides helpers for validating ResourceSlice counter
// structures introduced by the DRAPartitionableDevices feature (KEP-4815).
// It separates ResourceSlices into counter slices (SharedCounters only) and
// device slices (Devices with ConsumesCounters), matching the two-slice model
// that partitionable drivers publish.
type CounterValidator struct {
client kubernetes.Interface
driverName string
}

// NewCounterValidator creates a validator for the given driver.
func NewCounterValidator(client kubernetes.Interface, driverName string) *CounterValidator {
return &CounterValidator{
client: client,
driverName: driverName,
}
}

// GetResourceSlicesByType lists all ResourceSlices for the driver and separates
// them into counter slices (have SharedCounters but no Devices) and device slices
// (have Devices, may have ConsumesCounters on individual devices).
func (cv *CounterValidator) GetResourceSlicesByType(ctx context.Context) (counterSlices, deviceSlices []resourceapi.ResourceSlice, err error) {
sliceList, err := cv.client.ResourceV1().ResourceSlices().List(ctx, cv.listOptions())
if err != nil {
return nil, nil, fmt.Errorf("failed to list ResourceSlices: %w", err)
}

for _, slice := range sliceList.Items {
if len(slice.Spec.SharedCounters) > 0 && len(slice.Spec.Devices) == 0 {
counterSlices = append(counterSlices, slice)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this variable counterSlices defined somewhere already?

}
if len(slice.Spec.Devices) > 0 {
deviceSlices = append(deviceSlices, slice)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this variable deviceSlices defined somewhere already?

@sabujmaity sabujmaity Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both counterSlices & deviceSlices are named return values defined in the function signature on line 37. They are not defined anywhere else in the package. They are local to this function and returned to the caller

}
}
return counterSlices, deviceSlices, nil
}

// ValidateSharedCounters verifies that at least one counter slice exists and
// that every CounterSet in those slices contains the expected counters with
// non-zero values. Returns an error describing the first violation found.
func (cv *CounterValidator) ValidateSharedCounters(ctx context.Context, expectedCounters []string) error {
counterSlices, _, err := cv.GetResourceSlicesByType(ctx)
if err != nil {
return err
}
if len(counterSlices) == 0 {
return fmt.Errorf("no ResourceSlices with SharedCounters found for driver %s", cv.driverName)
}

for _, slice := range counterSlices {
for _, cs := range slice.Spec.SharedCounters {
for _, name := range expectedCounters {
counter, exists := cs.Counters[name]
if !exists {
return fmt.Errorf("CounterSet %q in slice %s missing counter %q", cs.Name, slice.Name, name)
}
if counter.Value.IsZero() {
return fmt.Errorf("CounterSet %q counter %q has zero value", cs.Name, name)
}
framework.Logf("CounterSet %s: %s=%s", cs.Name, name, counter.Value.String())
}
}
}
framework.Logf("Validated SharedCounters across %d counter slice(s) for driver %s", len(counterSlices), cv.driverName)
return nil
}

// ValidateDeviceConsumesCounters verifies that every device in the driver's
// device slices has at least one ConsumesCounters entry pointing to a named
// CounterSet.
func (cv *CounterValidator) ValidateDeviceConsumesCounters(ctx context.Context) error {
_, deviceSlices, err := cv.GetResourceSlicesByType(ctx)
if err != nil {
return err
}
if len(deviceSlices) == 0 {
return fmt.Errorf("no ResourceSlices with Devices found for driver %s", cv.driverName)
}

for _, slice := range deviceSlices {
for _, device := range slice.Spec.Devices {
if len(device.ConsumesCounters) == 0 {
return fmt.Errorf("device %s in slice %s has no ConsumesCounters", device.Name, slice.Name)
}
for _, cc := range device.ConsumesCounters {
if cc.CounterSet == "" {
return fmt.Errorf("device %s has ConsumesCounters with empty CounterSet name", device.Name)
}
}
framework.Logf("Device %s consumes from %d counter set(s)", device.Name, len(device.ConsumesCounters))
}
}
return nil
}

// CountPartitionDevices returns the number of partition devices across all
// device slices for the driver. The upstream dra-example-driver names partition
// devices as "gpu-N-partition-M" when gpuPartitions > 0, so the substring
// "partition" reliably identifies them within this driver's naming convention.
func (cv *CounterValidator) CountPartitionDevices(ctx context.Context) (int, error) {
_, deviceSlices, err := cv.GetResourceSlicesByType(ctx)
if err != nil {
return 0, err
}
count := 0
for _, slice := range deviceSlices {
for _, device := range slice.Spec.Devices {
if strings.Contains(device.Name, "partition") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a valid condition to detect the partition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's valid for this driver. The upstream dra-example-driver names partition devices as gpu-N-partition-M when gpuPartitions > 0, so the substring match is reliable. Added a comment explaining the naming convention.

count++
}
}
}
return count, nil
}

// HasSharedCounters returns true if the driver is publishing ResourceSlices
// that contain SharedCounters.
func (cv *CounterValidator) HasSharedCounters(ctx context.Context) bool {
counterSlices, _, err := cv.GetResourceSlicesByType(ctx)
if err != nil {
framework.Logf("Failed to check for SharedCounters: %v", err)
return false
}
return len(counterSlices) > 0
}

// GetNodeWithDevices returns the name of a schedulable worker node where the
// driver is publishing devices. It avoids master/control-plane nodes whose
// taints would prevent regular test pods from being scheduled there. Falls
// back to any node with devices if no untainted node is found.
func (cv *CounterValidator) GetNodeWithDevices(ctx context.Context) (string, error) {
sliceList, err := cv.client.ResourceV1().ResourceSlices().List(ctx, cv.listOptions())
if err != nil {
return "", fmt.Errorf("failed to list ResourceSlices: %w", err)
}

nodeList, err := cv.client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
if err != nil {
return "", fmt.Errorf("failed to list nodes: %w", err)
}

taintedNodes := make(map[string]bool)
for _, node := range nodeList.Items {
for _, taint := range node.Spec.Taints {
if taint.Effect == corev1.TaintEffectNoSchedule || taint.Effect == corev1.TaintEffectNoExecute {
taintedNodes[node.Name] = true
break
}
}
}

var fallback string
for _, slice := range sliceList.Items {
if slice.Spec.NodeName == nil || *slice.Spec.NodeName == "" {
continue
}
name := *slice.Spec.NodeName
if !taintedNodes[name] {
return name, nil
}
if fallback == "" {
fallback = name
}
}
if fallback != "" {
framework.Logf("Warning: no untainted node with devices found, falling back to tainted node %s", fallback)
return fallback, nil
}
return "", fmt.Errorf("no node found publishing devices for driver %s", cv.driverName)
}
84 changes: 17 additions & 67 deletions test/extended/node/dra/common/crud_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,40 @@ package common
import (
"context"

resourceapi "k8s.io/api/resource/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/utils/ptr"
"k8s.io/client-go/kubernetes"
)

var (
DeviceClassGVR = schema.GroupVersionResource{
Group: "resource.k8s.io",
Version: "v1",
Resource: "deviceclasses",
}
ResourceClaimGVR = schema.GroupVersionResource{
Group: "resource.k8s.io",
Version: "v1",
Resource: "resourceclaims",
}
ResourceClaimTemplateGVR = schema.GroupVersionResource{
Group: "resource.k8s.io",
Version: "v1",
Resource: "resourceclaimtemplates",
}
)

// ConvertToUnstructured converts a typed object to Unstructured
func ConvertToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
unstructuredObj := &unstructured.Unstructured{}
content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
unstructuredObj.Object = content
return unstructuredObj, nil
}

// CreateDeviceClass creates a DeviceClass
func CreateDeviceClass(ctx context.Context, client dynamic.Interface, deviceClass interface{}) error {
unstructuredObj, err := ConvertToUnstructured(deviceClass)
if err != nil {
return err
}
_, err = client.Resource(DeviceClassGVR).Create(ctx, unstructuredObj, metav1.CreateOptions{})
// CreateDeviceClass creates a DeviceClass using the typed client.
func CreateDeviceClass(ctx context.Context, client kubernetes.Interface, deviceClass *resourceapi.DeviceClass) error {
_, err := client.ResourceV1().DeviceClasses().Create(ctx, deviceClass, metav1.CreateOptions{})
return err
}

// DeleteDeviceClass deletes a DeviceClass
func DeleteDeviceClass(ctx context.Context, client dynamic.Interface, name string) error {
return client.Resource(DeviceClassGVR).Delete(ctx, name, metav1.DeleteOptions{
GracePeriodSeconds: ptr.To[int64](0),
})
func DeleteDeviceClass(ctx context.Context, client kubernetes.Interface, name string) error {
return client.ResourceV1().DeviceClasses().Delete(ctx, name, metav1.DeleteOptions{})
}

// CreateResourceClaim creates a ResourceClaim
func CreateResourceClaim(ctx context.Context, client dynamic.Interface, namespace string, claim interface{}) error {
unstructuredObj, err := ConvertToUnstructured(claim)
if err != nil {
return err
}
_, err = client.Resource(ResourceClaimGVR).Namespace(namespace).Create(ctx, unstructuredObj, metav1.CreateOptions{})
// CreateResourceClaim creates a ResourceClaim using the typed client.
func CreateResourceClaim(ctx context.Context, client kubernetes.Interface, namespace string, claim *resourceapi.ResourceClaim) error {
_, err := client.ResourceV1().ResourceClaims(namespace).Create(ctx, claim, metav1.CreateOptions{})
return err
}

// DeleteResourceClaim deletes a ResourceClaim
func DeleteResourceClaim(ctx context.Context, client dynamic.Interface, namespace, name string) error {
return client.Resource(ResourceClaimGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{
GracePeriodSeconds: ptr.To[int64](0),
})
func DeleteResourceClaim(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.ResourceV1().ResourceClaims(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}

// CreateResourceClaimTemplate creates a ResourceClaimTemplate
func CreateResourceClaimTemplate(ctx context.Context, client dynamic.Interface, namespace string, template interface{}) error {
unstructuredObj, err := ConvertToUnstructured(template)
if err != nil {
return err
}
_, err = client.Resource(ResourceClaimTemplateGVR).Namespace(namespace).Create(ctx, unstructuredObj, metav1.CreateOptions{})
// CreateResourceClaimTemplate creates a ResourceClaimTemplate using the typed client.
func CreateResourceClaimTemplate(ctx context.Context, client kubernetes.Interface, namespace string, template *resourceapi.ResourceClaimTemplate) error {
_, err := client.ResourceV1().ResourceClaimTemplates(namespace).Create(ctx, template, metav1.CreateOptions{})
return err
}

// DeleteResourceClaimTemplate deletes a ResourceClaimTemplate
func DeleteResourceClaimTemplate(ctx context.Context, client dynamic.Interface, namespace, name string) error {
return client.Resource(ResourceClaimTemplateGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{
GracePeriodSeconds: ptr.To[int64](0),
})
func DeleteResourceClaimTemplate(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.ResourceV1().ResourceClaimTemplates(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
15 changes: 8 additions & 7 deletions test/extended/node/dra/example/device_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ func (dv *DeviceValidator) ValidateDeviceAllocation(ctx context.Context, namespa
func (dv *DeviceValidator) ValidateResourceSlice(ctx context.Context, nodeName string) (*resourceapi.ResourceSlice, error) {
framework.Logf("Validating ResourceSlice for node %s", nodeName)

sliceList, err := dv.client.ResourceV1().ResourceSlices().List(ctx, metav1.ListOptions{})
sliceList, err := dv.client.ResourceV1().ResourceSlices().List(ctx, metav1.ListOptions{
FieldSelector: resourceapi.ResourceSliceSelectorDriver + "=" + exampleDriverName,
})
if err != nil {
return nil, fmt.Errorf("failed to list ResourceSlices: %w", err)
}
Expand All @@ -85,8 +87,7 @@ func (dv *DeviceValidator) ValidateResourceSlice(ctx context.Context, nodeName s
totalDevices := 0
for i := range sliceList.Items {
slice := &sliceList.Items[i]
if slice.Spec.NodeName != nil && *slice.Spec.NodeName == nodeName &&
slice.Spec.Driver == exampleDriverName {
if slice.Spec.NodeName != nil && *slice.Spec.NodeName == nodeName {
totalDevices += len(slice.Spec.Devices)
if nodeSlice == nil && len(slice.Spec.Devices) > 0 {
nodeSlice = slice
Expand All @@ -107,16 +108,16 @@ func (dv *DeviceValidator) ValidateResourceSlice(ctx context.Context, nodeName s
func (dv *DeviceValidator) GetTotalDeviceCount(ctx context.Context) (int, error) {
framework.Logf("Counting total devices from %s driver via ResourceSlices", exampleDriverName)

sliceList, err := dv.client.ResourceV1().ResourceSlices().List(ctx, metav1.ListOptions{})
sliceList, err := dv.client.ResourceV1().ResourceSlices().List(ctx, metav1.ListOptions{
FieldSelector: resourceapi.ResourceSliceSelectorDriver + "=" + exampleDriverName,
})
if err != nil {
return 0, fmt.Errorf("failed to list ResourceSlices: %w", err)
}

totalDevices := 0
for _, slice := range sliceList.Items {
if slice.Spec.Driver == exampleDriverName {
totalDevices += len(slice.Spec.Devices)
}
totalDevices += len(slice.Spec.Devices)
}

framework.Logf("Found %d total device(s) from %s driver", totalDevices, exampleDriverName)
Expand Down
Loading