Skip to content

Commit 5ff334a

Browse files
committed
Add declarative validation to scheme
1 parent a5dda5d commit 5ff334a

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed

staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ limitations under the License.
1717
package runtime
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"reflect"
2223
"strings"
2324

25+
"k8s.io/apimachinery/pkg/api/operation"
2426
"k8s.io/apimachinery/pkg/conversion"
2527
"k8s.io/apimachinery/pkg/runtime/schema"
2628
"k8s.io/apimachinery/pkg/util/naming"
2729
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2830
"k8s.io/apimachinery/pkg/util/sets"
31+
"k8s.io/apimachinery/pkg/util/validation/field"
2932
)
3033

3134
// Scheme defines methods for serializing and deserializing API objects, a type
@@ -68,6 +71,12 @@ type Scheme struct {
6871
// the provided object must be a pointer.
6972
defaulterFuncs map[reflect.Type]func(interface{})
7073

74+
// validationFuncs is a map to funcs to be called with an object to perform validation.
75+
// The provided object must be a pointer.
76+
// If oldObject is non-nil, update validation is performed and may perform additional
77+
// validation such as transition rules and immutability checks.
78+
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList
79+
7180
// converter stores all registered conversion functions. It also has
7281
// default converting behavior.
7382
converter *conversion.Converter
@@ -96,6 +105,7 @@ func NewScheme() *Scheme {
96105
unversionedKinds: map[string]reflect.Type{},
97106
fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
98107
defaulterFuncs: map[reflect.Type]func(interface{}){},
108+
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresource ...string) field.ErrorList{},
99109
versionPriority: map[string][]string{},
100110
schemeName: naming.GetNameFromCallsite(internalPackages...),
101111
}
@@ -347,6 +357,35 @@ func (s *Scheme) Default(src Object) {
347357
}
348358
}
349359

360+
// AddValidationFunc registered a function that can validate the object, and
361+
// oldObject. These functions will be invoked when Validate() or ValidateUpdate()
362+
// is called. The function will never be called unless the validated object
363+
// matches srcType. If this function is invoked twice with the same srcType, the
364+
// fn passed to the later call will be used instead.
365+
func (s *Scheme) AddValidationFunc(srcType Object, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList) {
366+
s.validationFuncs[reflect.TypeOf(srcType)] = fn
367+
}
368+
369+
// Validate validates the provided Object according to the generated declarative validation code.
370+
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
371+
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
372+
func (s *Scheme) Validate(ctx context.Context, options sets.Set[string], object Object, subresources ...string) field.ErrorList {
373+
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
374+
return fn(ctx, operation.Operation{Type: operation.Create, Options: options}, object, nil, subresources...)
375+
}
376+
return nil
377+
}
378+
379+
// ValidateUpdate validates the provided object and oldObject according to the generated declarative validation code.
380+
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
381+
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
382+
func (s *Scheme) ValidateUpdate(ctx context.Context, options sets.Set[string], object, oldObject Object, subresources ...string) field.ErrorList {
383+
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
384+
return fn(ctx, operation.Operation{Type: operation.Update, Options: options}, object, oldObject, subresources...)
385+
}
386+
return nil
387+
}
388+
350389
// Convert will attempt to convert in into out. Both must be pointers. For easy
351390
// testing of conversion functions. Returns an error if the conversion isn't
352391
// possible. You can call this with types that haven't been registered (for example,

staging/src/k8s.io/apimachinery/pkg/runtime/scheme_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,25 @@ limitations under the License.
1717
package runtime_test
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"reflect"
2223
"strings"
2324
"testing"
2425

2526
"github.com/google/go-cmp/cmp"
27+
28+
"k8s.io/apimachinery/pkg/api/operation"
2629
"k8s.io/apimachinery/pkg/conversion"
2730
"k8s.io/apimachinery/pkg/runtime"
2831
"k8s.io/apimachinery/pkg/runtime/schema"
2932
"k8s.io/apimachinery/pkg/runtime/serializer"
3033
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
3134
"k8s.io/apimachinery/pkg/util/diff"
3235
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
36+
"k8s.io/apimachinery/pkg/util/sets"
37+
"k8s.io/apimachinery/pkg/util/validation/field"
38+
fieldtesting "k8s.io/apimachinery/pkg/util/validation/field/testing"
3339
)
3440

3541
type testConversions struct {
@@ -1009,3 +1015,101 @@ func TestMetaValuesUnregisteredConvert(t *testing.T) {
10091015
t.Errorf("Expected %v, got %v", e, a)
10101016
}
10111017
}
1018+
1019+
func TestRegisterValidate(t *testing.T) {
1020+
invalidValue := field.Invalid(field.NewPath("testString"), "", "Invalid value").WithOrigin("invalid-value")
1021+
invalidLength := field.Invalid(field.NewPath("testString"), "", "Invalid length").WithOrigin("invalid-length")
1022+
invalidStatusErr := field.Invalid(field.NewPath("testString"), "", "Invalid condition").WithOrigin("invalid-condition")
1023+
invalidIfOptionErr := field.Invalid(field.NewPath("testString"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set")
1024+
1025+
testCases := []struct {
1026+
name string
1027+
object runtime.Object
1028+
oldObject runtime.Object
1029+
subresource []string
1030+
options sets.Set[string]
1031+
expected field.ErrorList
1032+
}{
1033+
{
1034+
name: "single error",
1035+
object: &TestType1{},
1036+
expected: field.ErrorList{invalidValue},
1037+
},
1038+
{
1039+
name: "multiple errors",
1040+
object: &TestType2{},
1041+
expected: field.ErrorList{invalidValue, invalidLength},
1042+
},
1043+
{
1044+
name: "update error",
1045+
object: &TestType2{},
1046+
oldObject: &TestType2{},
1047+
expected: field.ErrorList{invalidLength},
1048+
},
1049+
{
1050+
name: "options error",
1051+
object: &TestType1{},
1052+
options: sets.New("option1"),
1053+
expected: field.ErrorList{invalidIfOptionErr},
1054+
},
1055+
{
1056+
name: "subresource error",
1057+
object: &TestType1{},
1058+
subresource: []string{"status"},
1059+
expected: field.ErrorList{invalidStatusErr},
1060+
},
1061+
}
1062+
1063+
s := runtime.NewScheme()
1064+
ctx := context.Background()
1065+
1066+
// register multiple types for testing to ensure registration is working as expected
1067+
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
1068+
if op.Options.Has("option1") {
1069+
return field.ErrorList{invalidIfOptionErr}
1070+
}
1071+
if len(subresources) == 1 && subresources[0] == "status" {
1072+
return field.ErrorList{invalidStatusErr}
1073+
}
1074+
return field.ErrorList{invalidValue}
1075+
})
1076+
1077+
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
1078+
if oldObject != nil {
1079+
return field.ErrorList{invalidLength}
1080+
}
1081+
return field.ErrorList{invalidValue, invalidLength}
1082+
})
1083+
1084+
for _, tc := range testCases {
1085+
t.Run(tc.name, func(t *testing.T) {
1086+
var results field.ErrorList
1087+
if tc.oldObject == nil {
1088+
results = s.Validate(ctx, tc.options, tc.object, tc.subresource...)
1089+
} else {
1090+
results = s.ValidateUpdate(ctx, tc.options, tc.object, tc.oldObject, tc.subresource...)
1091+
}
1092+
fieldtesting.MatchErrors(t, tc.expected, results, fieldtesting.Match().ByType().ByField().ByOrigin())
1093+
})
1094+
}
1095+
}
1096+
1097+
type TestType1 struct {
1098+
Version string `json:"apiVersion,omitempty"`
1099+
Kind string `json:"kind,omitempty"`
1100+
TestString string `json:"testString"`
1101+
}
1102+
1103+
func (TestType1) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
1104+
1105+
func (TestType1) DeepCopyObject() runtime.Object { return nil }
1106+
1107+
type TestType2 struct {
1108+
Version string `json:"apiVersion,omitempty"`
1109+
Kind string `json:"kind,omitempty"`
1110+
TestString string `json:"testString"`
1111+
}
1112+
1113+
func (TestType2) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
1114+
1115+
func (TestType2) DeepCopyObject() runtime.Object { return nil }

0 commit comments

Comments
 (0)