diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/required.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/required.go new file mode 100644 index 00000000000..d1829400d96 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/required.go @@ -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")} +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/required_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/required_test.go new file mode 100644 index 00000000000..6511271cd96 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/required_test.go @@ -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)) + } + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/util_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/util_test.go new file mode 100644 index 00000000000..28bd577c8d2 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/util_test.go @@ -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 "" + } + 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() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go new file mode 100644 index 00000000000..7b3d42e30ad --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go @@ -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 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