Add DV tag '+k8s:minItems'

This commit is contained in:
Tim Hockin
2026-03-09 18:38:59 -07:00
parent 383d74ba4f
commit 91ddc8ff78
13 changed files with 910 additions and 5 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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: "<non-negative integer>",
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
}