mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 22:46:12 +00:00
Add declarative validation to scheme
This commit is contained in:
parent
a5dda5d879
commit
5ff334a158
@ -17,15 +17,18 @@ limitations under the License.
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/naming"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Scheme defines methods for serializing and deserializing API objects, a type
|
||||
@ -68,6 +71,12 @@ type Scheme struct {
|
||||
// the provided object must be a pointer.
|
||||
defaulterFuncs map[reflect.Type]func(interface{})
|
||||
|
||||
// validationFuncs is a map to funcs to be called with an object to perform validation.
|
||||
// The provided object must be a pointer.
|
||||
// If oldObject is non-nil, update validation is performed and may perform additional
|
||||
// validation such as transition rules and immutability checks.
|
||||
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList
|
||||
|
||||
// converter stores all registered conversion functions. It also has
|
||||
// default converting behavior.
|
||||
converter *conversion.Converter
|
||||
@ -96,6 +105,7 @@ func NewScheme() *Scheme {
|
||||
unversionedKinds: map[string]reflect.Type{},
|
||||
fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
|
||||
defaulterFuncs: map[reflect.Type]func(interface{}){},
|
||||
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresource ...string) field.ErrorList{},
|
||||
versionPriority: map[string][]string{},
|
||||
schemeName: naming.GetNameFromCallsite(internalPackages...),
|
||||
}
|
||||
@ -347,6 +357,35 @@ func (s *Scheme) Default(src Object) {
|
||||
}
|
||||
}
|
||||
|
||||
// AddValidationFunc registered a function that can validate the object, and
|
||||
// oldObject. These functions will be invoked when Validate() or ValidateUpdate()
|
||||
// is called. The function will never be called unless the validated object
|
||||
// matches srcType. If this function is invoked twice with the same srcType, the
|
||||
// fn passed to the later call will be used instead.
|
||||
func (s *Scheme) AddValidationFunc(srcType Object, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList) {
|
||||
s.validationFuncs[reflect.TypeOf(srcType)] = fn
|
||||
}
|
||||
|
||||
// Validate validates the provided Object according to the generated declarative validation code.
|
||||
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
|
||||
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
|
||||
func (s *Scheme) Validate(ctx context.Context, options sets.Set[string], object Object, subresources ...string) field.ErrorList {
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Create, Options: options}, object, nil, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate validates the provided object and oldObject according to the generated declarative validation code.
|
||||
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
|
||||
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
|
||||
func (s *Scheme) ValidateUpdate(ctx context.Context, options sets.Set[string], object, oldObject Object, subresources ...string) field.ErrorList {
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Update, Options: options}, object, oldObject, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert will attempt to convert in into out. Both must be pointers. For easy
|
||||
// testing of conversion functions. Returns an error if the conversion isn't
|
||||
// possible. You can call this with types that haven't been registered (for example,
|
||||
|
@ -17,12 +17,15 @@ limitations under the License.
|
||||
package runtime_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@ -30,6 +33,9 @@ import (
|
||||
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
fieldtesting "k8s.io/apimachinery/pkg/util/validation/field/testing"
|
||||
)
|
||||
|
||||
type testConversions struct {
|
||||
@ -1009,3 +1015,101 @@ func TestMetaValuesUnregisteredConvert(t *testing.T) {
|
||||
t.Errorf("Expected %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterValidate(t *testing.T) {
|
||||
invalidValue := field.Invalid(field.NewPath("testString"), "", "Invalid value").WithOrigin("invalid-value")
|
||||
invalidLength := field.Invalid(field.NewPath("testString"), "", "Invalid length").WithOrigin("invalid-length")
|
||||
invalidStatusErr := field.Invalid(field.NewPath("testString"), "", "Invalid condition").WithOrigin("invalid-condition")
|
||||
invalidIfOptionErr := field.Invalid(field.NewPath("testString"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
oldObject runtime.Object
|
||||
subresource []string
|
||||
options sets.Set[string]
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "single error",
|
||||
object: &TestType1{},
|
||||
expected: field.ErrorList{invalidValue},
|
||||
},
|
||||
{
|
||||
name: "multiple errors",
|
||||
object: &TestType2{},
|
||||
expected: field.ErrorList{invalidValue, invalidLength},
|
||||
},
|
||||
{
|
||||
name: "update error",
|
||||
object: &TestType2{},
|
||||
oldObject: &TestType2{},
|
||||
expected: field.ErrorList{invalidLength},
|
||||
},
|
||||
{
|
||||
name: "options error",
|
||||
object: &TestType1{},
|
||||
options: sets.New("option1"),
|
||||
expected: field.ErrorList{invalidIfOptionErr},
|
||||
},
|
||||
{
|
||||
name: "subresource error",
|
||||
object: &TestType1{},
|
||||
subresource: []string{"status"},
|
||||
expected: field.ErrorList{invalidStatusErr},
|
||||
},
|
||||
}
|
||||
|
||||
s := runtime.NewScheme()
|
||||
ctx := context.Background()
|
||||
|
||||
// register multiple types for testing to ensure registration is working as expected
|
||||
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
|
||||
if op.Options.Has("option1") {
|
||||
return field.ErrorList{invalidIfOptionErr}
|
||||
}
|
||||
if len(subresources) == 1 && subresources[0] == "status" {
|
||||
return field.ErrorList{invalidStatusErr}
|
||||
}
|
||||
return field.ErrorList{invalidValue}
|
||||
})
|
||||
|
||||
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
|
||||
if oldObject != nil {
|
||||
return field.ErrorList{invalidLength}
|
||||
}
|
||||
return field.ErrorList{invalidValue, invalidLength}
|
||||
})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var results field.ErrorList
|
||||
if tc.oldObject == nil {
|
||||
results = s.Validate(ctx, tc.options, tc.object, tc.subresource...)
|
||||
} else {
|
||||
results = s.ValidateUpdate(ctx, tc.options, tc.object, tc.oldObject, tc.subresource...)
|
||||
}
|
||||
fieldtesting.MatchErrors(t, tc.expected, results, fieldtesting.Match().ByType().ByField().ByOrigin())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type TestType1 struct {
|
||||
Version string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
||||
|
||||
func (TestType1) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (TestType1) DeepCopyObject() runtime.Object { return nil }
|
||||
|
||||
type TestType2 struct {
|
||||
Version string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
||||
|
||||
func (TestType2) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (TestType2) DeepCopyObject() runtime.Object { return nil }
|
||||
|
Loading…
Reference in New Issue
Block a user