From ef2b68388eac3ce2f7ce2bfddd67c5ce9cc6e070 Mon Sep 17 00:00:00 2001 From: Aaron Prindle Date: Mon, 22 Sep 2025 20:49:27 +0000 Subject: [PATCH] add +k8s:maxItems tag logic and tests --- .../declarative_validation_test.go | 112 +++--------- .../apimachinery/pkg/api/validate/limits.go | 20 +++ .../pkg/api/validate/limits_test.go | 109 ++++++++++++ .../tags/maxitems/slice_of_primitive/doc.go | 43 +++++ .../maxitems/slice_of_primitive/doc_test.go | 79 +++++++++ .../zz_generated.validations.go | 134 ++++++++++++++ .../tags/maxitems/slice_of_struct/doc.go | 45 +++++ .../tags/maxitems/slice_of_struct/doc_test.go | 78 ++++++++ .../zz_generated.validations.go | 134 ++++++++++++++ .../tags/maxitems/typedef_to_slice/doc.go | 59 +++++++ .../maxitems/typedef_to_slice/doc_test.go | 85 +++++++++ .../zz_generated.validations.go | 167 ++++++++++++++++++ .../cmd/validation-gen/validators/limits.go | 61 ++++++- 13 files changed, 1035 insertions(+), 91 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/zz_generated.validations.go diff --git a/pkg/registry/resource/resourceclaim/declarative_validation_test.go b/pkg/registry/resource/resourceclaim/declarative_validation_test.go index fc93877a29c..cfc722a6227 100644 --- a/pkg/registry/resource/resourceclaim/declarative_validation_test.go +++ b/pkg/registry/resource/resourceclaim/declarative_validation_test.go @@ -22,14 +22,12 @@ import ( "testing" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/kubernetes/fake" - featuregatetesting "k8s.io/component-base/featuregate/testing" apitesting "k8s.io/kubernetes/pkg/api/testing" "k8s.io/kubernetes/pkg/apis/resource" - "k8s.io/kubernetes/pkg/features" pointer "k8s.io/utils/ptr" ) @@ -66,47 +64,31 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { input: mkValidResourceClaim(tweakDevicesConstraints(32)), }, "valid config, max allowed": { - input: mkValidResourceClaim(tweakDevicesRequests(32)), + input: mkValidResourceClaim(tweakDevicesConfigs(32)), }, "invalid requests, too many": { - input: mkValidResourceClaim(tweakDevicesConfigs(33)), + input: mkValidResourceClaim(tweakDevicesRequests(33)), expectedErrs: field.ErrorList{ - field.TooMany(field.NewPath("spec", "devices", "requests"), 33, 32), + field.TooMany(field.NewPath("spec", "devices", "requests"), 33, 32).WithOrigin("maxItems"), }, }, "invalid constraints, too many": { input: mkValidResourceClaim(tweakDevicesConstraints(33)), expectedErrs: field.ErrorList{ - field.TooMany(field.NewPath("spec", "devices", "constraints"), 33, 32), + field.TooMany(field.NewPath("spec", "devices", "constraints"), 33, 32).WithOrigin("maxItems"), }, }, "invalid config, too many": { - input: mkValidResourceClaim(tweakDevicesRequests(33)), + input: mkValidResourceClaim(tweakDevicesConfigs(33)), expectedErrs: field.ErrorList{ - field.TooMany(field.NewPath("spec", "devices", "config"), 33, 32), + field.TooMany(field.NewPath("spec", "devices", "config"), 33, 32).WithOrigin("maxItems"), }, }, // TODO: Add more test cases } for k, tc := range testCases { t.Run(k, func(t *testing.T) { - var declarativeTakeoverErrs field.ErrorList - var imperativeErrs field.ErrorList - for _, gateVal := range []bool{true, false} { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal) - - errs := Strategy.Validate(ctx, &tc.input) - if gateVal { - declarativeTakeoverErrs = errs - } else { - imperativeErrs = errs - } - } - equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() - equivalenceMatcher.Test(t, imperativeErrs, declarativeTakeoverErrs) - - apitesting.VerifyVersionedValidationEquivalence(t, &tc.input, nil) + apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs) }) } } @@ -129,7 +111,8 @@ func tweakDevicesConstraints(items int) func(*resource.ResourceClaim) { func tweakDevicesRequests(items int) func(*resource.ResourceClaim) { return func(rc *resource.ResourceClaim) { - for i := 0; i < items; i++ { + // The first request already exists in the valid template + for i := 1; i < items; i++ { rc.Spec.Devices.Requests = append(rc.Spec.Devices.Requests, mkDeviceRequest(fmt.Sprintf("req-%d", i))) } } @@ -138,13 +121,20 @@ func tweakDevicesRequests(items int) func(*resource.ResourceClaim) { func mkDeviceClaimConfiguration() resource.DeviceClaimConfiguration { return resource.DeviceClaimConfiguration{ Requests: []string{"req-0"}, + DeviceConfiguration: resource.DeviceConfiguration{ + Opaque: &resource.OpaqueDeviceConfiguration{ + Driver: "dra.example.com", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`), + }}, + }, } } func mkDeviceConstraint() resource.DeviceConstraint { return resource.DeviceConstraint{ Requests: []string{"req-0"}, - MatchAttribute: pointer.To(resource.FullyQualifiedName("a")), + MatchAttribute: pointer.To(resource.FullyQualifiedName("foo/bar")), } } @@ -189,23 +179,9 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { } for k, tc := range testCases { t.Run(k, func(t *testing.T) { - var declarativeTakeoverErrs field.ErrorList - var imperativeErrs field.ErrorList - for _, gateVal := range []bool{true, false} { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal) - - errs := Strategy.ValidateUpdate(ctx, &tc.update, &tc.old) - if gateVal { - declarativeTakeoverErrs = errs - } else { - imperativeErrs = errs - } - } - equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() - equivalenceMatcher.Test(t, imperativeErrs, declarativeTakeoverErrs) - - apitesting.VerifyVersionedValidationEquivalence(t, &tc.update, &tc.old) + tc.old.ResourceVersion = "1" + tc.update.ResourceVersion = "2" + apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, Strategy.ValidateUpdate, tc.expectedErrs) }) } } @@ -282,51 +258,7 @@ func TestValidateStatusUpdateForDeclarative(t *testing.T) { t.Run(k, func(t *testing.T) { tc.old.ObjectMeta.ResourceVersion = "1" tc.update.ObjectMeta.ResourceVersion = "1" - var declarativeTakeoverErrs field.ErrorList - var imperativeErrs field.ErrorList - for _, gateVal := range []bool{true, false} { - t.Run(fmt.Sprintf("gate=%v", gateVal), func(t *testing.T) { - // We only need to test both gate enabled and disabled together, because - // 1) the DeclarativeValidationTakeover won't take effect if DeclarativeValidation is disabled. - // 2) the validation output, when only DeclarativeValidation is enabled, is the same as when both gates are disabled. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal) - errs := strategy.ValidateUpdate(ctx, &tc.update, &tc.old) - if gateVal { - declarativeTakeoverErrs = errs - } else { - imperativeErrs = errs - } - // The errOutputMatcher is used to verify the output matches the expected errors in test cases. - errOutputMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() - - if len(tc.expectedErrs) > 0 { - errOutputMatcher.Test(t, tc.expectedErrs, errs) - } else if len(errs) != 0 { - t.Errorf("expected no errors, but got: %v", errs) - } - }) - } - // 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() - // TODO: remove this once ErrorMatcher has been extended to handle this form of deduplication. - dedupedImperativeErrs := field.ErrorList{} - for _, err := range imperativeErrs { - found := false - for _, existingErr := range dedupedImperativeErrs { - if equivalenceMatcher.Matches(existingErr, err) { - found = true - break - } - } - if !found { - dedupedImperativeErrs = append(dedupedImperativeErrs, err) - } - } - equivalenceMatcher.Test(t, dedupedImperativeErrs, declarativeTakeoverErrs) - - apitesting.VerifyVersionedValidationEquivalence(t, &tc.update, &tc.old) + apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, strategy.ValidateUpdate, tc.expectedErrs, "status") }) } } diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go index 5f5fe83a4df..b6db5e08cbe 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go @@ -25,6 +25,26 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) +// MaxLength verifies that the specified value is not longer than max +// characters. +func MaxLength[T ~string](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, max int) field.ErrorList { + if value == nil { + return nil + } + if len(*value) > max { + return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxLength")} + } + return nil +} + +// MaxItems verifies that the specified slice is not longer than max items. +func MaxItems[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T, max int) field.ErrorList { + if len(value) > max { + return field.ErrorList{field.TooMany(fldPath, len(value), max).WithOrigin("maxItems")} + } + return nil +} + // Minimum verifies that the specified value is greater than or equal to min. func Minimum[T constraints.Integer](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, min T) field.ErrorList { if value == nil { diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go index 60b785b758d..c1039c4ecde 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -26,6 +26,115 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) +func TestMaxLength(t *testing.T) { + cases := []struct { + name string + value string + max int + wantErrs field.ErrorList // regex + }{{ + name: "empty string", + value: "", + max: 0, + wantErrs: nil, + }, { + name: "zero length", + value: "0", + max: 0, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), nil, 0).WithOrigin("maxLength"), + }, + }, { + name: "one character", + value: "0", + max: 1, + wantErrs: nil, + }, { + name: "two characters", + value: "01", + max: 1, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), nil, 1).WithOrigin("maxLength"), + }, + }, { + value: "", + max: -1, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), nil, -1).WithOrigin("maxLength"), + }, + }} + + matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType() + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + v := tc.value + gotErrs := MaxLength(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.max) + matcher.Test(t, tc.wantErrs, gotErrs) + }) + } +} + +func TestMaxItems(t *testing.T) { + cases := []struct { + fn func(op operation.Operation, fp *field.Path) field.ErrorList + err string // regex + }{{ + fn: func(op operation.Operation, fp *field.Path) field.ErrorList { + value := make([]string, 0) + max := 0 + return MaxItems(context.Background(), op, fp, value, nil, max) + }, + }, { + fn: func(op operation.Operation, fp *field.Path) field.ErrorList { + value := make([]string, 1) + max := 0 + return MaxItems(context.Background(), op, fp, value, nil, max) + }, + err: "fldpath: Too many.*must have at most", + }, { + fn: func(op operation.Operation, fp *field.Path) field.ErrorList { + value := make([]int, 1) + max := 1 + return MaxItems(context.Background(), op, fp, value, nil, max) + }, + }, { + fn: func(op operation.Operation, fp *field.Path) field.ErrorList { + value := make([]int, 2) + max := 1 + return MaxItems(context.Background(), op, fp, value, nil, max) + }, + err: "fldpath: Too many.*must have at most", + }, { + fn: func(op operation.Operation, fp *field.Path) field.ErrorList { + value := make([]bool, 0) + max := -1 + return MaxItems(context.Background(), op, fp, value, nil, max) + }, + err: "fldpath: Too many.*too many items", + }} + + for i, tc := range cases { + result := tc.fn(operation.Operation{}, field.NewPath("fldpath")) + if len(result) > 0 && tc.err == "" { + t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result)) + continue + } + if len(result) == 0 && tc.err != "" { + t.Errorf("case %d: unexpected success: expected %q", i, tc.err) + continue + } + if len(result) > 0 { + if len(result) > 1 { + t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result)) + continue + } + if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) { + t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result)) + } + } + } +} + func TestMinimum(t *testing.T) { testMinimumPositive[int](t) testMinimumNegative[int](t) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc.go new file mode 100644 index 00000000000..fd7f085c7cb --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc.go @@ -0,0 +1,43 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package sliceofprimitive + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:maxItems=0 + Max0Field []int `json:"max0Field"` + + // +k8s:maxItems=10 + Max10Field []int `json:"max10Field"` + + // +k8s:maxItems=0 + Max0TypedefField []IntType `json:"max0TypedefField"` + + // +k8s:maxItems=10 + Max10TypedefField []IntType `json:"max10TypedefField"` +} + +type IntType int diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc_test.go new file mode 100644 index 00000000000..6611443de03 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/doc_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sliceofprimitive + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // All zero values + }).ExpectValid() + + st.Value(&Struct{ + Max0Field: make([]int, 0), + Max10Field: make([]int, 0), + Max0TypedefField: make([]IntType, 0), + Max10TypedefField: make([]IntType, 0), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: make([]int, 1), + Max10TypedefField: make([]IntType, 1), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: make([]int, 9), + Max10TypedefField: make([]IntType, 9), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: make([]int, 10), + Max10TypedefField: make([]IntType, 10), + }).ExpectValid() + + testVal := &Struct{ + Max0Field: make([]int, 1), + Max10Field: make([]int, 11), + Max0TypedefField: make([]IntType, 1), + Max10TypedefField: make([]IntType, 11), + } + st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooMany(field.NewPath("max0Field"), 1, 0), + field.TooMany(field.NewPath("max10Field"), 11, 10), + field.TooMany(field.NewPath("max0TypedefField"), 1, 0), + field.TooMany(field.NewPath("max10TypedefField"), 11, 10), + }) + // Test validation ratcheting + st.Value(&Struct{ + Max0Field: make([]int, 1), + Max10Field: make([]int, 11), + Max0TypedefField: make([]IntType, 1), + Max10TypedefField: make([]IntType, 11), + }).OldValue(&Struct{ + Max0Field: make([]int, 1), + Max10Field: make([]int, 11), + Max0TypedefField: make([]IntType, 1), + Max10TypedefField: make([]IntType, 11), + }).ExpectValid() + +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/zz_generated.validations.go new file mode 100644 index 00000000000..84c26a90c24 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_primitive/zz_generated.validations.go @@ -0,0 +1,134 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package sliceofprimitive + +import ( + context "context" + fmt "fmt" + + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.Max0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max0Field"), obj.Max0Field, safe.Field(oldObj, func(oldObj *Struct) []int { return oldObj.Max0Field }))...) + + // field Struct.Max10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 10); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max10Field"), obj.Max10Field, safe.Field(oldObj, func(oldObj *Struct) []int { return oldObj.Max10Field }))...) + + // field Struct.Max0TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []IntType) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max0TypedefField"), obj.Max0TypedefField, safe.Field(oldObj, func(oldObj *Struct) []IntType { return oldObj.Max0TypedefField }))...) + + // field Struct.Max10TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []IntType) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 10); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max10TypedefField"), obj.Max10TypedefField, safe.Field(oldObj, func(oldObj *Struct) []IntType { return oldObj.Max10TypedefField }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc.go new file mode 100644 index 00000000000..b8bf98fc13a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc.go @@ -0,0 +1,45 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package sliceofstruct + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:maxItems=0 + Max0Field []OtherStruct `json:"max0Field"` + + // +k8s:maxItems=10 + Max10Field []OtherStruct `json:"max10Field"` + + // +k8s:maxItems=0 + Max0TypedefField []OtherTypedefStruct `json:"max0TypedefField"` + + // +k8s:maxItems=10 + Max10TypedefField []OtherTypedefStruct `json:"max10TypedefField"` +} + +type OtherStruct struct{} + +type OtherTypedefStruct OtherStruct diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc_test.go new file mode 100644 index 00000000000..ce38b50a75b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/doc_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sliceofstruct + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // All zero values + }).ExpectValid() + + st.Value(&Struct{ + Max0Field: make([]OtherStruct, 0), + Max10Field: make([]OtherStruct, 0), + Max0TypedefField: make([]OtherTypedefStruct, 0), + Max10TypedefField: make([]OtherTypedefStruct, 0), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: make([]OtherStruct, 1), + Max10TypedefField: make([]OtherTypedefStruct, 1), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: make([]OtherStruct, 9), + Max10TypedefField: make([]OtherTypedefStruct, 9), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: make([]OtherStruct, 10), + Max10TypedefField: make([]OtherTypedefStruct, 10), + }).ExpectValid() + + testVal := &Struct{ + Max0Field: make([]OtherStruct, 1), + Max10Field: make([]OtherStruct, 11), + Max0TypedefField: make([]OtherTypedefStruct, 1), + Max10TypedefField: make([]OtherTypedefStruct, 11), + } + st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooMany(field.NewPath("max0Field"), 1, 0), + field.TooMany(field.NewPath("max10Field"), 11, 10), + field.TooMany(field.NewPath("max0TypedefField"), 1, 0), + field.TooMany(field.NewPath("max10TypedefField"), 11, 10), + }) + // Test validation ratcheting + st.Value(&Struct{ + Max0Field: make([]OtherStruct, 1), + Max10Field: make([]OtherStruct, 11), + Max0TypedefField: make([]OtherTypedefStruct, 1), + Max10TypedefField: make([]OtherTypedefStruct, 11), + }).OldValue(&Struct{ + Max0Field: make([]OtherStruct, 1), + Max10Field: make([]OtherStruct, 11), + Max0TypedefField: make([]OtherTypedefStruct, 1), + Max10TypedefField: make([]OtherTypedefStruct, 11), + }).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/zz_generated.validations.go new file mode 100644 index 00000000000..8e4fe3160a9 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/slice_of_struct/zz_generated.validations.go @@ -0,0 +1,134 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package sliceofstruct + +import ( + context "context" + fmt "fmt" + + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.Max0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherStruct) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max0Field"), obj.Max0Field, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.Max0Field }))...) + + // field Struct.Max10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherStruct) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 10); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max10Field"), obj.Max10Field, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.Max10Field }))...) + + // field Struct.Max0TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherTypedefStruct) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max0TypedefField"), obj.Max0TypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.Max0TypedefField }))...) + + // field Struct.Max10TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherTypedefStruct) (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.MaxItems(ctx, op, fldPath, obj, oldObj, 10); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("max10TypedefField"), obj.Max10TypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.Max10TypedefField }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc.go new file mode 100644 index 00000000000..c8cc890c445 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc.go @@ -0,0 +1,59 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package typedeftoslice + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +// Note: no validation here +type UnvalidatedType []int + +// +k8s:maxItems=0 +type Max0Type []int + +// +k8s:maxItems=10 +type Max10Type []int + +// Note: no validation here +type UnvalidatedPtrType []*int + +type SliceType []int + +// +k8s:maxItems=0 +type Max0TypedefType SliceType + +// +k8s:maxItems=10 +type Max10TypedefType SliceType + +type Struct struct { + TypeMeta int + + UnvalidatedField UnvalidatedType `json:"unvalidatedField"` + + Max0Field Max0Type `json:"max0Field"` + + Max10Field Max10Type `json:"max10Field"` + + Max0TypedefField Max0TypedefType `json:"max0TypedefField"` + + Max10TypedefField Max10TypedefType `json:"max10TypedefField"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc_test.go new file mode 100644 index 00000000000..b5fe133e2d4 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/doc_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package typedeftoslice + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // All zero values + }).ExpectValid() + + st.Value(&Struct{ + UnvalidatedField: make(UnvalidatedType, 0), + Max0Field: make(Max0Type, 0), + Max10Field: make(Max10Type, 0), + Max0TypedefField: make(Max0TypedefType, 0), + Max10TypedefField: make(Max10TypedefType, 0), + }).ExpectValid() + + st.Value(&Struct{ + UnvalidatedField: make(UnvalidatedType, 1), + Max10Field: make(Max10Type, 1), + Max10TypedefField: make(Max10TypedefType, 1), + }).ExpectValid() + + st.Value(&Struct{ + UnvalidatedField: make(UnvalidatedType, 9), + Max10Field: make(Max10Type, 9), + Max10TypedefField: make(Max10TypedefType, 9), + }).ExpectValid() + + st.Value(&Struct{ + UnvalidatedField: make(UnvalidatedType, 10), + Max10Field: make(Max10Type, 10), + Max10TypedefField: make(Max10TypedefType, 10), + }).ExpectValid() + + testVal := &Struct{ + UnvalidatedField: make(UnvalidatedType, 11), + Max0Field: make(Max0Type, 1), + Max10Field: make(Max10Type, 11), + Max0TypedefField: make(Max0TypedefType, 1), + Max10TypedefField: make(Max10TypedefType, 11), + } + st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooMany(field.NewPath("max0Field"), 1, 0), + field.TooMany(field.NewPath("max10Field"), 11, 10), + field.TooMany(field.NewPath("max0TypedefField"), 1, 0), + field.TooMany(field.NewPath("max10TypedefField"), 11, 10), + }) + // Test validation ratcheting + st.Value(&Struct{ + UnvalidatedField: make(UnvalidatedType, 1), + Max0Field: make(Max0Type, 1), + Max10Field: make(Max10Type, 11), + Max0TypedefField: make(Max0TypedefType, 1), + Max10TypedefField: make(Max10TypedefType, 11), + }).OldValue(&Struct{ + UnvalidatedField: make(UnvalidatedType, 1), + Max0Field: make(Max0Type, 1), + Max10Field: make(Max10Type, 11), + Max0TypedefField: make(Max0TypedefType, 1), + Max10TypedefField: make(Max10TypedefType, 11), + }).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/zz_generated.validations.go new file mode 100644 index 00000000000..3627bfccfca --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxitems/typedef_to_slice/zz_generated.validations.go @@ -0,0 +1,167 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package typedeftoslice + +import ( + context "context" + fmt "fmt" + + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_Max0Type validates an instance of Max0Type according +// to declarative validation rules in the API schema. +func Validate_Max0Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Max0Type) (errs field.ErrorList) { + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + + return errs +} + +// Validate_Max0TypedefType validates an instance of Max0TypedefType according +// to declarative validation rules in the API schema. +func Validate_Max0TypedefType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Max0TypedefType) (errs field.ErrorList) { + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + + return errs +} + +// Validate_Max10Type validates an instance of Max10Type according +// to declarative validation rules in the API schema. +func Validate_Max10Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Max10Type) (errs field.ErrorList) { + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 10); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + + return errs +} + +// Validate_Max10TypedefType validates an instance of Max10TypedefType according +// to declarative validation rules in the API schema. +func Validate_Max10TypedefType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Max10TypedefType) (errs field.ErrorList) { + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 10); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + + return errs +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + // field Struct.UnvalidatedField has no validation + + // field Struct.Max0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Max0Type) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max0Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max0Field"), obj.Max0Field, safe.Field(oldObj, func(oldObj *Struct) Max0Type { return oldObj.Max0Field }))...) + + // field Struct.Max10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Max10Type) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max10Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max10Field"), obj.Max10Field, safe.Field(oldObj, func(oldObj *Struct) Max10Type { return oldObj.Max10Field }))...) + + // field Struct.Max0TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Max0TypedefType) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max0TypedefType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max0TypedefField"), obj.Max0TypedefField, safe.Field(oldObj, func(oldObj *Struct) Max0TypedefType { return oldObj.Max0TypedefField }))...) + + // field Struct.Max10TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Max10TypedefType) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max10TypedefType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max10TypedefField"), obj.Max10TypedefField, safe.Field(oldObj, func(oldObj *Struct) Max10TypedefType { return oldObj.Max10TypedefField }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go index 728af9f1d21..5a972843385 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -27,13 +27,72 @@ import ( ) const ( - minimumTagName = "k8s:minimum" + maxItemsTagName = "k8s:maxItems" + minimumTagName = "k8s:minimum" ) func init() { + RegisterTagValidator(maxItemsTagValidator{}) RegisterTagValidator(minimumTagValidator{}) } +type maxItemsTagValidator struct{} + +func (maxItemsTagValidator) Init(_ Config) {} + +func (maxItemsTagValidator) TagName() string { + return maxItemsTagName +} + +var maxItemsTagValidScopes = sets.New( + ScopeType, + ScopeField, + ScopeListVal, + ScopeMapVal, +) + +func (maxItemsTagValidator) ValidScopes() sets.Set[Scope] { + return maxItemsTagValidScopes +} + +var ( + maxItemsValidator = types.Name{Package: libValidationPkg, Name: "MaxItems"} +) + +func (maxItemsTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + var result Validations + + // NOTE: pointers to lists are not supported, so we should never see a pointer here. + if t := util.NativeType(context.Type); t.Kind != types.Slice && t.Kind != types.Array { + return Validations{}, fmt.Errorf("can only be used on list types (%s)", rootTypeString(context.Type, t)) + } + + intVal, err := strconv.Atoi(tag.Value) + if err != nil { + return result, fmt.Errorf("failed to parse tag payload as int: %w", err) + } + if intVal < 0 { + return result, fmt.Errorf("must be greater than or equal to zero") + } + // Note: maxItems short-circuits other validations. + result.AddFunction(Function(maxItemsTagName, ShortCircuit, maxItemsValidator, intVal)) + return result, nil +} + +func (mitv maxItemsTagValidator) Docs() TagDoc { + return TagDoc{ + Tag: mitv.TagName(), + Scopes: mitv.ValidScopes().UnsortedList(), + Description: "Indicates that a list field has a limit on its size.", + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "This field must be no more than X items long.", + }}, + PayloadsType: codetags.ValueTypeInt, + PayloadsRequired: true, + } +} + type minimumTagValidator struct{} func (minimumTagValidator) Init(_ Config) {}