add +k8s:maxItems tag logic and tests

This commit is contained in:
Aaron Prindle
2025-09-22 20:49:27 +00:00
parent 03a1a22478
commit ef2b68388e
13 changed files with 1035 additions and 91 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: "<non-negative integer>",
Docs: "This field must be no more than X items long.",
}},
PayloadsType: codetags.ValueTypeInt,
PayloadsRequired: true,
}
}
type minimumTagValidator struct{}
func (minimumTagValidator) Init(_ Config) {}