diff --git a/pkg/registry/resource/deviceclass/declarative_validation_test.go b/pkg/registry/resource/deviceclass/declarative_validation_test.go new file mode 100644 index 00000000000..8b6f0202f73 --- /dev/null +++ b/pkg/registry/resource/deviceclass/declarative_validation_test.go @@ -0,0 +1,129 @@ +/* +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 deviceclass + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + apitesting "k8s.io/kubernetes/pkg/api/testing" + "k8s.io/kubernetes/pkg/apis/resource" + _ "k8s.io/kubernetes/pkg/apis/resource/install" +) + +var apiVersions = []string{"v1", "v1beta1", "v1beta2"} + +func TestDeclarativeValidate(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIGroup: "resource.k8s.io", + APIVersion: apiVersion, + Resource: "deviceclasses", + }) + + strategy := Strategy + + testCases := map[string]struct { + input resource.DeviceClass + expectedErrs field.ErrorList + }{ + "valid": { + input: mkDeviceClass(), + }, + // TODO: Add more test cases + } + + for k, tc := range testCases { + t.Run(k, func(t *testing.T) { + apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, strategy.Validate, tc.expectedErrs) + }) + } + }) + } +} + +func TestDeclarativeValidateUpdate(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIGroup: "resource.k8s.io", + APIVersion: apiVersion, + Resource: "deviceclasses", + }) + + strategy := Strategy + + testCases := map[string]struct { + old resource.DeviceClass + update resource.DeviceClass + expectedErrs field.ErrorList + }{ + "valid no changes": { + old: mkDeviceClass(), + update: mkDeviceClass(), + }, + // TODO: Add more test cases + } + + for k, tc := range testCases { + t.Run(k, func(t *testing.T) { + tc.old.ResourceVersion = "1" + tc.update.ResourceVersion = "1" + apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, strategy.ValidateUpdate, tc.expectedErrs) + }) + } + }) + } +} + +// Helper function to create a DeviceClass with default values and optional mutators +func mkDeviceClass(mutators ...func(*resource.DeviceClass)) resource.DeviceClass { + dc := resource.DeviceClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-class", + }, + Spec: resource.DeviceClassSpec{ + Selectors: []resource.DeviceSelector{ + { + CEL: &resource.CELDeviceSelector{ + Expression: "device.driver == \"test.driver.io\"", + }, + }, + }, + Config: []resource.DeviceClassConfiguration{ + { + DeviceConfiguration: resource.DeviceConfiguration{ + Opaque: &resource.OpaqueDeviceConfiguration{ + Driver: "test.driver.io", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"key":"value"}`), + }, + }, + }, + }, + }, + }, + } + for _, mutate := range mutators { + mutate(&dc) + } + return dc +} diff --git a/pkg/registry/resource/deviceclass/strategy.go b/pkg/registry/resource/deviceclass/strategy.go index d682ef28ebb..ba32cb199fd 100644 --- a/pkg/registry/resource/deviceclass/strategy.go +++ b/pkg/registry/resource/deviceclass/strategy.go @@ -22,6 +22,7 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/storage/names" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" @@ -50,7 +51,18 @@ func (deviceClassStrategy) PrepareForCreate(ctx context.Context, obj runtime.Obj func (deviceClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { deviceClass := obj.(*resource.DeviceClass) - return validation.ValidateDeviceClass(deviceClass) + errorList := validation.ValidateDeviceClass(deviceClass) + + if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + declarativeErrs := rest.ValidateDeclaratively(ctx, legacyscheme.Scheme, deviceClass, rest.WithTakeover(takeover)) + validationIdentifier := "deviceclass_create" + rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errorList, declarativeErrs, takeover, validationIdentifier) + if takeover { + errorList = append(errorList.RemoveCoveredByDeclarative(), declarativeErrs...) + } + } + return errorList } func (deviceClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -77,8 +89,22 @@ func (deviceClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runtim } func (deviceClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - errorList := validation.ValidateDeviceClass(obj.(*resource.DeviceClass)) - return append(errorList, validation.ValidateDeviceClassUpdate(obj.(*resource.DeviceClass), old.(*resource.DeviceClass))...) + newClass := obj.(*resource.DeviceClass) + oldClass := old.(*resource.DeviceClass) + + errorList := validation.ValidateDeviceClass(newClass) + errorList = append(errorList, validation.ValidateDeviceClassUpdate(newClass, oldClass)...) + + if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClass, oldClass, rest.WithTakeover(takeover)) + validationIdentifier := "deviceclass_update" + rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errorList, declarativeErrs, takeover, validationIdentifier) + if takeover { + errorList = append(errorList.RemoveCoveredByDeclarative(), declarativeErrs...) + } + } + return errorList } func (deviceClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {