From e693da729066f1f773973f02a2da4f9475562aab Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Fri, 26 Sep 2025 23:08:17 +0000 Subject: [PATCH 1/4] 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. --- .../resource/v1/zz_generated.validations.go | 84 ++++++++++++++++++ .../v1beta1/zz_generated.validations.go | 86 +++++++++++++++++++ .../v1beta2/zz_generated.validations.go | 86 +++++++++++++++++++ pkg/apis/resource/validation/validation.go | 4 +- .../validation/validation_deviceclass_test.go | 2 +- .../declarative_validation_test.go | 28 +++++- .../k8s.io/api/resource/v1/generated.proto | 2 + staging/src/k8s.io/api/resource/v1/types.go | 2 + .../api/resource/v1beta1/generated.proto | 2 + .../src/k8s.io/api/resource/v1beta1/types.go | 2 + .../api/resource/v1beta2/generated.proto | 2 + .../src/k8s.io/api/resource/v1beta2/types.go | 2 + 12 files changed, 298 insertions(+), 4 deletions(-) diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go index 053c2ddb815..be7cef47a32 100644 --- a/pkg/apis/resource/v1/zz_generated.validations.go +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -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) { diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go index dc47632c819..677a671a965 100644 --- a/pkg/apis/resource/v1beta1/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -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) { diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go index df1d7265d92..dee0e59dfac 100644 --- a/pkg/apis/resource/v1beta2/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -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) { diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index 31ee383698c..8a0020cf4fa 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -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) } diff --git a/pkg/apis/resource/validation/validation_deviceclass_test.go b/pkg/apis/resource/validation/validation_deviceclass_test.go index 7ccb8f634e6..99effb6663e 100644 --- a/pkg/apis/resource/validation/validation_deviceclass_test.go +++ b/pkg/apis/resource/validation/validation_deviceclass_test.go @@ -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) diff --git a/pkg/registry/resource/deviceclass/declarative_validation_test.go b/pkg/registry/resource/deviceclass/declarative_validation_test.go index 8b6f0202f73..364eedc01e2 100644 --- a/pkg/registry/resource/deviceclass/declarative_validation_test.go +++ b/pkg/registry/resource/deviceclass/declarative_validation_test.go @@ -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), + }, + }) + } + } +} diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto index 7c3998d895e..40a8816de87 100644 --- a/staging/src/k8s.io/api/resource/v1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1/generated.proto @@ -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. diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index 5b47a7ffaed..0261bf012fc 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -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. diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto index cc9469bb646..4ecd7724c2d 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto @@ -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. diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index ee8e1d99bac..470693d30d3 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -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. diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto index 076a43c93ac..a9d036cf9b9 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto @@ -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. diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index 1ade5acf1b4..2e91e9ea8b0 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -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. From 515b579945ab54ef4e008a9fbe6d3bb19123594d Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Fri, 26 Sep 2025 23:57:08 +0000 Subject: [PATCH 2/4] feat(resource): Add maxItems validation for DeviceClass configs --- .../resource/v1/zz_generated.validations.go | 19 +++++++++++- .../v1beta1/zz_generated.validations.go | 21 ++++++++++++- .../v1beta2/zz_generated.validations.go | 21 ++++++++++++- pkg/apis/resource/validation/validation.go | 3 +- .../validation/validation_deviceclass_test.go | 2 +- .../declarative_validation_test.go | 31 +++++++++++++++++++ .../k8s.io/api/resource/v1/generated.proto | 2 ++ staging/src/k8s.io/api/resource/v1/types.go | 2 ++ .../api/resource/v1beta1/generated.proto | 2 ++ .../src/k8s.io/api/resource/v1beta1/types.go | 2 ++ .../api/resource/v1beta2/generated.proto | 2 ++ .../src/k8s.io/api/resource/v1beta2/types.go | 2 ++ 12 files changed, 104 insertions(+), 5 deletions(-) diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go index be7cef47a32..eed7dc77203 100644 --- a/pkg/apis/resource/v1/zz_generated.validations.go +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -245,7 +245,24 @@ func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPa 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.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 } diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go index 677a671a965..4755383a69c 100644 --- a/pkg/apis/resource/v1beta1/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -253,7 +253,26 @@ func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPa return oldObj.Selectors }))...) - // field resourcev1beta1.DeviceClassSpec.Config has no validation + // 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 } diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go index dee0e59dfac..d8796033e4f 100644 --- a/pkg/apis/resource/v1beta2/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -253,7 +253,26 @@ func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPa return oldObj.Selectors }))...) - // field resourcev1beta2.DeviceClassSpec.Config has no validation + // 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 } diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index 8a0020cf4fa..5583a2a53cc 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -546,6 +546,7 @@ func validateDeviceClassSpec(spec, oldSpec *resource.DeviceClassSpec, fldPath *f }, 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")) diff --git a/pkg/apis/resource/validation/validation_deviceclass_test.go b/pkg/apis/resource/validation/validation_deviceclass_test.go index 99effb6663e..0cfa8b0189e 100644 --- a/pkg/apis/resource/validation/validation_deviceclass_test.go +++ b/pkg/apis/resource/validation/validation_deviceclass_test.go @@ -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) diff --git a/pkg/registry/resource/deviceclass/declarative_validation_test.go b/pkg/registry/resource/deviceclass/declarative_validation_test.go index 364eedc01e2..7b2a86a9b01 100644 --- a/pkg/registry/resource/deviceclass/declarative_validation_test.go +++ b/pkg/registry/resource/deviceclass/declarative_validation_test.go @@ -55,6 +55,12 @@ func TestDeclarativeValidate(t *testing.T) { field.TooMany(field.NewPath("spec", "selectors"), 33, 32).WithOrigin("maxItems"), }, }, + "too many configs": { + input: mkDeviceClass(tweakConfig(33)), + expectedErrs: field.ErrorList{ + field.TooMany(field.NewPath("spec", "config"), 33, 32).WithOrigin("maxItems"), + }, + }, // TODO: Add more test cases } @@ -94,6 +100,14 @@ func TestDeclarativeValidateUpdate(t *testing.T) { field.TooMany(field.NewPath("spec", "selectors"), 33, 32).WithOrigin("maxItems"), }, }, + + "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 } @@ -153,3 +167,20 @@ func tweakSelectors(count int) func(*resource.DeviceClass) { } } } + +func tweakConfig(count int) func(*resource.DeviceClass) { + return func(dc *resource.DeviceClass) { + 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)), + }, + }, + }, + }) + } + } +} diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto index 40a8816de87..642bbfc2d0d 100644 --- a/staging/src/k8s.io/api/resource/v1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1/generated.proto @@ -654,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. diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index 0261bf012fc..d39a9e0a00a 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -1684,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 diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto index 4ecd7724c2d..367ea268bbf 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto @@ -662,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. diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index 470693d30d3..f0a4767ad93 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -1692,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 diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto index a9d036cf9b9..d386bca1873 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto @@ -654,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. diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index 2e91e9ea8b0..d511bbe9e2f 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -1684,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 From c4c7efc4f95b3d9e482f364396f26117ccce4f8c Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Wed, 1 Oct 2025 23:42:05 +0000 Subject: [PATCH 3/4] apply feedback --- .../declarative_validation_test.go | 22 ++++- .../pkg/api/validate/limits_test.go | 89 +++++++++---------- .../cmd/validation-gen/validators/limits.go | 4 +- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/pkg/registry/resource/deviceclass/declarative_validation_test.go b/pkg/registry/resource/deviceclass/declarative_validation_test.go index 7b2a86a9b01..0676e875c81 100644 --- a/pkg/registry/resource/deviceclass/declarative_validation_test.go +++ b/pkg/registry/resource/deviceclass/declarative_validation_test.go @@ -49,18 +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 } @@ -93,6 +101,10 @@ 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)), @@ -100,7 +112,10 @@ func TestDeclarativeValidateUpdate(t *testing.T) { 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)), @@ -130,7 +145,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\"", }, @@ -158,6 +173,8 @@ func mkDeviceClass(mutators ...func(*resource.DeviceClass)) resource.DeviceClass 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{ @@ -170,6 +187,7 @@ func tweakSelectors(count int) func(*resource.DeviceClass) { 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{ diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go index c1039c4ecde..f17937c6913 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -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)) + } + } + }) } } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go index 5a972843385..4992e79400e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -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: "", - 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, From f79fb9e59e0e90d7ecfedbe083b8d6816944ef16 Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Thu, 2 Oct 2025 17:23:25 +0000 Subject: [PATCH 4/4] Fix rebase issues --- pkg/apis/resource/validation/validation.go | 2 +- pkg/registry/resource/deviceclass/strategy.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index 5583a2a53cc..f072a41a1ad 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -1129,7 +1129,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().WithOrigin("maxItems") + err = err.MarkCoveredByDeclarative() } allErrs = append(allErrs, err) } diff --git a/pkg/registry/resource/deviceclass/strategy.go b/pkg/registry/resource/deviceclass/strategy.go index 8abe8d061c8..3854b37d081 100644 --- a/pkg/registry/resource/deviceclass/strategy.go +++ b/pkg/registry/resource/deviceclass/strategy.go @@ -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) }