mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 08:17:26 +00:00
Return CR validation errors as field errors
This commit is contained in:
parent
eadf68ebd9
commit
4bf7e3f2b1
@ -7,6 +7,7 @@ go 1.12
|
|||||||
require (
|
require (
|
||||||
github.com/coreos/etcd v3.3.13+incompatible
|
github.com/coreos/etcd v3.3.13+incompatible
|
||||||
github.com/emicklei/go-restful v2.9.5+incompatible
|
github.com/emicklei/go-restful v2.9.5+incompatible
|
||||||
|
github.com/go-openapi/errors v0.19.2
|
||||||
github.com/go-openapi/spec v0.19.2
|
github.com/go-openapi/spec v0.19.2
|
||||||
github.com/go-openapi/strfmt v0.19.0
|
github.com/go-openapi/strfmt v0.19.0
|
||||||
github.com/go-openapi/validate v0.19.2
|
github.com/go-openapi/validate v0.19.2
|
||||||
|
@ -820,9 +820,8 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
|||||||
|
|
||||||
// validate the default value with user the provided schema.
|
// validate the default value with user the provided schema.
|
||||||
validator := govalidate.NewSchemaValidator(s.ToGoOpenAPI(), nil, "", strfmt.Default)
|
validator := govalidate.NewSchemaValidator(s.ToGoOpenAPI(), nil, "", strfmt.Default)
|
||||||
if err := apiservervalidation.ValidateCustomResource(interface{}(*schema.Default), validator); err != nil {
|
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, fmt.Sprintf("must validate: %v", err)))
|
allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(fldPath.Child("default"), interface{}(*schema.Default), validator)...)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
detail := "must not be set"
|
detail := "must not be set"
|
||||||
|
@ -1944,8 +1944,8 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
errors: []validationMatch{
|
errors: []validationMatch{
|
||||||
invalid("spec", "validation", "openAPIV3Schema", "properties[a]", "default"),
|
invalid("spec", "validation", "openAPIV3Schema", "properties[a]", "default"),
|
||||||
invalid("spec", "validation", "openAPIV3Schema", "properties[c]", "default"),
|
invalid("spec", "validation", "openAPIV3Schema", "properties[c]", "default", "foo"),
|
||||||
invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "default"),
|
invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "default", "bad"),
|
||||||
invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "properties[bad]", "pattern"),
|
invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "properties[bad]", "pattern"),
|
||||||
// we also expected unpruned and valid defaults under x-kubernetes-preserve-unknown-fields. We could be more
|
// we also expected unpruned and valid defaults under x-kubernetes-preserve-unknown-fields. We could be more
|
||||||
// strict here, but want to encourage proper specifications by forbidding other defaults.
|
// strict here, but want to encourage proper specifications by forbidding other defaults.
|
||||||
|
@ -13,6 +13,8 @@ go_library(
|
|||||||
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/validation",
|
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/validation",
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
|
"//vendor/github.com/go-openapi/errors:go_default_library",
|
||||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||||
"//vendor/github.com/go-openapi/strfmt:go_default_library",
|
"//vendor/github.com/go-openapi/strfmt:go_default_library",
|
||||||
"//vendor/github.com/go-openapi/validate:go_default_library",
|
"//vendor/github.com/go-openapi/validate:go_default_library",
|
||||||
@ -45,6 +47,7 @@ go_test(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -17,11 +17,16 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
openapierrors "github.com/go-openapi/errors"
|
||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/go-openapi/validate"
|
"github.com/go-openapi/validate"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSchemaValidator creates an openapi schema validator for the given CRD validation.
|
// NewSchemaValidator creates an openapi schema validator for the given CRD validation.
|
||||||
@ -39,16 +44,50 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
|
|||||||
|
|
||||||
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
|
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
|
||||||
// CustomResource is a JSON data structure.
|
// CustomResource is a JSON data structure.
|
||||||
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
|
func ValidateCustomResource(fldPath *field.Path, customResource interface{}, validator *validate.SchemaValidator) field.ErrorList {
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := validator.Validate(customResource)
|
result := validator.Validate(customResource)
|
||||||
if result.AsError() != nil {
|
if result.IsValid() {
|
||||||
return result.AsError()
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
var allErrs field.ErrorList
|
||||||
|
for _, err := range result.Errors {
|
||||||
|
switch err := err.(type) {
|
||||||
|
|
||||||
|
case *openapierrors.Validation:
|
||||||
|
switch err.Code() {
|
||||||
|
|
||||||
|
case openapierrors.RequiredFailCode:
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child(strings.TrimPrefix(err.Name, ".")), ""))
|
||||||
|
|
||||||
|
case openapierrors.EnumFailCode:
|
||||||
|
values := []string{}
|
||||||
|
for _, allowedValue := range err.Values {
|
||||||
|
if s, ok := allowedValue.(string); ok {
|
||||||
|
values = append(values, s)
|
||||||
|
} else {
|
||||||
|
allowedJSON, _ := json.Marshal(allowedValue)
|
||||||
|
values = append(values, string(allowedJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child(strings.TrimPrefix(err.Name, ".")), err.Value, values))
|
||||||
|
|
||||||
|
default:
|
||||||
|
value := interface{}("")
|
||||||
|
if err.Value != nil {
|
||||||
|
value = err.Value
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child(strings.TrimPrefix(err.Name, ".")), value, err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath, "", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema.
|
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema.
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
||||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
@ -29,6 +30,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRoundTrip checks the conversion to go-openapi types.
|
// TestRoundTrip checks the conversion to go-openapi types.
|
||||||
@ -121,12 +123,17 @@ func stripIntOrStringType(x interface{}) interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type failingObject struct {
|
||||||
|
object interface{}
|
||||||
|
expectErrs []string
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateCustomResource(t *testing.T) {
|
func TestValidateCustomResource(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
schema apiextensions.JSONSchemaProps
|
schema apiextensions.JSONSchemaProps
|
||||||
objects []interface{}
|
objects []interface{}
|
||||||
failingObjects []interface{}
|
failingObjects []failingObject
|
||||||
}{
|
}{
|
||||||
{name: "!nullable",
|
{name: "!nullable",
|
||||||
schema: apiextensions.JSONSchemaProps{
|
schema: apiextensions.JSONSchemaProps{
|
||||||
@ -141,12 +148,13 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
map[string]interface{}{},
|
map[string]interface{}{},
|
||||||
map[string]interface{}{"field": map[string]interface{}{}},
|
map[string]interface{}{"field": map[string]interface{}{}},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{
|
failingObjects: []failingObject{
|
||||||
map[string]interface{}{"field": "foo"},
|
{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
|
||||||
map[string]interface{}{"field": 42},
|
{object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
|
||||||
map[string]interface{}{"field": true},
|
{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
|
||||||
map[string]interface{}{"field": 1.2},
|
{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
|
||||||
map[string]interface{}{"field": []interface{}{}},
|
{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type object: "array"`}},
|
||||||
|
{object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type object: "null"`}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "nullable",
|
{name: "nullable",
|
||||||
@ -163,12 +171,12 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
map[string]interface{}{"field": map[string]interface{}{}},
|
map[string]interface{}{"field": map[string]interface{}{}},
|
||||||
map[string]interface{}{"field": nil},
|
map[string]interface{}{"field": nil},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{
|
failingObjects: []failingObject{
|
||||||
map[string]interface{}{"field": "foo"},
|
{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
|
||||||
map[string]interface{}{"field": 42},
|
{object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
|
||||||
map[string]interface{}{"field": true},
|
{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
|
||||||
map[string]interface{}{"field": 1.2},
|
{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
|
||||||
map[string]interface{}{"field": []interface{}{}},
|
{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type object: "array"`}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "nullable and no type",
|
{name: "nullable and no type",
|
||||||
@ -203,12 +211,12 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
map[string]interface{}{"field": 42},
|
map[string]interface{}{"field": 42},
|
||||||
map[string]interface{}{"field": "foo"},
|
map[string]interface{}{"field": "foo"},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{
|
failingObjects: []failingObject{
|
||||||
map[string]interface{}{"field": nil},
|
{object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type integer,string: "null"`}},
|
||||||
map[string]interface{}{"field": true},
|
{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
|
||||||
map[string]interface{}{"field": 1.2},
|
{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
|
||||||
map[string]interface{}{"field": map[string]interface{}{}},
|
{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
|
||||||
map[string]interface{}{"field": []interface{}{}},
|
{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type integer,string: "array"`}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "nullable and x-kubernetes-int-or-string",
|
{name: "nullable and x-kubernetes-int-or-string",
|
||||||
@ -226,11 +234,11 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
map[string]interface{}{"field": "foo"},
|
map[string]interface{}{"field": "foo"},
|
||||||
map[string]interface{}{"field": nil},
|
map[string]interface{}{"field": nil},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{
|
failingObjects: []failingObject{
|
||||||
map[string]interface{}{"field": true},
|
{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
|
||||||
map[string]interface{}{"field": 1.2},
|
{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
|
||||||
map[string]interface{}{"field": map[string]interface{}{}},
|
{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
|
||||||
map[string]interface{}{"field": []interface{}{}},
|
{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type integer,string: "array"`}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "nullable, x-kubernetes-int-or-string and user-provided anyOf",
|
{name: "nullable, x-kubernetes-int-or-string and user-provided anyOf",
|
||||||
@ -252,11 +260,27 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
map[string]interface{}{"field": 42},
|
map[string]interface{}{"field": 42},
|
||||||
map[string]interface{}{"field": "foo"},
|
map[string]interface{}{"field": "foo"},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{
|
failingObjects: []failingObject{
|
||||||
map[string]interface{}{"field": true},
|
{object: map[string]interface{}{"field": true}, expectErrs: []string{
|
||||||
map[string]interface{}{"field": 1.2},
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
map[string]interface{}{"field": map[string]interface{}{}},
|
`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`,
|
||||||
map[string]interface{}{"field": []interface{}{}},
|
`field: Invalid value: "boolean": field in body must be of type integer: "boolean"`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{
|
||||||
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
|
`field: Invalid value: "number": field in body must be of type integer,string: "number"`,
|
||||||
|
`field: Invalid value: "number": field in body must be of type integer: "number"`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
|
||||||
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
|
`field: Invalid value: "object": field in body must be of type integer,string: "object"`,
|
||||||
|
`field: Invalid value: "object": field in body must be of type integer: "object"`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{
|
||||||
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
|
`field: Invalid value: "array": field in body must be of type integer,string: "array"`,
|
||||||
|
`field: Invalid value: "array": field in body must be of type integer: "array"`,
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "nullable, x-kubernetes-int-or-string and user-provider allOf",
|
{name: "nullable, x-kubernetes-int-or-string and user-provider allOf",
|
||||||
@ -282,11 +306,31 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
map[string]interface{}{"field": 42},
|
map[string]interface{}{"field": 42},
|
||||||
map[string]interface{}{"field": "foo"},
|
map[string]interface{}{"field": "foo"},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{
|
failingObjects: []failingObject{
|
||||||
map[string]interface{}{"field": true},
|
{object: map[string]interface{}{"field": true}, expectErrs: []string{
|
||||||
map[string]interface{}{"field": 1.2},
|
`: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
|
||||||
map[string]interface{}{"field": map[string]interface{}{}},
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
map[string]interface{}{"field": []interface{}{}},
|
`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`,
|
||||||
|
`field: Invalid value: "boolean": field in body must be of type integer: "boolean"`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{
|
||||||
|
`: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
|
||||||
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
|
`field: Invalid value: "number": field in body must be of type integer,string: "number"`,
|
||||||
|
`field: Invalid value: "number": field in body must be of type integer: "number"`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
|
||||||
|
`: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
|
||||||
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
|
`field: Invalid value: "object": field in body must be of type integer,string: "object"`,
|
||||||
|
`field: Invalid value: "object": field in body must be of type integer: "object"`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{
|
||||||
|
`: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
|
||||||
|
`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
|
||||||
|
`field: Invalid value: "array": field in body must be of type integer,string: "array"`,
|
||||||
|
`field: Invalid value: "array": field in body must be of type integer: "array"`,
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "invalid regex",
|
{name: "invalid regex",
|
||||||
@ -298,7 +342,59 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
failingObjects: []interface{}{map[string]interface{}{"field": "foo"}},
|
failingObjects: []failingObject{
|
||||||
|
{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{"field: Invalid value: \"\": field in body should match '+, but pattern is invalid: error parsing regexp: missing argument to repetition operator: `+`'"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "required field",
|
||||||
|
schema: apiextensions.JSONSchemaProps{
|
||||||
|
Required: []string{"field"},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"field": {
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"nested"},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"nested": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failingObjects: []failingObject{
|
||||||
|
{object: map[string]interface{}{"test": "a"}, expectErrs: []string{`field: Required value`}},
|
||||||
|
{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field.nested: Required value`}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "enum",
|
||||||
|
schema: apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"field": {
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"nestedint", "nestedstring"},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"nestedint": {
|
||||||
|
Type: "integer",
|
||||||
|
Enum: []apiextensions.JSON{1, 2},
|
||||||
|
},
|
||||||
|
"nestedstring": {
|
||||||
|
Type: "string",
|
||||||
|
Enum: []apiextensions.JSON{"a", "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failingObjects: []failingObject{
|
||||||
|
{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
|
||||||
|
`field.nestedint: Required value`,
|
||||||
|
`field.nestedstring: Required value`,
|
||||||
|
}},
|
||||||
|
{object: map[string]interface{}{"field": map[string]interface{}{"nestedint": "x", "nestedstring": true}}, expectErrs: []string{
|
||||||
|
`field.nestedint: Invalid value: "string": field.nestedint in body must be of type integer: "string"`,
|
||||||
|
`field.nestedint: Unsupported value: "x": supported values: "1", "2"`,
|
||||||
|
`field.nestedstring: Invalid value: "boolean": field.nestedstring in body must be of type string: "boolean"`,
|
||||||
|
`field.nestedstring: Unsupported value: true: supported values: "a", "b"`,
|
||||||
|
}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -308,13 +404,25 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, obj := range tt.objects {
|
for _, obj := range tt.objects {
|
||||||
if err := ValidateCustomResource(obj, validator); err != nil {
|
if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
|
||||||
t.Errorf("unexpected validation error for %v: %v", obj, err)
|
t.Errorf("unexpected validation error for %v: %v", obj, errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, obj := range tt.failingObjects {
|
for i, failingObject := range tt.failingObjects {
|
||||||
if err := ValidateCustomResource(obj, validator); err == nil {
|
if errs := ValidateCustomResource(nil, failingObject.object, validator); len(errs) == 0 {
|
||||||
t.Errorf("missing error for %v", obj)
|
t.Errorf("missing error for %v", failingObject.object)
|
||||||
|
} else {
|
||||||
|
sawErrors := sets.NewString()
|
||||||
|
for _, err := range errs {
|
||||||
|
sawErrors.Insert(err.Error())
|
||||||
|
}
|
||||||
|
expectErrs := sets.NewString(failingObject.expectErrs...)
|
||||||
|
for _, unexpectedError := range sawErrors.Difference(expectErrs).List() {
|
||||||
|
t.Errorf("%d: unexpected error: %s", i, unexpectedError)
|
||||||
|
}
|
||||||
|
for _, missingError := range expectErrs.Difference(sawErrors).List() {
|
||||||
|
t.Errorf("%d: missing error: %s", i, missingError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -367,11 +475,11 @@ func TestItemsProperty(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := ValidateCustomResource(tt.args.object, validator); (err != nil) != tt.wantErr {
|
if errs := ValidateCustomResource(nil, tt.args.object, validator); (len(errs) > 0) != tt.wantErr {
|
||||||
if err == nil {
|
if len(errs) == 0 {
|
||||||
t.Error("expected error, but didn't get one")
|
t.Error("expected error, but didn't get one")
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("unexpected validation error: %v", err)
|
t.Errorf("unexpected validation error: %v", errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -59,9 +59,7 @@ func (a customResourceValidator) Validate(ctx context.Context, obj runtime.Objec
|
|||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
allErrs = append(allErrs, validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))...)
|
||||||
if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
|
allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
|
||||||
allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
|
|
||||||
}
|
|
||||||
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
|
||||||
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
||||||
|
|
||||||
@ -89,9 +87,7 @@ func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old ru
|
|||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
||||||
if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
|
allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
|
||||||
allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
|
|
||||||
}
|
|
||||||
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
|
||||||
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
||||||
|
|
||||||
@ -119,9 +115,7 @@ func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj,
|
|||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
||||||
if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
|
allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
|
||||||
allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
|
|
||||||
}
|
|
||||||
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
|
@ -321,9 +321,9 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
ns := "not-the-default"
|
ns := "not-the-default"
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
instanceFn func() *unstructured.Unstructured
|
instanceFn func() *unstructured.Unstructured
|
||||||
expectedError string
|
expectedErrors []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "bad alpha",
|
name: "bad alpha",
|
||||||
@ -332,7 +332,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
instance.Object["alpha"] = "foo_123!"
|
instance.Object["alpha"] = "foo_123!"
|
||||||
return instance
|
return instance
|
||||||
},
|
},
|
||||||
expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'",
|
expectedErrors: []string{"alpha in body should match '^[a-zA-Z0-9_]*$'"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad beta",
|
name: "bad beta",
|
||||||
@ -341,7 +341,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
instance.Object["beta"] = 5
|
instance.Object["beta"] = 5
|
||||||
return instance
|
return instance
|
||||||
},
|
},
|
||||||
expectedError: "beta in body should be greater than or equal to 10",
|
expectedErrors: []string{"beta in body should be greater than or equal to 10"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad gamma",
|
name: "bad gamma",
|
||||||
@ -350,7 +350,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
instance.Object["gamma"] = "qux"
|
instance.Object["gamma"] = "qux"
|
||||||
return instance
|
return instance
|
||||||
},
|
},
|
||||||
expectedError: "gamma in body should be one of [foo bar baz]",
|
expectedErrors: []string{`gamma: Unsupported value: "qux": supported values: "foo", "bar", "baz"`},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad delta",
|
name: "bad delta",
|
||||||
@ -359,7 +359,10 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
instance.Object["delta"] = "foobarbaz"
|
instance.Object["delta"] = "foobarbaz"
|
||||||
return instance
|
return instance
|
||||||
},
|
},
|
||||||
expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long",
|
expectedErrors: []string{
|
||||||
|
"must validate at least one schema (anyOf)",
|
||||||
|
"delta in body should be at most 5 chars long",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "absent alpha and beta",
|
name: "absent alpha and beta",
|
||||||
@ -377,7 +380,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
},
|
},
|
||||||
expectedError: ".alpha in body is required\n.beta in body is required",
|
expectedErrors: []string{"alpha: Required value", "beta: Required value"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,13 +391,14 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||||||
instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
|
instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
|
||||||
_, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
|
_, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("%v: expected %v", tc.name, tc.expectedError)
|
t.Errorf("%v: expected %v", tc.name, tc.expectedErrors)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
|
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
|
||||||
if !strings.Contains(err.Error(), tc.expectedError) {
|
for _, expectedError := range tc.expectedErrors {
|
||||||
t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
continue
|
t.Errorf("%v: expected %v, got %v", tc.name, expectedError, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user