Merge pull request #81212 from liggitt/crd-validation
Return CR validation errors as field errors
This commit is contained in:
		@@ -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)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user