Merge pull request #130730 from jpbetz/minimum-tag

Add +k8s:minimum validation tag
This commit is contained in:
Kubernetes Prow Robot 2025-03-11 15:59:46 -07:00 committed by GitHub
commit f3a23cfe90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 582 additions and 13 deletions

View File

@ -0,0 +1,32 @@
/*
Copyright 2025 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 constraints
// Signed is a constraint that permits any signed integer type.
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned is a constraint that permits any unsigned integer type.
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// Integer is a constraint that permits any integer type.
type Integer interface {
Signed | Unsigned
}

View File

@ -0,0 +1,29 @@
/*
Copyright 2014 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 content
import (
"fmt"
"k8s.io/apimachinery/pkg/api/validate/constraints"
)
// MinError returns a string explanation of a "must be greater than or equal"
// validation failure.
func MinError[T constraints.Integer](min T) string {
return fmt.Sprintf("must be greater than or equal to %d", min)
}

View File

@ -0,0 +1,37 @@
/*
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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate/constraints"
"k8s.io/apimachinery/pkg/api/validate/content"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// 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 {
return nil
}
if *value < min {
return field.ErrorList{field.Invalid(fldPath, *value, content.MinError(min)).WithOrigin("minimum")}
}
return nil
}

View File

@ -0,0 +1,111 @@
/*
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 validate
import (
"context"
"regexp"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate/constraints"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestMinimum(t *testing.T) {
testMinimumPositive[int](t)
testMinimumNegative[int](t)
testMinimumPositive[int8](t)
testMinimumNegative[int8](t)
testMinimumPositive[int16](t)
testMinimumNegative[int16](t)
testMinimumPositive[int32](t)
testMinimumNegative[int32](t)
testMinimumPositive[int64](t)
testMinimumNegative[int64](t)
testMinimumPositive[uint](t)
testMinimumPositive[uint8](t)
testMinimumPositive[uint16](t)
testMinimumPositive[uint32](t)
testMinimumPositive[uint64](t)
}
type minimumTestCase[T constraints.Integer] struct {
value T
min T
err string // regex
}
func testMinimumPositive[T constraints.Integer](t *testing.T) {
t.Helper()
cases := []minimumTestCase[T]{{
value: 0,
min: 0,
}, {
value: 0,
min: 1,
err: "fldpath: Invalid value.*must be greater than or equal to",
}, {
value: 1,
min: 1,
}, {
value: 1,
min: 2,
err: "fldpath: Invalid value.*must be greater than or equal to",
}}
doTestMinimum[T](t, cases)
}
func testMinimumNegative[T constraints.Signed](t *testing.T) {
t.Helper()
cases := []minimumTestCase[T]{{
value: -1,
min: -1,
}, {
value: -2,
min: -1,
err: "fldpath: Invalid value.*must be greater than or equal to",
}}
doTestMinimum[T](t, cases)
}
func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[T]) {
t.Helper()
for i, tc := range cases {
v := tc.value
result := Minimum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.min)
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))
}
}
}
}

View File

@ -0,0 +1,63 @@
/*
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 minimum
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
type Struct struct {
TypeMeta int
// +k8s:minimum=1
IntField int `json:"intField"`
// +k8s:minimum=1
IntPtrField *int `json:"intPtrField"`
// "int8" becomes "byte" somewhere in gengo. We don't need it so just skip it.
// +k8s:minimum=1
Int16Field int16 `json:"int16Field"`
// +k8s:minimum=1
Int32Field int32 `json:"int32Field"`
// +k8s:minimum=1
Int64Field int64 `json:"int64Field"`
// +k8s:minimum=1
UintField uint `json:"uintField"`
// +k8s:minimum=1
UintPtrField *uint `json:"uintPtrField"`
// +k8s:minimum=1
Uint16Field uint16 `json:"uint16Field"`
// +k8s:minimum=1
Uint32Field uint32 `json:"uint32Field"`
// +k8s:minimum=1
Uint64Field uint64 `json:"uint64Field"`
// +k8s:minimum=1
TypedefField IntType `json:"typedefField"`
// +k8s:minimum=1
TypedefPtrField *IntType `json:"typedefPtrField"`
}
// +k8s:minimum=1
type IntType int

View File

@ -0,0 +1,64 @@
/*
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 minimum
import (
"testing"
"k8s.io/apimachinery/pkg/api/validate/content"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func Test(t *testing.T) {
st := localSchemeBuilder.Test(t)
st.Value(&Struct{
// all zero values
IntPtrField: ptr.To(0),
UintPtrField: ptr.To(uint(0)),
TypedefPtrField: ptr.To(IntType(0)),
}).ExpectInvalid(
field.Invalid(field.NewPath("intField"), 0, content.MinError(1)),
field.Invalid(field.NewPath("intPtrField"), 0, content.MinError(1)),
field.Invalid(field.NewPath("int16Field"), 0, content.MinError(1)),
field.Invalid(field.NewPath("int32Field"), 0, content.MinError(1)),
field.Invalid(field.NewPath("int64Field"), 0, content.MinError(1)),
field.Invalid(field.NewPath("uintField"), uint(0), content.MinError(1)),
field.Invalid(field.NewPath("uintPtrField"), uint(0), content.MinError(1)),
field.Invalid(field.NewPath("uint16Field"), uint(0), content.MinError(1)),
field.Invalid(field.NewPath("uint32Field"), uint(0), content.MinError(1)),
field.Invalid(field.NewPath("uint64Field"), uint(0), content.MinError(1)),
field.Invalid(field.NewPath("typedefField"), 0, content.MinError(1)),
field.Invalid(field.NewPath("typedefPtrField"), 0, content.MinError(1)),
)
st.Value(&Struct{
IntField: 1,
IntPtrField: ptr.To(1),
Int16Field: 1,
Int32Field: 1,
Int64Field: 1,
UintField: 1,
Uint16Field: 1,
Uint32Field: 1,
Uint64Field: 1,
UintPtrField: ptr.To(uint(1)),
TypedefField: IntType(1),
TypedefPtrField: ptr.To(IntType(1)),
}).ExpectValid()
}

View File

@ -0,0 +1,146 @@
//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 minimum
import (
context "context"
fmt "fmt"
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 {
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}, subresources ...string) field.ErrorList {
if len(subresources) == 0 {
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, subresources: %v", obj, subresources))}
})
return nil
}
func Validate_IntType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) {
// type IntType
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return errs
}
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.IntField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }))...)
// field Struct.IntPtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *Struct) *int { return oldObj.IntPtrField }))...)
// field Struct.Int16Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int16) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("int16Field"), &obj.Int16Field, safe.Field(oldObj, func(oldObj *Struct) *int16 { return &oldObj.Int16Field }))...)
// field Struct.Int32Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("int32Field"), &obj.Int32Field, safe.Field(oldObj, func(oldObj *Struct) *int32 { return &oldObj.Int32Field }))...)
// field Struct.Int64Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int64) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("int64Field"), &obj.Int64Field, safe.Field(oldObj, func(oldObj *Struct) *int64 { return &oldObj.Int64Field }))...)
// field Struct.UintField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *uint) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("uintField"), &obj.UintField, safe.Field(oldObj, func(oldObj *Struct) *uint { return &oldObj.UintField }))...)
// field Struct.UintPtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *uint) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("uintPtrField"), obj.UintPtrField, safe.Field(oldObj, func(oldObj *Struct) *uint { return oldObj.UintPtrField }))...)
// field Struct.Uint16Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *uint16) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("uint16Field"), &obj.Uint16Field, safe.Field(oldObj, func(oldObj *Struct) *uint16 { return &oldObj.Uint16Field }))...)
// field Struct.Uint32Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *uint32) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("uint32Field"), &obj.Uint32Field, safe.Field(oldObj, func(oldObj *Struct) *uint32 { return &oldObj.Uint32Field }))...)
// field Struct.Uint64Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *uint64) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("uint64Field"), &obj.Uint64Field, safe.Field(oldObj, func(oldObj *Struct) *uint64 { return &oldObj.Uint64Field }))...)
// field Struct.TypedefField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
errs = append(errs, Validate_IntType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("typedefField"), &obj.TypedefField, safe.Field(oldObj, func(oldObj *Struct) *IntType { return &oldObj.TypedefField }))...)
// field Struct.TypedefPtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) {
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
errs = append(errs, Validate_IntType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("typedefPtrField"), obj.TypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *IntType { return oldObj.TypedefPtrField }))...)
return errs
}

View File

@ -49,3 +49,23 @@ func isNilableType(t *types.Type) bool {
} }
return false return false
} }
func realType(t *types.Type) *types.Type {
for {
if t.Kind == types.Alias {
t = t.Underlying
} else if t.Kind == types.Pointer {
t = t.Elem
} else {
break
}
}
return t
}
func rootTypeString(src, dst *types.Type) string {
if src == dst {
return src.String()
}
return src.String() + " -> " + dst.String()
}

View File

@ -107,19 +107,6 @@ func (lttv listTypeTagValidator) GetValidations(context Context, _ []string, pay
return Validations{}, nil return Validations{}, nil
} }
func realType(t *types.Type) *types.Type {
for {
if t.Kind == types.Alias {
t = t.Underlying
} else if t.Kind == types.Pointer {
t = t.Elem
} else {
break
}
}
return t
}
func (lttv listTypeTagValidator) Docs() TagDoc { func (lttv listTypeTagValidator) Docs() TagDoc {
doc := TagDoc{ doc := TagDoc{
Tag: lttv.TagName(), Tag: lttv.TagName(),

View File

@ -0,0 +1,80 @@
/*
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 validators
import (
"fmt"
"strconv"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/gengo/v2/types"
)
const (
minimumTagName = "k8s:minimum"
)
func init() {
RegisterTagValidator(minimumTagValidator{})
}
type minimumTagValidator struct{}
func (minimumTagValidator) Init(_ Config) {}
func (minimumTagValidator) TagName() string {
return minimumTagName
}
var minimumTagValidScopes = sets.New(
ScopeAny,
)
func (minimumTagValidator) ValidScopes() sets.Set[Scope] {
return minimumTagValidScopes
}
var (
minimumValidator = types.Name{Package: libValidationPkg, Name: "Minimum"}
)
func (minimumTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
var result Validations
if t := realType(context.Type); !types.IsInteger(t) {
return result, fmt.Errorf("can only be used on integer types (%s)", rootTypeString(context.Type, t))
}
intVal, err := strconv.Atoi(payload)
if err != nil {
return result, fmt.Errorf("failed to parse tag payload as int: %w", err)
}
result.AddFunction(Function(minimumTagName, DefaultFlags, minimumValidator, intVal))
return result, nil
}
func (mtv minimumTagValidator) Docs() TagDoc {
return TagDoc{
Tag: mtv.TagName(),
Scopes: mtv.ValidScopes().UnsortedList(),
Description: "Indicates that a numeric field has a minimum value.",
Payloads: []TagPayloadDoc{{
Description: "<integer>",
Docs: "This field must be greater than or equal to x.",
}},
}
}