feat(resource): Add maxItems validation for DeviceClass selectors

This commit introduces a validation rule to limit the number of selectors
in a DeviceClass to 32. This is done by adding the `+k8s:maxItems=32`
marker to the DeviceClassSpec and running the code generator.

The following changes are included:
- Updated `staging/src/k8s.io/api/resource/v*/types.go` with the validation marker.
- Regenerated validation code in `pkg/apis/resource/v*/zz_generated.validations.go`.
- Improved error message in `pkg/apis/resource/validation/validation.go`.
- Added tests in `pkg/registry/resource/deviceclass/declarative_validation_test.go`
  to cover the new validation.
This commit is contained in:
Lalit Chauhan
2025-09-26 23:08:17 +00:00
parent 48390fb98e
commit e693da7290
12 changed files with 298 additions and 4 deletions

View File

@@ -39,6 +39,22 @@ func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *runtime.Scheme) error {
// type DeviceClass
scheme.AddValidationFunc((*resourcev1.DeviceClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_DeviceClass(ctx, op, nil /* fldPath */, obj.(*resourcev1.DeviceClass), safe.Cast[*resourcev1.DeviceClass](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type DeviceClassList
scheme.AddValidationFunc((*resourcev1.DeviceClassList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_DeviceClassList(ctx, op, nil /* fldPath */, obj.(*resourcev1.DeviceClassList), safe.Cast[*resourcev1.DeviceClassList](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type ResourceClaim
scheme.AddValidationFunc((*resourcev1.ResourceClaim)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
@@ -166,6 +182,74 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
return errs
}
// Validate_DeviceClass validates an instance of DeviceClass according
// to declarative validation rules in the API schema.
func Validate_DeviceClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceClass) (errs field.ErrorList) {
// field resourcev1.DeviceClass.TypeMeta has no validation
// field resourcev1.DeviceClass.ObjectMeta has no validation
// field resourcev1.DeviceClass.Spec
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *resourcev1.DeviceClassSpec) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call the type's validation function
errs = append(errs, Validate_DeviceClassSpec(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClass) *resourcev1.DeviceClassSpec { return &oldObj.Spec }))...)
return errs
}
// Validate_DeviceClassList validates an instance of DeviceClassList according
// to declarative validation rules in the API schema.
func Validate_DeviceClassList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceClassList) (errs field.ErrorList) {
// field resourcev1.DeviceClassList.TypeMeta has no validation
// field resourcev1.DeviceClassList.ListMeta has no validation
// field resourcev1.DeviceClassList.Items
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1.DeviceClass) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceClass)...)
return
}(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClassList) []resourcev1.DeviceClass { return oldObj.Items }))...)
return errs
}
// Validate_DeviceClassSpec validates an instance of DeviceClassSpec according
// to declarative validation rules in the API schema.
func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceClassSpec) (errs field.ErrorList) {
// field resourcev1.DeviceClassSpec.Selectors
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1.DeviceSelector) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClassSpec) []resourcev1.DeviceSelector { return oldObj.Selectors }))...)
// field resourcev1.DeviceClassSpec.Config has no validation
// field resourcev1.DeviceClassSpec.ExtendedResourceName has no validation
return errs
}
// Validate_DeviceRequestAllocationResult validates an instance of DeviceRequestAllocationResult according
// to declarative validation rules in the API schema.
func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceRequestAllocationResult) (errs field.ErrorList) {

View File

@@ -39,6 +39,22 @@ func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *runtime.Scheme) error {
// type DeviceClass
scheme.AddValidationFunc((*resourcev1beta1.DeviceClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_DeviceClass(ctx, op, nil /* fldPath */, obj.(*resourcev1beta1.DeviceClass), safe.Cast[*resourcev1beta1.DeviceClass](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type DeviceClassList
scheme.AddValidationFunc((*resourcev1beta1.DeviceClassList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_DeviceClassList(ctx, op, nil /* fldPath */, obj.(*resourcev1beta1.DeviceClassList), safe.Cast[*resourcev1beta1.DeviceClassList](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type ResourceClaim
scheme.AddValidationFunc((*resourcev1beta1.ResourceClaim)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
@@ -172,6 +188,76 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
return errs
}
// Validate_DeviceClass validates an instance of DeviceClass according
// to declarative validation rules in the API schema.
func Validate_DeviceClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceClass) (errs field.ErrorList) {
// field resourcev1beta1.DeviceClass.TypeMeta has no validation
// field resourcev1beta1.DeviceClass.ObjectMeta has no validation
// field resourcev1beta1.DeviceClass.Spec
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceClassSpec) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call the type's validation function
errs = append(errs, Validate_DeviceClassSpec(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClass) *resourcev1beta1.DeviceClassSpec { return &oldObj.Spec }))...)
return errs
}
// Validate_DeviceClassList validates an instance of DeviceClassList according
// to declarative validation rules in the API schema.
func Validate_DeviceClassList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceClassList) (errs field.ErrorList) {
// field resourcev1beta1.DeviceClassList.TypeMeta has no validation
// field resourcev1beta1.DeviceClassList.ListMeta has no validation
// field resourcev1beta1.DeviceClassList.Items
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceClass) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceClass)...)
return
}(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClassList) []resourcev1beta1.DeviceClass { return oldObj.Items }))...)
return errs
}
// Validate_DeviceClassSpec validates an instance of DeviceClassSpec according
// to declarative validation rules in the API schema.
func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceClassSpec) (errs field.ErrorList) {
// field resourcev1beta1.DeviceClassSpec.Selectors
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceSelector) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClassSpec) []resourcev1beta1.DeviceSelector {
return oldObj.Selectors
}))...)
// field resourcev1beta1.DeviceClassSpec.Config has no validation
// field resourcev1beta1.DeviceClassSpec.ExtendedResourceName has no validation
return errs
}
// Validate_DeviceRequestAllocationResult validates an instance of DeviceRequestAllocationResult according
// to declarative validation rules in the API schema.
func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceRequestAllocationResult) (errs field.ErrorList) {

View File

@@ -39,6 +39,22 @@ func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *runtime.Scheme) error {
// type DeviceClass
scheme.AddValidationFunc((*resourcev1beta2.DeviceClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_DeviceClass(ctx, op, nil /* fldPath */, obj.(*resourcev1beta2.DeviceClass), safe.Cast[*resourcev1beta2.DeviceClass](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type DeviceClassList
scheme.AddValidationFunc((*resourcev1beta2.DeviceClassList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_DeviceClassList(ctx, op, nil /* fldPath */, obj.(*resourcev1beta2.DeviceClassList), safe.Cast[*resourcev1beta2.DeviceClassList](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type ResourceClaim
scheme.AddValidationFunc((*resourcev1beta2.ResourceClaim)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
@@ -172,6 +188,76 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
return errs
}
// Validate_DeviceClass validates an instance of DeviceClass according
// to declarative validation rules in the API schema.
func Validate_DeviceClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceClass) (errs field.ErrorList) {
// field resourcev1beta2.DeviceClass.TypeMeta has no validation
// field resourcev1beta2.DeviceClass.ObjectMeta has no validation
// field resourcev1beta2.DeviceClass.Spec
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceClassSpec) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call the type's validation function
errs = append(errs, Validate_DeviceClassSpec(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClass) *resourcev1beta2.DeviceClassSpec { return &oldObj.Spec }))...)
return errs
}
// Validate_DeviceClassList validates an instance of DeviceClassList according
// to declarative validation rules in the API schema.
func Validate_DeviceClassList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceClassList) (errs field.ErrorList) {
// field resourcev1beta2.DeviceClassList.TypeMeta has no validation
// field resourcev1beta2.DeviceClassList.ListMeta has no validation
// field resourcev1beta2.DeviceClassList.Items
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta2.DeviceClass) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceClass)...)
return
}(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClassList) []resourcev1beta2.DeviceClass { return oldObj.Items }))...)
return errs
}
// Validate_DeviceClassSpec validates an instance of DeviceClassSpec according
// to declarative validation rules in the API schema.
func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceClassSpec) (errs field.ErrorList) {
// field resourcev1beta2.DeviceClassSpec.Selectors
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta2.DeviceSelector) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClassSpec) []resourcev1beta2.DeviceSelector {
return oldObj.Selectors
}))...)
// field resourcev1beta2.DeviceClassSpec.Config has no validation
// field resourcev1beta2.DeviceClassSpec.ExtendedResourceName has no validation
return errs
}
// Validate_DeviceRequestAllocationResult validates an instance of DeviceRequestAllocationResult according
// to declarative validation rules in the API schema.
func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceRequestAllocationResult) (errs field.ErrorList) {

View File

@@ -544,7 +544,7 @@ func validateDeviceClassSpec(spec, oldSpec *resource.DeviceClassSpec, fldPath *f
func(selector resource.DeviceSelector, fldPath *field.Path) field.ErrorList {
return validateSelector(selector, fldPath, stored)
},
fldPath.Child("selectors"))...)
fldPath.Child("selectors"), sizeCovered)...)
// Same logic as above for configs.
if oldSpec != nil {
stored = apiequality.Semantic.DeepEqual(spec.Config, oldSpec.Config)
@@ -1128,7 +1128,7 @@ func validateSlice[T any](slice []T, maxSize int, validateItem func(T, *field.Pa
// just shows the number of entries.
err := field.TooMany(fldPath, len(slice), maxSize).WithOrigin("maxItems")
if slices.Contains(opts, sizeCovered) {
err = err.MarkCoveredByDeclarative()
err = err.MarkCoveredByDeclarative().WithOrigin("maxItems")
}
allErrs = append(allErrs, err)
}

View File

@@ -219,7 +219,7 @@ func TestValidateClass(t *testing.T) {
},
"too-many-selectors": {
wantFailures: field.ErrorList{
field.TooMany(field.NewPath("spec", "selectors"), resource.DeviceSelectorsMaxSize+1, resource.DeviceSelectorsMaxSize),
field.TooMany(field.NewPath("spec", "selectors"), resource.DeviceSelectorsMaxSize+1, resource.DeviceSelectorsMaxSize).WithOrigin("maxItems").MarkCoveredByDeclarative(),
},
class: func() *resource.DeviceClass {
class := testClass(goodName)

View File

@@ -17,6 +17,7 @@ limitations under the License.
package deviceclass
import (
"fmt"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -48,6 +49,12 @@ func TestDeclarativeValidate(t *testing.T) {
"valid": {
input: mkDeviceClass(),
},
"too many selectors": {
input: mkDeviceClass(tweakSelectors(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "selectors"), 33, 32).WithOrigin("maxItems"),
},
},
// TODO: Add more test cases
}
@@ -80,6 +87,13 @@ func TestDeclarativeValidateUpdate(t *testing.T) {
old: mkDeviceClass(),
update: mkDeviceClass(),
},
"update with too many selectors": {
old: mkDeviceClass(),
update: mkDeviceClass(tweakSelectors(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "selectors"), 33, 32).WithOrigin("maxItems"),
},
},
// TODO: Add more test cases
}
@@ -102,7 +116,7 @@ func mkDeviceClass(mutators ...func(*resource.DeviceClass)) resource.DeviceClass
},
Spec: resource.DeviceClassSpec{
Selectors: []resource.DeviceSelector{
{
resource.DeviceSelector{
CEL: &resource.CELDeviceSelector{
Expression: "device.driver == \"test.driver.io\"",
},
@@ -127,3 +141,15 @@ func mkDeviceClass(mutators ...func(*resource.DeviceClass)) resource.DeviceClass
}
return dc
}
func tweakSelectors(count int) func(*resource.DeviceClass) {
return func(dc *resource.DeviceClass) {
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),
},
})
}
}
}

View File

@@ -642,6 +642,8 @@ message DeviceClassSpec {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
repeated DeviceSelector selectors = 1;
// Config defines configuration parameters that apply to each device that is claimed via this class.

View File

@@ -1672,6 +1672,8 @@ type DeviceClassSpec struct {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,1,opt,name=selectors"`
// Config defines configuration parameters that apply to each device that is claimed via this class.

View File

@@ -650,6 +650,8 @@ message DeviceClassSpec {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
repeated DeviceSelector selectors = 1;
// Config defines configuration parameters that apply to each device that is claimed via this class.

View File

@@ -1680,6 +1680,8 @@ type DeviceClassSpec struct {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,1,opt,name=selectors"`
// Config defines configuration parameters that apply to each device that is claimed via this class.

View File

@@ -642,6 +642,8 @@ message DeviceClassSpec {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
repeated DeviceSelector selectors = 1;
// Config defines configuration parameters that apply to each device that is claimed via this class.

View File

@@ -1672,6 +1672,8 @@ type DeviceClassSpec struct {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,1,opt,name=selectors"`
// Config defines configuration parameters that apply to each device that is claimed via this class.