From 8f0a6583ca81a64475e69a8b6b4d06bbe0c2ac67 Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Fri, 24 Oct 2025 00:35:10 +0000 Subject: [PATCH] Use eachKey DV in DRA resources. --- .../resource/v1/zz_generated.validations.go | 86 +++++++++++++++++- .../v1beta1/zz_generated.validations.go | 90 ++++++++++++++++++- .../v1beta2/zz_generated.validations.go | 90 ++++++++++++++++++- pkg/apis/resource/validation/validation.go | 15 +++- .../validation_resourceslice_test.go | 20 ++--- .../declarative_validation_test.go | 64 +++++++++++++ .../k8s.io/api/resource/v1/generated.proto | 4 + staging/src/k8s.io/api/resource/v1/types.go | 4 + .../api/resource/v1beta1/generated.proto | 4 + .../src/k8s.io/api/resource/v1beta1/types.go | 4 + .../api/resource/v1beta2/generated.proto | 4 + .../src/k8s.io/api/resource/v1beta2/types.go | 4 + .../cmd/validation-gen/validators/each.go | 2 +- 13 files changed, 370 insertions(+), 21 deletions(-) diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go index 7cd96d63b21..fb5f8b860dd 100644 --- a/pkg/apis/resource/v1/zz_generated.validations.go +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -193,6 +193,34 @@ func Validate_AllocationResult(ctx context.Context, op operation.Operation, fldP return errs } +// Validate_CounterSet validates an instance of CounterSet according +// to declarative validation rules in the API schema. +func Validate_CounterSet(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.CounterSet) (errs field.ErrorList) { + // field resourcev1.CounterSet.Name has no validation + + // field resourcev1.CounterSet.Counters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]resourcev1.Counter) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName)...) + return + }(fldPath.Child("counters"), obj.Counters, safe.Field(oldObj, func(oldObj *resourcev1.CounterSet) map[string]resourcev1.Counter { return oldObj.Counters }))...) + + return errs +} + // Validate_Device validates an instance of Device according // to declarative validation rules in the API schema. func Validate_Device(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.Device) (errs field.ErrorList) { @@ -213,7 +241,19 @@ func Validate_Device(ctx context.Context, op operation.Operation, fldPath *field }))...) // field resourcev1.Device.Capacity has no validation - // field resourcev1.Device.ConsumesCounters has no validation + + // field resourcev1.Device.ConsumesCounters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1.DeviceCounterConsumption) (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_DeviceCounterConsumption)...) + return + }(fldPath.Child("consumesCounters"), obj.ConsumesCounters, safe.Field(oldObj, func(oldObj *resourcev1.Device) []resourcev1.DeviceCounterConsumption { return oldObj.ConsumesCounters }))...) + // field resourcev1.Device.NodeName has no validation // field resourcev1.Device.NodeSelector has no validation // field resourcev1.Device.AllNodes has no validation @@ -811,6 +851,36 @@ func Validate_DeviceConstraint(ctx context.Context, op operation.Operation, fldP return errs } +// Validate_DeviceCounterConsumption validates an instance of DeviceCounterConsumption according +// to declarative validation rules in the API schema. +func Validate_DeviceCounterConsumption(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceCounterConsumption) (errs field.ErrorList) { + // field resourcev1.DeviceCounterConsumption.CounterSet has no validation + + // field resourcev1.DeviceCounterConsumption.Counters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]resourcev1.Counter) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName)...) + return + }(fldPath.Child("counters"), obj.Counters, safe.Field(oldObj, func(oldObj *resourcev1.DeviceCounterConsumption) map[string]resourcev1.Counter { + return oldObj.Counters + }))...) + + return errs +} + // Validate_DeviceRequest validates an instance of DeviceRequest according // to declarative validation rules in the API schema. func Validate_DeviceRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceRequest) (errs field.ErrorList) { @@ -1615,6 +1685,18 @@ func Validate_ResourceSliceSpec(ctx context.Context, op operation.Operation, fld }(fldPath.Child("devices"), obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1.ResourceSliceSpec) []resourcev1.Device { return oldObj.Devices }))...) // field resourcev1.ResourceSliceSpec.PerDeviceNodeSelection has no validation - // field resourcev1.ResourceSliceSpec.SharedCounters has no validation + + // field resourcev1.ResourceSliceSpec.SharedCounters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1.CounterSet) (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_CounterSet)...) + return + }(fldPath.Child("sharedCounters"), obj.SharedCounters, safe.Field(oldObj, func(oldObj *resourcev1.ResourceSliceSpec) []resourcev1.CounterSet { return oldObj.SharedCounters }))...) + return errs } diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go index 368d8956807..bbf65dbb704 100644 --- a/pkg/apis/resource/v1beta1/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -213,7 +213,21 @@ func Validate_BasicDevice(ctx context.Context, op operation.Operation, fldPath * }))...) // field resourcev1beta1.BasicDevice.Capacity has no validation - // field resourcev1beta1.BasicDevice.ConsumesCounters has no validation + + // field resourcev1beta1.BasicDevice.ConsumesCounters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceCounterConsumption) (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_DeviceCounterConsumption)...) + return + }(fldPath.Child("consumesCounters"), obj.ConsumesCounters, safe.Field(oldObj, func(oldObj *resourcev1beta1.BasicDevice) []resourcev1beta1.DeviceCounterConsumption { + return oldObj.ConsumesCounters + }))...) + // field resourcev1beta1.BasicDevice.NodeName has no validation // field resourcev1beta1.BasicDevice.NodeSelector has no validation // field resourcev1beta1.BasicDevice.AllNodes has no validation @@ -280,6 +294,34 @@ func Validate_BasicDevice(ctx context.Context, op operation.Operation, fldPath * return errs } +// Validate_CounterSet validates an instance of CounterSet according +// to declarative validation rules in the API schema. +func Validate_CounterSet(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.CounterSet) (errs field.ErrorList) { + // field resourcev1beta1.CounterSet.Name has no validation + + // field resourcev1beta1.CounterSet.Counters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]resourcev1beta1.Counter) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName)...) + return + }(fldPath.Child("counters"), obj.Counters, safe.Field(oldObj, func(oldObj *resourcev1beta1.CounterSet) map[string]resourcev1beta1.Counter { return oldObj.Counters }))...) + + return errs +} + // Validate_Device validates an instance of Device according // to declarative validation rules in the API schema. func Validate_Device(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.Device) (errs field.ErrorList) { @@ -839,6 +881,36 @@ func Validate_DeviceConstraint(ctx context.Context, op operation.Operation, fldP return errs } +// Validate_DeviceCounterConsumption validates an instance of DeviceCounterConsumption according +// to declarative validation rules in the API schema. +func Validate_DeviceCounterConsumption(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceCounterConsumption) (errs field.ErrorList) { + // field resourcev1beta1.DeviceCounterConsumption.CounterSet has no validation + + // field resourcev1beta1.DeviceCounterConsumption.Counters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]resourcev1beta1.Counter) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName)...) + return + }(fldPath.Child("counters"), obj.Counters, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceCounterConsumption) map[string]resourcev1beta1.Counter { + return oldObj.Counters + }))...) + + return errs +} + // Validate_DeviceRequest validates an instance of DeviceRequest according // to declarative validation rules in the API schema. func Validate_DeviceRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceRequest) (errs field.ErrorList) { @@ -1647,6 +1719,20 @@ func Validate_ResourceSliceSpec(ctx context.Context, op operation.Operation, fld }(fldPath.Child("devices"), obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceSliceSpec) []resourcev1beta1.Device { return oldObj.Devices }))...) // field resourcev1beta1.ResourceSliceSpec.PerDeviceNodeSelection has no validation - // field resourcev1beta1.ResourceSliceSpec.SharedCounters has no validation + + // field resourcev1beta1.ResourceSliceSpec.SharedCounters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta1.CounterSet) (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_CounterSet)...) + return + }(fldPath.Child("sharedCounters"), obj.SharedCounters, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceSliceSpec) []resourcev1beta1.CounterSet { + return oldObj.SharedCounters + }))...) + return errs } diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go index edea193b6d2..518a606e8d4 100644 --- a/pkg/apis/resource/v1beta2/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -195,6 +195,34 @@ func Validate_AllocationResult(ctx context.Context, op operation.Operation, fldP return errs } +// Validate_CounterSet validates an instance of CounterSet according +// to declarative validation rules in the API schema. +func Validate_CounterSet(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.CounterSet) (errs field.ErrorList) { + // field resourcev1beta2.CounterSet.Name has no validation + + // field resourcev1beta2.CounterSet.Counters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]resourcev1beta2.Counter) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName)...) + return + }(fldPath.Child("counters"), obj.Counters, safe.Field(oldObj, func(oldObj *resourcev1beta2.CounterSet) map[string]resourcev1beta2.Counter { return oldObj.Counters }))...) + + return errs +} + // Validate_Device validates an instance of Device according // to declarative validation rules in the API schema. func Validate_Device(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.Device) (errs field.ErrorList) { @@ -215,7 +243,21 @@ func Validate_Device(ctx context.Context, op operation.Operation, fldPath *field }))...) // field resourcev1beta2.Device.Capacity has no validation - // field resourcev1beta2.Device.ConsumesCounters has no validation + + // field resourcev1beta2.Device.ConsumesCounters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta2.DeviceCounterConsumption) (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_DeviceCounterConsumption)...) + return + }(fldPath.Child("consumesCounters"), obj.ConsumesCounters, safe.Field(oldObj, func(oldObj *resourcev1beta2.Device) []resourcev1beta2.DeviceCounterConsumption { + return oldObj.ConsumesCounters + }))...) + // field resourcev1beta2.Device.NodeName has no validation // field resourcev1beta2.Device.NodeSelector has no validation // field resourcev1beta2.Device.AllNodes has no validation @@ -821,6 +863,36 @@ func Validate_DeviceConstraint(ctx context.Context, op operation.Operation, fldP return errs } +// Validate_DeviceCounterConsumption validates an instance of DeviceCounterConsumption according +// to declarative validation rules in the API schema. +func Validate_DeviceCounterConsumption(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceCounterConsumption) (errs field.ErrorList) { + // field resourcev1beta2.DeviceCounterConsumption.CounterSet has no validation + + // field resourcev1beta2.DeviceCounterConsumption.Counters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]resourcev1beta2.Counter) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName)...) + return + }(fldPath.Child("counters"), obj.Counters, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceCounterConsumption) map[string]resourcev1beta2.Counter { + return oldObj.Counters + }))...) + + return errs +} + // Validate_DeviceRequest validates an instance of DeviceRequest according // to declarative validation rules in the API schema. func Validate_DeviceRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceRequest) (errs field.ErrorList) { @@ -1649,6 +1721,20 @@ func Validate_ResourceSliceSpec(ctx context.Context, op operation.Operation, fld }(fldPath.Child("devices"), obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceSliceSpec) []resourcev1beta2.Device { return oldObj.Devices }))...) // field resourcev1beta2.ResourceSliceSpec.PerDeviceNodeSelection has no validation - // field resourcev1beta2.ResourceSliceSpec.SharedCounters has no validation + + // field resourcev1beta2.ResourceSliceSpec.SharedCounters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta2.CounterSet) (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_CounterSet)...) + return + }(fldPath.Child("sharedCounters"), obj.SharedCounters, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceSliceSpec) []resourcev1beta2.CounterSet { + return oldObj.SharedCounters + }))...) + return errs } diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index d573961b8f3..e5e8ea36159 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -751,7 +751,7 @@ func validateCounterSet(counterSet resource.CounterSet, fldPath *field.Path) fie } else { // The size limit is enforced for across all sets by the caller. allErrs = append(allErrs, validateMap(counterSet.Counters, -1, validation.DNS1123LabelMaxLength, - validateCounterName, validateDeviceCounter, fldPath.Child("counters"))...) + validateCounterName, validateDeviceCounter, fldPath.Child("counters"), keysCovered)...) } return allErrs @@ -876,7 +876,7 @@ func validateDeviceCounterConsumption(deviceCounterConsumption resource.DeviceCo } else { // The size limit is enforced for the entire device. allErrs = append(allErrs, validateMap(deviceCounterConsumption.Counters, -1, validation.DNS1123LabelMaxLength, - validateCounterName, validateDeviceCounter, fldPath.Child("counters"))...) + validateCounterName, validateDeviceCounter, fldPath.Child("counters"), keysCovered)...) } return allErrs } @@ -1142,6 +1142,8 @@ const ( sizeCovered // The uniqueness check is covered by declarative validation. uniquenessCovered + // key validation is covered by declarative validation. + keysCovered ) // validateSlice ensures that a slice does not exceed a certain maximum size @@ -1210,14 +1212,19 @@ func quantityKey(item apiresource.Quantity) string { // small limit gets increased because it is okay to include more details. // This is not used for validation of keys, which has to be done by // the callback function. -func validateMap[K ~string, T any](m map[K]T, maxSize, truncateKeyLen int, validateKey func(K, *field.Path) field.ErrorList, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path) field.ErrorList { +func validateMap[K ~string, T any](m map[K]T, maxSize, truncateKeyLen int, validateKey func(K, *field.Path) field.ErrorList, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path, opts ...validationOption) field.ErrorList { var allErrs field.ErrorList if maxSize >= 0 && len(m) > maxSize { allErrs = append(allErrs, field.TooMany(fldPath, len(m), maxSize)) } for key, item := range m { keyPath := fldPath.Key(truncateIfTooLong(string(key), truncateKeyLen)) - allErrs = append(allErrs, validateKey(key, keyPath)...) + + keyValidationErrors := validateKey(key, fldPath) + if slices.Contains(opts, keysCovered) { + keyValidationErrors = keyValidationErrors.MarkCoveredByDeclarative() + } + allErrs = append(allErrs, keyValidationErrors...) allErrs = append(allErrs, validateItem(item, keyPath)...) } return allErrs diff --git a/pkg/apis/resource/validation/validation_resourceslice_test.go b/pkg/apis/resource/validation/validation_resourceslice_test.go index d113d68aad4..885d8e02b19 100644 --- a/pkg/apis/resource/validation/validation_resourceslice_test.go +++ b/pkg/apis/resource/validation/validation_resourceslice_test.go @@ -335,7 +335,7 @@ func TestValidateResourceSlice(t *testing.T) { }, "bad-attribute": { wantFailures: field.ErrorList{ - field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(badName), badName, "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"), + field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes"), badName, "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"), field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(badName), "", "exactly one value must be specified").MarkCoveredByDeclarative(), field.Invalid(field.NewPath("spec", "devices").Index(2).Child("attributes").Key(goodName), resourceapi.DeviceAttribute{StringValue: ptr.To("x"), VersionValue: ptr.To("1.2.3")}, "exactly one value must be specified").MarkCoveredByDeclarative(), field.Invalid(field.NewPath("spec", "devices").Index(3).Child("attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), "must be a string compatible with semver.org spec 2.0.0"), @@ -371,8 +371,8 @@ func TestValidateResourceSlice(t *testing.T) { }, "bad-attribute-c-identifier": { wantFailures: field.ErrorList{ - field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength), - field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"), + field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("attributes"), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength), + field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes"), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"), }, slice: func() *resourceapi.ResourceSlice { slice := testResourceSlice(goodName, goodName, goodName, 2) @@ -384,8 +384,8 @@ func TestValidateResourceSlice(t *testing.T) { }, "bad-attribute-domain": { wantFailures: field.ErrorList{ - field.TooLong(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength), - field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"), + field.TooLong(field.NewPath("spec", "devices").Index(1).Child("attributes"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength), + field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"), }, slice: func() *resourceapi.ResourceSlice { slice := testResourceSlice(goodName, goodName, goodName, 2) @@ -397,8 +397,8 @@ func TestValidateResourceSlice(t *testing.T) { }, "bad-key-too-long": { wantFailures: field.ErrorList{ - field.TooLong(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength), - field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("y", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength), + field.TooLong(field.NewPath("spec", "devices").Index(1).Child("attributes"), strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength), + field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("attributes"), strings.Repeat("y", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength), }, slice: func() *resourceapi.ResourceSlice { slice := testResourceSlice(goodName, goodName, goodName, 2) @@ -410,8 +410,8 @@ func TestValidateResourceSlice(t *testing.T) { }, "bad-attribute-empty-domain-and-c-identifier": { wantFailures: field.ErrorList{ - field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("/"), "the domain must not be empty"), - field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("/"), "the name must not be empty"), + field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes"), "the domain must not be empty"), + field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes"), "the name must not be empty"), }, slice: func() *resourceapi.ResourceSlice { slice := testResourceSlice(goodName, goodName, goodName, 2) @@ -663,7 +663,7 @@ func TestValidateResourceSlice(t *testing.T) { }, "bad-countername-shared-counters": { wantFailures: field.ErrorList{ - field.Invalid(field.NewPath("spec", "sharedCounters").Index(0).Child("counters").Key(badName), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"), + field.Invalid(field.NewPath("spec", "sharedCounters").Index(0).Child("counters"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')").MarkCoveredByDeclarative(), }, slice: func() *resourceapi.ResourceSlice { slice := testResourceSlice(goodName, goodName, driverName, 1) diff --git a/pkg/registry/resource/resourceslice/declarative_validation_test.go b/pkg/registry/resource/resourceslice/declarative_validation_test.go index 44aeba1803f..5d05f74c25f 100644 --- a/pkg/registry/resource/resourceslice/declarative_validation_test.go +++ b/pkg/registry/resource/resourceslice/declarative_validation_test.go @@ -122,6 +122,27 @@ func TestDeclarativeValidate(t *testing.T) { field.Invalid(field.NewPath("spec", "devices").Index(0).Child("attributes").Key("test.io/multiple"), "", ""), }, }, + // spec.sharedCounters.counters + "invalid: shared counter key with uppercase": { + input: mkResourceSlice(tweakSharedCounter("InvalidKey")), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("spec", "sharedCounters").Index(0).Child("counters"), "InvalidKey", "").WithOrigin("format=k8s-short-name"), + }, + }, + "valid: shared counter key": { + input: mkResourceSlice(tweakSharedCounter("valid-key")), + }, + // spec.devices.consumesCounters.counters + "invalid: device counter key with uppercase": { + input: mkResourceSlice(tweakSharedCounter("InvalidKey"), tweakDeviceCounter("InvalidKey")), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("spec", "sharedCounters").Index(0).Child("counters"), "InvalidKey", "").WithOrigin("format=k8s-short-name"), + field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counters"), "InvalidKey", "").WithOrigin("format=k8s-short-name"), + }, + }, + "valid: device counter key": { + input: mkResourceSlice(tweakSharedCounter("valid-key"), tweakDeviceCounter("valid-key")), + }, // TODO: Add more test cases } @@ -212,6 +233,23 @@ func TestDeclarativeValidateUpdate(t *testing.T) { field.Invalid(field.NewPath("spec", "devices").Index(0).Child("attributes").Key("test.io/multiple"), "", "may have only one of the following fields set: bool, int, string, version"), }, }, + // spec.sharedCounters.counters + "invalid update: shared counter key with uppercase": { + old: mkResourceSlice(), + update: mkResourceSlice(tweakSharedCounter("InvalidKey")), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("spec", "sharedCounters").Index(0).Child("counters"), "InvalidKey", "").WithOrigin("format=k8s-short-name"), + }, + }, + // spec.devices.consumesCounters.counters + "invalid update: device counter key with uppercase": { + old: mkResourceSlice(), + update: mkResourceSlice(tweakSharedCounter("InvalidKey"), tweakDeviceCounter("InvalidKey")), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("spec", "sharedCounters").Index(0).Child("counters"), "InvalidKey", "").WithOrigin("format=k8s-short-name"), + field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counters"), "InvalidKey", "").WithOrigin("format=k8s-short-name"), + }, + }, } for k, tc := range testCases { t.Run(k, func(t *testing.T) { @@ -294,3 +332,29 @@ func tweakDeviceAttribute(name resource.QualifiedName, value resource.DeviceAttr rs.Spec.Devices[0].Attributes[name] = value } } + +func tweakSharedCounter(key string) func(*resource.ResourceSlice) { + return func(rs *resource.ResourceSlice) { + rs.Spec.SharedCounters = []resource.CounterSet{ + { + Name: "shared-counter-set", + Counters: map[string]resource.Counter{ + key: {}, + }, + }, + } + } +} + +func tweakDeviceCounter(key string) func(*resource.ResourceSlice) { + return func(rs *resource.ResourceSlice) { + rs.Spec.Devices[0].ConsumesCounters = []resource.DeviceCounterConsumption{ + { + CounterSet: "shared-counter-set", + Counters: map[string]resource.Counter{ + key: {}, + }, + }, + } + } +} diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto index c1e7da7c4c4..ecf11dd45bc 100644 --- a/staging/src/k8s.io/api/resource/v1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1/generated.proto @@ -315,6 +315,8 @@ message CounterSet { // The maximum number of counters in all sets is 32. // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name map counters = 2; } @@ -779,6 +781,8 @@ message DeviceCounterConsumption { // 16 counters each). // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name map counters = 2; } diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index 5483c13a1cf..8de3a3033ff 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -202,6 +202,8 @@ type CounterSet struct { // The maximum number of counters in all sets is 32. // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,name=counters"` } @@ -417,6 +419,8 @@ type DeviceCounterConsumption struct { // 16 counters each). // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"` } diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto index 0f4428154d9..a2c68d5beb0 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto @@ -451,6 +451,8 @@ message CounterSet { // The maximum number of counters is 32. // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name map counters = 2; } @@ -787,6 +789,8 @@ message DeviceCounterConsumption { // 16 counters each). // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name map counters = 2; } diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index 5fb5c6c75db..52227808abe 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -202,6 +202,8 @@ type CounterSet struct { // The maximum number of counters is 32. // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,name=counters"` } @@ -429,6 +431,8 @@ type DeviceCounterConsumption struct { // 16 counters each). // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"` } diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto index a60cb6d02ab..8ae2bc7210d 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto @@ -315,6 +315,8 @@ message CounterSet { // The maximum number of counters in all sets is 32. // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name map counters = 2; } @@ -779,6 +781,8 @@ message DeviceCounterConsumption { // 16 counters each). // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name map counters = 2; } diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index b71d39d4900..69eb365f3e2 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -202,6 +202,8 @@ type CounterSet struct { // The maximum number of counters in all sets is 32. // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,name=counters"` } @@ -417,6 +419,8 @@ type DeviceCounterConsumption struct { // 16 counters each). // // +required + // +k8s:required + // +k8s:eachKey=+k8s:format=k8s-short-name Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"` } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go index 4961f46d26d..bbdc08204ca 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go @@ -269,7 +269,7 @@ func (ektv eachKeyTagValidator) GetValidations(context Context, tag codetags.Tag elemContext := Context{ Scope: ScopeMapKey, - Type: nt.Elem, + Type: nt.Key, Path: context.Path.Key("(keys)"), Member: nil, // NA for map keys ParentPath: context.Path,