From 91ddc8ff78b31ff8337f2913d317941da59426e1 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Mon, 9 Mar 2026 18:38:59 -0700 Subject: [PATCH] Add DV tag '+k8s:minItems' --- .../apimachinery/pkg/api/validate/limits.go | 8 + .../pkg/api/validate/limits_test.go | 48 ++++++ .../pkg/util/validation/field/errors.go | 32 +++- .../tags/minitems/slice_of_primitive/doc.go | 44 ++++++ .../minitems/slice_of_primitive/doc_test.go | 87 +++++++++++ .../zz_generated.validations.go | 106 +++++++++++++ .../tags/minitems/slice_of_struct/doc.go | 46 ++++++ .../tags/minitems/slice_of_struct/doc_test.go | 87 +++++++++++ .../zz_generated.validations.go | 106 +++++++++++++ .../tags/minitems/typedef_to_slice/doc.go | 60 ++++++++ .../minitems/typedef_to_slice/doc_test.go | 87 +++++++++++ .../zz_generated.validations.go | 139 ++++++++++++++++++ .../cmd/validation-gen/validators/limits.go | 65 +++++++- 13 files changed, 910 insertions(+), 5 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/zz_generated.validations.go 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 15c8728e957..dcc7f518d81 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go @@ -78,6 +78,14 @@ func MaxItems[T any](_ context.Context, _ operation.Operation, fldPath *field.Pa return nil } +// MinItems verifies that the specified slice is not shorter than min items. +func MinItems[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T, min int) field.ErrorList { + if len(value) < min { + return field.ErrorList{field.TooFew(fldPath, len(value), min).WithOrigin("minItems")} + } + 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 f3a43c4efcd..6dee7b395fc 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 @@ -116,6 +116,54 @@ func TestMaxLength(t *testing.T) { } } +func TestMinItems(t *testing.T) { + cases := []struct { + name string + items int + min int + wantErrs field.ErrorList + }{{ + name: "0 items, min 0", + items: 0, + min: 0, + }, { + name: "1 item, min 0", + items: 1, + min: 0, + }, { + name: "1 item, min 1", + items: 1, + min: 1, + }, { + name: "0 items, min 1", + items: 0, + min: 1, + wantErrs: field.ErrorList{ + field.TooFew(field.NewPath("fldpath"), 0, 1).WithOrigin("minItems"), + }, + }, { + name: "1 item, min 2", + items: 1, + min: 2, + wantErrs: field.ErrorList{ + field.TooFew(field.NewPath("fldpath"), 1, 2).WithOrigin("minItems"), + }, + }, { + name: "0 items, min -1", + items: 0, + min: -1, + }} + + matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType() + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + value := make([]bool, tc.items) + gotErrs := MinItems(context.Background(), operation.Operation{}, field.NewPath("fldpath"), value, nil, tc.min) + matcher.Test(t, tc.wantErrs, gotErrs) + }) + } +} + func TestMaxItems(t *testing.T) { cases := []struct { name string diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go index edeca895b98..9357fb306ce 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go @@ -112,7 +112,7 @@ func (e *Error) ErrorBody() string { case ErrorTypeRequired, ErrorTypeForbidden, ErrorTypeTooLong, ErrorTypeTooShort, ErrorTypeInternal: s = e.Type.String() case ErrorTypeInvalid, ErrorTypeTypeInvalid, ErrorTypeNotSupported, - ErrorTypeNotFound, ErrorTypeDuplicate, ErrorTypeTooMany: + ErrorTypeNotFound, ErrorTypeDuplicate, ErrorTypeTooMany, ErrorTypeTooFew: if e.BadValue == omitValue { s = e.Type.String() break @@ -201,6 +201,10 @@ const ( // report that a given list has too many items. This is similar to FieldValueTooLong, // but the error indicates quantity instead of length. ErrorTypeTooMany ErrorType = "FieldValueTooMany" + // ErrorTypeTooFew is used to report "too few". This is used to + // report that a given list has too few items. This is similar to FieldValueTooLong, + // but the error indicates quantity instead of length. + ErrorTypeTooFew ErrorType = "FieldValueTooFew" // ErrorTypeInternal is used to report other errors that are not related // to user input. See InternalError(). ErrorTypeInternal ErrorType = "InternalError" @@ -230,6 +234,8 @@ func (t ErrorType) String() string { return "Too long" case ErrorTypeTooMany: return "Too many" + case ErrorTypeTooFew: + return "Too few" case ErrorTypeInternal: return "Internal error" case ErrorTypeTypeInvalid: @@ -583,3 +589,27 @@ func (list ErrorList) RemoveCoveredByDeclarative() ErrorList { } return newList } + +// TooFew returns a *Error indicating "too few". This is used to +// report that a given list has too few items. This is similar to TooLong, +// but the returned error indicates quantity instead of length. +func TooFew(field *Path, actualQuantity, minQuantity int) *Error { + var msg string + + if minQuantity >= 0 { + is := "items" + if minQuantity == 1 { + is = "item" + } + msg = fmt.Sprintf("must have at least %d %s", minQuantity, is) + } else { + msg = "has too few items" + } + + return &Error{ + Type: ErrorTypeTooFew, + Field: field.String(), + BadValue: actualQuantity, + Detail: msg, + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc.go new file mode 100644 index 00000000000..ba34ea6b576 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc.go @@ -0,0 +1,44 @@ +/* +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. +// +k8s:validation-gen-nolint +package sliceofprimitive + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:minItems=0 + Min0Field []int `json:"min0Field"` + + // +k8s:minItems=10 + Min10Field []int `json:"min10Field"` + + // +k8s:minItems=0 + Min0TypedefField []IntType `json:"min0TypedefField"` + + // +k8s:minItems=10 + Min10TypedefField []IntType `json:"min10TypedefField"` +} + +type IntType int diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc_test.go new file mode 100644 index 00000000000..12b092016d4 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/doc_test.go @@ -0,0 +1,87 @@ +/* +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 + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 0, 10), + field.TooFew(field.NewPath("min10TypedefField"), 0, 10), + }) + + st.Value(&Struct{ + Min0Field: make([]int, 0), + Min10Field: make([]int, 0), + Min0TypedefField: make([]IntType, 0), + Min10TypedefField: make([]IntType, 0), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 0, 10), + field.TooFew(field.NewPath("min10TypedefField"), 0, 10), + }) + + st.Value(&Struct{ + Min10Field: make([]int, 1), + Min10TypedefField: make([]IntType, 1), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 1, 10), + field.TooFew(field.NewPath("min10TypedefField"), 1, 10), + }) + + st.Value(&Struct{ + Min10Field: make([]int, 9), + Min10TypedefField: make([]IntType, 9), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 9, 10), + field.TooFew(field.NewPath("min10TypedefField"), 9, 10), + }) + + st.Value(&Struct{ + Min10Field: make([]int, 10), + Min10TypedefField: make([]IntType, 10), + }).ExpectValid() + + testVal := &Struct{ + Min0Field: make([]int, 1), + Min10Field: make([]int, 11), + Min0TypedefField: make([]IntType, 1), + Min10TypedefField: make([]IntType, 11), + } + st.Value(testVal).ExpectValid() + + // Test validation ratcheting + st.Value(&Struct{ + Min0Field: make([]int, 1), + Min10Field: make([]int, 1), + Min0TypedefField: make([]IntType, 1), + Min10TypedefField: make([]IntType, 1), + }).OldValue(&Struct{ + Min0Field: make([]int, 1), + Min10Field: make([]int, 1), + Min0TypedefField: make([]IntType, 1), + Min10TypedefField: make([]IntType, 1), + }).ExpectValid() + +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/zz_generated.validations.go new file mode 100644 index 00000000000..bfe3ce23a4e --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_primitive/zz_generated.validations.go @@ -0,0 +1,106 @@ +//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.Min0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("min0Field"), obj.Min0Field, safe.Field(oldObj, func(oldObj *Struct) []int { return oldObj.Min0Field }), oldObj != nil)...) + + // field Struct.Min10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("min10Field"), obj.Min10Field, safe.Field(oldObj, func(oldObj *Struct) []int { return oldObj.Min10Field }), oldObj != nil)...) + + // field Struct.Min0TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []IntType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("min0TypedefField"), obj.Min0TypedefField, safe.Field(oldObj, func(oldObj *Struct) []IntType { return oldObj.Min0TypedefField }), oldObj != nil)...) + + // field Struct.Min10TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []IntType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("min10TypedefField"), obj.Min10TypedefField, safe.Field(oldObj, func(oldObj *Struct) []IntType { return oldObj.Min10TypedefField }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc.go new file mode 100644 index 00000000000..29c71e494ab --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc.go @@ -0,0 +1,46 @@ +/* +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. +// +k8s:validation-gen-nolint +package sliceofstruct + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:minItems=0 + Min0Field []OtherStruct `json:"min0Field"` + + // +k8s:minItems=10 + Min10Field []OtherStruct `json:"min10Field"` + + // +k8s:minItems=0 + Min0TypedefField []OtherTypedefStruct `json:"min0TypedefField"` + + // +k8s:minItems=10 + Min10TypedefField []OtherTypedefStruct `json:"min10TypedefField"` +} + +type OtherStruct struct{} + +type OtherTypedefStruct OtherStruct diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc_test.go new file mode 100644 index 00000000000..110b2937614 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/doc_test.go @@ -0,0 +1,87 @@ +/* +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 + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 0, 10), + field.TooFew(field.NewPath("min10TypedefField"), 0, 10), + }) + + st.Value(&Struct{ + Min0Field: make([]OtherStruct, 0), + Min10Field: make([]OtherStruct, 0), + Min0TypedefField: make([]OtherTypedefStruct, 0), + Min10TypedefField: make([]OtherTypedefStruct, 0), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 0, 10), + field.TooFew(field.NewPath("min10TypedefField"), 0, 10), + }) + + st.Value(&Struct{ + Min10Field: make([]OtherStruct, 1), + Min10TypedefField: make([]OtherTypedefStruct, 1), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 1, 10), + field.TooFew(field.NewPath("min10TypedefField"), 1, 10), + }) + + st.Value(&Struct{ + Min10Field: make([]OtherStruct, 9), + Min10TypedefField: make([]OtherTypedefStruct, 9), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 9, 10), + field.TooFew(field.NewPath("min10TypedefField"), 9, 10), + }) + + st.Value(&Struct{ + Min10Field: make([]OtherStruct, 10), + Min10TypedefField: make([]OtherTypedefStruct, 10), + }).ExpectValid() + + testVal := &Struct{ + Min0Field: make([]OtherStruct, 1), + Min10Field: make([]OtherStruct, 11), + Min0TypedefField: make([]OtherTypedefStruct, 1), + Min10TypedefField: make([]OtherTypedefStruct, 11), + } + st.Value(testVal).ExpectValid() + + // Test validation ratcheting + st.Value(&Struct{ + Min0Field: make([]OtherStruct, 1), + Min10Field: make([]OtherStruct, 1), + Min0TypedefField: make([]OtherTypedefStruct, 1), + Min10TypedefField: make([]OtherTypedefStruct, 1), + }).OldValue(&Struct{ + Min0Field: make([]OtherStruct, 1), + Min10Field: make([]OtherStruct, 1), + Min0TypedefField: make([]OtherTypedefStruct, 1), + Min10TypedefField: make([]OtherTypedefStruct, 1), + }).ExpectValid() + +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/zz_generated.validations.go new file mode 100644 index 00000000000..8e1c61b0d1b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/slice_of_struct/zz_generated.validations.go @@ -0,0 +1,106 @@ +//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.Min0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherStruct, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("min0Field"), obj.Min0Field, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.Min0Field }), oldObj != nil)...) + + // field Struct.Min10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherStruct, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("min10Field"), obj.Min10Field, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.Min10Field }), oldObj != nil)...) + + // field Struct.Min0TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherTypedefStruct, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("min0TypedefField"), obj.Min0TypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.Min0TypedefField }), oldObj != nil)...) + + // field Struct.Min10TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []OtherTypedefStruct, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("min10TypedefField"), obj.Min10TypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.Min10TypedefField }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc.go new file mode 100644 index 00000000000..9c4990b910b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc.go @@ -0,0 +1,60 @@ +/* +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. +// +k8s:validation-gen-nolint +package typedeftoslice + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +// Note: no validation here +type UnvalidatedType []int + +// +k8s:minItems=0 +type Min0Type []int + +// +k8s:minItems=10 +type Min10Type []int + +// Note: no validation here +type UnvalidatedPtrType []*int + +type SliceType []int + +// +k8s:minItems=0 +type Min0TypedefType SliceType + +// +k8s:minItems=10 +type Min10TypedefType SliceType + +type Struct struct { + TypeMeta int + + UnvalidatedField UnvalidatedType `json:"unvalidatedField"` + + Min0Field Min0Type `json:"min0Field"` + + Min10Field Min10Type `json:"min10Field"` + + Min0TypedefField Min0TypedefType `json:"min0TypedefField"` + + Min10TypedefField Min10TypedefType `json:"min10TypedefField"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc_test.go new file mode 100644 index 00000000000..e82ebcb7e3d --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/doc_test.go @@ -0,0 +1,87 @@ +/* +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 + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 0, 10), + field.TooFew(field.NewPath("min10TypedefField"), 0, 10), + }) + + st.Value(&Struct{ + Min0Field: make(Min0Type, 0), + Min10Field: make(Min10Type, 0), + Min0TypedefField: make(Min0TypedefType, 0), + Min10TypedefField: make(Min10TypedefType, 0), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 0, 10), + field.TooFew(field.NewPath("min10TypedefField"), 0, 10), + }) + + st.Value(&Struct{ + Min10Field: make(Min10Type, 1), + Min10TypedefField: make(Min10TypedefType, 1), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 1, 10), + field.TooFew(field.NewPath("min10TypedefField"), 1, 10), + }) + + st.Value(&Struct{ + Min10Field: make(Min10Type, 9), + Min10TypedefField: make(Min10TypedefType, 9), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooFew(field.NewPath("min10Field"), 9, 10), + field.TooFew(field.NewPath("min10TypedefField"), 9, 10), + }) + + st.Value(&Struct{ + Min10Field: make(Min10Type, 10), + Min10TypedefField: make(Min10TypedefType, 10), + }).ExpectValid() + + testVal := &Struct{ + Min0Field: make(Min0Type, 1), + Min10Field: make(Min10Type, 11), + Min0TypedefField: make(Min0TypedefType, 1), + Min10TypedefField: make(Min10TypedefType, 11), + } + st.Value(testVal).ExpectValid() + + // Test validation ratcheting + st.Value(&Struct{ + Min0Field: make(Min0Type, 1), + Min10Field: make(Min10Type, 1), + Min0TypedefField: make(Min0TypedefType, 1), + Min10TypedefField: make(Min10TypedefType, 1), + }).OldValue(&Struct{ + Min0Field: make(Min0Type, 1), + Min10Field: make(Min10Type, 1), + Min0TypedefField: make(Min0TypedefType, 1), + Min10TypedefField: make(Min10TypedefType, 1), + }).ExpectValid() + +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/zz_generated.validations.go new file mode 100644 index 00000000000..ef980137c79 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minitems/typedef_to_slice/zz_generated.validations.go @@ -0,0 +1,139 @@ +//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_Min0Type validates an instance of Min0Type according +// to declarative validation rules in the API schema. +func Validate_Min0Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Min0Type) (errs field.ErrorList) { + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 0)...) + + return errs +} + +// Validate_Min0TypedefType validates an instance of Min0TypedefType according +// to declarative validation rules in the API schema. +func Validate_Min0TypedefType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Min0TypedefType) (errs field.ErrorList) { + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 0)...) + + return errs +} + +// Validate_Min10Type validates an instance of Min10Type according +// to declarative validation rules in the API schema. +func Validate_Min10Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Min10Type) (errs field.ErrorList) { + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 10)...) + + return errs +} + +// Validate_Min10TypedefType validates an instance of Min10TypedefType according +// to declarative validation rules in the API schema. +func Validate_Min10TypedefType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj Min10TypedefType) (errs field.ErrorList) { + errs = append(errs, validate.MinItems(ctx, op, fldPath, obj, oldObj, 10)...) + + 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.Min0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Min0Type, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Min0Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("min0Field"), obj.Min0Field, safe.Field(oldObj, func(oldObj *Struct) Min0Type { return oldObj.Min0Field }), oldObj != nil)...) + + // field Struct.Min10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Min10Type, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Min10Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("min10Field"), obj.Min10Field, safe.Field(oldObj, func(oldObj *Struct) Min10Type { return oldObj.Min10Field }), oldObj != nil)...) + + // field Struct.Min0TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Min0TypedefType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Min0TypedefType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("min0TypedefField"), obj.Min0TypedefField, safe.Field(oldObj, func(oldObj *Struct) Min0TypedefType { return oldObj.Min0TypedefField }), oldObj != nil)...) + + // field Struct.Min10TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj Min10TypedefType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Min10TypedefType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("min10TypedefField"), obj.Min10TypedefField, safe.Field(oldObj, func(oldObj *Struct) Min10TypedefType { return oldObj.Min10TypedefField }), oldObj != nil)...) + + 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 68cabb9a4e3..79e7a86715d 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 @@ -26,6 +26,7 @@ import ( ) const ( + minItemsTagName = "k8s:minItems" maxItemsTagName = "k8s:maxItems" minimumTagName = "k8s:minimum" maximumTagName = "k8s:maximum" @@ -35,6 +36,7 @@ const ( ) func init() { + RegisterTagValidator(minItemsTagValidator{}) RegisterTagValidator(maxItemsTagValidator{}) RegisterTagValidator(minimumTagValidator{}) RegisterTagValidator(maximumTagValidator{}) @@ -132,11 +134,11 @@ func (maxBytesTagValidator) GetValidations(context Context, tag codetags.Tag) (V return result, nil } -func (mltv maxBytesTagValidator) Docs() TagDoc { +func (mbtv maxBytesTagValidator) Docs() TagDoc { return TagDoc{ - Tag: mltv.TagName(), + Tag: mbtv.TagName(), StabilityLevel: TagStabilityLevelBeta, - Scopes: sets.List(mltv.ValidScopes()), + Scopes: sets.List(mbtv.ValidScopes()), Description: `Indicates that a string field has a limit on its length in bytes. This could only allow as few as N/4 multi-byte characters. If you want to limit length of characters specifically, use maxLength.`, @@ -149,6 +151,61 @@ func (mltv maxBytesTagValidator) Docs() TagDoc { } } +type minItemsTagValidator struct{} + +func (minItemsTagValidator) Init(_ Config) {} + +func (minItemsTagValidator) TagName() string { + return minItemsTagName +} + +var minItemsTagValidScopes = sets.New( + ScopeType, + ScopeField, + ScopeListVal, + ScopeMapVal, +) + +func (minItemsTagValidator) ValidScopes() sets.Set[Scope] { + return minItemsTagValidScopes +} + +var minItemsValidator = types.Name{Package: libValidationPkg, Name: "MinItems"} + +func (minItemsTagValidator) 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 := util.ParseInt(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") + } + result.AddFunction(Function(minItemsTagName, DefaultFlags, minItemsValidator, intVal)) + return result, nil +} + +func (mitv minItemsTagValidator) Docs() TagDoc { + return TagDoc{ + Tag: mitv.TagName(), + StabilityLevel: TagStabilityLevelBeta, + Scopes: mitv.ValidScopes().UnsortedList(), + Description: "Indicates that a list has a minimum size.", + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "This list must be at least X items long.", + }}, + PayloadsType: codetags.ValueTypeInt, + PayloadsRequired: true, + } +} + type maxItemsTagValidator struct{} func (maxItemsTagValidator) Init(_ Config) {} @@ -185,7 +242,7 @@ func (maxItemsTagValidator) GetValidations(context Context, tag codetags.Tag) (V if intVal < 0 { return result, fmt.Errorf("must be greater than or equal to zero") } - // Note: maxItems short-circuits other validations. + // Note: maxItems short-circuits other validations for safety. result.AddFunction(Function(maxItemsTagName, ShortCircuit, maxItemsValidator, intVal)) return result, nil }