Merge pull request #134302 from lalitc375/device-class-selectors

feat(resource): Add validation for DeviceClass selectors and configs
This commit is contained in:
Kubernetes Prow Robot
2025-10-02 18:30:56 -07:00
committed by GitHub
15 changed files with 460 additions and 55 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,91 @@ 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
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1.DeviceClassConfiguration) (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("config"), obj.Config, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClassSpec) []resourcev1.DeviceClassConfiguration { return oldObj.Config }))...)
// 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,95 @@ 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
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceClassConfiguration) (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("config"), obj.Config, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClassSpec) []resourcev1beta1.DeviceClassConfiguration {
return oldObj.Config
}))...)
// 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,95 @@ 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
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta2.DeviceClassConfiguration) (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("config"), obj.Config, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClassSpec) []resourcev1beta2.DeviceClassConfiguration {
return oldObj.Config
}))...)
// 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,8 +544,9 @@ 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.
stored = false
if oldSpec != nil {
stored = apiequality.Semantic.DeepEqual(spec.Config, oldSpec.Config)
}
@@ -553,7 +554,7 @@ func validateDeviceClassSpec(spec, oldSpec *resource.DeviceClassSpec, fldPath *f
func(config resource.DeviceClassConfiguration, fldPath *field.Path) field.ErrorList {
return validateDeviceClassConfiguration(config, fldPath, stored)
},
fldPath.Child("config"))...)
fldPath.Child("config"), sizeCovered)...)
if spec.ExtendedResourceName != nil && !v1helper.IsExtendedResourceName(corev1.ResourceName(*spec.ExtendedResourceName)) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("extendedResourceName"), *spec.ExtendedResourceName,
"must be a valid extended resource name"))

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)
@@ -313,7 +313,7 @@ func TestValidateClass(t *testing.T) {
},
"too-many-configs": {
wantFailures: field.ErrorList{
field.TooMany(field.NewPath("spec", "config"), resource.DeviceConfigMaxSize+1, resource.DeviceConfigMaxSize),
field.TooMany(field.NewPath("spec", "config"), resource.DeviceConfigMaxSize+1, resource.DeviceConfigMaxSize).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,26 @@ func TestDeclarativeValidate(t *testing.T) {
"valid": {
input: mkDeviceClass(),
},
// 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"),
},
},
// spec.config
"too many configs": {
input: mkDeviceClass(tweakConfig(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "config"), 33, 32).WithOrigin("maxItems"),
},
},
"valid: at limit configs": {
input: mkDeviceClass(tweakConfig(32)),
},
// TODO: Add more test cases
}
@@ -80,6 +101,28 @@ func TestDeclarativeValidateUpdate(t *testing.T) {
old: mkDeviceClass(),
update: mkDeviceClass(),
},
"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"),
},
},
"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"),
},
},
// TODO: Add more test cases
}
@@ -127,3 +170,35 @@ func mkDeviceClass(mutators ...func(*resource.DeviceClass)) resource.DeviceClass
}
return dc
}
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)),
},
},
},
})
}
}
}

View File

@@ -84,8 +84,7 @@ func (deviceClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.
newClass := obj.(*resource.DeviceClass)
oldClass := old.(*resource.DeviceClass)
errorList := validation.ValidateDeviceClass(newClass)
errorList = append(errorList, validation.ValidateDeviceClassUpdate(newClass, oldClass)...)
errorList := validation.ValidateDeviceClassUpdate(newClass, oldClass)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newClass, oldClass, errorList, operation.Update)
}

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.
@@ -652,6 +654,8 @@ message DeviceClassSpec {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
repeated DeviceClassConfiguration config = 2;
// ExtendedResourceName is the extended resource name for the devices of 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.
@@ -1682,6 +1684,8 @@ type DeviceClassSpec struct {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
Config []DeviceClassConfiguration `json:"config,omitempty" protobuf:"bytes,2,opt,name=config"`
// SuitableNodes is tombstoned since Kubernetes 1.32 where

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.
@@ -660,6 +662,8 @@ message DeviceClassSpec {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
repeated DeviceClassConfiguration config = 2;
// ExtendedResourceName is the extended resource name for the devices of 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.
@@ -1690,6 +1692,8 @@ type DeviceClassSpec struct {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
Config []DeviceClassConfiguration `json:"config,omitempty" protobuf:"bytes,2,opt,name=config"`
// SuitableNodes is tombstoned since Kubernetes 1.32 where

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.
@@ -652,6 +654,8 @@ message DeviceClassSpec {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
repeated DeviceClassConfiguration config = 2;
// ExtendedResourceName is the extended resource name for the devices of 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.
@@ -1682,6 +1684,8 @@ type DeviceClassSpec struct {
//
// +optional
// +listType=atomic
// +k8s:optional
// +k8s:maxItems=32
Config []DeviceClassConfiguration `json:"config,omitempty" protobuf:"bytes,2,opt,name=config"`
// SuitableNodes is tombstoned since Kubernetes 1.32 where

View File

@@ -76,62 +76,57 @@ func TestMaxLength(t *testing.T) {
func TestMaxItems(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
name string
items int
max int
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := make([]string, 0)
max := 0
return MaxItems(context.Background(), op, fp, value, nil, max)
},
name: "0 items, max 0",
items: 0,
max: 0,
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := make([]string, 1)
max := 0
return MaxItems(context.Background(), op, fp, value, nil, max)
},
err: "fldpath: Too many.*must have at most",
name: "1 item, max 0",
items: 1,
max: 0,
err: "fldpath: Too many.*must have at most",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := make([]int, 1)
max := 1
return MaxItems(context.Background(), op, fp, value, nil, max)
},
name: "1 item, max 1",
items: 1,
max: 1,
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := make([]int, 2)
max := 1
return MaxItems(context.Background(), op, fp, value, nil, max)
},
err: "fldpath: Too many.*must have at most",
name: "2 items, max 1",
items: 2,
max: 1,
err: "fldpath: Too many.*must have at most",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := make([]bool, 0)
max := -1
return MaxItems(context.Background(), op, fp, value, nil, max)
},
err: "fldpath: Too many.*too many items",
name: "0 items, max -1",
items: 0,
max: -1,
err: "fldpath: Too many.*too many items",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
value := make([]bool, tc.items)
result := MaxItems(context.Background(), operation.Operation{}, field.NewPath("fldpath"), value, nil, tc.max)
if len(result) > 0 && tc.err == "" {
t.Errorf("unexpected failure: %v", fmtErrs(result))
return
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
if len(result) == 0 && tc.err != "" {
t.Errorf("unexpected success: expected %q", tc.err)
return
}
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("unexepected multi-error: %v", fmtErrs(result))
return
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("wrong error\nexpected: %q\n got: %v", tc.err, fmtErrs(result))
}
}
})
}
}

View File

@@ -83,10 +83,10 @@ func (mitv maxItemsTagValidator) Docs() TagDoc {
return TagDoc{
Tag: mitv.TagName(),
Scopes: mitv.ValidScopes().UnsortedList(),
Description: "Indicates that a list field has a limit on its size.",
Description: "Indicates that a list has a limit on its size.",
Payloads: []TagPayloadDoc{{
Description: "<non-negative integer>",
Docs: "This field must be no more than X items long.",
Docs: "This list must be no more than X items long.",
}},
PayloadsType: codetags.ValueTypeInt,
PayloadsRequired: true,