Add constant discovery, ScopeConst and StructLiberal. Remove ScopeAny.

# Conflicts:
#	staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go

# Conflicts:
#	staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/options/zz_generated.validations.go
This commit is contained in:
Joe Betz
2025-08-18 15:57:24 -04:00
parent be361a18dd
commit ed170c1c0a
12 changed files with 281 additions and 31 deletions

View File

@@ -0,0 +1,131 @@
//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 options
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{}) 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
}
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.ObjectMeta
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *ObjectMeta) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ObjectMeta) field.ErrorList {
return validate.Subfield(ctx, op, fldPath, obj, oldObj, "xEnabledField", func(o *ObjectMeta) *string { return &o.XEnabledField }, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ObjectMeta.XEnabledField")
})
})...)
return
}(fldPath.Child("metadata"), &obj.ObjectMeta, safe.Field(oldObj, func(oldObj *Struct) *ObjectMeta { return &oldObj.ObjectMeta }))...)
// field Struct.XEnabledField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XEnabledField")
})...)
return
}(fldPath.Child("xEnabledField"), &obj.XEnabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.XEnabledField }))...)
// field Struct.XDisabledField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XDisabledField")
})...)
return
}(fldPath.Child("xDisabledField"), &obj.XDisabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.XDisabledField }))...)
// field Struct.YEnabledField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.YEnabledField")
})...)
return
}(fldPath.Child("yEnabledField"), &obj.YEnabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.YEnabledField }))...)
// field Struct.YDisabledField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.YDisabledField")
})...)
return
}(fldPath.Child("yDisabledField"), &obj.YDisabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.YDisabledField }))...)
// field Struct.XYMixedField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XYMixedField/Y")
})...)
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XYMixedField/X")
})...)
return
}(fldPath.Child("xyMixedField"), &obj.XYMixedField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.XYMixedField }))...)
return errs
}

View File

@@ -287,6 +287,9 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target {
// Create a type discoverer for all types of all inputs.
td := NewTypeDiscoverer(validator, inputToPkg)
if err := td.Init(context); err != nil {
klog.Fatalf("Error discovering constants: %v", err)
}
// Create a linter to collect errors as we go.
linter := newLinter()

View File

@@ -148,23 +148,62 @@ func (g *genValidations) GenerateType(c *generator.Context, t *types.Type, w io.
// typeDiscoverer contains fields necessary to build graphs of types.
type typeDiscoverer struct {
validator validators.Validator
inputToPkg map[string]string
initialized bool
validator validators.Validator
inputToPkg map[string]string
// constantsByType holds a map of type to constants of that type.
constantsByType map[*types.Type][]*validators.Constant
// typeNodes holds a map of gengo Type to typeNode for all of the types
// encountered during discovery.
typeNodes map[*types.Type]*typeNode
}
// NewTypeDiscoverer creates and initializes a NewTypeDiscoverer.
// NewTypeDiscoverer creates a NewTypeDiscoverer.
// Init must be called before calling DiscoverType.
func NewTypeDiscoverer(validator validators.Validator, inputToPkg map[string]string) *typeDiscoverer {
return &typeDiscoverer{
validator: validator,
inputToPkg: inputToPkg,
typeNodes: map[*types.Type]*typeNode{},
validator: validator,
inputToPkg: inputToPkg,
constantsByType: map[*types.Type][]*validators.Constant{},
typeNodes: map[*types.Type]*typeNode{},
}
}
// Init uses the generator context to prepare for type discovery.
func (td *typeDiscoverer) Init(c *generator.Context) error {
packages := c.Universe
for _, pkg := range packages {
// We only care about packages we are generating for or are readonly.
if _, ok := td.inputToPkg[pkg.Path]; !ok {
continue
}
for _, cnst := range pkg.Constants {
context := validators.Context{
Scope: validators.ScopeConst,
Type: cnst.Underlying,
Path: nil, // NA when discovering a constant
Member: nil, // NA when discovering a constant
ParentPath: nil, // NA when discovering a constant
}
tgs, err := td.validator.ExtractTags(context, cnst.CommentLines)
if err != nil {
return fmt.Errorf("constant %s: %w", cnst.Name, err)
}
if len(tgs) > 0 {
// Also check that the tgs are valid.
if _, err := td.validator.ExtractValidations(context, tgs...); err != nil {
return fmt.Errorf("constant %s: %w", cnst.Name, err)
}
}
td.constantsByType[cnst.Underlying] = append(td.constantsByType[cnst.Underlying], &validators.Constant{Constant: cnst, Tags: tgs})
}
}
td.initialized = true
return nil
}
// childNode represents a type which is used in another type (e.g. a struct
// field).
type childNode struct {
@@ -210,6 +249,9 @@ type typeNode struct {
// typeDiscoverer. If this is called multiple times for different types, the
// graphs will be will be merged.
func (td *typeDiscoverer) DiscoverType(t *types.Type) error {
if !td.initialized {
return fmt.Errorf("typeDiscoverer not initialized")
}
if t.Kind == types.Pointer {
return fmt.Errorf("type %v: pointer root-types are not supported", t)
}
@@ -349,11 +391,14 @@ func (td *typeDiscoverer) discoverType(t *types.Type, fldPath *field.Path) (*typ
if fldPath.String() != t.String() {
panic(fmt.Sprintf("path for type != the type name: %s, %s", t.String(), fldPath.String()))
}
consts, _ := td.constantsByType[t]
context := validators.Context{
Scope: validators.ScopeType,
Type: t,
ParentPath: nil,
Path: fldPath,
Member: nil, // NA when discovering a type
ParentPath: nil, // NA when discovering a type
Constants: consts,
}
extractedTags, err := td.validator.ExtractTags(context, t.CommentLines)
if err != nil {
@@ -1440,6 +1485,29 @@ func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context
sw.Do(")", nil)
}
sw.Do(" { $.$ }", v.Body)
case validators.StructLiteral:
targs := generator.Args{
"type": c.Universe.Type(v.Type),
}
sw.Do("$.type|raw$", targs)
if len(v.TypeArgs) > 0 {
sw.Do("[", nil)
for i, typeArg := range v.TypeArgs {
if i > 0 {
sw.Do(", ", nil)
}
sw.Do("$.|raw$", typeArg)
}
sw.Do("]", nil)
}
sw.Do("{\n", nil)
for _, f := range v.Fields {
sw.Do(f.Name, nil)
sw.Do(": ", nil)
toGolangSourceDataLiteral(sw, c, f.Value)
sw.Do(", ", nil)
}
sw.Do("}", targs)
default:
rv := reflect.ValueOf(value)
switch rv.Kind() {

View File

@@ -81,7 +81,7 @@ func init() {
}
// This applies to all tags in this file.
var listTagsValidScopes = sets.New(ScopeAny)
var listTagsValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
// listMetadata collects information about a single list with map or set semantics.
type listMetadata struct {

View File

@@ -42,7 +42,7 @@ func (neqTagValidator) TagName() string {
return neqTagName
}
var neqTagValidScopes = sets.New(ScopeAny)
var neqTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
func (neqTagValidator) ValidScopes() sets.Set[Scope] {
return neqTagValidScopes

View File

@@ -58,7 +58,7 @@ func (itemTagValidator) TagName() string {
return itemTagName
}
var itemTagValidScopes = sets.New(ScopeAny)
var itemTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
func (itemTagValidator) ValidScopes() sets.Set[Scope] {
return itemTagValidScopes

View File

@@ -42,9 +42,7 @@ func (minimumTagValidator) TagName() string {
return minimumTagName
}
var minimumTagValidScopes = sets.New(
ScopeAny,
)
var minimumTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
func (minimumTagValidator) ValidScopes() sets.Set[Scope] {
return minimumTagValidScopes

View File

@@ -38,7 +38,7 @@ func (opaqueTypeTagValidator) TagName() string {
}
func (opaqueTypeTagValidator) ValidScopes() sets.Set[Scope] {
return sets.New(ScopeAny)
return sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
}
func (opaqueTypeTagValidator) GetValidations(_ Context, _ codetags.Tag) (Validations, error) {

View File

@@ -152,7 +152,8 @@ func (reg *registry) ExtractValidations(context Context, tags ...codetags.Tag) (
for _, tags := range phases {
for _, tag := range tags {
tv := reg.tagValidators[tag.Name]
if scopes := tv.ValidScopes(); !scopes.Has(context.Scope) && !scopes.Has(ScopeAny) {
// At this point we know tv exists and is not nil due to the upfront check
if scopes := tv.ValidScopes(); !scopes.Has(context.Scope) {
return Validations{}, fmt.Errorf("tag %q cannot be specified on %s", tv.TagName(), context.Scope)
}
if err := typeCheck(tag, tv.Docs()); err != nil {

View File

@@ -45,7 +45,7 @@ func (subfieldTagValidator) TagName() string {
return subfieldTagName
}
var subfieldTagValidScopes = sets.New(ScopeAny)
var subfieldTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
func (subfieldTagValidator) ValidScopes() sets.Set[Scope] {
return subfieldTagValidScopes

View File

@@ -56,7 +56,7 @@ func (frtv fixedResultTagValidator) TagName() string {
return validateFalseTagName
}
var fixedResultTagValidScopes = sets.New(ScopeAny)
var fixedResultTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
func (fixedResultTagValidator) ValidScopes() sets.Set[Scope] {
return fixedResultTagValidScopes

View File

@@ -152,11 +152,6 @@ type Scope string
// Note: All of these values should be strings which can be used in an error
// message such as "may not be used in %s".
const (
// ScopeAny indicates that a validator may be use in any context. This value
// should never appear in a Context struct, since that indicates a
// specific use.
ScopeAny Scope = "anywhere"
// ScopeType indicates a validation on a type definition, which applies to
// all instances of that type.
ScopeType Scope = "type definitions"
@@ -177,6 +172,9 @@ const (
// field or type.
ScopeMapVal Scope = "map values"
// ScopeConst indicates a validation which applies to constant values only.
ScopeConst Scope = "constant values"
// TODO: It's not clear if we need to distinguish (e.g.) list values of
// fields from list values of typedefs. We could make {type,field} be
// orthogonal to {scalar, list, list-value, map, map-key, map-value} (and
@@ -195,22 +193,58 @@ type Context struct {
// this is the field's type (which may be a pointer, an alias, or both).
// When Scope indicates a list-value, map-key, or map-value, this is the
// type of that key or value (which, again, may be a pointer, and alias, or
// both).
// both). When Scope is ScopeConst this is the constant's type.
Type *types.Type
// ParentPath provides the field path to the parent type or field, enabling
// unique identification of validation contexts for the same type in
// different locations.
ParentPath *field.Path
// Path provides a path to the type or field being validated. This is
// useful for identifying an exact context, e.g. to track information
// between related tags. When Scope is ScopeType, this is the Go package
// path and type name (e.g. "k8s.io/api/core/v1.Pod"). When Scope is
// ScopeField, this is the field path (e.g. "spec.containers[*].image").
// When Scope indicates a list-value, map-key, or map-value, this is the
// type or field path, as described above, with a suffix indicating
// that it refers to the keys or values. For ScopeConst, this will be nil.
Path *field.Path
// Member provides details about a field within a struct when Scope is
// ScopeField. For all other values of Scope, this will be nil.
Member *types.Member
// Path provides the field path to the type or field being validated. This
// is useful for identifying an exact context, e.g. to track information
// between related tags.
Path *field.Path
// ListSelector provides a list of key-value pairs that represent criteria
// for selecting one or more items from a list. When Scope is
// ScopeListVal, this will be non-nil. An empty selector means that
// all items in the list should be selected. For all other values of
// Scope, this will be nil.
ListSelector []ListSelectorTerm
// ParentPath provides a path to the parent type or field of the object
// being validated, when applicable. enabling unique identification of
// validation contexts for the same type in different locations. When
// Scope is ScopeField, this is the path to the containing struct type or
// field (depending on where the validation tag was sepcified). When Scope
// indicates a list-value, map-key, or map-value, this is the path to the
// list or map type or field (depending on where the validation tag was
// specified). When Scope is ScopeType, this is nil.
ParentPath *field.Path
// Constants provides access to all constants of the type being
// validated. Only set when Scope is ScopeType.
Constants []*Constant
}
// Constant represents a constant value.
type Constant struct {
Constant *types.Type
Tags []codetags.Tag
}
// ListSelectorTerm represents a field name and value pair.
type ListSelectorTerm struct {
// Field is the JSON name of the field to match.
Field string
// Value is the value to match. This must be a primitive type which can
// be used as list-map keys: string, int, or bool.
Value any
}
// TagDoc describes a comment-tag and its usage.
@@ -499,6 +533,21 @@ type FunctionLiteral struct {
Body string
}
// StructLiteral represents a struct literal expression that can be used as
// an argument to a validator.
type StructLiteral struct {
// Type is the type of the struct literal to be generated.
Type types.Name
// TypeArgs are the generic type arguments for the struct type.
TypeArgs []*types.Type
Fields []StructLiteralField
}
type StructLiteralField struct {
Name string
Value any
}
// ParamResult represents a parameter or a result of a function.
type ParamResult struct {
Name string