apiextensions: implement x-kubernetes-int-or-string validation

This commit is contained in:
Dr. Stefan Schimanski 2019-06-08 21:45:16 +02:00
parent 78220fe380
commit bfa4b66bc9
3 changed files with 185 additions and 81 deletions

View File

@ -123,6 +123,9 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
if len(obj.Type) == 0 { if len(obj.Type) == 0 {
obj.Nullable = false // because this does not roundtrip through go-openapi obj.Nullable = false // because this does not roundtrip through go-openapi
} }
if obj.XIntOrString {
obj.Type = ""
}
}, },
func(obj *apiextensions.JSONSchemaPropsOrBool, c fuzz.Continue) { func(obj *apiextensions.JSONSchemaPropsOrBool, c fuzz.Continue) {
if c.RandBool() { if c.RandBool() {

View File

@ -71,9 +71,13 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
out.Description = in.Description out.Description = in.Description
if in.Type != "" { if in.Type != "" {
out.Type = spec.StringOrArray([]string{in.Type}) out.Type = spec.StringOrArray([]string{in.Type})
if in.Nullable { }
out.Type = append(out.Type, "null") if in.XIntOrString {
} out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
out.Type = spec.StringOrArray{"integer", "string"}
}
if out.Type != nil && in.Nullable {
out.Type = append(out.Type, "null")
} }
out.Format = in.Format out.Format = in.Format
out.Title = in.Title out.Title = in.Title
@ -201,9 +205,6 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
if in.XEmbeddedResource { if in.XEmbeddedResource {
out.VendorExtensible.AddExtension("x-kubernetes-embedded-resource", true) out.VendorExtensible.AddExtension("x-kubernetes-embedded-resource", true)
} }
if in.XIntOrString {
out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
}
return nil return nil
} }

View File

@ -73,6 +73,7 @@ func TestRoundTrip(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
j = convertNullTypeToNullable(j) j = convertNullTypeToNullable(j)
j = stripIntOrStringType(j)
openAPIJSON, err = json.Marshal(j) openAPIJSON, err = json.Marshal(j)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -139,19 +140,40 @@ func convertNullTypeToNullable(x interface{}) interface{} {
} }
} }
func TestValidateCustomResource(t *testing.T) { func stripIntOrStringType(x interface{}) interface{} {
type args struct { switch x := x.(type) {
schema apiextensions.JSONSchemaProps case map[string]interface{}:
object interface{} if t, found := x["type"]; found {
switch t := t.(type) {
case []interface{}:
if len(t) == 2 && t[0] == "integer" && t[1] == "string" && x["x-kubernetes-int-or-string"] == true {
delete(x, "type")
}
}
}
for k := range x {
x[k] = stripIntOrStringType(x[k])
}
return x
case []interface{}:
for i := range x {
x[i] = stripIntOrStringType(x[i])
}
return x
default:
return x
} }
}
func TestValidateCustomResource(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args schema apiextensions.JSONSchemaProps
wantErr bool objects []interface{}
failingObjects []interface{}
}{ }{
// TODO: make more complete {name: "!nullable",
{"!nullable against non-null", args{ schema: apiextensions.JSONSchemaProps{
apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"field": { "field": {
Type: "object", Type: "object",
@ -159,32 +181,20 @@ func TestValidateCustomResource(t *testing.T) {
}, },
}, },
}, },
map[string]interface{}{"field": map[string]interface{}{}}, objects: []interface{}{
}, false}, map[string]interface{}{},
{"!nullable against null", args{ map[string]interface{}{"field": map[string]interface{}{}},
apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
"field": {
Type: "object",
Nullable: false,
},
},
}, },
map[string]interface{}{"field": nil}, failingObjects: []interface{}{
}, true}, map[string]interface{}{"field": "foo"},
{"!nullable against undefined", args{ map[string]interface{}{"field": 42},
apiextensions.JSONSchemaProps{ map[string]interface{}{"field": true},
Properties: map[string]apiextensions.JSONSchemaProps{ map[string]interface{}{"field": 1.2},
"field": { map[string]interface{}{"field": []interface{}{}},
Type: "object",
Nullable: false,
},
},
}, },
map[string]interface{}{}, },
}, false}, {name: "nullable",
{"nullable against non-null", args{ schema: apiextensions.JSONSchemaProps{
apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"field": { "field": {
Type: "object", Type: "object",
@ -192,52 +202,139 @@ func TestValidateCustomResource(t *testing.T) {
}, },
}, },
}, },
map[string]interface{}{"field": map[string]interface{}{}}, objects: []interface{}{
}, false}, map[string]interface{}{},
{"nullable against null", args{ map[string]interface{}{"field": map[string]interface{}{}},
apiextensions.JSONSchemaProps{ map[string]interface{}{"field": nil},
Properties: map[string]apiextensions.JSONSchemaProps{
"field": {
Type: "object",
Nullable: true,
},
},
}, },
map[string]interface{}{"field": nil}, failingObjects: []interface{}{
}, false}, map[string]interface{}{"field": "foo"},
{"!nullable against undefined", args{ map[string]interface{}{"field": 42},
apiextensions.JSONSchemaProps{ map[string]interface{}{"field": true},
Properties: map[string]apiextensions.JSONSchemaProps{ map[string]interface{}{"field": 1.2},
"field": { map[string]interface{}{"field": []interface{}{}},
Type: "object",
Nullable: true,
},
},
}, },
map[string]interface{}{}, },
}, false}, {name: "nullable and no type",
{"nullable and no type against non-nil", args{ schema: apiextensions.JSONSchemaProps{
apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"field": { "field": {
Nullable: true, Nullable: true,
}, },
}, },
}, },
map[string]interface{}{"field": 42}, objects: []interface{}{
}, false}, map[string]interface{}{},
{"nullable and no type against nil", args{ map[string]interface{}{"field": map[string]interface{}{}},
apiextensions.JSONSchemaProps{ map[string]interface{}{"field": nil},
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{}{}},
},
},
{name: "x-kubernetes-int-or-string",
schema: apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"field": { "field": {
Nullable: true, XIntOrString: true,
}, },
}, },
}, },
map[string]interface{}{"field": nil}, objects: []interface{}{
}, false}, map[string]interface{}{},
{"invalid regex", args{ map[string]interface{}{"field": 42},
apiextensions.JSONSchemaProps{ 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{}{}},
},
},
{name: "nullable and x-kubernetes-int-or-string",
schema: apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
"field": {
Nullable: true,
XIntOrString: true,
},
},
},
objects: []interface{}{
map[string]interface{}{},
map[string]interface{}{"field": 42},
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{}{}},
},
},
{name: "nullable, x-kubernetes-int-or-string and user-provided anyOf",
schema: apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
"field": {
Nullable: true,
XIntOrString: true,
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "integer"},
{Type: "string"},
},
},
},
},
objects: []interface{}{
map[string]interface{}{},
map[string]interface{}{"field": nil},
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{}{}},
},
},
{name: "nullable, x-kubernetes-int-or-string and user-provider allOf",
schema: apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
"field": {
Nullable: true,
XIntOrString: true,
AllOf: []apiextensions.JSONSchemaProps{
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "integer"},
{Type: "string"},
},
},
},
},
},
},
objects: []interface{}{
map[string]interface{}{},
map[string]interface{}{"field": nil},
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{}{}},
},
},
{name: "invalid regex",
schema: apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"field": { "field": {
Type: "string", Type: "string",
@ -245,20 +342,23 @@ func TestValidateCustomResource(t *testing.T) {
}, },
}, },
}, },
map[string]interface{}{"field": "foo"}, failingObjects: []interface{}{map[string]interface{}{"field": "foo"}},
}, true}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
validator, _, err := NewSchemaValidator(&apiextensions.CustomResourceValidation{OpenAPIV3Schema: &tt.args.schema}) validator, _, err := NewSchemaValidator(&apiextensions.CustomResourceValidation{OpenAPIV3Schema: &tt.schema})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := ValidateCustomResource(tt.args.object, validator); (err != nil) != tt.wantErr { for _, obj := range tt.objects {
if err == nil { if err := ValidateCustomResource(obj, validator); err != nil {
t.Error("expected error, but didn't get one") t.Errorf("unexpected validation error for %v: %v", obj, err)
} else { }
t.Errorf("unexpected validation error: %v", err) }
for _, obj := range tt.failingObjects {
if err := ValidateCustomResource(obj, validator); err == nil {
t.Errorf("missing error for %v", obj)
} }
} }
}) })