Merge pull request #134408 from yongruilin/vg_resourceclaim

feat(validation-gen): add path normalization options & migration k8s:maxItem on ResourceClaimSpec fields
This commit is contained in:
Kubernetes Prow Robot
2025-10-06 14:53:06 -07:00
committed by GitHub
16 changed files with 709 additions and 28 deletions

View File

@@ -156,6 +156,28 @@ func convertToInternal(t *testing.T, scheme *runtime.Scheme, obj runtime.Object)
return scheme.ConvertToVersion(obj, schema.GroupVersion{Group: gvk.Group, Version: runtime.APIVersionInternal})
}
type ValidationTestConfig func(*validationOption)
// validationOptions encapsulates optional parameters for validation equivalence tests.
type validationOption struct {
// SubResources are the subresources to validate.
SubResources []string
// NormalizationRules are the rules to apply to field paths before comparison.
NormalizationRules []field.NormalizationRule
}
func WithSubResources(subResources ...string) ValidationTestConfig {
return func(o *validationOption) {
o.SubResources = subResources
}
}
func WithNormalizationRules(rules ...field.NormalizationRule) ValidationTestConfig {
return func(o *validationOption) {
o.NormalizationRules = rules
}
}
// VerifyValidationEquivalence provides a helper for testing the migration from
// hand-written imperative validation to declarative validation. It ensures that
// the validation logic remains consistent before and after the feature is enabled.
@@ -169,12 +191,16 @@ func convertToInternal(t *testing.T, scheme *runtime.Scheme, obj runtime.Object)
// guaranteeing a safe migration. It also checks the errors against an expected set.
// It compares errors by field, origin and type; all three should match to be called equivalent.
// It also make sure all versions of the given API returns equivalent errors.
func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime.Object, validateFn ValidateFunc, expectedErrs field.ErrorList, subResources ...string) {
func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime.Object, validateFn ValidateFunc, expectedErrs field.ErrorList, testConfigs ...ValidationTestConfig) {
t.Helper()
opts := &validationOption{}
for _, testcfg := range testConfigs {
testcfg(opts)
}
verifyValidationEquivalence(t, expectedErrs, func() field.ErrorList {
return validateFn(ctx, obj)
})
VerifyVersionedValidationEquivalence(t, obj, nil, subResources...)
}, opts)
VerifyVersionedValidationEquivalence(t, obj, nil, opts.SubResources...)
}
// VerifyUpdateValidationEquivalence provides a helper for testing the migration from
@@ -190,22 +216,26 @@ func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime.
// guaranteeing a safe migration. It also checks the errors against an expected set.
// It compares errors by field, origin and type; all three should match to be called equivalent.
// It also make sure all versions of the given API returns equivalent errors.
func VerifyUpdateValidationEquivalence(t *testing.T, ctx context.Context, obj, old runtime.Object, validateUpdateFn ValidateUpdateFunc, expectedErrs field.ErrorList, subResources ...string) {
func VerifyUpdateValidationEquivalence(t *testing.T, ctx context.Context, obj, old runtime.Object, validateUpdateFn ValidateUpdateFunc, expectedErrs field.ErrorList, testConfigs ...ValidationTestConfig) {
t.Helper()
opts := &validationOption{}
for _, testcfg := range testConfigs {
testcfg(opts)
}
verifyValidationEquivalence(t, expectedErrs, func() field.ErrorList {
return validateUpdateFn(ctx, obj, old)
})
VerifyVersionedValidationEquivalence(t, obj, old, subResources...)
}, opts)
VerifyVersionedValidationEquivalence(t, obj, old, opts.SubResources...)
}
// verifyValidationEquivalence is a generic helper that verifies validation equivalence with and without declarative validation.
func verifyValidationEquivalence(t *testing.T, expectedErrs field.ErrorList, runValidations func() field.ErrorList) {
func verifyValidationEquivalence(t *testing.T, expectedErrs field.ErrorList, runValidations func() field.ErrorList, opt *validationOption) {
t.Helper()
var declarativeTakeoverErrs field.ErrorList
var imperativeErrs field.ErrorList
// The errOutputMatcher is used to verify the output matches the expected errors in test cases.
errOutputMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
errOutputMatcher := field.ErrorMatcher{}.ByType().ByOrigin().ByFieldNormalized(opt.NormalizationRules)
// We only need to test both gate enabled and disabled together, because
// 1) the DeclarativeValidationTakeover won't take effect if DeclarativeValidation is disabled.
@@ -241,7 +271,12 @@ func verifyValidationEquivalence(t *testing.T, expectedErrs field.ErrorList, run
// The equivalenceMatcher is used to verify the output errors from hand-written imperative validation
// are equivalent to the output errors when DeclarativeValidationTakeover is enabled.
equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
equivalenceMatcher := field.ErrorMatcher{}.ByType().ByOrigin()
if len(opt.NormalizationRules) > 0 {
equivalenceMatcher = equivalenceMatcher.ByFieldNormalized(opt.NormalizationRules)
} else {
equivalenceMatcher = equivalenceMatcher.ByField()
}
// The imperative validation may produce duplicate errors, which is not supported by the ErrorMatcher.
// TODO: remove this once ErrorMatcher has been extended to handle this form of deduplication.

View File

@@ -146,6 +146,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceRequest)...)
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClaim) []resourcev1.DeviceRequest { return oldObj.Requests }))...)
@@ -161,6 +163,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceConstraint)...)
return
}(fldPath.Child("constraints"), obj.Constraints, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClaim) []resourcev1.DeviceConstraint { return oldObj.Constraints }))...)
@@ -176,12 +180,36 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceClaimConfiguration)...)
return
}(fldPath.Child("config"), obj.Config, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClaim) []resourcev1.DeviceClaimConfiguration { return oldObj.Config }))...)
return errs
}
// Validate_DeviceClaimConfiguration validates an instance of DeviceClaimConfiguration according
// to declarative validation rules in the API schema.
func Validate_DeviceClaimConfiguration(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceClaimConfiguration) (errs field.ErrorList) {
// field resourcev1.DeviceClaimConfiguration.Requests
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (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
}
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1.DeviceClaimConfiguration) []string { return oldObj.Requests }))...)
// field resourcev1.DeviceClaimConfiguration.DeviceConfiguration has no validation
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) {
@@ -267,6 +295,70 @@ func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPa
return errs
}
// Validate_DeviceConstraint validates an instance of DeviceConstraint according
// to declarative validation rules in the API schema.
func Validate_DeviceConstraint(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceConstraint) (errs field.ErrorList) {
// field resourcev1.DeviceConstraint.Requests
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (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
}
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1.DeviceConstraint) []string { return oldObj.Requests }))...)
// field resourcev1.DeviceConstraint.MatchAttribute has no validation
// field resourcev1.DeviceConstraint.DistinctAttribute has no validation
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) {
// field resourcev1.DeviceRequest.Name has no validation
// field resourcev1.DeviceRequest.Exactly
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *resourcev1.ExactDeviceRequest) (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.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
// call the type's validation function
errs = append(errs, Validate_ExactDeviceRequest(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("exactly"), obj.Exactly, safe.Field(oldObj, func(oldObj *resourcev1.DeviceRequest) *resourcev1.ExactDeviceRequest { return oldObj.Exactly }))...)
// field resourcev1.DeviceRequest.FirstAvailable
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1.DeviceSubRequest) (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, 8); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceSubRequest)...)
return
}(fldPath.Child("firstAvailable"), obj.FirstAvailable, safe.Field(oldObj, func(oldObj *resourcev1.DeviceRequest) []resourcev1.DeviceSubRequest { return oldObj.FirstAvailable }))...)
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) {
@@ -299,6 +391,62 @@ func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Op
return errs
}
// Validate_DeviceSubRequest validates an instance of DeviceSubRequest according
// to declarative validation rules in the API schema.
func Validate_DeviceSubRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceSubRequest) (errs field.ErrorList) {
// field resourcev1.DeviceSubRequest.Name has no validation
// field resourcev1.DeviceSubRequest.DeviceClassName has no validation
// field resourcev1.DeviceSubRequest.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
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1.DeviceSubRequest) []resourcev1.DeviceSelector { return oldObj.Selectors }))...)
// field resourcev1.DeviceSubRequest.AllocationMode has no validation
// field resourcev1.DeviceSubRequest.Count has no validation
// field resourcev1.DeviceSubRequest.Tolerations has no validation
// field resourcev1.DeviceSubRequest.Capacity has no validation
return errs
}
// Validate_ExactDeviceRequest validates an instance of ExactDeviceRequest according
// to declarative validation rules in the API schema.
func Validate_ExactDeviceRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.ExactDeviceRequest) (errs field.ErrorList) {
// field resourcev1.ExactDeviceRequest.DeviceClassName has no validation
// field resourcev1.ExactDeviceRequest.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
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1.ExactDeviceRequest) []resourcev1.DeviceSelector { return oldObj.Selectors }))...)
// field resourcev1.ExactDeviceRequest.AllocationMode has no validation
// field resourcev1.ExactDeviceRequest.Count has no validation
// field resourcev1.ExactDeviceRequest.AdminAccess has no validation
// field resourcev1.ExactDeviceRequest.Tolerations has no validation
// field resourcev1.ExactDeviceRequest.Capacity has no validation
return errs
}
// Validate_ResourceClaim validates an instance of ResourceClaim according
// to declarative validation rules in the API schema.
func Validate_ResourceClaim(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.ResourceClaim) (errs field.ErrorList) {

View File

@@ -148,6 +148,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceRequest)...)
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClaim) []resourcev1beta1.DeviceRequest { return oldObj.Requests }))...)
@@ -163,6 +165,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceConstraint)...)
return
}(fldPath.Child("constraints"), obj.Constraints, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClaim) []resourcev1beta1.DeviceConstraint {
return oldObj.Constraints
@@ -180,6 +184,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceClaimConfiguration)...)
return
}(fldPath.Child("config"), obj.Config, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClaim) []resourcev1beta1.DeviceClaimConfiguration {
return oldObj.Config
@@ -188,6 +194,28 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
return errs
}
// Validate_DeviceClaimConfiguration validates an instance of DeviceClaimConfiguration according
// to declarative validation rules in the API schema.
func Validate_DeviceClaimConfiguration(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceClaimConfiguration) (errs field.ErrorList) {
// field resourcev1beta1.DeviceClaimConfiguration.Requests
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (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
}
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceClaimConfiguration) []string { return oldObj.Requests }))...)
// field resourcev1beta1.DeviceClaimConfiguration.DeviceConfiguration has no validation
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) {
@@ -277,6 +305,78 @@ func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPa
return errs
}
// Validate_DeviceConstraint validates an instance of DeviceConstraint according
// to declarative validation rules in the API schema.
func Validate_DeviceConstraint(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceConstraint) (errs field.ErrorList) {
// field resourcev1beta1.DeviceConstraint.Requests
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (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
}
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceConstraint) []string { return oldObj.Requests }))...)
// field resourcev1beta1.DeviceConstraint.MatchAttribute has no validation
// field resourcev1beta1.DeviceConstraint.DistinctAttribute has no validation
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) {
// field resourcev1beta1.DeviceRequest.Name has no validation
// field resourcev1beta1.DeviceRequest.DeviceClassName has no validation
// field resourcev1beta1.DeviceRequest.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
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceRequest) []resourcev1beta1.DeviceSelector { return oldObj.Selectors }))...)
// field resourcev1beta1.DeviceRequest.AllocationMode has no validation
// field resourcev1beta1.DeviceRequest.Count has no validation
// field resourcev1beta1.DeviceRequest.AdminAccess has no validation
// field resourcev1beta1.DeviceRequest.FirstAvailable
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceSubRequest) (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, 8); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceSubRequest)...)
return
}(fldPath.Child("firstAvailable"), obj.FirstAvailable, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceRequest) []resourcev1beta1.DeviceSubRequest {
return oldObj.FirstAvailable
}))...)
// field resourcev1beta1.DeviceRequest.Tolerations has no validation
// field resourcev1beta1.DeviceRequest.Capacity 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) {
@@ -301,7 +401,24 @@ func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Op
// field resourcev1beta1.DeviceRequestAllocationResult.Device has no validation
// field resourcev1beta1.DeviceRequestAllocationResult.AdminAccess has no validation
// field resourcev1beta1.DeviceRequestAllocationResult.Tolerations has no validation
// field resourcev1beta1.DeviceRequestAllocationResult.Tolerations
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceToleration) (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, 16); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
return
}(fldPath.Child("tolerations"), obj.Tolerations, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceRequestAllocationResult) []resourcev1beta1.DeviceToleration {
return oldObj.Tolerations
}))...)
// field resourcev1beta1.DeviceRequestAllocationResult.BindingConditions has no validation
// field resourcev1beta1.DeviceRequestAllocationResult.BindingFailureConditions has no validation
// field resourcev1beta1.DeviceRequestAllocationResult.ShareID has no validation
@@ -309,6 +426,36 @@ func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Op
return errs
}
// Validate_DeviceSubRequest validates an instance of DeviceSubRequest according
// to declarative validation rules in the API schema.
func Validate_DeviceSubRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceSubRequest) (errs field.ErrorList) {
// field resourcev1beta1.DeviceSubRequest.Name has no validation
// field resourcev1beta1.DeviceSubRequest.DeviceClassName has no validation
// field resourcev1beta1.DeviceSubRequest.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
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceSubRequest) []resourcev1beta1.DeviceSelector {
return oldObj.Selectors
}))...)
// field resourcev1beta1.DeviceSubRequest.AllocationMode has no validation
// field resourcev1beta1.DeviceSubRequest.Count has no validation
// field resourcev1beta1.DeviceSubRequest.Tolerations has no validation
// field resourcev1beta1.DeviceSubRequest.Capacity has no validation
return errs
}
// Validate_ResourceClaim validates an instance of ResourceClaim according
// to declarative validation rules in the API schema.
func Validate_ResourceClaim(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.ResourceClaim) (errs field.ErrorList) {

View File

@@ -148,6 +148,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceRequest)...)
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClaim) []resourcev1beta2.DeviceRequest { return oldObj.Requests }))...)
@@ -163,6 +165,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceConstraint)...)
return
}(fldPath.Child("constraints"), obj.Constraints, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClaim) []resourcev1beta2.DeviceConstraint {
return oldObj.Constraints
@@ -180,6 +184,8 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceClaimConfiguration)...)
return
}(fldPath.Child("config"), obj.Config, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClaim) []resourcev1beta2.DeviceClaimConfiguration {
return oldObj.Config
@@ -188,6 +194,28 @@ func Validate_DeviceClaim(ctx context.Context, op operation.Operation, fldPath *
return errs
}
// Validate_DeviceClaimConfiguration validates an instance of DeviceClaimConfiguration according
// to declarative validation rules in the API schema.
func Validate_DeviceClaimConfiguration(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceClaimConfiguration) (errs field.ErrorList) {
// field resourcev1beta2.DeviceClaimConfiguration.Requests
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (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
}
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceClaimConfiguration) []string { return oldObj.Requests }))...)
// field resourcev1beta2.DeviceClaimConfiguration.DeviceConfiguration has no validation
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) {
@@ -277,6 +305,72 @@ func Validate_DeviceClassSpec(ctx context.Context, op operation.Operation, fldPa
return errs
}
// Validate_DeviceConstraint validates an instance of DeviceConstraint according
// to declarative validation rules in the API schema.
func Validate_DeviceConstraint(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceConstraint) (errs field.ErrorList) {
// field resourcev1beta2.DeviceConstraint.Requests
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (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
}
return
}(fldPath.Child("requests"), obj.Requests, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceConstraint) []string { return oldObj.Requests }))...)
// field resourcev1beta2.DeviceConstraint.MatchAttribute has no validation
// field resourcev1beta2.DeviceConstraint.DistinctAttribute has no validation
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) {
// field resourcev1beta2.DeviceRequest.Name has no validation
// field resourcev1beta2.DeviceRequest.Exactly
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *resourcev1beta2.ExactDeviceRequest) (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.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
// call the type's validation function
errs = append(errs, Validate_ExactDeviceRequest(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("exactly"), obj.Exactly, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceRequest) *resourcev1beta2.ExactDeviceRequest { return oldObj.Exactly }))...)
// field resourcev1beta2.DeviceRequest.FirstAvailable
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []resourcev1beta2.DeviceSubRequest) (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, 8); len(e) != 0 {
errs = append(errs, e...)
return // do not proceed
}
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceSubRequest)...)
return
}(fldPath.Child("firstAvailable"), obj.FirstAvailable, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceRequest) []resourcev1beta2.DeviceSubRequest {
return oldObj.FirstAvailable
}))...)
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) {
@@ -309,6 +403,66 @@ func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Op
return errs
}
// Validate_DeviceSubRequest validates an instance of DeviceSubRequest according
// to declarative validation rules in the API schema.
func Validate_DeviceSubRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceSubRequest) (errs field.ErrorList) {
// field resourcev1beta2.DeviceSubRequest.Name has no validation
// field resourcev1beta2.DeviceSubRequest.DeviceClassName has no validation
// field resourcev1beta2.DeviceSubRequest.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
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceSubRequest) []resourcev1beta2.DeviceSelector {
return oldObj.Selectors
}))...)
// field resourcev1beta2.DeviceSubRequest.AllocationMode has no validation
// field resourcev1beta2.DeviceSubRequest.Count has no validation
// field resourcev1beta2.DeviceSubRequest.Tolerations has no validation
// field resourcev1beta2.DeviceSubRequest.Capacity has no validation
return errs
}
// Validate_ExactDeviceRequest validates an instance of ExactDeviceRequest according
// to declarative validation rules in the API schema.
func Validate_ExactDeviceRequest(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.ExactDeviceRequest) (errs field.ErrorList) {
// field resourcev1beta2.ExactDeviceRequest.DeviceClassName has no validation
// field resourcev1beta2.ExactDeviceRequest.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
}
return
}(fldPath.Child("selectors"), obj.Selectors, safe.Field(oldObj, func(oldObj *resourcev1beta2.ExactDeviceRequest) []resourcev1beta2.DeviceSelector {
return oldObj.Selectors
}))...)
// field resourcev1beta2.ExactDeviceRequest.AllocationMode has no validation
// field resourcev1beta2.ExactDeviceRequest.Count has no validation
// field resourcev1beta2.ExactDeviceRequest.AdminAccess has no validation
// field resourcev1beta2.ExactDeviceRequest.Tolerations has no validation
// field resourcev1beta2.ExactDeviceRequest.Capacity has no validation
return errs
}
// Validate_ResourceClaim validates an instance of ResourceClaim according
// to declarative validation rules in the API schema.
func Validate_ResourceClaim(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.ResourceClaim) (errs field.ErrorList) {

View File

@@ -203,7 +203,7 @@ func validateDeviceRequest(request resource.DeviceRequest, fldPath *field.Path,
func(subRequest resource.DeviceSubRequest) (string, string) {
return subRequest.Name, "name"
},
fldPath.Child("firstAvailable"))...)
fldPath.Child("firstAvailable"), sizeCovered)...)
}
if request.Exactly != nil {
@@ -275,7 +275,7 @@ func validateSelectorSlice(selectors []resource.DeviceSelector, fldPath *field.P
func(selector resource.DeviceSelector, fldPath *field.Path) field.ErrorList {
return validateSelector(selector, fldPath, stored)
},
fldPath)
fldPath, sizeCovered)
}
func validateSelector(selector resource.DeviceSelector, fldPath *field.Path, stored bool) field.ErrorList {
@@ -331,7 +331,7 @@ func validateDeviceConstraint(constraint resource.DeviceConstraint, fldPath *fie
func(name string, fldPath *field.Path) field.ErrorList {
return validateRequestNameRef(name, fldPath, requestNames)
},
stringKey, fldPath.Child("requests"))...)
stringKey, fldPath.Child("requests"), sizeCovered)...)
if constraint.MatchAttribute != nil {
allErrs = append(allErrs, validateFullyQualifiedName(*constraint.MatchAttribute, fldPath.Child("matchAttribute"))...)
} else if constraint.DistinctAttribute != nil {
@@ -349,7 +349,7 @@ func validateDeviceClaimConfiguration(config resource.DeviceClaimConfiguration,
allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize,
func(name string, fldPath *field.Path) field.ErrorList {
return validateRequestNameRef(name, fldPath, requestNames)
}, stringKey, fldPath.Child("requests"))...)
}, stringKey, fldPath.Child("requests"), sizeCovered)...)
allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored)...)
return allErrs
}

View File

@@ -648,7 +648,7 @@ func TestValidateClaim(t *testing.T) {
},
"prioritized-list-too-many-subrequests": {
wantFailures: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices", "requests").Index(0).Child("firstAvailable"), 9, 8),
field.TooMany(field.NewPath("spec", "devices", "requests").Index(0).Child("firstAvailable"), 9, 8).MarkCoveredByDeclarative(),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)

View File

@@ -84,11 +84,58 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
field.TooMany(field.NewPath("spec", "devices", "config"), 33, 32).WithOrigin("maxItems"),
},
},
"invalid firstAvailable, too many": {
input: mkValidResourceClaim(tweakFirstAvailable(9)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices", "requests").Index(0).Child("firstAvailable"), 9, 8).WithOrigin("maxItems"),
},
},
"invalid selectors, too many": {
input: mkValidResourceClaim(tweakExactlySelectors(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices", "requests").Index(0).Child("exactly", "selectors"), 33, 32).WithOrigin("maxItems").MarkCoveredByDeclarative(),
},
},
"invalid subrequest selectors, too many": {
input: mkValidResourceClaim(tweakSubRequestSelectors(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices", "requests").Index(0).Child("firstAvailable").Index(0).Child("selectors"), 33, 32).WithOrigin("maxItems"),
},
},
"invalid constraint requests, too many": {
input: mkValidResourceClaim(tweakConstraintRequests(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices", "requests"), 33, 32).WithOrigin("maxItems"),
field.TooMany(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests"), 33, 32).WithOrigin("maxItems"),
},
},
"invalid config requests, too many": {
input: mkValidResourceClaim(tweakConfigRequests(33)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices", "requests"), 33, 32).WithOrigin("maxItems"),
field.TooMany(field.NewPath("spec", "devices", "config").Index(0).Child("requests"), 33, 32).WithOrigin("maxItems"),
},
},
"valid firstAvailable, max allowed": {
input: mkValidResourceClaim(tweakFirstAvailable(8)),
},
"valid selectors, max allowed": {
input: mkValidResourceClaim(tweakExactlySelectors(32)),
},
"valid subrequest selectors, max allowed": {
input: mkValidResourceClaim(tweakSubRequestSelectors(32)),
},
"valid constraint requests, max allowed": {
input: mkValidResourceClaim(tweakConstraintRequests(32)),
},
"valid config requests, max allowed": {
input: mkValidResourceClaim(tweakConfigRequests(32)),
},
// TODO: Add more test cases
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs)
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs, apitesting.WithNormalizationRules(resourceClaimNormalizationRules...))
})
}
}
@@ -118,6 +165,83 @@ func tweakDevicesRequests(items int) func(*resource.ResourceClaim) {
}
}
func tweakExactlySelectors(items int) func(*resource.ResourceClaim) {
return func(rc *resource.ResourceClaim) {
for i := 0; i < items; i++ {
rc.Spec.Devices.Requests[0].Exactly.Selectors = append(rc.Spec.Devices.Requests[0].Exactly.Selectors,
resource.DeviceSelector{
CEL: &resource.CELDeviceSelector{
Expression: fmt.Sprintf("device.driver == \"test.driver.io%d\"", i),
},
},
)
}
}
}
func tweakSubRequestSelectors(items int) func(*resource.ResourceClaim) {
return func(rc *resource.ResourceClaim) {
rc.Spec.Devices.Requests[0].Exactly = nil
rc.Spec.Devices.Requests[0].FirstAvailable = []resource.DeviceSubRequest{
{
Name: "sub-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
}
for i := 0; i < items; i++ {
rc.Spec.Devices.Requests[0].FirstAvailable[0].Selectors = append(rc.Spec.Devices.Requests[0].FirstAvailable[0].Selectors,
resource.DeviceSelector{
CEL: &resource.CELDeviceSelector{
Expression: fmt.Sprintf("device.driver == \"test.driver.io%d\"", i),
},
},
)
}
}
}
func tweakConstraintRequests(count int) func(*resource.ResourceClaim) {
return func(rc *resource.ResourceClaim) {
tweakDevicesRequests(count)(rc)
if len(rc.Spec.Devices.Constraints) == 0 {
rc.Spec.Devices.Constraints = append(rc.Spec.Devices.Constraints, mkDeviceConstraint())
}
rc.Spec.Devices.Constraints[0].Requests = []string{}
for i := 0; i < count; i++ {
rc.Spec.Devices.Constraints[0].Requests = append(rc.Spec.Devices.Constraints[0].Requests, fmt.Sprintf("req-%d", i))
}
}
}
func tweakConfigRequests(count int) func(*resource.ResourceClaim) {
return func(rc *resource.ResourceClaim) {
tweakDevicesRequests(count)(rc)
if len(rc.Spec.Devices.Config) == 0 {
rc.Spec.Devices.Config = append(rc.Spec.Devices.Config, mkDeviceClaimConfiguration())
}
rc.Spec.Devices.Config[0].Requests = []string{}
for i := 0; i < count; i++ {
rc.Spec.Devices.Config[0].Requests = append(rc.Spec.Devices.Config[0].Requests, fmt.Sprintf("req-%d", i))
}
}
}
func tweakFirstAvailable(items int) func(*resource.ResourceClaim) {
return func(rc *resource.ResourceClaim) {
rc.Spec.Devices.Requests[0].Exactly = nil
for i := 0; i < items; i++ {
rc.Spec.Devices.Requests[0].FirstAvailable = append(rc.Spec.Devices.Requests[0].FirstAvailable,
resource.DeviceSubRequest{
Name: fmt.Sprintf("sub-%d", i),
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
)
}
}
}
func mkDeviceClaimConfiguration() resource.DeviceClaimConfiguration {
return resource.DeviceClaimConfiguration{
Requests: []string{"req-0"},
@@ -181,7 +305,7 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
t.Run(k, func(t *testing.T) {
tc.old.ResourceVersion = "1"
tc.update.ResourceVersion = "2"
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, Strategy.ValidateUpdate, tc.expectedErrs)
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, Strategy.ValidateUpdate, tc.expectedErrs, apitesting.WithNormalizationRules(resourceClaimNormalizationRules...))
})
}
}
@@ -258,7 +382,7 @@ func TestValidateStatusUpdateForDeclarative(t *testing.T) {
t.Run(k, func(t *testing.T) {
tc.old.ObjectMeta.ResourceVersion = "1"
tc.update.ObjectMeta.ResourceVersion = "1"
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, strategy.ValidateUpdate, tc.expectedErrs, "status")
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, strategy.ValidateUpdate, tc.expectedErrs, apitesting.WithSubResources("status"))
})
}
}

View File

@@ -19,6 +19,7 @@ package resourceclaim
import (
"context"
"errors"
"regexp"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
@@ -52,6 +53,15 @@ type resourceclaimStrategy struct {
nsClient v1.NamespaceInterface
}
var resourceClaimNormalizationRules = []field.NormalizationRule{
{
// The "exactly" struct was added in v1beta2. In earlier API
// versions, its fields were directly part of the DeviceRequest.
Regexp: regexp.MustCompile(`spec\.devices\.requests\[(\d+)\]\.selectors`),
Replacement: "spec.devices.requests[$1].exactly.selectors",
},
}
// NewStrategy is the default logic that applies when creating and updating ResourceClaim objects.
func NewStrategy(nsClient v1.NamespaceInterface) *resourceclaimStrategy {
return &resourceclaimStrategy{
@@ -100,7 +110,7 @@ func (s *resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object
allErrs := resourceutils.AuthorizedForAdmin(ctx, claim.Spec.Devices.Requests, claim.Namespace, s.nsClient)
allErrs = append(allErrs, validation.ValidateResourceClaim(claim)...)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, claim, nil, allErrs, operation.Create)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, claim, nil, allErrs, operation.Create, rest.WithNormalizationRules(resourceClaimNormalizationRules))
}
func (*resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
@@ -128,7 +138,7 @@ func (s *resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old run
// AuthorizedForAdmin isn't needed here because the spec is immutable.
errorList := validation.ValidateResourceClaim(newClaim)
errorList = append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newClaim, oldClaim, errorList, operation.Update)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newClaim, oldClaim, errorList, operation.Update, rest.WithNormalizationRules(resourceClaimNormalizationRules))
}
func (*resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {

View File

@@ -592,6 +592,7 @@ message DeviceClaimConfiguration {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated string requests = 1;
optional DeviceConfiguration deviceConfiguration = 2;
@@ -698,6 +699,7 @@ message DeviceConstraint {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated string requests = 1;
// MatchAttribute requires that all devices in question have this
@@ -780,6 +782,7 @@ message DeviceRequest {
//
// +optional
// +oneOf=deviceRequestType
// +k8s:optional
optional ExactDeviceRequest exactly = 2;
// FirstAvailable contains subrequests, of which exactly one will be
@@ -800,6 +803,7 @@ message DeviceRequest {
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
// +k8s:maxItems=8
repeated DeviceSubRequest firstAvailable = 3;
}
@@ -962,6 +966,7 @@ message DeviceSubRequest {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated DeviceSelector selectors = 3;
// AllocationMode and its related fields define how devices are allocated
@@ -1133,6 +1138,7 @@ message ExactDeviceRequest {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated DeviceSelector selectors = 2;
// AllocationMode and its related fields define how devices are allocated

View File

@@ -792,6 +792,7 @@ type DeviceRequest struct {
//
// +optional
// +oneOf=deviceRequestType
// +k8s:optional
Exactly *ExactDeviceRequest `json:"exactly,omitempty" protobuf:"bytes,2,name=exactly"`
// FirstAvailable contains subrequests, of which exactly one will be
@@ -812,6 +813,7 @@ type DeviceRequest struct {
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
// +k8s:maxItems=8
FirstAvailable []DeviceSubRequest `json:"firstAvailable,omitempty" protobuf:"bytes,3,name=firstAvailable"`
}
@@ -839,6 +841,7 @@ type ExactDeviceRequest struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,2,name=selectors"`
// AllocationMode and its related fields define how devices are allocated
@@ -965,6 +968,7 @@ type DeviceSubRequest struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,3,name=selectors"`
// AllocationMode and its related fields define how devices are allocated
@@ -1190,6 +1194,7 @@ type DeviceConstraint struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"`
// MatchAttribute requires that all devices in question have this
@@ -1247,6 +1252,7 @@ type DeviceClaimConfiguration struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"`
DeviceConfiguration `json:",inline" protobuf:"bytes,2,name=deviceConfiguration"`

View File

@@ -600,6 +600,7 @@ message DeviceClaimConfiguration {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated string requests = 1;
optional DeviceConfiguration deviceConfiguration = 2;
@@ -706,6 +707,7 @@ message DeviceConstraint {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated string requests = 1;
// MatchAttribute requires that all devices in question have this
@@ -804,6 +806,7 @@ message DeviceRequest {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated DeviceSelector selectors = 3;
// AllocationMode and its related fields define how devices are allocated
@@ -878,6 +881,7 @@ message DeviceRequest {
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
// +k8s:maxItems=8
repeated DeviceSubRequest firstAvailable = 7;
// If specified, the request's tolerations.
@@ -987,6 +991,7 @@ message DeviceRequestAllocationResult {
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
// +k8s:maxItems=16
repeated DeviceToleration tolerations = 6;
// BindingConditions contains a copy of the BindingConditions
@@ -1084,6 +1089,7 @@ message DeviceSubRequest {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated DeviceSelector selectors = 3;
// AllocationMode and its related fields define how devices are allocated

View File

@@ -812,6 +812,7 @@ type DeviceRequest struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,3,name=selectors"`
// AllocationMode and its related fields define how devices are allocated
@@ -886,6 +887,7 @@ type DeviceRequest struct {
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
// +k8s:maxItems=8
FirstAvailable []DeviceSubRequest `json:"firstAvailable,omitempty" protobuf:"bytes,7,name=firstAvailable"`
// If specified, the request's tolerations.
@@ -973,6 +975,7 @@ type DeviceSubRequest struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,3,name=selectors"`
// AllocationMode and its related fields define how devices are allocated
@@ -1198,6 +1201,7 @@ type DeviceConstraint struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"`
// MatchAttribute requires that all devices in question have this
@@ -1255,6 +1259,7 @@ type DeviceClaimConfiguration struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"`
DeviceConfiguration `json:",inline" protobuf:"bytes,2,name=deviceConfiguration"`
@@ -1552,6 +1557,7 @@ type DeviceRequestAllocationResult struct {
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
// +k8s:maxItems=16
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,6,opt,name=tolerations"`
// BindingConditions contains a copy of the BindingConditions

View File

@@ -592,6 +592,7 @@ message DeviceClaimConfiguration {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated string requests = 1;
optional DeviceConfiguration deviceConfiguration = 2;
@@ -698,6 +699,7 @@ message DeviceConstraint {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated string requests = 1;
// MatchAttribute requires that all devices in question have this
@@ -780,6 +782,7 @@ message DeviceRequest {
//
// +optional
// +oneOf=deviceRequestType
// +k8s:optional
optional ExactDeviceRequest exactly = 2;
// FirstAvailable contains subrequests, of which exactly one will be
@@ -800,6 +803,7 @@ message DeviceRequest {
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
// +k8s:maxItems=8
repeated DeviceSubRequest firstAvailable = 3;
}
@@ -962,6 +966,7 @@ message DeviceSubRequest {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated DeviceSelector selectors = 3;
// AllocationMode and its related fields define how devices are allocated
@@ -1133,6 +1138,7 @@ message ExactDeviceRequest {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
repeated DeviceSelector selectors = 2;
// AllocationMode and its related fields define how devices are allocated

View File

@@ -792,6 +792,7 @@ type DeviceRequest struct {
//
// +optional
// +oneOf=deviceRequestType
// +k8s:optional
Exactly *ExactDeviceRequest `json:"exactly,omitempty" protobuf:"bytes,2,name=exactly"`
// FirstAvailable contains subrequests, of which exactly one will be
@@ -812,6 +813,7 @@ type DeviceRequest struct {
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
// +k8s:maxItems=8
FirstAvailable []DeviceSubRequest `json:"firstAvailable,omitempty" protobuf:"bytes,3,name=firstAvailable"`
}
@@ -839,6 +841,7 @@ type ExactDeviceRequest struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,2,name=selectors"`
// AllocationMode and its related fields define how devices are allocated
@@ -965,6 +968,7 @@ type DeviceSubRequest struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,3,name=selectors"`
// AllocationMode and its related fields define how devices are allocated
@@ -1190,6 +1194,7 @@ type DeviceConstraint struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"`
// MatchAttribute requires that all devices in question have this
@@ -1247,6 +1252,7 @@ type DeviceClaimConfiguration struct {
//
// +optional
// +listType=atomic
// +k8s:maxItems=32
Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"`
DeviceConfiguration `json:",inline" protobuf:"bytes,2,name=deviceConfiguration"`

View File

@@ -68,12 +68,20 @@ func WithSubresourceMapper(subresourceMapper GroupVersionKindProvider) Validatio
}
}
// WithNormalizationRules sets the normalization rules for validation.
func WithNormalizationRules(rules []field.NormalizationRule) ValidationConfig {
return func(config *validationConfigOption) {
config.normalizationRules = rules
}
}
type validationConfigOption struct {
opType operation.Type
options []string
takeover bool
subresourceGVKMapper GroupVersionKindProvider
validationIdentifier string
normalizationRules []field.NormalizationRule
}
// validateDeclaratively validates obj and oldObj against declarative
@@ -146,9 +154,9 @@ func parseSubresourcePath(subresourcePath string) ([]string, error) {
// compareDeclarativeErrorsAndEmitMismatches checks for mismatches between imperative and declarative validation
// and logs + emits metrics when inconsistencies are found
func compareDeclarativeErrorsAndEmitMismatches(ctx context.Context, imperativeErrs, declarativeErrs field.ErrorList, takeover bool, validationIdentifier string) {
func compareDeclarativeErrorsAndEmitMismatches(ctx context.Context, imperativeErrs, declarativeErrs field.ErrorList, takeover bool, validationIdentifier string, normalizationRules []field.NormalizationRule) {
logger := klog.FromContext(ctx)
mismatchDetails := gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs, takeover)
mismatchDetails := gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs, takeover, normalizationRules)
for _, detail := range mismatchDetails {
// Log information about the mismatch using contextual logger
logger.Error(nil, detail)
@@ -160,7 +168,7 @@ func compareDeclarativeErrorsAndEmitMismatches(ctx context.Context, imperativeEr
// gatherDeclarativeValidationMismatches compares imperative and declarative validation errors
// and returns detailed information about any mismatches found. Errors are compared via type, field, and origin
func gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs field.ErrorList, takeover bool) []string {
func gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs field.ErrorList, takeover bool, normalizationRules []field.NormalizationRule) []string {
var mismatchDetails []string
// short circuit here to minimize allocs for usual case of 0 validation errors
if len(imperativeErrs) == 0 && len(declarativeErrs) == 0 {
@@ -171,7 +179,7 @@ func gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs field
if takeover {
recommendation = "Consider disabling the DeclarativeValidationTakeover feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes."
}
fuzzyMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin().RequireOriginWhenInvalid()
fuzzyMatcher := field.ErrorMatcher{}.ByType().ByOrigin().RequireOriginWhenInvalid().ByFieldNormalized(normalizationRules)
exactMatcher := field.ErrorMatcher{}.Exactly()
// Dedupe imperative errors of exact error matches as they are
@@ -353,8 +361,7 @@ func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runti
// Call the panic-safe wrapper with the real validation function.
declarativeErrs := panicSafeValidateFunc(validateDeclaratively, cfg.takeover, cfg.validationIdentifier)(ctx, scheme, obj, oldObj, cfg)
compareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier)
compareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier, cfg.normalizationRules)
if takeover {
errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...)
}

View File

@@ -213,6 +213,7 @@ func TestGatherDeclarativeValidationMismatches(t *testing.T) {
errB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum")
coveredErrB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum")
errBWithDiffDetail := field.Invalid(minReadySecondsPath, -1, "covered error B - different detail").WithOrigin("minimum")
errBWithDiffPath := field.Invalid(field.NewPath("spec").Child("fakeminReadySeconds"), -1, "covered error B").WithOrigin("minimum")
coveredErrB.CoveredByDeclarative = true
errC := field.Invalid(replicasPath, nil, "covered error C").WithOrigin("minimum")
coveredErrC := field.Invalid(replicasPath, nil, "covered error C").WithOrigin("minimum")
@@ -227,6 +228,7 @@ func TestGatherDeclarativeValidationMismatches(t *testing.T) {
takeover bool
expectMismatches bool
expectDetailsContaining []string
normalizedRules []field.NormalizationRule
}{
{
name: "Declarative and imperative return 0 errors - no mismatch",
@@ -358,11 +360,29 @@ func TestGatherDeclarativeValidationMismatches(t *testing.T) {
expectMismatches: false,
expectDetailsContaining: []string{},
},
{
name: "Field normalization, errors don't match - mismatch",
imperativeErrors: field.ErrorList{
coveredErrB,
},
declarativeErrors: field.ErrorList{
errBWithDiffPath,
},
normalizedRules: []field.NormalizationRule{
{
Regexp: regexp.MustCompile(`spec.fakeminReadySeconds`),
Replacement: "spec.minReadySeconds",
},
},
takeover: false,
expectMismatches: false,
expectDetailsContaining: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
details := gatherDeclarativeValidationMismatches(tc.imperativeErrors, tc.declarativeErrors, tc.takeover)
details := gatherDeclarativeValidationMismatches(tc.imperativeErrors, tc.declarativeErrors, tc.takeover, tc.normalizedRules)
// Check if mismatches were found if expected
if tc.expectMismatches && len(details) == 0 {
t.Errorf("Expected mismatches but got none")
@@ -429,7 +449,7 @@ func TestCompareDeclarativeErrorsAndEmitMismatches(t *testing.T) {
defer klog.LogToStderr(true)
ctx := context.Background()
compareDeclarativeErrorsAndEmitMismatches(ctx, tc.imperativeErrs, tc.declarativeErrs, tc.takeover, "test_validationIdentifier")
compareDeclarativeErrorsAndEmitMismatches(ctx, tc.imperativeErrs, tc.declarativeErrs, tc.takeover, "test_validationIdentifier", nil)
klog.Flush()
logOutput := buf.String()