kubernetes/test/declarative_validation/resource/deviceclass/declarative_validation_test.go
Yongrui Lin a791288d81 test/declarative_validation: migrate DV equivalence tests to new tree
Move the existing declarative_validation_test.go files out of
pkg/registry/ into a top-level tree at
test/declarative_validation/<group>/<kind>/. The new location pairs
each hand-written test with the per-Kind TestMain and version-init
files emitted by validation-gen, so the coverage gate runs alongside
the equivalence checks and apiVersions no longer needs to be
hand-maintained.
2026-05-09 19:11:01 +00:00

375 lines
13 KiB
Go

/*
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 (
"fmt"
"strings"
"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"
registry "k8s.io/kubernetes/pkg/registry/resource/deviceclass"
"k8s.io/utils/ptr"
)
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 := registry.Strategy
testCases := map[string]struct {
input resource.DeviceClass
expectedErrs field.ErrorList
}{
"valid": {
input: mkDeviceClass(),
},
// metadata.name
"name: empty": {
input: mkDeviceClass(tweakName("")),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("metadata", "name"), "").MarkFromImperative(),
},
},
"name: valid": {
input: mkDeviceClass(tweakName("example.com")),
},
"name: invalid (uppercase)": {
input: mkDeviceClass(tweakName("Invalid-Name")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("metadata", "name"), "Invalid-Name", "").WithOrigin("format=k8s-long-name").MarkAlpha(),
},
},
"name: invalid (start with dash)": {
input: mkDeviceClass(tweakName("-invalid")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("metadata", "name"), "-invalid", "").WithOrigin("format=k8s-long-name").MarkAlpha(),
},
},
"name: max length": {
input: mkDeviceClass(tweakName(strings.Repeat("a", 253))),
},
"name: too long": {
input: mkDeviceClass(tweakName(strings.Repeat("a", 254))),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("metadata", "name"), strings.Repeat("a", 254), "").WithOrigin("format=k8s-long-name").MarkAlpha(),
},
},
// spec.selectors.
"valid: at limit selectors": {
input: mkDeviceClass(tweakSelectors(32)),
},
"too many selectors": {
input: mkDeviceClass(tweakSelectors(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "selectors"), 33, 32).WithOrigin("maxItems").MarkAlpha(),
},
},
// spec.config
"too many configs": {
input: mkDeviceClass(tweakConfig(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "config"), 33, 32).WithOrigin("maxItems").MarkAlpha(),
},
},
"valid: at limit configs": {
input: mkDeviceClass(tweakConfig(32)),
},
// spec.config[*].opaque.driver
"valid opaque driver, lowercase": {
input: mkDeviceClass(tweakConfigOpaqueDriver("dra.example.com")),
},
"valid opaque driver, max length": {
input: mkDeviceClass(tweakConfigOpaqueDriver(strings.Repeat("a", 63))),
},
"invalid opaque driver, empty": {
input: mkDeviceClass(tweakConfigOpaqueDriver("")),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("spec", "config").Index(0).Child("opaque", "driver"), "").MarkAlpha(),
},
},
"invalid opaque driver, too long": {
input: mkDeviceClass(tweakConfigOpaqueDriver(strings.Repeat("a", 64))),
expectedErrs: field.ErrorList{
field.TooLong(field.NewPath("spec", "config").Index(0).Child("opaque", "driver"), "", 63).WithOrigin("maxLength").MarkAlpha(),
},
},
"invalid opaque driver, invalid character": {
input: mkDeviceClass(tweakConfigOpaqueDriver("dra_example.com")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "config").Index(0).Child("opaque", "driver"), "dra_example.com", "").WithOrigin("format=k8s-long-name-caseless").MarkAlpha(),
},
},
// ExtendedResourceName
"valid extended resource name": {
input: mkDeviceClass(tweakExtendedResourceName("example.com/my-resource")),
},
"invalid extended resource name": {
input: mkDeviceClass(tweakExtendedResourceName("invalid_name")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "invalid_name", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name, no slash": {
input: mkDeviceClass(tweakExtendedResourceName("noslash")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "noslash", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name, kubernetes.io domain": {
input: mkDeviceClass(tweakExtendedResourceName("kubernetes.io/foo")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "kubernetes.io/foo", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name, requests. prefix": {
input: mkDeviceClass(tweakExtendedResourceName("requests.example.com/foo")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "requests.example.com/foo", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name, too long": {
input: mkDeviceClass(tweakExtendedResourceName("example.com/" + strings.Repeat("a", 64))),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "example.com/"+strings.Repeat("a", 64), "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
// TODO: Add more test cases
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, strategy, 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 := registry.Strategy
testCases := map[string]struct {
old resource.DeviceClass
update resource.DeviceClass
expectedErrs field.ErrorList
}{
"valid no changes": {
old: mkDeviceClass(),
update: mkDeviceClass(),
},
// metadata.name
"name: changed": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakName("test-classx")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("metadata", "name"), "test-classx", "field is immutable").MarkFromImperative(),
},
},
"valid update: at limit selectors": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakSelectors(32)),
},
"update with too many selectors": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakSelectors(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "selectors"), 33, 32).WithOrigin("maxItems").MarkAlpha(),
},
},
"valid update: at limit configs": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakConfig(32)),
},
"update with too many configs": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakConfig(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "config"), 33, 32).WithOrigin("maxItems").MarkAlpha(),
},
},
// spec.ExtendedResourceName
"valid extended resource name update": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakExtendedResourceName("example.com/my-resource")),
},
"invalid extended resource name update": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakExtendedResourceName("invalid_name")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "invalid_name", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name update, no slash": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakExtendedResourceName("noslash")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "noslash", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name update, kubernetes.io domain": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakExtendedResourceName("kubernetes.io/foo")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "kubernetes.io/foo", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name update, requests. prefix": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakExtendedResourceName("requests.example.com/foo")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "requests.example.com/foo", "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
"invalid extended resource name update, too long": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakExtendedResourceName("example.com/" + strings.Repeat("a", 64))),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "extendedResourceName"), "example.com/"+strings.Repeat("a", 64), "").WithOrigin("format=k8s-extended-resource-name").MarkAlpha(),
},
},
// 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, 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{
ExtendedResourceName: ptr.To("example.com/my-resource"),
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
}
func tweakName(name string) func(*resource.DeviceClass) {
return func(dc *resource.DeviceClass) {
dc.Name = name
}
}
func tweakSelectors(count int) func(*resource.DeviceClass) {
return func(dc *resource.DeviceClass) {
dc.Spec.Selectors = []resource.DeviceSelector{}
for i := 0; i < count; i++ {
dc.Spec.Selectors = append(dc.Spec.Selectors, resource.DeviceSelector{
CEL: &resource.CELDeviceSelector{
Expression: fmt.Sprintf("device.driver == \"test.driver.io%d\"", i),
},
})
}
}
}
func tweakConfig(count int) func(*resource.DeviceClass) {
return func(dc *resource.DeviceClass) {
dc.Spec.Config = []resource.DeviceClassConfiguration{}
for i := 0; i < count; i++ {
dc.Spec.Config = append(dc.Spec.Config, resource.DeviceClassConfiguration{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "test.driver.io",
Parameters: runtime.RawExtension{
Raw: []byte(fmt.Sprintf(`{"key":"value%d"}`, i)),
},
},
},
})
}
}
}
func tweakConfigOpaqueDriver(driver string) func(*resource.DeviceClass) {
return func(dc *resource.DeviceClass) {
dc.Spec.Config = []resource.DeviceClassConfiguration{
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: driver,
Parameters: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)},
},
},
},
}
}
}
func tweakExtendedResourceName(name string) func(*resource.DeviceClass) {
return func(dc *resource.DeviceClass) {
nameCopy := name
dc.Spec.ExtendedResourceName = &nameCopy
}
}