diff --git a/staging/src/k8s.io/apiextensions-apiserver/go.mod b/staging/src/k8s.io/apiextensions-apiserver/go.mod index f27a553fc96..b755b96103c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/go.mod +++ b/staging/src/k8s.io/apiextensions-apiserver/go.mod @@ -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 diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index 3f7cfd812a3..648aa205dca 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -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" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go index a340bf8a5f4..67806c34d90 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go @@ -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. diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD index 38117619d08..4c6d5d23cb3 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD @@ -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", ], ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go index 928dc9f8ed1..f5e549600ed 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go @@ -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. diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go index d0d2182eedb..54361461ede 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go @@ -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) } } }) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go index 5206a17ebe9..6d89e1bbe24 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go @@ -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 diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go index 0fe71006059..4ac2dbacce2 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go @@ -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) + } } } }