From 0379d829068a77d92c2ed515c621f74dc17578c2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Feb 2026 20:00:38 +0000 Subject: [PATCH] Add +k8s:alpha and +k8s:beta tags support --- .../cmd/validation-gen/validation.go | 12 ++ .../cmd/validation-gen/validators/levels.go | 117 ++++++++++++++++++ .../validation-gen/validators/validators.go | 22 ++++ 3 files changed, 151 insertions(+) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/validators/levels.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go index b4a6b51ef38..5de95734728 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go @@ -1419,6 +1419,12 @@ func emitCallsToValidators(c *generator.Context, validations []validators.Functi toGolangSourceDataLiteral(sw, c, arg) } sw.Do(")", targs) + switch v.StabilityLevel { + case validators.ValidationStabilityLevelAlpha: + sw.Do(".MarkAlpha()", nil) + case validators.ValidationStabilityLevelBeta: + sw.Do(".MarkBeta()", nil) + } } // If validation is conditional, wrap the validation function with a conditions check. @@ -1786,6 +1792,12 @@ func emitFunctionCall(sw *generator.SnippetWriter, c *generator.Context, v valid toGolangSourceDataLiteral(sw, c, arg) } sw.Do(")", nil) + switch v.StabilityLevel { + case validators.ValidationStabilityLevelAlpha: + sw.Do(".MarkAlpha()", nil) + case validators.ValidationStabilityLevelBeta: + sw.Do(".MarkBeta()", nil) + } } // getLeafTypeAndPrefixes returns the "leaf value type" for a given type, as diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/levels.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/levels.go new file mode 100644 index 00000000000..b3939c540e0 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/levels.go @@ -0,0 +1,117 @@ +/* +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 ( + "fmt" + "regexp" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/gengo/v2/codetags" +) + +var kubeVersionRegex = regexp.MustCompile(`^1\.\d+$`) + +const ( + alphaTagName = "k8s:alpha" + betaTagName = "k8s:beta" +) + +func init() { + RegisterTagValidator(&levelTagValidator{tagName: alphaTagName, level: ValidationStabilityLevelAlpha}) + RegisterTagValidator(&levelTagValidator{tagName: betaTagName, level: ValidationStabilityLevelBeta}) +} + +type levelTagValidator struct { + validator Validator + tagName string + level ValidationStabilityLevel +} + +func (ltv *levelTagValidator) Init(cfg Config) { + ltv.validator = cfg.Validator +} + +func (ltv *levelTagValidator) TagName() string { + return ltv.tagName +} + +var levelTagsValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal) + +func (levelTagValidator) ValidScopes() sets.Set[Scope] { + return levelTagsValidScopes +} + +// LateTagValidator indicates that this validator has to run AFTER the listType +// and listMapKey tags. +func (levelTagValidator) LateTagValidator() {} + +func (ltv *levelTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + if tag.ValueType != codetags.ValueTypeTag || tag.ValueTag == nil { + return Validations{}, fmt.Errorf("requires a validation tag as its value payload") + } + + if len(tag.Args) > 1 { + return Validations{}, fmt.Errorf("at most one optional kubernetes version argument is supported") + } + + var version string + if len(tag.Args) == 1 { + arg := tag.Args[0] + version = arg.Value + if !kubeVersionRegex.MatchString(version) { + return Validations{}, fmt.Errorf("invalid kubernetes version format, expected 1., got %s", version) + } + } + + context.StabilityLevel = ltv.level + validations, err := ltv.validator.ExtractTagValidations(context, *tag.ValueTag) + if err != nil { + return Validations{}, err + } + + result := Validations{} + result.Variables = append(result.Variables, validations.Variables...) + for _, fn := range validations.Functions { + f := fn + f.StabilityLevel = ltv.level + result.AddFunction(f) + } + + return result, nil +} + +func (ltv *levelTagValidator) Docs() TagDoc { + doc := TagDoc{ + Tag: ltv.TagName(), + StabilityLevel: TagStabilityLevelAlpha, + Scopes: ltv.ValidScopes().UnsortedList(), + Description: fmt.Sprintf("Marks the given payload validation as a %s validation of the handwritten validation code. An optional Kubernetes version can be specified.", ltv.level), + Args: []TagArgDoc{{ + Description: "The Kubernetes version (e.g. `1.34`) at which this validation was added.", + Type: codetags.ArgTypeString, + Name: "since", + }}, + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: fmt.Sprintf("The validation tag to evaluate as a %s validation.", ltv.level), + }}, + PayloadsType: codetags.ValueTypeTag, + PayloadsRequired: true, + } + return doc +} 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 6739358674b..14627ec2b2d 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 @@ -230,6 +230,9 @@ type Context struct { // Constants provides access to all constants of the type being // validated. Only set when Scope is ScopeType. Constants []*Constant + + // StabilityLevel indicates the stability on the corresponding validation. + StabilityLevel ValidationStabilityLevel } // Constant represents a constant value. @@ -267,6 +270,16 @@ var stabilityOrder = map[TagStabilityLevel]int{ TagStabilityLevelStable: 2, } +// Validation stability level denotes the stability of a validation. +type ValidationStabilityLevel string + +const ( + // Alpha denotes the declarative validations should be run with the handwritten validation. But the handwritten validations are the authoritative. + ValidationStabilityLevelAlpha ValidationStabilityLevel = "Alpha" + // Beta denotes the declarative validations should be run with the handwritten validation. Declarative validations are authoritative. + ValidationStabilityLevelBeta ValidationStabilityLevel = "Beta" +) + // Min returns the minimum of two stability levels, or an error if either // stability level is unknown. func (s TagStabilityLevel) Min(other TagStabilityLevel) (TagStabilityLevel, error) { @@ -527,6 +540,9 @@ type FunctionGen struct { // Comments holds optional comments that should be added to the generated // code (without the leading "//"). Comments []string + + // StabilityLevel indicates the stability level of the corresponding validation. + StabilityLevel ValidationStabilityLevel } // WithTypeArgs returns a derived FunctionGen with type arguments. @@ -552,6 +568,12 @@ func (fg FunctionGen) WithComment(comment string) FunctionGen { return fg.WithComments(comment) } +// WithStabilityLevel returns a new FunctionGen with the given stability level. +func (fg FunctionGen) WithStabilityLevel(level ValidationStabilityLevel) FunctionGen { + fg.StabilityLevel = level + return fg +} + // Variable creates a VariableGen for a given variable name and init value. func Variable(variable PrivateVar, initializer any) VariableGen { return VariableGen{