Merge pull request #132823 from yongruilin/master_vg_enum

feat(validation-gen): add k8s:enum validators
This commit is contained in:
Kubernetes Prow Robot
2025-07-09 17:53:35 -07:00
committed by GitHub
6 changed files with 633 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
/*
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"
"slices"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Enum verifies that the specified value is one of the valid symbols.
// This is for string enums only.
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, symbols sets.Set[T]) field.ErrorList {
if value == nil {
return nil
}
if !symbols.Has(*value) {
symbolList := symbols.UnsortedList()
slices.Sort(symbolList)
return field.ErrorList{field.NotSupported[T](fldPath, *value, symbolList)}
}
return nil
}

View File

@@ -0,0 +1,107 @@
/*
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"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestEnum(t *testing.T) {
cases := []struct {
value string
valid sets.Set[string]
err bool
}{{
value: "a",
valid: sets.New("a", "b", "c"),
err: false,
}, {
value: "x",
valid: sets.New("c", "a", "b"),
err: true,
}}
for i, tc := range cases {
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
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", i)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if want, got := `supported values: "a", "b", "c"`, result[0].Detail; got != want {
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
}
}
}
}
func TestEnumTypedef(t *testing.T) {
type StringType string
const (
NotStringFoo StringType = "foo"
NotStringBar StringType = "bar"
NotStringQux StringType = "qux"
)
cases := []struct {
value StringType
valid sets.Set[StringType]
err bool
}{{
value: "foo",
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
err: false,
}, {
value: "x",
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
err: true,
}}
for i, tc := range cases {
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
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", i)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if want, got := `supported values: "bar", "foo", "qux"`, result[0].Detail; got != want {
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
}
}
}
}

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.
*/
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package.
package enum
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
type Struct struct {
TypeMeta int
Enum0Field Enum0 `json:"enum0Field"`
Enum0PtrField *Enum0 `json:"enum0PtrField"`
Enum1Field Enum1 `json:"enum1Field"`
Enum1PtrField *Enum1 `json:"enum1PtrField"`
Enum2Field Enum2 `json:"enum2Field"`
Enum2PtrField *Enum2 `json:"enum2PtrField"`
NotEnumField NotEnum `json:"notEnumField"`
NotEnumPtrField *NotEnum `json:"notEnumPtrField"`
}
// +k8s:enum
type Enum0 string // Note: this enum has no values
// +k8s:enum
type Enum1 string // Note: this enum has 1 value
const (
E1V1 Enum1 = "e1v1"
)
// +k8s:enum
type Enum2 string // Note: this enum has 2 values
const (
E2V1 Enum2 = "e2v1"
E2V2 Enum2 = "e2v2"
)
// Note: this is not an enum because the const values are of type Enum2, and
// because go elides intermediate typedefs (this is modelled as "NotEnum" ->
// "string" in the AST).
type NotEnum Enum2

View File

@@ -0,0 +1,67 @@
/*
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 enum
import (
"testing"
"k8s.io/utils/ptr"
)
func Test(t *testing.T) {
st := localSchemeBuilder.Test(t)
st.Value(&Struct{
// All zero vals
}).ExpectRegexpsByPath(map[string][]string{
"enum0Field": {"Unsupported value: \"\"$"},
"enum1Field": {"Unsupported value: \"\": supported values: \"e1v1\""},
"enum2Field": {"Unsupported value: \"\": supported values: \"e2v1\", \"e2v2\""},
})
st.Value(&Struct{
Enum0Field: "", // no valid value exists
Enum0PtrField: ptr.To(Enum0("")), // no valid value exists
Enum1Field: E1V1,
Enum1PtrField: ptr.To(E1V1),
Enum2Field: E2V1,
Enum2PtrField: ptr.To(E2V1),
NotEnumField: "x",
NotEnumPtrField: ptr.To(NotEnum("x")),
}).ExpectRegexpsByPath(map[string][]string{
"enum0Field": {"Unsupported value: \"\"$"},
"enum0PtrField": {"Unsupported value: \"\"$"},
})
st.Value(&Struct{
Enum0Field: "x", // no valid value exists
Enum0PtrField: ptr.To(Enum0("x")), // no valid value exists
Enum1Field: "x",
Enum1PtrField: ptr.To(Enum1("x")),
Enum2Field: "x",
Enum2PtrField: ptr.To(Enum2("x")),
NotEnumField: "x",
NotEnumPtrField: ptr.To(NotEnum("x")),
}).ExpectRegexpsByPath(map[string][]string{
"enum0Field": {"Unsupported value: \"x\"$"},
"enum0PtrField": {"Unsupported value: \"x\"$"},
"enum1Field": {"Unsupported value: \"x\": supported values: \"e1v1\""},
"enum1PtrField": {"Unsupported value: \"x\": supported values: \"e1v1\""},
"enum2Field": {"Unsupported value: \"x\": supported values: \"e2v1\", \"e2v2\""},
"enum2PtrField": {"Unsupported value: \"x\": supported values: \"e2v1\", \"e2v2\""},
})
}

View File

@@ -0,0 +1,135 @@
//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 enum
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"
sets "k8s.io/apimachinery/pkg/util/sets"
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{}) 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
}
var symbolsForEnum0 = sets.New[Enum0]()
func Validate_Enum0(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum0) (errs field.ErrorList) {
// type Enum0
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum0)...)
return errs
}
var symbolsForEnum1 = sets.New[Enum1](E1V1)
func Validate_Enum1(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
// type Enum1
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum1)...)
return errs
}
var symbolsForEnum2 = sets.New[Enum2](E2V1, E2V2)
func Validate_Enum2(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
// type Enum2
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum2)...)
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.Enum0Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *Enum0) (errs field.ErrorList) {
errs = append(errs, Validate_Enum0(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("enum0Field"), &obj.Enum0Field, safe.Field(oldObj, func(oldObj *Struct) *Enum0 { return &oldObj.Enum0Field }))...)
// field Struct.Enum0PtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *Enum0) (errs field.ErrorList) {
errs = append(errs, Validate_Enum0(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("enum0PtrField"), obj.Enum0PtrField, safe.Field(oldObj, func(oldObj *Struct) *Enum0 { return oldObj.Enum0PtrField }))...)
// field Struct.Enum1Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
errs = append(errs, Validate_Enum1(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("enum1Field"), &obj.Enum1Field, safe.Field(oldObj, func(oldObj *Struct) *Enum1 { return &oldObj.Enum1Field }))...)
// field Struct.Enum1PtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
errs = append(errs, Validate_Enum1(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("enum1PtrField"), obj.Enum1PtrField, safe.Field(oldObj, func(oldObj *Struct) *Enum1 { return oldObj.Enum1PtrField }))...)
// field Struct.Enum2Field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
errs = append(errs, Validate_Enum2(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("enum2Field"), &obj.Enum2Field, safe.Field(oldObj, func(oldObj *Struct) *Enum2 { return &oldObj.Enum2Field }))...)
// field Struct.Enum2PtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
errs = append(errs, Validate_Enum2(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("enum2PtrField"), obj.Enum2PtrField, safe.Field(oldObj, func(oldObj *Struct) *Enum2 { return oldObj.Enum2PtrField }))...)
// field Struct.NotEnumField has no validation
// field Struct.NotEnumPtrField has no validation
return errs
}

View File

@@ -0,0 +1,220 @@
/*
Copyright 2021 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 (
"cmp"
"fmt"
"slices"
"sort"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/code-generator/cmd/validation-gen/util"
"k8s.io/gengo/v2/codetags"
"k8s.io/gengo/v2/generator"
"k8s.io/gengo/v2/types"
)
const enumTagName = "k8s:enum"
func init() {
RegisterTagValidator(&enumTagValidator{})
}
type enumTagValidator struct {
enumContext *enumContext
}
func (etv *enumTagValidator) Init(cfg Config) {
etv.enumContext = newEnumContext(cfg.GengoContext)
}
func (enumTagValidator) TagName() string {
return enumTagName
}
var enumTagValidScopes = sets.New(ScopeType)
func (enumTagValidator) ValidScopes() sets.Set[Scope] {
return enumTagValidScopes
}
var (
enumValidator = types.Name{Package: libValidationPkg, Name: "Enum"}
)
var setsNew = types.Name{Package: "k8s.io/apimachinery/pkg/util/sets", Name: "New"}
func (etv *enumTagValidator) GetValidations(context Context, _ codetags.Tag) (Validations, error) {
// NOTE: typedefs to pointers are not supported, so we should never see a pointer here.
if t := util.NativeType(context.Type); t != types.String {
return Validations{}, fmt.Errorf("can only be used on string types (%s)", rootTypeString(context.Type, t))
}
var result Validations
if enum, ok := etv.enumContext.EnumType(context.Type); ok {
// TODO: Avoid the "local" here. This was added to to avoid errors caused when the package is an empty string.
// The correct package would be the output package but is not known here. This does not show up in generated code.
// TODO: Append a consistent hash suffix to avoid generated name conflicts?
supportVarName := PrivateVar{Name: "SymbolsFor" + context.Type.Name.Name, Package: "local"}
supportVar := Variable(supportVarName, Function(enumTagName, DefaultFlags, setsNew, enum.ValueArgs()...).WithTypeArgs(enum.Name))
result.AddVariable(supportVar)
fn := Function(enumTagName, DefaultFlags, enumValidator, supportVarName)
result.AddFunction(fn)
}
return result, nil
}
func (etv *enumTagValidator) Docs() TagDoc {
return TagDoc{
Tag: etv.TagName(),
Scopes: etv.ValidScopes().UnsortedList(),
Description: "Indicates that a string type is an enum. All const values of this type are considered values in the enum.",
}
}
func (et *enumType) ValueArgs() []any {
var values []any
for _, value := range et.SymbolConstants() {
values = append(values, value)
}
return values
}
func (et *enumType) SymbolConstants() []Identifier {
var values []Identifier
for _, value := range et.Values {
values = append(values, Identifier(value.Name))
}
slices.SortFunc(values, func(a, b Identifier) int {
return cmp.Compare(a.Name, b.Name)
})
return values
}
// TODO: Everything below this comment is copied from kube-openapi's enum.go.
type enumValue struct {
Name types.Name
Value string
Comment string
}
type enumType struct {
Name types.Name
Values []*enumValue
}
// enumMap is a map from the name to the matching enum type.
type enumMap map[types.Name]*enumType
type enumContext struct {
enumTypes enumMap
}
func newEnumContext(c *generator.Context) *enumContext {
return &enumContext{enumTypes: parseEnums(c)}
}
// EnumType checks and finds the enumType for a given type.
// If the given type is a known enum type, returns the enumType, true
// Otherwise, returns nil, false
func (ec *enumContext) EnumType(t *types.Type) (enum *enumType, isEnum bool) {
// if t is a pointer, use its underlying type instead
if t.Kind == types.Pointer {
t = t.Elem
}
enum, ok := ec.enumTypes[t.Name]
return enum, ok
}
// ValueStrings returns all possible values of the enum type as strings
// the results are sorted and quoted as Go literals.
func (et *enumType) ValueStrings() []string {
var values []string
for _, value := range et.Values {
// use "%q" format to generate a Go literal of the string const value
values = append(values, fmt.Sprintf("%q", value.Value))
}
sort.Strings(values)
return values
}
func parseEnums(c *generator.Context) enumMap {
// find all enum types.
enumTypes := make(enumMap)
for _, p := range c.Universe {
for _, t := range p.Types {
if isEnumType(t) {
if _, ok := enumTypes[t.Name]; !ok {
enumTypes[t.Name] = &enumType{
Name: t.Name,
}
}
}
}
}
// find all enum values from constants, and try to match each with its type.
for _, p := range c.Universe {
for _, c := range p.Constants {
enumType := c.Underlying
if _, ok := enumTypes[enumType.Name]; ok {
value := &enumValue{
Name: c.Name,
Value: *c.ConstValue,
Comment: strings.Join(c.CommentLines, " "),
}
enumTypes[enumType.Name].addIfNotPresent(value)
}
}
}
return enumTypes
}
func (et *enumType) addIfNotPresent(value *enumValue) {
// If we already have an enum case with the same value, then ignore this new
// one. This can happen if an enum aliases one from another package and
// re-exports the cases.
for i, existing := range et.Values {
if existing.Value == value.Value {
// Take the value of the longer comment (or some other deterministic tie breaker)
if len(existing.Comment) < len(value.Comment) || (len(existing.Comment) == len(value.Comment) && existing.Comment > value.Comment) {
et.Values[i] = value
}
return
}
}
et.Values = append(et.Values, value)
}
// isEnumType checks if a given type is an enum by the definition
// An enum type should be an alias of string and has tag '+enum' in its comment.
// Additionally, pass the type of builtin 'string' to check against.
func isEnumType(t *types.Type) bool {
return t.Kind == types.Alias && t.Underlying == types.String && hasEnumTag(t)
}
func hasEnumTag(t *types.Type) bool {
return codetags.Extract("+", t.CommentLines)[enumTagName] != nil
}