Add validators: optional/required/forbidden

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:51 -05:00
parent 31f4637217
commit 63050550c3
4 changed files with 1308 additions and 0 deletions

View File

@ -0,0 +1,133 @@
/*
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"
)
// RequiredValue verifies that the specified value is not the zero-value for
// its type.
func RequiredValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
var zero T
if *value != zero {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// RequiredPointer verifies that the specified pointer is not nil.
func RequiredPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value != nil {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// RequiredSlice verifies that the specified slice is not empty.
func RequiredSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// RequiredMap verifies that the specified map is not empty.
func RequiredMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// ForbiddenValue verifies that the specified value is the zero-value for its
// type.
func ForbiddenValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
var zero T
if *value == zero {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// ForbiddenPointer verifies that the specified pointer is nil.
func ForbiddenPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value == nil {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// ForbiddenSlice verifies that the specified slice is empty.
func ForbiddenSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
if len(value) == 0 {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// RequiredMap verifies that the specified map is empty.
func ForbiddenMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
if len(value) == 0 {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// OptionalValue verifies that the specified value is not the zero-value for
// its type. This is identical to RequiredValue, but the caller should treat an
// error here as an indication that the optional value was not specified.
func OptionalValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
var zero T
if *value != zero {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}
// OptionalPointer verifies that the specified pointer is not nil. This is
// identical to RequiredPointer, but the caller should treat an error here as an
// indication that the optional value was not specified.
func OptionalPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value != nil {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}
// OptionalSlice verifies that the specified slice is not empty. This is
// identical to RequiredSlice, but the caller should treat an error here as an
// indication that the optional value was not specified.
func OptionalSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}
// OptionalMap verifies that the specified map is not empty. This is identical
// to RequiredMap, but the caller should treat an error here as an indication that
// the optional value was not specified.
func OptionalMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}

View File

@ -0,0 +1,924 @@
/*
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"
"regexp"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestRequiredValue(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "value"
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "" // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 123
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0 // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := true
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{"value"}
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{} // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ptr.To("")
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil) // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestRequiredPointer(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*string)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*int)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*bool)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*struct{ S string })(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (**string)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestRequiredSlice(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{""}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{0}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{false}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{nil}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestRequiredMap(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{"": ""}
return RequiredMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{}
return RequiredMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{0: 0}
return RequiredMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{}
return RequiredMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[bool]bool{false: false}
return RequiredMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]bool{}
return RequiredMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalValue(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "value"
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "" // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 123
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0 // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := true
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{"value"}
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{} // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ptr.To("")
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil) // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalPointer(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*string)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*int)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*bool)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*struct{ S string })(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (**string)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalSlice(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{""}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{0}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{false}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{nil}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalMap(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{"": ""}
return OptionalMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{}
return OptionalMap(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{0: 0}
return OptionalMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{}
return OptionalMap(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[bool]bool{false: false}
return OptionalMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]bool{}
return OptionalMap(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenValue(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "value"
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 123
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := true
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{"value"}
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ptr.To("")
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenPointer(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*string)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*int)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*bool)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*struct{ S string })(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (**string)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenSlice(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{""}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{0}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{false}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{nil}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenMap(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{"": ""}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{0: 0}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]bool{}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[bool]bool{false: false}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}

View File

@ -0,0 +1,41 @@
/*
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 (
"bytes"
"strconv"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// fmtErrs is a helper for nicer test output. It will use multiple lines if
// errs has more than 1 item.
func fmtErrs(errs field.ErrorList) string {
if len(errs) == 0 {
return "<no errors>"
}
if len(errs) == 1 {
return strconv.Quote(errs[0].Error())
}
buf := bytes.Buffer{}
for _, e := range errs {
buf.WriteString("\n")
buf.WriteString(strconv.Quote(e.Error()))
}
return buf.String()
}

View File

@ -0,0 +1,210 @@
/*
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 (
"fmt"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/gengo/v2/types"
)
const (
requiredTagName = "k8s:required"
optionalTagName = "k8s:optional"
forbiddenTagName = "k8s:forbidden"
)
func init() {
RegisterTagValidator(requirednessTagValidator{requirednessRequired})
RegisterTagValidator(requirednessTagValidator{requirednessOptional})
RegisterTagValidator(requirednessTagValidator{requirednessForbidden})
}
// requirednessTagValidator implements multiple modes of requiredness.
type requirednessTagValidator struct {
mode requirednessMode
}
type requirednessMode string
const (
requirednessRequired requirednessMode = requiredTagName
requirednessOptional requirednessMode = optionalTagName
requirednessForbidden requirednessMode = forbiddenTagName
)
func (requirednessTagValidator) Init(_ Config) {}
func (rtv requirednessTagValidator) TagName() string {
return string(rtv.mode)
}
var requirednessTagValidScopes = sets.New(ScopeField)
func (requirednessTagValidator) ValidScopes() sets.Set[Scope] {
return requirednessTagValidScopes
}
func (rtv requirednessTagValidator) GetValidations(context Context, _ []string, _ string) (Validations, error) {
if context.Type.Kind == types.Alias {
panic("alias type should already have been unwrapped")
}
switch rtv.mode {
case requirednessRequired:
return rtv.doRequired(context)
case requirednessOptional:
return rtv.doOptional(context)
case requirednessForbidden:
return rtv.doForbidden(context)
}
panic(fmt.Sprintf("unknown requiredness mode: %q", rtv.mode))
}
var (
requiredValueValidator = types.Name{Package: libValidationPkg, Name: "RequiredValue"}
requiredPointerValidator = types.Name{Package: libValidationPkg, Name: "RequiredPointer"}
requiredSliceValidator = types.Name{Package: libValidationPkg, Name: "RequiredSlice"}
requiredMapValidator = types.Name{Package: libValidationPkg, Name: "RequiredMap"}
)
// TODO: It might be valuable to have a string payload for when requiredness is
// conditional (e.g. required when <otherfield> is specified).
func (requirednessTagValidator) doRequired(context Context) (Validations, error) {
// Most validators don't care whether the value they are validating was
// originally defined as a value-type or a pointer-type in the API. This
// one does. Since Go doesn't do partial specialization of templates, we
// do manual dispatch here.
switch context.Type.Kind {
case types.Slice:
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredSliceValidator)}}, nil
case types.Map:
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredMapValidator)}}, nil
case types.Pointer:
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredPointerValidator)}}, nil
case types.Struct:
// The +required tag on a non-pointer struct is only for documentation.
// We don't perform validation here and defer the validation to
// the struct's fields.
return Validations{Comments: []string{"required non-pointer structs are purely documentation"}}, nil
}
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredValueValidator)}}, nil
}
var (
optionalValueValidator = types.Name{Package: libValidationPkg, Name: "OptionalValue"}
optionalPointerValidator = types.Name{Package: libValidationPkg, Name: "OptionalPointer"}
optionalSliceValidator = types.Name{Package: libValidationPkg, Name: "OptionalSlice"}
optionalMapValidator = types.Name{Package: libValidationPkg, Name: "OptionalMap"}
)
func (requirednessTagValidator) doOptional(context Context) (Validations, error) {
// Most validators don't care whether the value they are validating was
// originally defined as a value-type or a pointer-type in the API. This
// one does. Since Go doesn't do partial specialization of templates, we
// do manual dispatch here.
switch context.Type.Kind {
case types.Slice:
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalSliceValidator)}}, nil
case types.Map:
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalMapValidator)}}, nil
case types.Pointer:
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalPointerValidator)}}, nil
case types.Struct:
// Specifying that a non-pointer struct is optional doesn't actually
// make sense technically almost ever, and is better described as a
// union inside the struct. It does, however, make sense as
// documentation.
return Validations{Comments: []string{"optional non-pointer structs are purely documentation"}}, nil
}
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalValueValidator)}}, nil
}
var (
forbiddenValueValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenValue"}
forbiddenPointerValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenPointer"}
forbiddenSliceValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenSlice"}
forbiddenMapValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenMap"}
)
// TODO: It might be valuable to have a string payload for when forbidden is
// conditional (e.g. forbidden when <option> is disabled).
func (requirednessTagValidator) doForbidden(context Context) (Validations, error) {
// Forbidden is weird. Each of these emits two checks, which are polar
// opposites. If the field fails the forbidden check, it will
// short-circuit and not run the optional check. If it passes the
// forbidden check, it must not be specified, so it will "fail" the
// optional check and short-circuit (but without error). Why? For
// example, this prevents any further validation from trying to run on a
// nil pointer.
switch context.Type.Kind {
case types.Slice:
return Validations{
Functions: []FunctionGen{
Function(forbiddenTagName, ShortCircuit, forbiddenSliceValidator),
Function(forbiddenTagName, ShortCircuit|NonError, optionalSliceValidator),
},
}, nil
case types.Map:
return Validations{
Functions: []FunctionGen{
Function(forbiddenTagName, ShortCircuit, forbiddenMapValidator),
Function(forbiddenTagName, ShortCircuit|NonError, optionalMapValidator),
},
}, nil
case types.Pointer:
return Validations{
Functions: []FunctionGen{
Function(forbiddenTagName, ShortCircuit, forbiddenPointerValidator),
Function(forbiddenTagName, ShortCircuit|NonError, optionalPointerValidator),
},
}, nil
case types.Struct:
// The +forbidden tag on a non-pointer struct is not supported.
// If you encounter this error and believe you have a valid use case
// for forbiddening a non-pointer struct, please let us know! We need
// to understand your scenario to determine if we need to adjust
// this behavior or provide alternative validation mechanisms.
return Validations{}, fmt.Errorf("non-pointer structs cannot use the %q tag", forbiddenTagName)
}
return Validations{
Functions: []FunctionGen{
Function(forbiddenTagName, ShortCircuit, forbiddenValueValidator),
Function(forbiddenTagName, ShortCircuit|NonError, optionalValueValidator),
},
}, nil
}
func (rtv requirednessTagValidator) Docs() TagDoc {
doc := TagDoc{
Tag: rtv.TagName(),
Scopes: rtv.ValidScopes().UnsortedList(),
}
switch rtv.mode {
case requirednessRequired:
doc.Description = "Indicates that a field must be specified by clients."
case requirednessOptional:
doc.Description = "Indicates that a field is optional to clients."
case requirednessForbidden:
doc.Description = "Indicates that a field may not be specified."
default:
panic(fmt.Sprintf("unknown requiredness mode: %q", rtv.mode))
}
return doc
}