Merge pull request #137240 from yykkibbb/dra-api-fuzz-roundtrip-test

Add fuzz roundtrip test for DRA API conversion and fix v1beta1 bugs
This commit is contained in:
Kubernetes Prow Robot
2026-03-03 21:15:29 +05:30
committed by GitHub
6 changed files with 328 additions and 2 deletions

View File

@@ -0,0 +1,119 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"testing"
"github.com/google/go-cmp/cmp"
resourceapi "k8s.io/api/resource/v1"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
apitest "k8s.io/dynamic-resource-allocation/api/internal/test"
"sigs.k8s.io/randfill"
)
// v1FillFuncs returns custom fill functions needed for v1 types where
// the default random filling would create objects that cannot round-trip
// due to pointer-to-value conversions in the internal api types.
func v1FillFuncs(codecs serializer.CodecFactory) []interface{} {
return []interface{}{
// The internal api types use bool instead of *bool for AllNodes.
// Converting nil -> false -> &false means nil doesn't round-trip.
// Ensure AllNodes is always non-nil to avoid this.
func(s *resourceapi.ResourceSliceSpec, c randfill.Continue) {
c.FillNoCustom(s)
if s.AllNodes == nil {
s.AllNodes = new(bool)
}
},
// The internal api types use bool instead of *bool for BindsToNode.
// Converting nil -> false -> &false means nil doesn't round-trip.
// Ensure BindsToNode is always non-nil to avoid this.
func(d *resourceapi.Device, c randfill.Continue) {
c.FillNoCustom(d)
if d.BindsToNode == nil {
d.BindsToNode = new(bool)
}
},
}
}
// TestConversionRoundTrip verifies that the conversion code for the internal
// DRA API correctly round-trips between v1 and the internal api types.
// For v1 -> api -> v1, the conversion should be lossless.
//
// api -> v1 -> api is not lossless because of
// additional fields in the internal representation.
// Those would get lost during conversion.
// But this doesn't matter because in practice,
// the internal representation is never converted back,
// therefore this direction is not tested.
func TestConversionRoundTrip(t *testing.T) {
scheme := runtime.NewScheme()
if err := resourceapi.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := AddToScheme(scheme); err != nil {
t.Fatal(err)
}
filler := apitest.NewFiller(t, scheme, fuzzer.FuzzerFuncs(v1FillFuncs))
testCases := []struct {
name string
v1Type func() runtime.Object
apiNew func() interface{}
}{
{
name: "ResourceSlice",
v1Type: func() runtime.Object { return &resourceapi.ResourceSlice{} },
apiNew: func() interface{} { return &ResourceSlice{} },
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for i := range apitest.FuzzIterations {
// Create and fuzz v1 object.
v1Obj := tc.v1Type()
filler.Fill(v1Obj)
// Convert v1 -> api.
apiObj := tc.apiNew()
if err := scheme.Convert(v1Obj, apiObj, nil); err != nil {
t.Fatalf("iteration %d: v1 -> api: %v", i, err)
}
// Convert api -> v1 (round-trip).
roundTripped := tc.v1Type()
if err := scheme.Convert(apiObj, roundTripped, nil); err != nil {
t.Fatalf("iteration %d: api -> v1: %v", i, err)
}
// The round-tripped object must be equal to the original.
if !apiequality.Semantic.DeepEqual(v1Obj, roundTripped) {
t.Errorf("iteration %d: round-trip v1 -> api -> v1 failed\ndiff (-want +got):\n%s",
i, cmp.Diff(v1Obj, roundTripped))
}
}
})
}
}

View File

@@ -0,0 +1,113 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package test provides test utilities for DRA API conversion testing.
package test
import (
"math/rand"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
apiequality "k8s.io/apimachinery/pkg/api/equality"
genericfuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"sigs.k8s.io/randfill"
)
// FuzzIterations is the number of fuzz iterations to run for each type.
const FuzzIterations = 100
// NewFiller creates a new randfill.Filler for fuzzing objects.
func NewFiller(t *testing.T, scheme *runtime.Scheme, customFuncs fuzzer.FuzzerFuncs) *randfill.Filler {
seed := rand.Int63()
t.Logf("Using seed: %d", seed)
codecs := serializer.NewCodecFactory(scheme)
return fuzzer.FuzzerFor(
fuzzer.MergeFuzzerFuncs(genericfuzzer.Funcs, customFuncs),
rand.NewSource(seed),
codecs,
)
}
// ConversionRoundTrip tests that all non-list types in srcGV can be converted
// to dstGV and back without loss of information.
func ConversionRoundTrip(t *testing.T, scheme *runtime.Scheme, filler *randfill.Filler, srcGV, dstGV schema.GroupVersion) {
t.Helper()
tested := 0
for kind := range scheme.KnownTypes(srcGV) {
if strings.HasSuffix(kind, "List") {
continue
}
srcGVK := srcGV.WithKind(kind)
dstGVK := dstGV.WithKind(kind)
if _, err := scheme.New(dstGVK); err != nil {
// Kind does not exist in the destination version.
continue
}
tested++
t.Run(kind, func(t *testing.T) {
for i := range FuzzIterations {
// Create and fuzz source object.
srcObj, err := scheme.New(srcGVK)
if err != nil {
t.Fatal(err)
}
filler.Fill(srcObj)
// Convert source -> destination.
dstObj, err := scheme.New(dstGVK)
if err != nil {
t.Fatal(err)
}
if err := scheme.Convert(srcObj, dstObj, nil); err != nil {
t.Fatalf("iteration %d: %v -> %v: %v", i, srcGVK, dstGVK, err)
}
// Convert destination -> source (round-trip).
roundTripped, err := scheme.New(srcGVK)
if err != nil {
t.Fatal(err)
}
if err := scheme.Convert(dstObj, roundTripped, nil); err != nil {
t.Fatalf("iteration %d: %v -> %v: %v", i, dstGVK, srcGVK, err)
}
// The round-tripped object must be equal to the original.
// Use Semantic.DeepEqual which treats nil and empty
// slices/maps as equivalent.
if !apiequality.Semantic.DeepEqual(srcObj, roundTripped) {
t.Errorf("iteration %d: round-trip %v -> %v -> %v failed\ndiff (-want +got):\n%s",
i, srcGVK, dstGVK, srcGVK,
cmp.Diff(srcObj, roundTripped))
}
}
})
}
if tested == 0 {
t.Fatal("no types were tested")
}
t.Logf("tested %d types for %v <-> %v", tested, srcGV, dstGV)
}

View File

@@ -58,6 +58,7 @@ func Convert_v1beta1_DeviceRequest_To_v1_DeviceRequest(in *resourcev1beta1.Devic
tolerations = append(tolerations, toleration)
}
exactDeviceRequest.Tolerations = tolerations
exactDeviceRequest.Capacity = (*resourceapi.CapacityRequirements)(unsafe.Pointer(in.Capacity))
out.Exactly = &exactDeviceRequest
}
return nil
@@ -69,7 +70,8 @@ func hasAnyMainRequestFieldsSet(deviceRequest *resourcev1beta1.DeviceRequest) bo
deviceRequest.AllocationMode != "" ||
deviceRequest.Count != 0 ||
deviceRequest.AdminAccess != nil ||
deviceRequest.Tolerations != nil
deviceRequest.Tolerations != nil ||
deviceRequest.Capacity != nil
}
func Convert_v1_DeviceRequest_To_v1beta1_DeviceRequest(in *resourceapi.DeviceRequest, out *resourcev1beta1.DeviceRequest, s conversion.Scope) error {
@@ -102,6 +104,7 @@ func Convert_v1_DeviceRequest_To_v1beta1_DeviceRequest(in *resourceapi.DeviceReq
tolerations = append(tolerations, toleration)
}
out.Tolerations = tolerations
out.Capacity = (*resourcev1beta1.CapacityRequirements)(unsafe.Pointer(in.Exactly.Capacity))
}
return nil
}
@@ -181,6 +184,10 @@ func Convert_v1beta1_Device_To_v1_Device(in *resourcev1beta1.Device, out *resour
taints = append(taints, taint)
}
out.Taints = taints
out.BindsToNode = basic.BindsToNode
out.BindingConditions = basic.BindingConditions
out.BindingFailureConditions = basic.BindingFailureConditions
out.AllowMultipleAllocations = basic.AllowMultipleAllocations
}
return nil
}
@@ -226,6 +233,10 @@ func Convert_v1_Device_To_v1beta1_Device(in *resourceapi.Device, out *resourcev1
taints = append(taints, taint)
}
out.Basic.Taints = taints
out.Basic.BindsToNode = in.BindsToNode
out.Basic.BindingConditions = in.BindingConditions
out.Basic.BindingFailureConditions = in.BindingFailureConditions
out.Basic.AllowMultipleAllocations = in.AllowMultipleAllocations
return nil
}

View File

@@ -24,7 +24,12 @@ import (
"github.com/google/go-cmp/cmp"
resourceapi "k8s.io/api/resource/v1"
resourcev1beta1 "k8s.io/api/resource/v1beta1"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
apitest "k8s.io/dynamic-resource-allocation/api/internal/test"
"sigs.k8s.io/randfill"
)
func TestConversion(t *testing.T) {
@@ -331,3 +336,49 @@ func TestConversion(t *testing.T) {
}
}
// v1beta1FillFuncs returns custom fill functions needed for v1beta1
// types where the default random filling would create objects that
// cannot round-trip due to structural differences between versions.
func v1beta1FillFuncs(codecs serializer.CodecFactory) []interface{} {
return []interface{}{
// v1 -> v1beta1 conversion always creates a non-nil Basic,
// so we must ensure it is non-nil in the source object too.
func(d *resourcev1beta1.Device, c randfill.Continue) {
c.FillNoCustom(d)
if d.Basic == nil {
d.Basic = &resourcev1beta1.BasicDevice{}
}
},
}
}
// TestConversionRoundTrip verifies that the automatically generated and
// hand-written conversion code for the DRA API correctly round-trips
// between v1beta1 and v1. For each non-list type registered in v1beta1,
// a fuzzed object is converted to v1 and back, and the result must be
// equal to the original.
//
// Note that this only covers conversion code which is called
// while converting the top-level API types. Types embedded
// inside those have their own conversion functions, but those
// are not necessarily called.
func TestConversionRoundTrip(t *testing.T) {
scheme := runtime.NewScheme()
if err := resourceapi.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := resourcev1beta1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := AddToScheme(scheme); err != nil {
t.Fatal(err)
}
filler := apitest.NewFiller(t, scheme, fuzzer.FuzzerFuncs(v1beta1FillFuncs))
apitest.ConversionRoundTrip(t, scheme, filler,
schema.GroupVersion{Group: resourcev1beta1.GroupName, Version: "v1beta1"},
schema.GroupVersion{Group: resourceapi.GroupName, Version: "v1"},
)
}

View File

@@ -25,6 +25,8 @@ import (
resourceapi "k8s.io/api/resource/v1"
resourcev1beta2 "k8s.io/api/resource/v1beta2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
apitest "k8s.io/dynamic-resource-allocation/api/internal/test"
)
func TestConversion(t *testing.T) {
@@ -335,3 +337,33 @@ func TestConversion(t *testing.T) {
}
}
// TestConversionRoundTrip verifies that the automatically generated and
// hand-written conversion code for the DRA API correctly round-trips
// between v1beta2 and v1. For each non-list type registered in v1beta2,
// a fuzzed object is converted to v1 and back, and the result must be
// equal to the original.
//
// Note that this only covers conversion code which is called
// while converting the top-level API types. Types embedded
// inside those have their own conversion functions, but those
// are not necessarily called.
func TestConversionRoundTrip(t *testing.T) {
scheme := runtime.NewScheme()
if err := resourceapi.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := resourcev1beta2.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := AddToScheme(scheme); err != nil {
t.Fatal(err)
}
filler := apitest.NewFiller(t, scheme, nil)
apitest.ConversionRoundTrip(t, scheme, filler,
schema.GroupVersion{Group: resourcev1beta2.GroupName, Version: "v1beta2"},
schema.GroupVersion{Group: resourceapi.GroupName, Version: "v1"},
)
}

View File

@@ -23,6 +23,7 @@ require (
k8s.io/klog/v2 v2.130.1
k8s.io/kubelet v0.0.0
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/randfill v1.0.0
)
require (
@@ -78,7 +79,6 @@ require (
k8s.io/component-base v0.0.0 // indirect
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)