mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	Return CR validation errors as field errors
This commit is contained in:
		@@ -7,6 +7,7 @@ go 1.12
 | 
			
		||||
require (
 | 
			
		||||
	github.com/coreos/etcd v3.3.13+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/strfmt v0.19.0
 | 
			
		||||
	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.
 | 
			
		||||
				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 {
 | 
			
		||||
			detail := "must not be set"
 | 
			
		||||
 
 | 
			
		||||
@@ -1944,8 +1944,8 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			errors: []validationMatch{
 | 
			
		||||
				invalid("spec", "validation", "openAPIV3Schema", "properties[a]", "default"),
 | 
			
		||||
				invalid("spec", "validation", "openAPIV3Schema", "properties[c]", "default"),
 | 
			
		||||
				invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "default"),
 | 
			
		||||
				invalid("spec", "validation", "openAPIV3Schema", "properties[c]", "default", "foo"),
 | 
			
		||||
				invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "default", "bad"),
 | 
			
		||||
				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
 | 
			
		||||
				// 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",
 | 
			
		||||
    deps = [
 | 
			
		||||
        "//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/strfmt: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/serializer: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",
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,16 @@ limitations under the License.
 | 
			
		||||
package validation
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	openapierrors "github.com/go-openapi/errors"
 | 
			
		||||
	"github.com/go-openapi/spec"
 | 
			
		||||
	"github.com/go-openapi/strfmt"
 | 
			
		||||
	"github.com/go-openapi/validate"
 | 
			
		||||
 | 
			
		||||
	"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.
 | 
			
		||||
@@ -39,16 +44,50 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
 | 
			
		||||
 | 
			
		||||
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
 | 
			
		||||
// 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 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result := validator.Validate(customResource)
 | 
			
		||||
	if result.AsError() != nil {
 | 
			
		||||
		return result.AsError()
 | 
			
		||||
	if result.IsValid() {
 | 
			
		||||
		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.
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-openapi/spec"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
 | 
			
		||||
	apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
 | 
			
		||||
	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 | 
			
		||||
@@ -29,6 +30,7 @@ import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime/serializer"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/json"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		schema         apiextensions.JSONSchemaProps
 | 
			
		||||
		objects        []interface{}
 | 
			
		||||
		failingObjects []interface{}
 | 
			
		||||
		failingObjects []failingObject
 | 
			
		||||
	}{
 | 
			
		||||
		{name: "!nullable",
 | 
			
		||||
			schema: apiextensions.JSONSchemaProps{
 | 
			
		||||
@@ -141,12 +148,13 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				map[string]interface{}{},
 | 
			
		||||
				map[string]interface{}{"field": map[string]interface{}{}},
 | 
			
		||||
			},
 | 
			
		||||
			failingObjects: []interface{}{
 | 
			
		||||
				map[string]interface{}{"field": "foo"},
 | 
			
		||||
				map[string]interface{}{"field": 42},
 | 
			
		||||
				map[string]interface{}{"field": true},
 | 
			
		||||
				map[string]interface{}{"field": 1.2},
 | 
			
		||||
				map[string]interface{}{"field": []interface{}{}},
 | 
			
		||||
			failingObjects: []failingObject{
 | 
			
		||||
				{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
 | 
			
		||||
				{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",
 | 
			
		||||
@@ -163,12 +171,12 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				map[string]interface{}{"field": map[string]interface{}{}},
 | 
			
		||||
				map[string]interface{}{"field": nil},
 | 
			
		||||
			},
 | 
			
		||||
			failingObjects: []interface{}{
 | 
			
		||||
				map[string]interface{}{"field": "foo"},
 | 
			
		||||
				map[string]interface{}{"field": 42},
 | 
			
		||||
				map[string]interface{}{"field": true},
 | 
			
		||||
				map[string]interface{}{"field": 1.2},
 | 
			
		||||
				map[string]interface{}{"field": []interface{}{}},
 | 
			
		||||
			failingObjects: []failingObject{
 | 
			
		||||
				{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
 | 
			
		||||
				{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",
 | 
			
		||||
@@ -203,12 +211,12 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				map[string]interface{}{"field": 42},
 | 
			
		||||
				map[string]interface{}{"field": "foo"},
 | 
			
		||||
			},
 | 
			
		||||
			failingObjects: []interface{}{
 | 
			
		||||
				map[string]interface{}{"field": nil},
 | 
			
		||||
				map[string]interface{}{"field": true},
 | 
			
		||||
				map[string]interface{}{"field": 1.2},
 | 
			
		||||
				map[string]interface{}{"field": map[string]interface{}{}},
 | 
			
		||||
				map[string]interface{}{"field": []interface{}{}},
 | 
			
		||||
			failingObjects: []failingObject{
 | 
			
		||||
				{object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type integer,string: "null"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
 | 
			
		||||
				{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",
 | 
			
		||||
@@ -226,11 +234,11 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				map[string]interface{}{"field": "foo"},
 | 
			
		||||
				map[string]interface{}{"field": nil},
 | 
			
		||||
			},
 | 
			
		||||
			failingObjects: []interface{}{
 | 
			
		||||
				map[string]interface{}{"field": true},
 | 
			
		||||
				map[string]interface{}{"field": 1.2},
 | 
			
		||||
				map[string]interface{}{"field": map[string]interface{}{}},
 | 
			
		||||
				map[string]interface{}{"field": []interface{}{}},
 | 
			
		||||
			failingObjects: []failingObject{
 | 
			
		||||
				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
 | 
			
		||||
				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
 | 
			
		||||
				{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",
 | 
			
		||||
@@ -252,11 +260,27 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				map[string]interface{}{"field": 42},
 | 
			
		||||
				map[string]interface{}{"field": "foo"},
 | 
			
		||||
			},
 | 
			
		||||
			failingObjects: []interface{}{
 | 
			
		||||
				map[string]interface{}{"field": true},
 | 
			
		||||
				map[string]interface{}{"field": 1.2},
 | 
			
		||||
				map[string]interface{}{"field": map[string]interface{}{}},
 | 
			
		||||
				map[string]interface{}{"field": []interface{}{}},
 | 
			
		||||
			failingObjects: []failingObject{
 | 
			
		||||
				{object: map[string]interface{}{"field": true}, expectErrs: []string{
 | 
			
		||||
					`: Invalid value: "": "field" must validate at least one schema (anyOf)`,
 | 
			
		||||
					`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 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",
 | 
			
		||||
@@ -282,11 +306,31 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				map[string]interface{}{"field": 42},
 | 
			
		||||
				map[string]interface{}{"field": "foo"},
 | 
			
		||||
			},
 | 
			
		||||
			failingObjects: []interface{}{
 | 
			
		||||
				map[string]interface{}{"field": true},
 | 
			
		||||
				map[string]interface{}{"field": 1.2},
 | 
			
		||||
				map[string]interface{}{"field": map[string]interface{}{}},
 | 
			
		||||
				map[string]interface{}{"field": []interface{}{}},
 | 
			
		||||
			failingObjects: []failingObject{
 | 
			
		||||
				{object: map[string]interface{}{"field": true}, 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: "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",
 | 
			
		||||
@@ -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 {
 | 
			
		||||
@@ -308,13 +404,25 @@ func TestValidateCustomResource(t *testing.T) {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			for _, obj := range tt.objects {
 | 
			
		||||
				if err := ValidateCustomResource(obj, validator); err != nil {
 | 
			
		||||
					t.Errorf("unexpected validation error for %v: %v", obj, err)
 | 
			
		||||
				if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
 | 
			
		||||
					t.Errorf("unexpected validation error for %v: %v", obj, errs)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			for _, obj := range tt.failingObjects {
 | 
			
		||||
				if err := ValidateCustomResource(obj, validator); err == nil {
 | 
			
		||||
					t.Errorf("missing error for %v", obj)
 | 
			
		||||
			for i, failingObject := range tt.failingObjects {
 | 
			
		||||
				if errs := ValidateCustomResource(nil, failingObject.object, validator); len(errs) == 0 {
 | 
			
		||||
					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 {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			if err := ValidateCustomResource(tt.args.object, validator); (err != nil) != tt.wantErr {
 | 
			
		||||
				if err == nil {
 | 
			
		||||
			if errs := ValidateCustomResource(nil, tt.args.object, validator); (len(errs) > 0) != tt.wantErr {
 | 
			
		||||
				if len(errs) == 0 {
 | 
			
		||||
					t.Error("expected error, but didn't get one")
 | 
			
		||||
				} 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
 | 
			
		||||
 | 
			
		||||
	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, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
 | 
			
		||||
	}
 | 
			
		||||
	allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
 | 
			
		||||
	allErrs = append(allErrs, a.ValidateScaleSpec(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
 | 
			
		||||
 | 
			
		||||
	allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
 | 
			
		||||
	if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
 | 
			
		||||
		allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
 | 
			
		||||
	}
 | 
			
		||||
	allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
 | 
			
		||||
	allErrs = append(allErrs, a.ValidateScaleSpec(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
 | 
			
		||||
 | 
			
		||||
	allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
 | 
			
		||||
	if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
 | 
			
		||||
		allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
 | 
			
		||||
	}
 | 
			
		||||
	allErrs = append(allErrs, apiservervalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
 | 
			
		||||
	allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
 | 
			
		||||
 | 
			
		||||
	return allErrs
 | 
			
		||||
 
 | 
			
		||||
@@ -321,9 +321,9 @@ func TestCustomResourceValidationErrors(t *testing.T) {
 | 
			
		||||
		ns := "not-the-default"
 | 
			
		||||
 | 
			
		||||
		tests := []struct {
 | 
			
		||||
			name          string
 | 
			
		||||
			instanceFn    func() *unstructured.Unstructured
 | 
			
		||||
			expectedError string
 | 
			
		||||
			name           string
 | 
			
		||||
			instanceFn     func() *unstructured.Unstructured
 | 
			
		||||
			expectedErrors []string
 | 
			
		||||
		}{
 | 
			
		||||
			{
 | 
			
		||||
				name: "bad alpha",
 | 
			
		||||
@@ -332,7 +332,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
 | 
			
		||||
					instance.Object["alpha"] = "foo_123!"
 | 
			
		||||
					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",
 | 
			
		||||
@@ -341,7 +341,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
 | 
			
		||||
					instance.Object["beta"] = 5
 | 
			
		||||
					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",
 | 
			
		||||
@@ -350,7 +350,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
 | 
			
		||||
					instance.Object["gamma"] = "qux"
 | 
			
		||||
					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",
 | 
			
		||||
@@ -359,7 +359,10 @@ func TestCustomResourceValidationErrors(t *testing.T) {
 | 
			
		||||
					instance.Object["delta"] = "foobarbaz"
 | 
			
		||||
					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",
 | 
			
		||||
@@ -377,7 +380,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
 | 
			
		||||
					}
 | 
			
		||||
					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)
 | 
			
		||||
				_, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					t.Errorf("%v: expected %v", tc.name, tc.expectedError)
 | 
			
		||||
					t.Errorf("%v: expected %v", tc.name, tc.expectedErrors)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				// 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) {
 | 
			
		||||
					t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
 | 
			
		||||
					continue
 | 
			
		||||
				for _, expectedError := range tc.expectedErrors {
 | 
			
		||||
					if !strings.Contains(err.Error(), expectedError) {
 | 
			
		||||
						t.Errorf("%v: expected %v, got %v", tc.name, expectedError, err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user