From bccf92dc3f6476a5a027ea0d7d3584d377d8b082 Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Tue, 6 Jan 2026 22:05:38 +0000 Subject: [PATCH] Add DV support for cross field validators --- .../cmd/validation-gen/validators/native.go | 60 +++++++++++++++++++ .../cmd/validation-gen/validators/registry.go | 18 ++++++ .../cmd/validation-gen/validators/union.go | 40 +++++++++++-- .../validation-gen/validators/validators.go | 28 +++++++++ .../validation-gen/validators/zeroorone.go | 22 ++++++- 5 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/validators/native.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/native.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/native.go new file mode 100644 index 00000000000..341c05b8f72 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/native.go @@ -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, + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/registry.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/registry.go index fa37bab8b8a..dc1dab8734b 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/registry.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/registry.go @@ -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 diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/union.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/union.go index 16efda6c310..8006423531a 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/union.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/union.go @@ -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) } } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go index 7dcf4858174..6d85eb83bcc 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go @@ -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. diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/zeroorone.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/zeroorone.go index 5b0e643cef4..1c2e0d8acde 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/zeroorone.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/zeroorone.go @@ -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})