Introduce validation-gen

Adds code-generator/cmd/validation-gen. This provides the machinery
to discover `//+` tags in types.go files, register plugins to handle
the tags, and generate validation code.

Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>
This commit is contained in:
Joe Betz 2025-03-03 09:49:50 -05:00
parent 7f5e1baeee
commit c1f9e6b8ee
13 changed files with 3792 additions and 0 deletions

View File

@ -61,6 +61,7 @@
- k8s.io/code-generator
- k8s.io/kube-openapi
- k8s.io/klog
- k8s.io/utils/ptr
- baseImportPath: "./staging/src/k8s.io/component-base"
allowedImports:

View File

@ -0,0 +1,56 @@
/*
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 operation
import "k8s.io/apimachinery/pkg/util/sets"
// Operation provides contextual information about a validation request and the API
// operation being validated.
// This type is intended for use with generate validation code and may be enhanced
// in the future to include other information needed to validate requests.
type Operation struct {
// Type is the category of operation being validated. This does not
// differentiate between HTTP verbs like PUT and PATCH, but rather merges
// those into a single "Update" category.
Type Type
// Options declare the options enabled for validation.
//
// Options should be set according to a resource validation strategy before validation
// is performed, and must be treated as read-only during validation.
//
// Options are identified by string names. Option string names may match the name of a feature
// gate, in which case the presence of the name in the set indicates that the feature is
// considered enabled for the resource being validated. Note that a resource may have a
// feature enabled even when the feature gate is disabled. This can happen when feature is
// already in-use by a resource, often because the feature gate was enabled when the
// resource first began using the feature.
//
// Unset options are disabled/false.
Options sets.Set[string]
}
// Code is the request operation to be validated.
type Type uint32
const (
// Create indicates the request being validated is for a resource create operation.
Create Type = iota
// Update indicates the request being validated is for a resource update operation.
Update
)

View File

@ -0,0 +1,37 @@
/*
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 safe
// Field takes a pointer to any value (which may or may not be nil) and
// a function that traverses to a target type R (a typical use case is to dereference a field),
// and returns the result of the traversal, or the zero value of the target type.
// This is roughly equivalent to "value != nil ? fn(value) : zero-value" in languages that support the ternary operator.
func Field[V any, R any](value *V, fn func(*V) R) R {
if value == nil {
var zero R
return zero
}
o := fn(value)
return o
}
// Cast takes any value, attempts to cast it to T, and returns the T value if
// the cast is successful, or else the zero value of T.
func Cast[T any](value any) T {
result, _ := value.(T)
return result
}

View File

@ -0,0 +1,28 @@
/*
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"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ValidateFunc is a function that validates a value, possibly considering the
// old value (if any).
type ValidateFunc[T any] func(ctx context.Context, op operation.Operation, fldPath *field.Path, newValue, oldValue T) field.ErrorList

View File

@ -0,0 +1,160 @@
/*
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 main
import (
"fmt"
"strings"
"k8s.io/gengo/v2/types"
"k8s.io/klog/v2"
)
// linter is a struct that holds the state of the linting process.
// It contains a map of types that have been linted, a list of linting rules,
// and a list of errors that occurred during the linting process.
type linter struct {
linted map[*types.Type]bool
rules []lintRule
// lintErrors is a list of errors that occurred during the linting process.
// lintErrors would be in the format:
// field <field_name>: <lint broken message>
// type <type_name>: <lint broken message>
lintErrors []error
}
var defaultRules = []lintRule{
ruleOptionalAndRequired,
ruleRequiredAndDefault,
}
func (l *linter) AddError(field, msg string) {
l.lintErrors = append(l.lintErrors, fmt.Errorf("%s: %s", field, msg))
}
func newLinter(rules ...lintRule) *linter {
if len(rules) == 0 {
rules = defaultRules
}
return &linter{
linted: make(map[*types.Type]bool),
rules: rules,
lintErrors: []error{},
}
}
func (l *linter) lintType(t *types.Type) error {
if _, ok := l.linted[t]; ok {
return nil
}
l.linted[t] = true
if t.CommentLines != nil {
klog.V(5).Infof("linting type %s", t.Name.String())
lintErrs, err := l.lintComments(t.CommentLines)
if err != nil {
return err
}
for _, lintErr := range lintErrs {
l.AddError("type "+t.Name.String(), lintErr)
}
}
switch t.Kind {
case types.Alias:
// Recursively lint the underlying type of the alias.
if err := l.lintType(t.Underlying); err != nil {
return err
}
case types.Struct:
// Recursively lint each member of the struct.
for _, member := range t.Members {
klog.V(5).Infof("linting comments for field %s of type %s", member.String(), t.Name.String())
lintErrs, err := l.lintComments(member.CommentLines)
if err != nil {
return err
}
for _, lintErr := range lintErrs {
l.AddError("type "+t.Name.String(), lintErr)
}
if err := l.lintType(member.Type); err != nil {
return err
}
}
case types.Slice, types.Array, types.Pointer:
// Recursively lint the element type of the slice or array.
if err := l.lintType(t.Elem); err != nil {
return err
}
case types.Map:
// Recursively lint the key and element types of the map.
if err := l.lintType(t.Key); err != nil {
return err
}
if err := l.lintType(t.Elem); err != nil {
return err
}
}
return nil
}
// lintRule is a function that validates a slice of comments.
// It returns a string as an error message if the comments are invalid,
// and an error there is an error happened during the linting process.
type lintRule func(comments []string) (string, error)
// lintComments runs all registered rules on a slice of comments.
func (l *linter) lintComments(comments []string) ([]string, error) {
var lintErrs []string
for _, rule := range l.rules {
if msg, err := rule(comments); err != nil {
return nil, err
} else if msg != "" {
lintErrs = append(lintErrs, msg)
}
}
return lintErrs, nil
}
// conflictingTagsRule checks for conflicting tags in a slice of comments.
func conflictingTagsRule(comments []string, tags ...string) (string, error) {
if len(tags) < 2 {
return "", fmt.Errorf("at least two tags must be provided")
}
tagCount := make(map[string]bool)
for _, comment := range comments {
for _, tag := range tags {
if strings.HasPrefix(comment, tag) {
tagCount[tag] = true
}
}
}
if len(tagCount) > 1 {
return fmt.Sprintf("conflicting tags: {%s}", strings.Join(tags, ", ")), nil
}
return "", nil
}
// ruleOptionalAndRequired checks for conflicting tags +k8s:optional and +k8s:required in a slice of comments.
func ruleOptionalAndRequired(comments []string) (string, error) {
return conflictingTagsRule(comments, "+k8s:optional", "+k8s:required")
}
// ruleRequiredAndDefault checks for conflicting tags +k8s:required and +default in a slice of comments.
func ruleRequiredAndDefault(comments []string) (string, error) {
return conflictingTagsRule(comments, "+k8s:required", "+default")
}

View File

@ -0,0 +1,412 @@
/*
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 main
import (
"errors"
"testing"
"k8s.io/gengo/v2/types"
)
func ruleAlwaysPass(comments []string) (string, error) {
return "", nil
}
func ruleAlwaysFail(comments []string) (string, error) {
return "lintfail", nil
}
func ruleAlwaysErr(comments []string) (string, error) {
return "", errors.New("linterr")
}
func mkCountRule(counter *int, realRule lintRule) lintRule {
return func(comments []string) (string, error) {
(*counter)++
return realRule(comments)
}
}
func TestLintCommentsRuleInvocation(t *testing.T) {
tests := []struct {
name string
rules []lintRule
commentLineGroups [][]string
wantErr bool
wantCount int
}{
{
name: "0 rules, 0 comments",
rules: []lintRule{},
commentLineGroups: [][]string{},
wantErr: false,
wantCount: 0,
},
{
name: "1 rule, 1 comment",
rules: []lintRule{ruleAlwaysPass},
commentLineGroups: [][]string{{"comment"}},
wantErr: false,
wantCount: 1,
},
{
name: "3 rules, 3 comments",
rules: []lintRule{ruleAlwaysPass, ruleAlwaysFail, ruleAlwaysErr},
commentLineGroups: [][]string{{"comment1"}, {"comment2"}, {"comment3"}},
wantErr: true,
wantCount: 9,
},
{
name: "1 rule, 1 comment, rule fails",
rules: []lintRule{ruleAlwaysFail},
commentLineGroups: [][]string{{"comment"}},
wantErr: false,
wantCount: 1,
},
{
name: "1 rule, 1 comment, rule errors",
rules: []lintRule{ruleAlwaysErr},
commentLineGroups: [][]string{{"comment"}},
wantErr: true,
wantCount: 1,
},
{
name: "3 rules, 1 comment, rule errors in the middle",
rules: []lintRule{ruleAlwaysPass, ruleAlwaysErr, ruleAlwaysFail},
commentLineGroups: [][]string{{"comment"}},
wantErr: true,
wantCount: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
counter := 0
rules := make([]lintRule, len(tt.rules))
for i, rule := range tt.rules {
rules[i] = mkCountRule(&counter, rule)
}
l := newLinter(rules...)
for _, commentLines := range tt.commentLineGroups {
_, err := l.lintComments(commentLines)
gotErr := err != nil
if gotErr != tt.wantErr {
t.Errorf("lintComments() error = %v, wantErr %v", err, tt.wantErr)
}
}
if counter != tt.wantCount {
t.Errorf("expected %d rule invocations, got %d", tt.wantCount, counter)
}
})
}
}
func TestRuleOptionalAndRequired(t *testing.T) {
tests := []struct {
name string
comments []string
wantMsg string
wantErr bool
}{
{
name: "no comments",
comments: []string{},
wantMsg: "",
},
{
name: "only optional",
comments: []string{"+k8s:optional"},
wantMsg: "",
},
{
name: "only required",
comments: []string{"+k8s:required"},
wantMsg: "",
},
{
name: "optional and required",
comments: []string{"+k8s:optional", "+k8s:required"},
wantMsg: "conflicting tags: {+k8s:optional, +k8s:required}",
},
{
name: "optional, empty, required",
comments: []string{"+k8s:optional", "", "+k8s:required"},
wantMsg: "conflicting tags: {+k8s:optional, +k8s:required}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg, _ := ruleOptionalAndRequired(tt.comments)
if msg != tt.wantMsg {
t.Errorf("ruleOptionalAndRequired() msg = %v, wantMsg %v", msg, tt.wantMsg)
}
})
}
}
func TestRuleRequiredAndDefault(t *testing.T) {
tests := []struct {
name string
comments []string
wantMsg string
}{
{
name: "no comments",
comments: []string{},
wantMsg: "",
},
{
name: "only required",
comments: []string{"+k8s:required"},
wantMsg: "",
},
{
name: "only default",
comments: []string{"+default=somevalue"},
wantMsg: "",
},
{
name: "required and default",
comments: []string{"+k8s:required", "+default=somevalue"},
wantMsg: "conflicting tags: {+k8s:required, +default}",
},
{
name: "required, empty, default",
comments: []string{"+k8s:required", "", "+default=somevalue"},
wantMsg: "conflicting tags: {+k8s:required, +default}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg, _ := ruleRequiredAndDefault(tt.comments)
if msg != tt.wantMsg {
t.Errorf("ruleRequiredAndDefault() msg = %v, wantMsg %v", msg, tt.wantMsg)
}
})
}
}
func TestConflictingTagsRule(t *testing.T) {
tests := []struct {
name string
comments []string
tags []string
wantMsg string
wantErr bool
}{
{
name: "no comments",
comments: []string{},
tags: []string{"+tag1", "+tag2"},
wantMsg: "",
},
{
name: "only tag1",
comments: []string{"+tag1"},
tags: []string{"+tag1", "+tag2"},
wantMsg: "",
},
{
name: "tag1, empty, tag2",
comments: []string{"+tag1", "", "+tag2"},
tags: []string{"+tag1", "+tag2"},
wantMsg: "conflicting tags: {+tag1, +tag2}",
},
{
name: "3 tags",
comments: []string{"tag1", "+tag2", "+tag3=value"},
tags: []string{"+tag1", "+tag2", "+tag3"},
wantMsg: "conflicting tags: {+tag1, +tag2, +tag3}",
},
{
name: "less than 2 tags",
comments: []string{"+tag1"},
tags: []string{"+tag1"},
wantMsg: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg, err := conflictingTagsRule(tt.comments, tt.tags...)
if (err != nil) != tt.wantErr {
t.Errorf("conflictingTagsRule() error = %v, wantErr %v", err, tt.wantErr)
return
}
if msg != tt.wantMsg {
t.Errorf("conflictingTagsRule() msg = %v, wantMsg %v", msg, tt.wantMsg)
}
})
}
}
func TestLintType(t *testing.T) {
tests := []struct {
name string
typeToLint *types.Type
wantCount int
expectError bool
}{
{
name: "No comments",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestType"},
CommentLines: nil,
},
wantCount: 0,
expectError: false,
},
{
name: "Valid comments",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestType"},
CommentLines: []string{"+k8s:optional"},
},
wantCount: 1,
expectError: false,
},
{
name: "Pointer type",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestPointer"},
Kind: types.Pointer,
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
CommentLines: []string{"+k8s:optional"},
},
wantCount: 2,
expectError: false,
},
{
name: "Slice of pointers",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestSlice"},
Kind: types.Slice,
Elem: &types.Type{
Name: types.Name{Package: "testpkg", Name: "PointerElem"},
Kind: types.Pointer,
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
CommentLines: []string{"+k8s:optional"},
},
CommentLines: []string{"+k8s:optional"},
},
wantCount: 3,
expectError: false,
},
{
name: "Map to pointers",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestMap"},
Kind: types.Map,
Key: &types.Type{Name: types.Name{Package: "testpkg", Name: "KeyType"}, CommentLines: []string{"+k8s:required"}},
Elem: &types.Type{
Name: types.Name{Package: "testpkg", Name: "PointerElem"},
Kind: types.Pointer,
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
CommentLines: []string{"+k8s:optional"},
},
CommentLines: []string{"+k8s:optional"},
},
wantCount: 4,
expectError: false,
},
{
name: "Alias to pointers",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestAlias"},
Kind: types.Alias,
Underlying: &types.Type{
Name: types.Name{Package: "testpkg", Name: "PointerElem"},
Kind: types.Pointer,
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
CommentLines: []string{"+k8s:optional"},
},
CommentLines: []string{"+k8s:optional"},
},
wantCount: 3,
expectError: false,
},
{
name: "Struct with members",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestStruct"},
Kind: types.Struct,
Members: []types.Member{
{
Name: "Field1",
Type: &types.Type{Name: types.Name{Package: "testpkg", Name: "FieldType"}},
CommentLines: []string{"+k8s:optional"},
},
{
Name: "Field2",
Type: &types.Type{Name: types.Name{Package: "testpkg", Name: "FieldType"}},
CommentLines: []string{"+k8s:required"},
},
},
},
wantCount: 2,
expectError: false,
},
{
name: "Nested types",
typeToLint: &types.Type{
Name: types.Name{Package: "testpkg", Name: "TestStruct"},
Kind: types.Struct,
Members: []types.Member{
{
Name: "Field1",
Type: &types.Type{
Name: types.Name{Package: "testpkg", Name: "NestedStruct"},
Kind: types.Struct,
CommentLines: []string{"+k8s:optional"},
Members: []types.Member{
{
Name: "NestedField1",
Type: &types.Type{Name: types.Name{Package: "testpkg", Name: "NestedFieldType"}},
CommentLines: []string{"+k8s:required"},
},
},
},
},
},
},
wantCount: 3,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
counter := 0
rules := []lintRule{mkCountRule(&counter, ruleAlwaysPass)}
l := newLinter(rules...)
if err := l.lintType(tt.typeToLint); err != nil {
t.Fatal(err)
}
gotErr := len(l.lintErrors) > 0
if gotErr != tt.expectError {
t.Errorf("LintType() errors = %v, expectError %v", l.lintErrors, tt.expectError)
}
if counter != tt.wantCount {
t.Errorf("expected %d rule invocations, got %d", tt.wantCount, counter)
}
})
}
}

View File

@ -0,0 +1,159 @@
/*
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.
*/
// validation-gen is a tool for auto-generating Validation functions.
package main
import (
"bytes"
"cmp"
"encoding/json"
"flag"
"fmt"
"os"
"slices"
"github.com/spf13/pflag"
"k8s.io/code-generator/cmd/validation-gen/validators"
"k8s.io/gengo/v2"
"k8s.io/gengo/v2/generator"
"k8s.io/gengo/v2/namer"
"k8s.io/gengo/v2/types"
"k8s.io/klog/v2"
)
func main() {
klog.InitFlags(nil)
args := &Args{}
args.AddFlags(pflag.CommandLine)
if err := flag.Set("logtostderr", "true"); err != nil {
klog.Fatalf("Error: %v", err)
}
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
if err := args.Validate(); err != nil {
klog.Fatalf("Error: %v", err)
}
if args.PrintDocs {
printDocs()
os.Exit(0)
}
myTargets := func(context *generator.Context) []generator.Target {
return GetTargets(context, args)
}
// Run it.
if err := gengo.Execute(
NameSystems(),
DefaultNameSystem(),
myTargets,
gengo.StdBuildTag,
pflag.Args(),
); err != nil {
klog.Fatalf("Error: %v", err)
}
klog.V(2).Info("Completed successfully.")
}
type Args struct {
OutputFile string
ExtraPkgs []string // Always consider these as last-ditch possibilities for validations.
GoHeaderFile string
PrintDocs bool
Lint bool
}
// AddFlags add the generator flags to the flag set.
func (args *Args) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&args.OutputFile, "output-file", "generated.validations.go",
"the name of the file to be generated")
fs.StringSliceVar(&args.ExtraPkgs, "extra-pkg", args.ExtraPkgs,
"the import path of a package whose validation can be used by generated code, but is not being generated for")
fs.StringVar(&args.GoHeaderFile, "go-header-file", "",
"the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year")
fs.BoolVar(&args.PrintDocs, "docs", false,
"print documentation for supported declarative validations, and then exit")
fs.BoolVar(&args.Lint, "lint", false,
"only run linting checks, do not generate code")
}
// Validate checks the given arguments.
func (args *Args) Validate() error {
if len(args.OutputFile) == 0 {
return fmt.Errorf("--output-file must be specified")
}
return nil
}
func printDocs() {
// We need a fake context to init the validator plugins.
c := &generator.Context{
Namers: namer.NameSystems{},
Universe: types.Universe{},
FileTypes: map[string]generator.FileType{},
}
// Initialize all registered validators.
validator := validators.InitGlobalValidator(c)
docs := validator.Docs()
for i := range docs {
d := &docs[i]
slices.Sort(d.Scopes)
if d.Usage == "" {
// Try to generate a usage string if none was provided.
usage := d.Tag
if len(d.Args) > 0 {
usage += "("
for i := range d.Args {
if i > 0 {
usage += ", "
}
usage += d.Args[i].Description
}
usage += ")"
}
if len(d.Payloads) > 0 {
usage += "="
if len(d.Payloads) == 1 {
usage += d.Payloads[0].Description
} else {
usage += "<payload>"
}
}
d.Usage = usage
}
}
slices.SortFunc(docs, func(a, b validators.TagDoc) int {
return cmp.Compare(a.Tag, b.Tag)
})
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
if err := encoder.Encode(docs); err != nil {
klog.Fatalf("failed to marshal docs: %v", err)
}
fmt.Println(buf.String())
}

View File

@ -0,0 +1,383 @@
/*
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 main
import (
"cmp"
"fmt"
"reflect"
"slices"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/code-generator/cmd/validation-gen/validators"
"k8s.io/gengo/v2"
"k8s.io/gengo/v2/generator"
"k8s.io/gengo/v2/namer"
"k8s.io/gengo/v2/types"
"k8s.io/klog/v2"
)
// These are the comment tags that carry parameters for validation generation.
const (
tagName = "k8s:validation-gen"
inputTagName = "k8s:validation-gen-input"
schemeRegistryTagName = "k8s:validation-gen-scheme-registry" // defaults to k8s.io/apimachinery/pkg.runtime.Scheme
testFixtureTagName = "k8s:validation-gen-test-fixture" // if set, generate go test files for test fixtures. Supported values: "validateFalse".
)
var (
runtimePkg = "k8s.io/apimachinery/pkg/runtime"
schemeType = types.Name{Package: runtimePkg, Name: "Scheme"}
)
func extractTag(comments []string) ([]string, bool) {
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{tagName}, comments)
if err != nil {
klog.Fatalf("Failed to extract tags: %v", err)
}
values, found := tags[tagName]
if !found || len(values) == 0 {
return nil, false
}
result := make([]string, len(values))
for i, tag := range values {
result[i] = tag.Value
}
return result, true
}
func extractInputTag(comments []string) []string {
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{inputTagName}, comments)
if err != nil {
klog.Fatalf("Failed to extract input tags: %v", err)
}
values, found := tags[inputTagName]
if !found {
return nil
}
result := make([]string, len(values))
for i, tag := range values {
result[i] = tag.Value
}
return result
}
func checkTag(comments []string, require ...string) bool {
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{tagName}, comments)
if err != nil {
klog.Fatalf("Failed to extract tags: %v", err)
}
values, found := tags[tagName]
if !found {
return false
}
if len(require) == 0 {
return len(values) == 1 && values[0].Value == ""
}
valueStrings := make([]string, len(values))
for i, tag := range values {
valueStrings[i] = tag.Value
}
return reflect.DeepEqual(valueStrings, require)
}
func schemeRegistryTag(pkg *types.Package) types.Name {
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{schemeRegistryTagName}, pkg.Comments)
if err != nil {
klog.Fatalf("Failed to extract scheme registry tags: %v", err)
}
values, found := tags[schemeRegistryTagName]
if !found || len(values) == 0 {
return schemeType // default
}
if len(values) > 1 {
panic(fmt.Sprintf("Package %q contains more than one usage of %q", pkg.Path, schemeRegistryTagName))
}
return types.ParseFullyQualifiedName(values[0].Value)
}
var testFixtureTagValues = sets.New("validateFalse")
func testFixtureTag(pkg *types.Package) sets.Set[string] {
result := sets.New[string]()
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{testFixtureTagName}, pkg.Comments)
if err != nil {
klog.Fatalf("Failed to extract test fixture tags: %v", err)
}
values, found := tags[testFixtureTagName]
if !found {
return result
}
for _, tag := range values {
if !testFixtureTagValues.Has(tag.Value) {
panic(fmt.Sprintf("Package %q: %s must be one of '%s', but got: %s", pkg.Path, testFixtureTagName, testFixtureTagValues.UnsortedList(), tag.Value))
}
result.Insert(tag.Value)
}
return result
}
// NameSystems returns the name system used by the generators in this package.
func NameSystems() namer.NameSystems {
return namer.NameSystems{
"public": namer.NewPublicNamer(1),
"raw": namer.NewRawNamer("", nil),
"objectvalidationfn": validationFnNamer(),
"private": namer.NewPrivateNamer(0),
"name": namer.NewPublicNamer(0),
}
}
func validationFnNamer() *namer.NameStrategy {
return &namer.NameStrategy{
Prefix: "Validate_",
Join: func(pre string, in []string, post string) string {
return pre + strings.Join(in, "_") + post
},
}
}
// DefaultNameSystem returns the default name system for ordering the types to be
// processed by the generators in this package.
func DefaultNameSystem() string {
return "public"
}
func GetTargets(context *generator.Context, args *Args) []generator.Target {
boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy)
if err != nil {
klog.Fatalf("Failed loading boilerplate: %v", err)
}
var targets []generator.Target
var lintErrs []error
// First load other "input" packages. We do this as a single call because
// it is MUCH faster.
inputPkgs := make([]string, 0, len(context.Inputs))
pkgToInput := map[string]string{}
for _, input := range context.Inputs {
klog.V(5).Infof("considering pkg %q", input)
pkg := context.Universe[input]
// if the types are not in the same package where the validation
// functions are to be emitted
inputTags := extractInputTag(pkg.Comments)
if len(inputTags) > 1 {
panic(fmt.Sprintf("there may only be one input tag, got %#v", inputTags))
}
if len(inputTags) == 1 {
inputPath := inputTags[0]
if strings.HasPrefix(inputPath, "./") || strings.HasPrefix(inputPath, "../") {
// this is a relative dir, which will not work under gomodules.
// join with the local package path, but warn
klog.Fatalf("relative path (%s=%s) is not supported; use full package path (as used by 'import') instead", inputTagName, inputPath)
}
klog.V(5).Infof(" input pkg %v", inputPath)
inputPkgs = append(inputPkgs, inputPath)
pkgToInput[input] = inputPath
} else {
pkgToInput[input] = input
}
}
// Make sure explicit extra-packages are added.
var extraPkgs []string
for _, pkg := range args.ExtraPkgs {
// In case someone specifies an extra as a path into vendor, convert
// it to its "real" package path.
if i := strings.Index(pkg, "/vendor/"); i != -1 {
pkg = pkg[i+len("/vendor/"):]
}
extraPkgs = append(extraPkgs, pkg)
}
if expanded, err := context.FindPackages(extraPkgs...); err != nil {
klog.Fatalf("cannot find extra packages: %v", err)
} else {
extraPkgs = expanded // now in fully canonical form
}
for _, extra := range extraPkgs {
inputPkgs = append(inputPkgs, extra)
pkgToInput[extra] = extra
}
// We also need the to be able to look up the packages of inputs
inputToPkg := make(map[string]string, len(pkgToInput))
for k, v := range pkgToInput {
inputToPkg[v] = k
}
if len(inputPkgs) > 0 {
if _, err := context.LoadPackages(inputPkgs...); err != nil {
klog.Fatalf("cannot load packages: %v", err)
}
}
// update context.Order to the latest context.Universe
orderer := namer.Orderer{Namer: namer.NewPublicNamer(1)}
context.Order = orderer.OrderUniverse(context.Universe)
// Initialize all validator plugins exactly once.
validator := validators.InitGlobalValidator(context)
// Build a cache of type->callNode for every type we need.
for _, input := range context.Inputs {
klog.V(2).InfoS("processing", "pkg", input)
pkg := context.Universe[input]
schemeRegistry := schemeRegistryTag(pkg)
typesWith, found := extractTag(pkg.Comments)
if !found {
klog.V(2).InfoS(" did not find required tag", "tag", tagName)
continue
}
if len(typesWith) == 1 && typesWith[0] == "" {
klog.Fatalf("found package tag %q with no value", tagName)
}
shouldCreateObjectValidationFn := func(t *types.Type) bool {
// opt-out
if checkTag(t.SecondClosestCommentLines, "false") {
return false
}
// opt-in
if checkTag(t.SecondClosestCommentLines, "true") {
return true
}
// all types
for _, v := range typesWith {
if v == "*" && !namer.IsPrivateGoName(t.Name.Name) {
return true
}
}
// For every k8s:validation-gen tag at the package level, interpret the value as a
// field name (like TypeMeta, ListMeta, ObjectMeta) and trigger validation generation
// for any type with any of the matching field names. Provides a more useful package
// level validation than global (because we only need validations on a subset of objects -
// usually those with TypeMeta).
return isTypeWith(t, typesWith)
}
// Find the right input pkg, which might not be this one.
inputPath := pkgToInput[input]
// typesPkg is where the types that need validation are defined.
// Sometimes it is different from pkg. For example, kubernetes core/v1
// types are defined in k8s.io/api/core/v1, while the pkg which holds
// defaulter code is at k/k/pkg/api/v1.
typesPkg := context.Universe[inputPath]
// Figure out which types we should be considering further.
var rootTypes []*types.Type
for _, t := range typesPkg.Types {
if shouldCreateObjectValidationFn(t) {
rootTypes = append(rootTypes, t)
} else {
klog.V(6).InfoS("skipping type", "type", t)
}
}
// Deterministic ordering helps in logs and debugging.
slices.SortFunc(rootTypes, func(a, b *types.Type) int {
return cmp.Compare(a.Name.String(), b.Name.String())
})
td := NewTypeDiscoverer(validator, inputToPkg)
for _, t := range rootTypes {
klog.V(4).InfoS("pre-processing", "type", t)
if err := td.DiscoverType(t); err != nil {
klog.Fatalf("failed to generate validations: %v", err)
}
}
l := newLinter()
for _, t := range rootTypes {
klog.V(4).InfoS("linting root-type", "type", t)
if err := l.lintType(t); err != nil {
klog.Fatalf("failed to lint type %q: %v", t.Name, err)
}
if len(l.lintErrors) > 0 {
lintErrs = append(lintErrs, l.lintErrors...)
}
}
if args.Lint {
klog.V(4).Info("Lint is set, skip appending targets")
continue
}
targets = append(targets,
&generator.SimpleTarget{
PkgName: pkg.Name,
PkgPath: pkg.Path,
PkgDir: pkg.Dir, // output pkg is the same as the input
HeaderComment: boilerplate,
FilterFunc: func(c *generator.Context, t *types.Type) bool {
return t.Name.Package == typesPkg.Path
},
GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) {
generators = []generator.Generator{
NewGenValidations(args.OutputFile, pkg.Path, rootTypes, td, inputToPkg, schemeRegistry),
}
testFixtureTags := testFixtureTag(pkg)
if testFixtureTags.Len() > 0 {
if !strings.HasSuffix(args.OutputFile, ".go") {
panic(fmt.Sprintf("%s requires that output file have .go suffix", testFixtureTagName))
}
filename := args.OutputFile[0:len(args.OutputFile)-3] + "_test.go"
generators = append(generators, FixtureTests(filename, testFixtureTags))
}
return generators
},
})
}
if len(lintErrs) > 0 {
var lintErrsStr string
for _, err := range lintErrs {
lintErrsStr += fmt.Sprintf("\n%s", err.Error())
}
if args.Lint {
klog.Fatalf("failed to lint comments: %s", lintErrsStr)
} else {
klog.Warningf("failed to lint comments: %s", lintErrsStr)
}
}
return targets
}
func isTypeWith(t *types.Type, typesWith []string) bool {
if t.Kind == types.Struct && len(typesWith) > 0 {
for _, field := range t.Members {
for _, s := range typesWith {
if field.Name == s {
return true
}
}
}
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,374 @@
/*
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 main
import (
"testing"
"k8s.io/gengo/v2/types"
)
func TestGetLeafTypeAndPrefixes(t *testing.T) {
stringType := &types.Type{
Name: types.Name{
Package: "",
Name: "string",
},
Kind: types.Builtin,
}
ptrTo := func(t *types.Type) *types.Type {
return &types.Type{
Name: types.Name{
Package: "",
Name: "*" + t.Name.String(),
},
Kind: types.Pointer,
Elem: t,
}
}
sliceOf := func(t *types.Type) *types.Type {
return &types.Type{
Name: types.Name{
Package: "",
Name: "[]" + t.Name.String(),
},
Kind: types.Slice,
Elem: t,
}
}
mapOf := func(t *types.Type) *types.Type {
return &types.Type{
Name: types.Name{
Package: "",
Name: "map[string]" + t.Name.String(),
},
Kind: types.Map,
Key: stringType,
Elem: t,
}
}
aliasOf := func(name string, t *types.Type) *types.Type {
return &types.Type{
Name: types.Name{
Package: "",
Name: "Alias_" + name,
},
Kind: types.Alias,
Underlying: t,
}
}
cases := []struct {
in *types.Type
expectedType *types.Type
expectedTypePfx string
expectedExprPfx string
}{{
// string
in: stringType,
expectedType: stringType,
expectedTypePfx: "*",
expectedExprPfx: "&",
}, {
// *string
in: ptrTo(stringType),
expectedType: stringType,
expectedTypePfx: "*",
expectedExprPfx: "",
}, {
// **string
in: ptrTo(ptrTo(stringType)),
expectedType: stringType,
expectedTypePfx: "*",
expectedExprPfx: "*",
}, {
// ***string
in: ptrTo(ptrTo(ptrTo(stringType))),
expectedType: stringType,
expectedTypePfx: "*",
expectedExprPfx: "**",
}, {
// []string
in: sliceOf(stringType),
expectedType: sliceOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *[]string
in: ptrTo(sliceOf(stringType)),
expectedType: sliceOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **[]string
in: ptrTo(ptrTo(sliceOf(stringType))),
expectedType: sliceOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***[]string
in: ptrTo(ptrTo(ptrTo(sliceOf(stringType)))),
expectedType: sliceOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "***",
}, {
// map[string]string
in: mapOf(stringType),
expectedType: mapOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *map[string]string
in: ptrTo(mapOf(stringType)),
expectedType: mapOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **map[string]string
in: ptrTo(ptrTo(mapOf(stringType))),
expectedType: mapOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***map[string]string
in: ptrTo(ptrTo(ptrTo(mapOf(stringType)))),
expectedType: mapOf(stringType),
expectedTypePfx: "",
expectedExprPfx: "***",
}, {
// alias of string
in: aliasOf("s", stringType),
expectedType: aliasOf("s", stringType),
expectedTypePfx: "*",
expectedExprPfx: "&",
}, {
// alias of *string
in: aliasOf("ps", ptrTo(stringType)),
expectedType: aliasOf("ps", stringType),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of **string
in: aliasOf("pps", ptrTo(ptrTo(stringType))),
expectedType: aliasOf("pps", stringType),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of ***string
in: aliasOf("ppps", ptrTo(ptrTo(ptrTo(stringType)))),
expectedType: aliasOf("ppps", stringType),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of []string
in: aliasOf("ls", sliceOf(stringType)),
expectedType: aliasOf("ls", sliceOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of *[]string
in: aliasOf("pls", ptrTo(sliceOf(stringType))),
expectedType: aliasOf("pls", sliceOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of **[]string
in: aliasOf("ppls", ptrTo(ptrTo(sliceOf(stringType)))),
expectedType: aliasOf("ppls", sliceOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of ***[]string
in: aliasOf("pppls", ptrTo(ptrTo(ptrTo(sliceOf(stringType))))),
expectedType: aliasOf("pppls", sliceOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of map[string]string
in: aliasOf("ms", mapOf(stringType)),
expectedType: aliasOf("ms", mapOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of *map[string]string
in: aliasOf("pms", ptrTo(mapOf(stringType))),
expectedType: aliasOf("pms", mapOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of **map[string]string
in: aliasOf("ppms", ptrTo(ptrTo(mapOf(stringType)))),
expectedType: aliasOf("ppms", mapOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// alias of ***map[string]string
in: aliasOf("pppms", ptrTo(ptrTo(ptrTo(mapOf(stringType))))),
expectedType: aliasOf("pppms", mapOf(stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *alias-of-string
in: ptrTo(aliasOf("s", stringType)),
expectedType: aliasOf("s", stringType),
expectedTypePfx: "*",
expectedExprPfx: "",
}, {
// **alias-of-string
in: ptrTo(ptrTo(aliasOf("s", stringType))),
expectedType: aliasOf("s", stringType),
expectedTypePfx: "*",
expectedExprPfx: "*",
}, {
// ***alias-of-string
in: ptrTo(ptrTo(ptrTo(aliasOf("s", stringType)))),
expectedType: aliasOf("s", stringType),
expectedTypePfx: "*",
expectedExprPfx: "**",
}, {
// []alias-of-string
in: sliceOf(aliasOf("s", stringType)),
expectedType: sliceOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *[]alias-of-string
in: ptrTo(sliceOf(aliasOf("s", stringType))),
expectedType: sliceOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **[]alias-of-string
in: ptrTo(ptrTo(sliceOf(aliasOf("s", stringType)))),
expectedType: sliceOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***[]alias-of-string
in: ptrTo(ptrTo(ptrTo(sliceOf(aliasOf("s", stringType))))),
expectedType: sliceOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "***",
}, {
// map[string]alias-of-string
in: mapOf(aliasOf("s", stringType)),
expectedType: mapOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *map[string]alias-of-string
in: ptrTo(mapOf(aliasOf("s", stringType))),
expectedType: mapOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **map[string]alias-of-string
in: ptrTo(ptrTo(mapOf(aliasOf("s", stringType)))),
expectedType: mapOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***map[string]alias-of-string
in: ptrTo(ptrTo(ptrTo(mapOf(aliasOf("s", stringType))))),
expectedType: mapOf(aliasOf("s", stringType)),
expectedTypePfx: "",
expectedExprPfx: "***",
}, {
// *alias-of-*string
in: ptrTo(aliasOf("ps", ptrTo(stringType))),
expectedType: aliasOf("ps", ptrTo(stringType)),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **alias-of-*string
in: ptrTo(ptrTo(aliasOf("ps", ptrTo(stringType)))),
expectedType: aliasOf("ps", ptrTo(stringType)),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***alias-of-*string
in: ptrTo(ptrTo(ptrTo(aliasOf("ps", ptrTo(stringType))))),
expectedType: aliasOf("ps", ptrTo(stringType)),
expectedTypePfx: "",
expectedExprPfx: "***",
}, {
// []alias-of-*string
in: sliceOf(aliasOf("ps", ptrTo(stringType))),
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *[]alias-of-*string
in: ptrTo(sliceOf(aliasOf("ps", ptrTo(stringType)))),
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **[]alias-of-*string
in: ptrTo(ptrTo(sliceOf(aliasOf("ps", ptrTo(stringType))))),
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***[]alias-of-*string
in: ptrTo(ptrTo(ptrTo(sliceOf(aliasOf("ps", ptrTo(stringType)))))),
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "***",
}, {
// map[string]alias-of-*string
in: mapOf(aliasOf("ps", ptrTo(stringType))),
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "",
}, {
// *map[string]alias-of-*string
in: ptrTo(mapOf(aliasOf("ps", ptrTo(stringType)))),
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "*",
}, {
// **map[string]alias-of-*string
in: ptrTo(ptrTo(mapOf(aliasOf("ps", ptrTo(stringType))))),
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "**",
}, {
// ***map[string]alias-of-*string
in: ptrTo(ptrTo(ptrTo(mapOf(aliasOf("ps", ptrTo(stringType)))))),
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
expectedTypePfx: "",
expectedExprPfx: "***",
}}
for _, tc := range cases {
leafType, typePfx, exprPfx := getLeafTypeAndPrefixes(tc.in)
if got, want := leafType.Name.String(), tc.expectedType.Name.String(); got != want {
t.Errorf("%q: wrong leaf type: expected %q, got %q", tc.in, want, got)
}
if got, want := typePfx, tc.expectedTypePfx; got != want {
t.Errorf("%q: wrong type prefix: expected %q, got %q", tc.in, want, got)
}
if got, want := exprPfx, tc.expectedExprPfx; got != want {
t.Errorf("%q: wrong expr prefix: expected %q, got %q", tc.in, want, got)
}
}
}

View File

@ -0,0 +1,51 @@
/*
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 validators
import (
"k8s.io/gengo/v2/parser/tags"
"k8s.io/gengo/v2/types"
)
const (
// libValidationPkg is the pkgpath to our "standard library" of validation
// functions.
libValidationPkg = "k8s.io/apimachinery/pkg/api/validate"
)
func getMemberByJSON(t *types.Type, jsonName string) *types.Member {
for i := range t.Members {
if jsonTag, ok := tags.LookupJSON(t.Members[i]); ok {
if jsonTag.Name == jsonName {
return &t.Members[i]
}
}
}
return nil
}
// isNilableType returns true if the argument type can be compared to nil.
func isNilableType(t *types.Type) bool {
for t.Kind == types.Alias {
t = t.Underlying
}
switch t.Kind {
case types.Pointer, types.Map, types.Slice, types.Interface: // Note: Arrays are not nilable
return true
}
return false
}

View File

@ -0,0 +1,240 @@
/*
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 validators
import (
"cmp"
"fmt"
"slices"
"sort"
"sync"
"sync/atomic"
"k8s.io/gengo/v2"
"k8s.io/gengo/v2/generator"
)
// This is the global registry of tag validators. For simplicity this is in
// the same package as the implementations, but it should not be used directly.
var globalRegistry = &registry{
tagValidators: map[string]TagValidator{},
}
// registry holds a list of registered tags.
type registry struct {
lock sync.Mutex
initialized atomic.Bool // init() was called
tagValidators map[string]TagValidator // keyed by tagname
tagIndex []string // all tag names
typeValidators []TypeValidator
}
func (reg *registry) addTagValidator(tv TagValidator) {
if reg.initialized.Load() {
panic("registry was modified after init")
}
reg.lock.Lock()
defer reg.lock.Unlock()
name := tv.TagName()
if _, exists := globalRegistry.tagValidators[name]; exists {
panic(fmt.Sprintf("tag %q was registered twice", name))
}
globalRegistry.tagValidators[name] = tv
}
func (reg *registry) addTypeValidator(tv TypeValidator) {
if reg.initialized.Load() {
panic("registry was modified after init")
}
reg.lock.Lock()
defer reg.lock.Unlock()
globalRegistry.typeValidators = append(globalRegistry.typeValidators, tv)
}
func (reg *registry) init(c *generator.Context) {
if reg.initialized.Load() {
panic("registry.init() was called twice")
}
reg.lock.Lock()
defer reg.lock.Unlock()
cfg := Config{
GengoContext: c,
Validator: reg,
}
for _, tv := range globalRegistry.tagValidators {
reg.tagIndex = append(reg.tagIndex, tv.TagName())
tv.Init(cfg)
}
sort.Strings(reg.tagIndex)
for _, tv := range reg.typeValidators {
tv.Init(cfg)
}
slices.SortFunc(reg.typeValidators, func(a, b TypeValidator) int {
return cmp.Compare(a.Name(), b.Name())
})
reg.initialized.Store(true)
}
// ExtractValidations considers the given context (e.g. a type definition) and
// evaluates registered validators. This includes type validators (which run
// against all types) and tag validators which run only if a specific tag is
// found in the associated comment block. Any matching validators produce zero
// or more validations, which will later be rendered by the code-generation
// logic.
func (reg *registry) ExtractValidations(context Context, comments []string) (Validations, error) {
if !reg.initialized.Load() {
panic("registry.init() was not called")
}
validations := Validations{}
// Extract tags and run matching tag-validators first.
tags, err := gengo.ExtractFunctionStyleCommentTags("+", reg.tagIndex, comments)
if err != nil {
return Validations{}, fmt.Errorf("failed to parse tags: %w", err)
}
phases := reg.sortTagsIntoPhases(tags)
for _, idx := range phases {
for _, tag := range idx {
vals := tags[tag]
tv := reg.tagValidators[tag]
if scopes := tv.ValidScopes(); !scopes.Has(context.Scope) && !scopes.Has(ScopeAny) {
return Validations{}, fmt.Errorf("tag %q cannot be specified on %s", tv.TagName(), context.Scope)
}
for _, val := range vals { // tags may have multiple values
if theseValidations, err := tv.GetValidations(context, val.Args, val.Value); err != nil {
return Validations{}, fmt.Errorf("tag %q: %w", tv.TagName(), err)
} else {
validations.Add(theseValidations)
}
}
}
}
// Run type-validators after tag validators are done.
if context.Scope == ScopeType {
// Run all type-validators.
for _, tv := range reg.typeValidators {
if theseValidations, err := tv.GetValidations(context); err != nil {
return Validations{}, fmt.Errorf("type validator %q: %w", tv.Name(), err)
} else {
validations.Add(theseValidations)
}
}
}
return validations, nil
}
func (reg *registry) sortTagsIntoPhases(tags map[string][]gengo.Tag) [][]string {
// First sort all tags by their name, so the final output is deterministic.
//
// It makes more sense to sort here, rather than when emitting because:
//
// Consider a type or field with the following comments:
//
// // +k8s:validateFalse="111"
// // +k8s:validateFalse="222"
// // +k8s:ifOptionEnabled(Foo)=+k8s:validateFalse="333"
//
// Tag extraction will retain the relative order between 111 and 222, but
// 333 is extracted as tag "k8s:ifOptionEnabled". Those are all in a map,
// which we iterate (in a random order). When it reaches the emit stage,
// the "ifOptionEnabled" part is gone, and we will have 3 functionGen
// objects, all with tag "k8s:validateFalse", in a non-deterministic order
// because of the map iteration. If we sort them at that point, we won't
// have enough information to do something smart, unless we look at the
// args, which are opaque to us.
//
// Sorting it earlier means we can sort "k8s:ifOptionEnabled" against
// "k8s:validateFalse". All of the records within each of those is
// relatively ordered, so the result here would be to put "ifOptionEnabled"
// before "validateFalse" (lexicographical is better than random).
sortedTags := []string{}
for tag := range tags {
sortedTags = append(sortedTags, tag)
}
sort.Strings(sortedTags)
// Now split them into phases.
phase0 := []string{} // regular tags
phase1 := []string{} // "late" tags
for _, tn := range sortedTags {
tv := reg.tagValidators[tn]
if _, ok := tv.(LateTagValidator); ok {
phase1 = append(phase1, tn)
} else {
phase0 = append(phase0, tn)
}
}
return [][]string{phase0, phase1}
}
// Docs returns documentation for each tag in this registry.
func (reg *registry) Docs() []TagDoc {
var result []TagDoc
for _, k := range reg.tagIndex {
v := reg.tagValidators[k]
result = append(result, v.Docs())
}
return result
}
// RegisterTagValidator must be called by any validator which wants to run when
// a specific tag is found.
func RegisterTagValidator(tv TagValidator) {
globalRegistry.addTagValidator(tv)
}
// RegisterTypeValidator must be called by any validator which wants to run
// against every type definition.
func RegisterTypeValidator(tv TypeValidator) {
globalRegistry.addTypeValidator(tv)
}
// Validator represents an aggregation of validator plugins.
type Validator interface {
// ExtractValidations considers the given context (e.g. a type definition) and
// evaluates registered validators. This includes type validators (which run
// against all types) and tag validators which run only if a specific tag is
// found in the associated comment block. Any matching validators produce zero
// or more validations, which will later be rendered by the code-generation
// logic.
ExtractValidations(context Context, comments []string) (Validations, error)
// Docs returns documentation for each known tag.
Docs() []TagDoc
}
// InitGlobalValidator must be called exactly once by the main application to
// initialize and safely access the global tag registry. Once this is called,
// no more validators may be registered.
func InitGlobalValidator(c *generator.Context) Validator {
globalRegistry.init(c)
return globalRegistry
}

View File

@ -0,0 +1,443 @@
/*
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 validators
import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/gengo/v2/generator"
"k8s.io/gengo/v2/types"
)
// TagValidator describes a single validation tag and how to use it.
type TagValidator interface {
// Init initializes the implementation. This will be called exactly once.
Init(cfg Config)
// TagName returns the full tag name (without the "marker" prefix) for this
// tag.
TagName() string
// ValidScopes returns the set of scopes where this tag may be used.
ValidScopes() sets.Set[Scope]
// GetValidations returns any validations described by this tag.
GetValidations(context Context, args []string, payload string) (Validations, error)
// Docs returns user-facing documentation for this tag.
Docs() TagDoc
}
// LateTagValidator is an optional extension to TagValidator. Any TagValidator
// which implements this interface will be evaluated after all TagValidators
// which do not.
type LateTagValidator interface {
LateTagValidator()
}
// TypeValidator describes a validator which runs on every type definition.
type TypeValidator interface {
// Init initializes the implementation. This will be called exactly once.
Init(cfg Config)
// Name returns a unique name for this validator. This is used for sorting
// and logging.
Name() string
// GetValidations returns any validations imposed by this validator for the
// given context.
//
// The way gengo handles type definitions varies between structs and other
// types. For struct definitions (e.g. `type Foo struct {}`), the realType
// is the struct itself (the Kind field will be `types.Struct`) and the
// parentType will be nil. For other types (e.g. `type Bar string`), the
// realType will be the underlying type and the parentType will be the
// newly defined type (the Kind field will be `types.Alias`).
GetValidations(context Context) (Validations, error)
}
// Config carries optional configuration information for use by validators.
type Config struct {
// GengoContext provides gengo's generator Context. This allows validators
// to look up all sorts of other information.
GengoContext *generator.Context
// Validator provides a way to compose validations.
//
// For example, it is possible to define a validation such as
// "+myValidator=+format=IP" by using the registry to extract the
// validation for the embedded "+format=IP" and use those to
// create the final Validations returned by the "+myValidator" tag.
//
// This field MUST NOT be used during init, since other validators may not
// be initialized yet.
Validator Validator
}
// Scope describes where a validation (or potential validation) is located.
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"
// ScopeField indicates a validation on a particular struct field, which
// applies only to that field of that struct.
ScopeField Scope = "struct fields"
// ScopeListVal indicates a validation which applies to all elements of a
// list field or type.
ScopeListVal Scope = "list values"
// ScopeMapKey indicates a validation which applies to all keys of a map
// field or type.
ScopeMapKey Scope = "map keys"
// ScopeMapVal indicates a validation which applies to all values of a map
// field or type.
ScopeMapVal Scope = "map 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
// maybe even pointers?), but that seems like extra work that is not needed
// for now.
)
// Context describes where a tag was used, so that the scope can be checked
// and so validators can handle different cases if they need.
type Context struct {
// Scope is where the validation is being considered.
Scope Scope
// Type provides details about the type being validated. When Scope is
// ScopeType, this is the underlying type. When Scope is ScopeField, this
// is the field's type (including any pointerness). When Scope indicates a
// list-value, map-key, or map-value, this is the type of that key or
// value.
Type *types.Type
// Parent provides details about the logical parent type of the type being
// validated, when applicable. When Scope is ScopeType, this is the
// newly-defined type (when it exists - gengo handles struct-type
// definitions differently that other "alias" type definitions). When
// Scope is ScopeField, this is the field's parent struct's type. When
// Scope indicates a list-value, map-key, or map-value, this is the type of
// the whole list or map.
//
// Because of how gengo handles struct-type definitions, this field may be
// nil in those cases.
Parent *types.Type
// 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
}
// TagDoc describes a comment-tag and its usage.
type TagDoc struct {
// Tag is the tag name, without the leading '+'.
Tag string
// Args lists any arguments this tag might take.
Args []TagArgDoc
// Usage is how the tag is used, including arguments.
Usage string
// Description is a short description of this tag's purpose.
Description string
// Docs is a human-oriented string explaining this tag.
Docs string
// Scopes lists the place or places this tag may be used.
Scopes []Scope
// Payloads lists zero or more varieties of value for this tag. If this tag
// never has a payload, this list should be empty, but if the payload is
// optional, this list should include an entry for "<none>".
Payloads []TagPayloadDoc
}
// TagArgDoc describes an argument for a tag (e.g. `+tagName(tagArg)`.
type TagArgDoc struct {
// Description is a short description of this arg (e.g. `<name>`).
Description string
}
// TagPayloadDoc describes a value for a tag (e.g. `+tagName=tagValue`). Some
// tags upport multiple payloads, including <none> (e.g. `+tagName`).
type TagPayloadDoc struct {
// Description is a short description of this payload (e.g. `<number>`).
Description string
// Docs is a human-orientd string explaining this payload.
Docs string
// Schema details a JSON payload's contents.
Schema []TagPayloadSchema
}
// TagPayloadSchema describes a JSON tag payload.
type TagPayloadSchema struct {
Key string
Value string
Docs string
Default string
}
// Validations defines the function calls and variables to generate to perform validation.
type Validations struct {
Functions []FunctionGen
Variables []VariableGen
Comments []string
OpaqueType bool
OpaqueKeyType bool
OpaqueValType bool
}
func (v *Validations) Empty() bool {
return v.Len() == 0
}
func (v *Validations) Len() int {
return len(v.Functions) + len(v.Variables) + len(v.Comments)
}
func (v *Validations) AddFunction(f FunctionGen) {
v.Functions = append(v.Functions, f)
}
func (v *Validations) AddVariable(variable VariableGen) {
v.Variables = append(v.Variables, variable)
}
func (v *Validations) AddComment(comment string) {
v.Comments = append(v.Comments, comment)
}
func (v *Validations) Add(o Validations) {
v.Functions = append(v.Functions, o.Functions...)
v.Variables = append(v.Variables, o.Variables...)
v.Comments = append(v.Comments, o.Comments...)
v.OpaqueType = v.OpaqueType || o.OpaqueType
v.OpaqueKeyType = v.OpaqueKeyType || o.OpaqueKeyType
v.OpaqueValType = v.OpaqueValType || o.OpaqueValType
}
// FunctionFlags define optional properties of a validator. Most validators
// can just use DefaultFlags.
type FunctionFlags uint32
// IsSet returns true if all of the wanted flags are set.
func (ff FunctionFlags) IsSet(wanted FunctionFlags) bool {
return (ff & wanted) == wanted
}
const (
// DefaultFlags is defined for clarity.
DefaultFlags FunctionFlags = 0
// ShortCircuit indicates that further validations should be skipped if
// this validator fails. Most validators are not fatal.
ShortCircuit FunctionFlags = 1 << iota
// NonError indicates that a failure of this validator should not be
// accumulated as an error, but should trigger other aspects of the failure
// path (e.g. early return when combined with ShortCircuit).
NonError
)
// FunctionGen provides validation-gen with the information needed to generate a
// validation function invocation.
type FunctionGen interface {
// TagName returns the tag which triggers this validator.
TagName() string
// SignatureAndArgs returns the function name and all extraArg value literals that are passed when the function
// invocation is generated.
//
// The function signature must be of the form:
// func(op operation.Operation,
// fldPath field.Path,
// value, oldValue <ValueType>, // always nilable
// extraArgs[0] <extraArgs[0]Type>, // optional
// ...,
// extraArgs[N] <extraArgs[N]Type>)
//
// extraArgs may contain:
// - data literals comprised of maps, slices, strings, ints, floats and bools
// - references, represented by types.Type (to reference any type in the universe), and types.Member (to reference members of the current value)
//
// If validation function to be called does not have a signature of this form, please introduce
// a function that does and use that function to call the validation function.
SignatureAndArgs() (function types.Name, extraArgs []any)
// TypeArgs assigns types to the type parameters of the function, for invocation.
TypeArgs() []types.Name
// Flags returns the options for this validator function.
Flags() FunctionFlags
// Conditions returns the conditions that must true for a resource to be
// validated by this function.
Conditions() Conditions
}
// Conditions defines what conditions must be true for a resource to be validated.
// If any of the conditions are not true, the resource is not validated.
type Conditions struct {
// OptionEnabled specifies an option name that must be set to true for the condition to be true.
OptionEnabled string
// OptionDisabled specifies an option name that must be set to false for the condition to be true.
OptionDisabled string
}
func (c Conditions) Empty() bool {
return len(c.OptionEnabled) == 0 && len(c.OptionDisabled) == 0
}
// Identifier is a name that the generator will output as an identifier.
// Identifiers are generated using the RawNamer strategy.
type Identifier types.Name
// PrivateVar is a variable name that the generator will output as a private identifier.
// PrivateVars are generated using the PrivateNamer strategy.
type PrivateVar types.Name
// VariableGen provides validation-gen with the information needed to generate variable.
// Variables typically support generated functions by providing static information such
// as the list of supported symbols for an enum.
type VariableGen interface {
// TagName returns the tag which triggers this validator.
TagName() string
// Var returns the variable identifier.
Var() PrivateVar
// Init generates the function call that the variable is assigned to.
Init() FunctionGen
}
// Function creates a FunctionGen for a given function name and extraArgs.
func Function(tagName string, flags FunctionFlags, function types.Name, extraArgs ...any) FunctionGen {
return GenericFunction(tagName, flags, function, nil, extraArgs...)
}
func GenericFunction(tagName string, flags FunctionFlags, function types.Name, typeArgs []types.Name, extraArgs ...any) FunctionGen {
// Callers of Signature don't care if the args are all of a known type, it just
// makes it easier to declare validators.
var anyArgs []any
if len(extraArgs) > 0 {
anyArgs = make([]any, len(extraArgs))
copy(anyArgs, extraArgs)
}
return &functionGen{tagName: tagName, flags: flags, function: function, extraArgs: anyArgs, typeArgs: typeArgs}
}
func WithCondition(fn FunctionGen, conditions Conditions) FunctionGen {
name, args := fn.SignatureAndArgs()
return &functionGen{
tagName: fn.TagName(), flags: fn.Flags(), function: name, extraArgs: args, typeArgs: fn.TypeArgs(),
conditions: conditions,
}
}
type functionGen struct {
tagName string
function types.Name
extraArgs []any
typeArgs []types.Name
flags FunctionFlags
conditions Conditions
}
func (v *functionGen) TagName() string {
return v.tagName
}
func (v *functionGen) SignatureAndArgs() (function types.Name, args []any) {
return v.function, v.extraArgs
}
func (v *functionGen) TypeArgs() []types.Name { return v.typeArgs }
func (v *functionGen) Flags() FunctionFlags {
return v.flags
}
func (v *functionGen) Conditions() Conditions { return v.conditions }
// Variable creates a VariableGen for a given function name and extraArgs.
func Variable(variable PrivateVar, init FunctionGen) VariableGen {
return &variableGen{
variable: variable,
init: init,
}
}
type variableGen struct {
variable PrivateVar
init FunctionGen
}
func (v variableGen) TagName() string {
return v.init.TagName()
}
func (v variableGen) Var() PrivateVar {
return v.variable
}
func (v variableGen) Init() FunctionGen {
return v.init
}
// WrapperFunction describes a function literal which has the fingerprint of a
// regular validation function (op, fldPath, obj, oldObj) and calls another
// validation function with the same signature, plus extra args if needed.
type WrapperFunction struct {
Function FunctionGen
ObjType *types.Type
}
// Literal is a literal value that, when used as an argument to a validator,
// will be emitted without any further interpretation. Use this with caution,
// it will not be subject to Namers.
type Literal string
// FunctionLiteral describes a function-literal expression that can be used as
// an argument to a validator. Unlike WrapperFunction, this does not
// necessarily have the same signature as a regular validation function.
type FunctionLiteral struct {
Parameters []ParamResult
Results []ParamResult
Body string
}
// ParamResult represents a parameter or a result of a function.
type ParamResult struct {
Name string
Type *types.Type
}