Skip to content

Commit c1f9e6b

Browse files
jpbetzthockinaaron-prindleyongruilin
committed
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 <[email protected]> Co-authored-by: Aaron Prindle <[email protected]> Co-authored-by: Yongrui Lin <[email protected]>
1 parent 7f5e1ba commit c1f9e6b

File tree

13 files changed

+3792
-0
lines changed

13 files changed

+3792
-0
lines changed

staging/publishing/import-restrictions.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
- k8s.io/code-generator
6262
- k8s.io/kube-openapi
6363
- k8s.io/klog
64+
- k8s.io/utils/ptr
6465

6566
- baseImportPath: "./staging/src/k8s.io/component-base"
6667
allowedImports:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package operation
18+
19+
import "k8s.io/apimachinery/pkg/util/sets"
20+
21+
// Operation provides contextual information about a validation request and the API
22+
// operation being validated.
23+
// This type is intended for use with generate validation code and may be enhanced
24+
// in the future to include other information needed to validate requests.
25+
type Operation struct {
26+
// Type is the category of operation being validated. This does not
27+
// differentiate between HTTP verbs like PUT and PATCH, but rather merges
28+
// those into a single "Update" category.
29+
Type Type
30+
31+
// Options declare the options enabled for validation.
32+
//
33+
// Options should be set according to a resource validation strategy before validation
34+
// is performed, and must be treated as read-only during validation.
35+
//
36+
// Options are identified by string names. Option string names may match the name of a feature
37+
// gate, in which case the presence of the name in the set indicates that the feature is
38+
// considered enabled for the resource being validated. Note that a resource may have a
39+
// feature enabled even when the feature gate is disabled. This can happen when feature is
40+
// already in-use by a resource, often because the feature gate was enabled when the
41+
// resource first began using the feature.
42+
//
43+
// Unset options are disabled/false.
44+
Options sets.Set[string]
45+
}
46+
47+
// Code is the request operation to be validated.
48+
type Type uint32
49+
50+
const (
51+
// Create indicates the request being validated is for a resource create operation.
52+
Create Type = iota
53+
54+
// Update indicates the request being validated is for a resource update operation.
55+
Update
56+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package safe
18+
19+
// Field takes a pointer to any value (which may or may not be nil) and
20+
// a function that traverses to a target type R (a typical use case is to dereference a field),
21+
// and returns the result of the traversal, or the zero value of the target type.
22+
// This is roughly equivalent to "value != nil ? fn(value) : zero-value" in languages that support the ternary operator.
23+
func Field[V any, R any](value *V, fn func(*V) R) R {
24+
if value == nil {
25+
var zero R
26+
return zero
27+
}
28+
o := fn(value)
29+
return o
30+
}
31+
32+
// Cast takes any value, attempts to cast it to T, and returns the T value if
33+
// the cast is successful, or else the zero value of T.
34+
func Cast[T any](value any) T {
35+
result, _ := value.(T)
36+
return result
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package validate
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/api/operation"
23+
"k8s.io/apimachinery/pkg/util/validation/field"
24+
)
25+
26+
// ValidateFunc is a function that validates a value, possibly considering the
27+
// old value (if any).
28+
type ValidateFunc[T any] func(ctx context.Context, op operation.Operation, fldPath *field.Path, newValue, oldValue T) field.ErrorList
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"k8s.io/gengo/v2/types"
24+
"k8s.io/klog/v2"
25+
)
26+
27+
// linter is a struct that holds the state of the linting process.
28+
// It contains a map of types that have been linted, a list of linting rules,
29+
// and a list of errors that occurred during the linting process.
30+
type linter struct {
31+
linted map[*types.Type]bool
32+
rules []lintRule
33+
// lintErrors is a list of errors that occurred during the linting process.
34+
// lintErrors would be in the format:
35+
// field <field_name>: <lint broken message>
36+
// type <type_name>: <lint broken message>
37+
lintErrors []error
38+
}
39+
40+
var defaultRules = []lintRule{
41+
ruleOptionalAndRequired,
42+
ruleRequiredAndDefault,
43+
}
44+
45+
func (l *linter) AddError(field, msg string) {
46+
l.lintErrors = append(l.lintErrors, fmt.Errorf("%s: %s", field, msg))
47+
}
48+
49+
func newLinter(rules ...lintRule) *linter {
50+
if len(rules) == 0 {
51+
rules = defaultRules
52+
}
53+
return &linter{
54+
linted: make(map[*types.Type]bool),
55+
rules: rules,
56+
lintErrors: []error{},
57+
}
58+
}
59+
60+
func (l *linter) lintType(t *types.Type) error {
61+
if _, ok := l.linted[t]; ok {
62+
return nil
63+
}
64+
l.linted[t] = true
65+
66+
if t.CommentLines != nil {
67+
klog.V(5).Infof("linting type %s", t.Name.String())
68+
lintErrs, err := l.lintComments(t.CommentLines)
69+
if err != nil {
70+
return err
71+
}
72+
for _, lintErr := range lintErrs {
73+
l.AddError("type "+t.Name.String(), lintErr)
74+
}
75+
}
76+
switch t.Kind {
77+
case types.Alias:
78+
// Recursively lint the underlying type of the alias.
79+
if err := l.lintType(t.Underlying); err != nil {
80+
return err
81+
}
82+
case types.Struct:
83+
// Recursively lint each member of the struct.
84+
for _, member := range t.Members {
85+
klog.V(5).Infof("linting comments for field %s of type %s", member.String(), t.Name.String())
86+
lintErrs, err := l.lintComments(member.CommentLines)
87+
if err != nil {
88+
return err
89+
}
90+
for _, lintErr := range lintErrs {
91+
l.AddError("type "+t.Name.String(), lintErr)
92+
}
93+
if err := l.lintType(member.Type); err != nil {
94+
return err
95+
}
96+
}
97+
case types.Slice, types.Array, types.Pointer:
98+
// Recursively lint the element type of the slice or array.
99+
if err := l.lintType(t.Elem); err != nil {
100+
return err
101+
}
102+
case types.Map:
103+
// Recursively lint the key and element types of the map.
104+
if err := l.lintType(t.Key); err != nil {
105+
return err
106+
}
107+
if err := l.lintType(t.Elem); err != nil {
108+
return err
109+
}
110+
}
111+
return nil
112+
}
113+
114+
// lintRule is a function that validates a slice of comments.
115+
// It returns a string as an error message if the comments are invalid,
116+
// and an error there is an error happened during the linting process.
117+
type lintRule func(comments []string) (string, error)
118+
119+
// lintComments runs all registered rules on a slice of comments.
120+
func (l *linter) lintComments(comments []string) ([]string, error) {
121+
var lintErrs []string
122+
for _, rule := range l.rules {
123+
if msg, err := rule(comments); err != nil {
124+
return nil, err
125+
} else if msg != "" {
126+
lintErrs = append(lintErrs, msg)
127+
}
128+
}
129+
130+
return lintErrs, nil
131+
}
132+
133+
// conflictingTagsRule checks for conflicting tags in a slice of comments.
134+
func conflictingTagsRule(comments []string, tags ...string) (string, error) {
135+
if len(tags) < 2 {
136+
return "", fmt.Errorf("at least two tags must be provided")
137+
}
138+
tagCount := make(map[string]bool)
139+
for _, comment := range comments {
140+
for _, tag := range tags {
141+
if strings.HasPrefix(comment, tag) {
142+
tagCount[tag] = true
143+
}
144+
}
145+
}
146+
if len(tagCount) > 1 {
147+
return fmt.Sprintf("conflicting tags: {%s}", strings.Join(tags, ", ")), nil
148+
}
149+
return "", nil
150+
}
151+
152+
// ruleOptionalAndRequired checks for conflicting tags +k8s:optional and +k8s:required in a slice of comments.
153+
func ruleOptionalAndRequired(comments []string) (string, error) {
154+
return conflictingTagsRule(comments, "+k8s:optional", "+k8s:required")
155+
}
156+
157+
// ruleRequiredAndDefault checks for conflicting tags +k8s:required and +default in a slice of comments.
158+
func ruleRequiredAndDefault(comments []string) (string, error) {
159+
return conflictingTagsRule(comments, "+k8s:required", "+default")
160+
}

0 commit comments

Comments
 (0)