mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 20:24:09 +00:00
apiextensions: implement x-kubernetes-int-or-string validation
This commit is contained in:
parent
78220fe380
commit
bfa4b66bc9
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user