mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-05-28 04:04:39 -04:00
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.
375 lines
13 KiB
Go
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
|
|
}
|
|
}
|