Skip to content

Commit d6ef05b

Browse files
thockinjpbetz
authored andcommitted
Add +k8s:minimum validation tag
1 parent 67e1a1e commit d6ef05b

File tree

10 files changed

+582
-13
lines changed

10 files changed

+582
-13
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2025 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 constraints
18+
19+
// Signed is a constraint that permits any signed integer type.
20+
type Signed interface {
21+
~int | ~int8 | ~int16 | ~int32 | ~int64
22+
}
23+
24+
// Unsigned is a constraint that permits any unsigned integer type.
25+
type Unsigned interface {
26+
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
27+
}
28+
29+
// Integer is a constraint that permits any integer type.
30+
type Integer interface {
31+
Signed | Unsigned
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 content
18+
19+
import (
20+
"fmt"
21+
22+
"k8s.io/apimachinery/pkg/api/validate/constraints"
23+
)
24+
25+
// MinError returns a string explanation of a "must be greater than or equal"
26+
// validation failure.
27+
func MinError[T constraints.Integer](min T) string {
28+
return fmt.Sprintf("must be greater than or equal to %d", min)
29+
}
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 validate
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/api/operation"
23+
"k8s.io/apimachinery/pkg/api/validate/constraints"
24+
"k8s.io/apimachinery/pkg/api/validate/content"
25+
"k8s.io/apimachinery/pkg/util/validation/field"
26+
)
27+
28+
// Minimum verifies that the specified value is greater than or equal to min.
29+
func Minimum[T constraints.Integer](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, min T) field.ErrorList {
30+
if value == nil {
31+
return nil
32+
}
33+
if *value < min {
34+
return field.ErrorList{field.Invalid(fldPath, *value, content.MinError(min)).WithOrigin("minimum")}
35+
}
36+
return nil
37+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
"regexp"
22+
"testing"
23+
24+
"k8s.io/apimachinery/pkg/api/operation"
25+
"k8s.io/apimachinery/pkg/api/validate/constraints"
26+
"k8s.io/apimachinery/pkg/util/validation/field"
27+
)
28+
29+
func TestMinimum(t *testing.T) {
30+
testMinimumPositive[int](t)
31+
testMinimumNegative[int](t)
32+
testMinimumPositive[int8](t)
33+
testMinimumNegative[int8](t)
34+
testMinimumPositive[int16](t)
35+
testMinimumNegative[int16](t)
36+
testMinimumPositive[int32](t)
37+
testMinimumNegative[int32](t)
38+
testMinimumPositive[int64](t)
39+
testMinimumNegative[int64](t)
40+
41+
testMinimumPositive[uint](t)
42+
testMinimumPositive[uint8](t)
43+
testMinimumPositive[uint16](t)
44+
testMinimumPositive[uint32](t)
45+
testMinimumPositive[uint64](t)
46+
}
47+
48+
type minimumTestCase[T constraints.Integer] struct {
49+
value T
50+
min T
51+
err string // regex
52+
}
53+
54+
func testMinimumPositive[T constraints.Integer](t *testing.T) {
55+
t.Helper()
56+
cases := []minimumTestCase[T]{{
57+
value: 0,
58+
min: 0,
59+
}, {
60+
value: 0,
61+
min: 1,
62+
err: "fldpath: Invalid value.*must be greater than or equal to",
63+
}, {
64+
value: 1,
65+
min: 1,
66+
}, {
67+
value: 1,
68+
min: 2,
69+
err: "fldpath: Invalid value.*must be greater than or equal to",
70+
}}
71+
doTestMinimum[T](t, cases)
72+
}
73+
74+
func testMinimumNegative[T constraints.Signed](t *testing.T) {
75+
t.Helper()
76+
cases := []minimumTestCase[T]{{
77+
value: -1,
78+
min: -1,
79+
}, {
80+
value: -2,
81+
min: -1,
82+
err: "fldpath: Invalid value.*must be greater than or equal to",
83+
}}
84+
85+
doTestMinimum[T](t, cases)
86+
}
87+
88+
func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[T]) {
89+
t.Helper()
90+
for i, tc := range cases {
91+
v := tc.value
92+
result := Minimum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.min)
93+
if len(result) > 0 && tc.err == "" {
94+
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
95+
continue
96+
}
97+
if len(result) == 0 && tc.err != "" {
98+
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
99+
continue
100+
}
101+
if len(result) > 0 {
102+
if len(result) > 1 {
103+
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
104+
continue
105+
}
106+
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
107+
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
108+
}
109+
}
110+
}
111+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 minimum
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:minimum=1
31+
IntField int `json:"intField"`
32+
// +k8s:minimum=1
33+
IntPtrField *int `json:"intPtrField"`
34+
35+
// "int8" becomes "byte" somewhere in gengo. We don't need it so just skip it.
36+
37+
// +k8s:minimum=1
38+
Int16Field int16 `json:"int16Field"`
39+
// +k8s:minimum=1
40+
Int32Field int32 `json:"int32Field"`
41+
// +k8s:minimum=1
42+
Int64Field int64 `json:"int64Field"`
43+
44+
// +k8s:minimum=1
45+
UintField uint `json:"uintField"`
46+
// +k8s:minimum=1
47+
UintPtrField *uint `json:"uintPtrField"`
48+
49+
// +k8s:minimum=1
50+
Uint16Field uint16 `json:"uint16Field"`
51+
// +k8s:minimum=1
52+
Uint32Field uint32 `json:"uint32Field"`
53+
// +k8s:minimum=1
54+
Uint64Field uint64 `json:"uint64Field"`
55+
56+
// +k8s:minimum=1
57+
TypedefField IntType `json:"typedefField"`
58+
// +k8s:minimum=1
59+
TypedefPtrField *IntType `json:"typedefPtrField"`
60+
}
61+
62+
// +k8s:minimum=1
63+
type IntType int
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 minimum
18+
19+
import (
20+
"testing"
21+
22+
"k8s.io/apimachinery/pkg/api/validate/content"
23+
"k8s.io/apimachinery/pkg/util/validation/field"
24+
"k8s.io/utils/ptr"
25+
)
26+
27+
func Test(t *testing.T) {
28+
st := localSchemeBuilder.Test(t)
29+
30+
st.Value(&Struct{
31+
// all zero values
32+
IntPtrField: ptr.To(0),
33+
UintPtrField: ptr.To(uint(0)),
34+
TypedefPtrField: ptr.To(IntType(0)),
35+
}).ExpectInvalid(
36+
field.Invalid(field.NewPath("intField"), 0, content.MinError(1)),
37+
field.Invalid(field.NewPath("intPtrField"), 0, content.MinError(1)),
38+
field.Invalid(field.NewPath("int16Field"), 0, content.MinError(1)),
39+
field.Invalid(field.NewPath("int32Field"), 0, content.MinError(1)),
40+
field.Invalid(field.NewPath("int64Field"), 0, content.MinError(1)),
41+
field.Invalid(field.NewPath("uintField"), uint(0), content.MinError(1)),
42+
field.Invalid(field.NewPath("uintPtrField"), uint(0), content.MinError(1)),
43+
field.Invalid(field.NewPath("uint16Field"), uint(0), content.MinError(1)),
44+
field.Invalid(field.NewPath("uint32Field"), uint(0), content.MinError(1)),
45+
field.Invalid(field.NewPath("uint64Field"), uint(0), content.MinError(1)),
46+
field.Invalid(field.NewPath("typedefField"), 0, content.MinError(1)),
47+
field.Invalid(field.NewPath("typedefPtrField"), 0, content.MinError(1)),
48+
)
49+
50+
st.Value(&Struct{
51+
IntField: 1,
52+
IntPtrField: ptr.To(1),
53+
Int16Field: 1,
54+
Int32Field: 1,
55+
Int64Field: 1,
56+
UintField: 1,
57+
Uint16Field: 1,
58+
Uint32Field: 1,
59+
Uint64Field: 1,
60+
UintPtrField: ptr.To(uint(1)),
61+
TypedefField: IntType(1),
62+
TypedefPtrField: ptr.To(IntType(1)),
63+
}).ExpectValid()
64+
}

0 commit comments

Comments
 (0)