Add DV support for cross field validators

This commit is contained in:
Lalit Chauhan
2026-01-06 22:05:38 +00:00
parent c6be052768
commit bccf92dc3f
5 changed files with 162 additions and 6 deletions

View File

@@ -0,0 +1,60 @@
/*
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 validators
import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/gengo/v2/codetags"
)
// declarativeValidationNative implements the TagValidator interface for the
// +k8s:declarativeValidationNative tag.
type declarativeValidationNative struct{}
func init() {
RegisterTagValidator(&declarativeValidationNative{})
}
func (d *declarativeValidationNative) Init(cfg Config) {}
func (d *declarativeValidationNative) TagName() string {
return "k8s:declarativeValidationNative"
}
func (d *declarativeValidationNative) ValidScopes() sets.Set[Scope] {
return sets.New(ScopeField, ScopeListVal)
}
func (d *declarativeValidationNative) LateTagValidator() {}
func (d *declarativeValidationNative) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
// Mark union members as declarative if this tag is present.
// This requires union processing to have run first, so we implement LateTagValidator.
MarkUnionDeclarative(context.ParentPath.String(), context.Member)
MarkZeroOrOneOfDeclarative(context.ParentPath.String(), context.Member)
// This tag is a marker and does not generate any validations itself.
return Validations{}, nil
}
func (d *declarativeValidationNative) Docs() TagDoc {
return TagDoc{
Tag: d.TagName(),
Description: "Indicates that all validations for the field, including any on the field's type, are declarative and do not have a corresponding handwritten equivalent. This is only allowed for validations that are 'Stable'. When used, validation errors will be marked to show they originated from a declarative-only validation.",
Scopes: d.ValidScopes().UnsortedList(),
StabilityLevel: Stable,
}
}

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"slices"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -286,6 +287,23 @@ type Validator interface {
// Docs returns documentation for each known tag.
Docs() []TagDoc
// Stability returns the stability level for a given tag.
Stability(tag string) (StabilityLevel, error)
}
// Stability returns the stability level for a given tag.
func (reg *registry) Stability(tag string) (StabilityLevel, error) {
tagName := strings.TrimPrefix(tag, "+")
tv, ok := reg.tagValidators[tagName]
if !ok {
return "", fmt.Errorf("tag %q doesn't have stability level", tag)
}
if tv.Docs().StabilityLevel == "" {
return "", fmt.Errorf("tag %q doesn't have stability level", tag)
}
return tv.Docs().StabilityLevel, nil
}
// InitGlobalValidator must be called exactly once by the main application to

View File

@@ -39,6 +39,10 @@ var newUnionMember = types.Name{Package: libValidationPkg, Name: "NewUnionMember
var newUnionMembership = types.Name{Package: libValidationPkg, Name: "NewUnionMembership"}
var unionVariablePrefix = "unionMembershipFor"
// unionDefinitions stores all union definitions found by tag validators.
// Key is the struct path.
var unionDefinitions = map[string]unions{}
func init() {
// Unions are comprised of multiple tags that need to share information.
// For field-based unions: tags are on struct fields, validation is on the struct
@@ -48,11 +52,29 @@ func init() {
// key examples:
// - struct union: "MyStruct" (validation on the struct type)
// - list union: "Pipeline.Tasks" (validation on the list field)
shared := map[string]unions{}
RegisterTypeValidator(unionTypeOrFieldValidator{shared})
RegisterFieldValidator(unionTypeOrFieldValidator{shared})
RegisterTagValidator(unionDiscriminatorTagValidator{shared})
RegisterTagValidator(unionMemberTagValidator{shared})
RegisterTypeValidator(unionTypeOrFieldValidator{unionDefinitions})
RegisterFieldValidator(unionTypeOrFieldValidator{unionDefinitions})
RegisterTagValidator(unionDiscriminatorTagValidator{unionDefinitions})
RegisterTagValidator(unionMemberTagValidator{unionDefinitions})
}
// MarkUnionDeclarative marks the union containing the given member as declarative.
// parentPath is the path to the struct.
// member is the field member (for struct unions).
// fieldName is the field name (for list unions).
func MarkUnionDeclarative(parentPath string, member *types.Member) {
us, ok := unionDefinitions[parentPath]
if !ok {
return
}
for _, u := range us {
// Check field members
for _, m := range u.fieldMembers {
if m == member {
u.isDeclarative = true
}
}
}
}
type unionTypeOrFieldValidator struct {
@@ -199,6 +221,8 @@ type union struct {
// "field name" (eg: `field[{"name": "succeeded"}]`), and the value is a
// list of selection criteria.
itemMembers map[string][]ListSelectorTerm
// isDeclarative indicates that the union is declarative.
isDeclarative bool
}
type unionMember struct {
@@ -303,6 +327,9 @@ func processUnionValidations(context Context, unions unions, varPrefix string,
extraArgs := append([]any{supportVarName, discriminatorExtractor}, extractorArgs...)
fn := Function(tagName, DefaultFlags, discriminatedValidator, extraArgs...)
if u.isDeclarative {
fn.Flags |= DeclarativeNative
}
result.Functions = append(result.Functions, fn)
} else {
supportVar := Variable(supportVarName, Function(tagName, DefaultFlags, newUnionMembership, getMemberArgs(u, context, false)...))
@@ -310,6 +337,9 @@ func processUnionValidations(context Context, unions unions, varPrefix string,
extraArgs := append([]any{supportVarName}, extractorArgs...)
fn := Function(tagName, DefaultFlags, undiscriminatedValidator, extraArgs...)
if u.isDeclarative {
fn.Flags |= DeclarativeNative
}
result.Functions = append(result.Functions, fn)
}
}

View File

@@ -261,6 +261,30 @@ const (
Stable StabilityLevel = "Stable"
)
var stabilityOrder = map[StabilityLevel]int{
Alpha: 0,
Beta: 1,
Stable: 2,
}
// Min returns the minimum of two stability levels, or an error if either
// stability level is unknown.
func (s StabilityLevel) Min(other StabilityLevel) (StabilityLevel, error) {
sOrder, okS := stabilityOrder[s]
if !okS {
return "", fmt.Errorf("unknown stability level %q", s)
}
otherOrder, okOther := stabilityOrder[other]
if !okOther {
return "", fmt.Errorf("unknown stability level %q", other)
}
if sOrder < otherOrder {
return s, nil
}
return other, nil
}
// TagDoc describes a comment-tag and its usage.
type TagDoc struct {
// Tag is the tag name, without the leading '+'.
@@ -426,6 +450,10 @@ const (
// accumulated as an error, but should trigger other aspects of the failure
// path (e.g. early return when combined with ShortCircuit).
NonError
// DeclarativeNative indicates that the validation function returns an error
// list which should be marked as declarative-native.
DeclarativeNative
)
// Conditions defines what conditions must be true for a resource to be validated.

View File

@@ -26,11 +26,31 @@ import (
var zeroOrOneOfUnionValidator = types.Name{Package: libValidationPkg, Name: "ZeroOrOneOfUnion"}
var zeroOrOneOfVariablePrefix = "zeroOrOneOfMembershipFor"
// zeroOrOneOfDefinitions stores all zero-or-one-of union definitions found by tag validators.
// Key is the struct path.
var zeroOrOneOfDefinitions = map[string]unions{}
// MarkZeroOrOneOfDeclarative marks the zero-or-one-of union containing the given member as declarative.
func MarkZeroOrOneOfDeclarative(parentPath string, member *types.Member) {
us, ok := zeroOrOneOfDefinitions[parentPath]
if !ok {
return
}
for _, u := range us {
// Check field members
for _, m := range u.fieldMembers {
if m == member {
u.isDeclarative = true
}
}
}
}
func init() {
// ZeroOrOneOf unions are comprised of multiple tags, which need to share information
// between them. The tags are on struct fields, but the validation
// actually pertains to the struct itself.
shared := map[string]unions{}
shared := zeroOrOneOfDefinitions
RegisterTypeValidator(zeroOrOneOfTypeOrFieldValidator{shared})
RegisterFieldValidator(zeroOrOneOfTypeOrFieldValidator{shared})
RegisterTagValidator(zeroOrOneOfMemberTagValidator{shared})