Skip to content

Commit 8c41bdf

Browse files
jpbetzthockinaaron-prindleyongruilin
committed
Add validation-gen test infrastructure
Introduces the infrastructure for testing validation-gen tags. Co-authored-by: Tim Hockin <[email protected]> Co-authored-by: Aaron Prindle <[email protected]> Co-authored-by: Yongrui Lin <[email protected]>
1 parent 3210f46 commit 8c41bdf

File tree

11 files changed

+1248
-0
lines changed

11 files changed

+1248
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# API validation
2+
3+
This package holds functions which validate fields and types in the Kubernetes
4+
API. It may be useful beyond API validation, but this is the primary goal.
5+
6+
Most of the public functions here have signatures which adhere to the following
7+
pattern, which is assumed by automation and code-generation:
8+
9+
```
10+
import (
11+
"context"
12+
"k8s.io/apimachinery/pkg/api/operation"
13+
"k8s.io/apimachinery/pkg/util/validation/field"
14+
)
15+
16+
func <Name>(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue <ValueType>, <OtherArgs...>) field.ErrorList
17+
```
18+
19+
The name of validator functions should consider that callers will generally be
20+
spelling out the package name and the function name, and so should aim for
21+
legibility. E.g. `validate.Concept()`.
22+
23+
The `ctx` argument is Go's usual Context.
24+
25+
The `opCtx` argument provides information about the API operation in question.
26+
27+
The `fldPath` argument indicates the path to the field in question, to be used
28+
in errors.
29+
30+
The `value` and `oldValue` arguments are the thing(s) being validated. For
31+
CREATE operations (`opCtx.Operation == operation.Create`), the `oldValue`
32+
argument will be nil. Many validators functions only look at the current value
33+
(`value`) and disregard `oldValue`.
34+
35+
The `value` and `oldValue` arguments are always nilable - pointers to primitive
36+
types, slices of any type, or maps of any type. Validator functions should
37+
avoid dereferencing nil. Callers are expected to not pass a nil `value` unless the
38+
API field itself was nilable. `oldValue` is always nil for CREATE operations and
39+
is also nil for UPDATE operations if the `value` is not correlated with an `oldValue`.
40+
41+
Simple content-validators may have no `<OtherArgs>`, but validator functions
42+
may take additional arguments. Some validator functions will be built as
43+
generics, e.g. to allow any integer type or to handle arbitrary slices.
44+
45+
Examples:
46+
47+
```
48+
// NonEmpty validates that a string is not empty.
49+
func NonEmpty(ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList
50+
51+
// Even validates that a slice has an even number of items.
52+
func Even[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList
53+
54+
// KeysMaxLen validates that all of the string keys in a map are under the
55+
// specified length.
56+
func KeysMaxLen[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ map[string]T, maxLen int) field.ErrorList
57+
```
58+
59+
Validator functions always return an `ErrorList` where each item is a distinct
60+
validation failure and a zero-length return value (not just nil) indicates
61+
success.
62+
63+
Good validation failure messages follow the Kubernetes API conventions, for
64+
example using "must" instead of "should".
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 holds API validation functions which are designed for use
18+
// with the k8s.io/code-generator/cmd/validation-gen tool. Each validation
19+
// function has a similar fingerprint:
20+
//
21+
// func <Name>(ctx context.Context,
22+
// op operation.Operation,
23+
// fldPath *field.Path,
24+
// value, oldValue <nilable type>,
25+
// <other args...>) field.ErrorList
26+
//
27+
// The value and oldValue arguments will always be a nilable type. If the
28+
// original value was a string, these will be a *string. If the original value
29+
// was a slice or map, these will be the same slice or map type.
30+
//
31+
// For a CREATE operation, the oldValue will always be nil. For an UPDATE
32+
// operation, either value or oldValue may be nil, e.g. when adding or removing
33+
// a value in a list-map. Validators which care about UPDATE operations should
34+
// look at the opCtx argument to know which operation is being executed.
35+
//
36+
// Tightened validation (also known as ratcheting validation) is supported by
37+
// defining a new validation function. For example:
38+
//
39+
// func TightenedMaxLength(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList {
40+
// if oldValue != nil && len(MaxLength(ctx, op, fldPath, oldValue, nil)) > 0 {
41+
// // old value is not valid, so this value skips the tightened validation
42+
// return nil
43+
// }
44+
// return MaxLength(ctx, op, fldPath, value, nil)
45+
// }
46+
//
47+
// In general, we cannot distinguish a non-specified slice or map from one that
48+
// is specified but empty. Validators should not rely on nil values, but use
49+
// len() instead.
50+
package validate
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
Copyright 2014 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+
// FixedResult asserts a fixed boolean result. This is mostly useful for
27+
// testing.
28+
func FixedResult[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ T, result bool, arg string) field.ErrorList {
29+
if result {
30+
return nil
31+
}
32+
return field.ErrorList{
33+
field.Invalid(fldPath, value, "forced failure: "+arg),
34+
}
35+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
"testing"
22+
23+
"k8s.io/apimachinery/pkg/api/operation"
24+
"k8s.io/apimachinery/pkg/util/validation/field"
25+
"k8s.io/utils/ptr"
26+
)
27+
28+
func TestFixedResult(t *testing.T) {
29+
cases := []struct {
30+
value any
31+
pass bool
32+
}{{
33+
value: "",
34+
pass: false,
35+
}, {
36+
value: "",
37+
pass: true,
38+
}, {
39+
value: "nonempty",
40+
pass: false,
41+
}, {
42+
value: "nonempty",
43+
pass: true,
44+
}, {
45+
value: 0,
46+
pass: false,
47+
}, {
48+
value: 0,
49+
pass: true,
50+
}, {
51+
value: 1,
52+
pass: false,
53+
}, {
54+
value: 1,
55+
pass: true,
56+
}, {
57+
value: false,
58+
pass: false,
59+
}, {
60+
value: false,
61+
pass: true,
62+
}, {
63+
value: true,
64+
pass: false,
65+
}, {
66+
value: true,
67+
pass: true,
68+
}, {
69+
value: nil,
70+
pass: false,
71+
}, {
72+
value: nil,
73+
pass: true,
74+
}, {
75+
value: ptr.To(""),
76+
pass: false,
77+
}, {
78+
value: ptr.To(""),
79+
pass: true,
80+
}, {
81+
value: ptr.To("nonempty"),
82+
pass: false,
83+
}, {
84+
value: ptr.To("nonempty"),
85+
pass: true,
86+
}, {
87+
value: []string(nil),
88+
pass: false,
89+
}, {
90+
value: []string(nil),
91+
pass: true,
92+
}, {
93+
value: []string{},
94+
pass: false,
95+
}, {
96+
value: []string{},
97+
pass: true,
98+
}, {
99+
value: []string{"s"},
100+
pass: false,
101+
}, {
102+
value: []string{"s"},
103+
pass: true,
104+
}, {
105+
value: map[string]string(nil),
106+
pass: false,
107+
}, {
108+
value: map[string]string(nil),
109+
pass: true,
110+
}, {
111+
value: map[string]string{},
112+
pass: false,
113+
}, {
114+
value: map[string]string{},
115+
pass: true,
116+
}, {
117+
value: map[string]string{"k": "v"},
118+
pass: false,
119+
}, {
120+
value: map[string]string{"k": "v"},
121+
pass: true,
122+
}}
123+
124+
for i, tc := range cases {
125+
result := FixedResult(context.Background(), operation.Operation{}, field.NewPath("fldpath"), tc.value, nil, tc.pass, "detail string")
126+
if len(result) != 0 && tc.pass {
127+
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
128+
continue
129+
}
130+
if len(result) == 0 && !tc.pass {
131+
t.Errorf("case %d: unexpected success", i)
132+
continue
133+
}
134+
if len(result) > 0 {
135+
if len(result) > 1 {
136+
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
137+
continue
138+
}
139+
if want, got := "forced failure: detail string", result[0].Detail; got != want {
140+
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
141+
}
142+
}
143+
}
144+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Tag tests
2+
3+
Tests in this directory are intended to validate specific tags, rather than
4+
general behavior of the code generator. Some tags are deeply integrated into
5+
the code-generation and will end up with similar tests elsewhere.
6+
7+
These test cases should be as focused as possible.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
// +k8s:validation-gen=TypeMeta
18+
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
19+
20+
// This is a test package.
21+
package validatefalse
22+
23+
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
24+
25+
var localSchemeBuilder = testscheme.New()
26+
27+
type Struct struct {
28+
TypeMeta int
29+
30+
// +k8s:validateFalse="field Struct.StringField"
31+
StringField string `json:"stringField"`
32+
}
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 validatefalse
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func Test(t *testing.T) {
24+
st := localSchemeBuilder.Test(t)
25+
26+
st.Value(&Struct{
27+
// All zero-values.
28+
}).ExpectValidateFalseByPath(map[string][]string{
29+
"stringField": {"field Struct.StringField"},
30+
})
31+
32+
st.Value(&Struct{
33+
StringField: "abc",
34+
}).ExpectValidateFalseByPath(map[string][]string{
35+
"stringField": {"field Struct.StringField"},
36+
})
37+
}

0 commit comments

Comments
 (0)