From 910a4ca69690a7ddc048183c37f3041e5b4cd16b Mon Sep 17 00:00:00 2001 From: yykkibbb Date: Mon, 2 Mar 2026 22:58:12 +0900 Subject: [PATCH 1/3] Fix missing field conversions in v1beta1 DRA API Fix missing fields in the v1beta1 hand-written conversion code: - Device: BindsToNode, BindingConditions, BindingFailureConditions, AllowMultipleAllocations were not copied when converting between v1beta1.BasicDevice (nested) and v1.Device (flat). - DeviceRequest: Capacity was not copied between v1beta1.DeviceRequest and v1.ExactDeviceRequest, and was missing from hasAnyMainRequestFieldsSet. These fields are for alpha features added after v1beta1, so while worth fixing, the impact in practice is limited. --- .../api/v1beta1/conversion.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion.go b/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion.go index ad85840fc2b..2289c7993ae 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion.go @@ -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 } From affb0927bdc6fca1b74e0a0362cba1184869566f Mon Sep 17 00:00:00 2001 From: yykkibbb Date: Mon, 2 Mar 2026 22:58:12 +0900 Subject: [PATCH 2/3] Add fuzz roundtrip test for DRA API conversion Add automated fuzz testing of conversion round-tripping for the DRA API. The test iterates over all non-list types registered in v1beta1 and v1beta2, fuzzes each object, converts it to v1 and back, and verifies the result matches the original using apiequality.Semantic.DeepEqual. Test structure: - Helper code in api/internal/test/roundtrip.go - TestConversionRoundTrip in each version's conversion_test.go - This ensures coverage statistics are visible per-package --- .../api/internal/test/roundtrip.go | 113 ++++++++++++++++++ .../api/v1beta1/conversion_test.go | 51 ++++++++ .../api/v1beta2/conversion_test.go | 32 +++++ .../k8s.io/dynamic-resource-allocation/go.mod | 2 +- 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go diff --git a/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go b/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go new file mode 100644 index 00000000000..4ba0b1e2ae9 --- /dev/null +++ b/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go @@ -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 = 20 + +// 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) +} diff --git a/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion_test.go b/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion_test.go index 85a53bef08a..b5fcfcc01c3 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion_test.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta1/conversion_test.go @@ -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"}, + ) +} diff --git a/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta2/conversion_test.go b/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta2/conversion_test.go index 480476948e6..ea899182619 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta2/conversion_test.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/api/v1beta2/conversion_test.go @@ -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"}, + ) +} diff --git a/staging/src/k8s.io/dynamic-resource-allocation/go.mod b/staging/src/k8s.io/dynamic-resource-allocation/go.mod index 43e0235eaf9..2ba332ddcd6 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/go.mod +++ b/staging/src/k8s.io/dynamic-resource-allocation/go.mod @@ -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 ) From 84b451715897972d28c66d89a10ce4d602ff0805 Mon Sep 17 00:00:00 2001 From: yykkibbb Date: Mon, 2 Mar 2026 22:58:12 +0900 Subject: [PATCH 3/3] Add fuzz roundtrip test for internal DRA API conversion Add conversion roundtrip testing for v1 -> api -> v1 to verify the internal API types (which use UniqueString for efficiency) correctly round-trip with the external v1 API. Increase FuzzIterations to 100 for better test coverage. --- .../api/conversion_test.go | 119 ++++++++++++++++++ .../api/internal/test/roundtrip.go | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/dynamic-resource-allocation/api/conversion_test.go diff --git a/staging/src/k8s.io/dynamic-resource-allocation/api/conversion_test.go b/staging/src/k8s.io/dynamic-resource-allocation/api/conversion_test.go new file mode 100644 index 00000000000..1311081f382 --- /dev/null +++ b/staging/src/k8s.io/dynamic-resource-allocation/api/conversion_test.go @@ -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)) + } + } + }) + } +} diff --git a/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go b/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go index 4ba0b1e2ae9..bceeeb66d9a 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/api/internal/test/roundtrip.go @@ -34,7 +34,7 @@ import ( ) // FuzzIterations is the number of fuzz iterations to run for each type. -const FuzzIterations = 20 +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 {