mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +00:00
Merge pull request #77772 from sttts/sttts-structural-schema-nullable-go-openapi
apiextensions: remove nullable roundtrip hacks after go-openapi gained support
This commit is contained in:
commit
7054e3ead7
@ -75,11 +75,6 @@ func TestStructuralRoundtripOrError(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f.Fuzz(x.Field(n).Addr().Interface())
|
f.Fuzz(x.Field(n).Addr().Interface())
|
||||||
if origSchema.Nullable {
|
|
||||||
// non-empty type for nullable. nullable:true with empty type does not roundtrip because
|
|
||||||
// go-openapi does not allow to encode that (we use type slices otherwise).
|
|
||||||
origSchema.Type = "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
// it roundtrips or NewStructural errors out. We should never drop anything
|
// it roundtrips or NewStructural errors out. We should never drop anything
|
||||||
orig, err := NewStructural(origSchema)
|
orig, err := NewStructural(origSchema)
|
||||||
@ -93,9 +88,8 @@ func TestStructuralRoundtripOrError(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
str := nullTypeRE.ReplaceAllString(string(bs), `"type":"$1","nullable":true`) // unfold nullable type:[<type>,"null"] -> type:<type>,nullable:true
|
|
||||||
v1beta1Schema := &apiextensionsv1beta1.JSONSchemaProps{}
|
v1beta1Schema := &apiextensionsv1beta1.JSONSchemaProps{}
|
||||||
err = json.Unmarshal([]byte(str), v1beta1Schema)
|
err = json.Unmarshal([]byte(bs), v1beta1Schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,7 @@ import (
|
|||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToGoOpenAPI converts a structural schema to go-openapi schema. It is faithful and roundtrippable
|
// ToGoOpenAPI converts a structural schema to go-openapi schema. It is faithful and roundtrippable.
|
||||||
// with the exception of `nullable:true` for empty type (`type:""`).
|
|
||||||
//
|
|
||||||
// WARNING: Do not use the returned schema to perform CRD validation until this restriction is solved.
|
|
||||||
//
|
|
||||||
// Nullable:true is mapped to `type:[<structural-type>,"null"]`
|
|
||||||
// if the structural type is non-empty, and nullable is dropped if the structural type is empty.
|
|
||||||
func (s *Structural) ToGoOpenAPI() *spec.Schema {
|
func (s *Structural) ToGoOpenAPI() *spec.Schema {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -57,14 +51,8 @@ func (g *Generic) toGoOpenAPI(ret *spec.Schema) {
|
|||||||
|
|
||||||
if len(g.Type) != 0 {
|
if len(g.Type) != 0 {
|
||||||
ret.Type = spec.StringOrArray{g.Type}
|
ret.Type = spec.StringOrArray{g.Type}
|
||||||
if g.Nullable {
|
|
||||||
// go-openapi does not support nullable, but multiple type values.
|
|
||||||
// Only when type is already non-empty, adding null to the types is correct though.
|
|
||||||
// If you add null as only type, you enforce null, in contrast to nullable being
|
|
||||||
// ineffective if no type is provided in a schema.
|
|
||||||
ret.Type = append(ret.Type, "null")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ret.Nullable = g.Nullable
|
||||||
if g.AdditionalProperties != nil {
|
if g.AdditionalProperties != nil {
|
||||||
ret.AdditionalProperties = &spec.SchemaOrBool{
|
ret.AdditionalProperties = &spec.SchemaOrBool{
|
||||||
Allows: g.AdditionalProperties.Bool,
|
Allows: g.AdditionalProperties.Bool,
|
||||||
|
@ -49,25 +49,13 @@ func TestStructuralRoundtrip(t *testing.T) {
|
|||||||
case 2:
|
case 2:
|
||||||
s.Object = ""
|
s.Object = ""
|
||||||
case 3:
|
case 3:
|
||||||
s.Object = []string{}
|
s.Object = []interface{}{}
|
||||||
case 4:
|
case 4:
|
||||||
s.Object = map[string]interface{}{}
|
s.Object = map[string]interface{}{}
|
||||||
case 5:
|
case 5:
|
||||||
s.Object = nil
|
s.Object = nil
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
func(g *Generic, c fuzz.Continue) {
|
|
||||||
c.FuzzNoCustom(g)
|
|
||||||
|
|
||||||
// TODO: make nullable in case of empty type survive go-openapi JSON -> API schema roundtrip
|
|
||||||
// go-openapi does not support nullable. Adding it to a type slice produces OpenAPI v3
|
|
||||||
// incompatible JSON which we cannot unmarshal (without string-replace magic to transform
|
|
||||||
// null types back into nullable). If type is empty, nullable:true is not preserved
|
|
||||||
// at all.
|
|
||||||
if len(g.Type) == 0 {
|
|
||||||
g.Nullable = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
f.MaxDepth(3)
|
f.MaxDepth(3)
|
||||||
f.NilChance(0.5)
|
f.NilChance(0.5)
|
||||||
@ -93,9 +81,8 @@ func TestStructuralRoundtrip(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
str := nullTypeRE.ReplaceAllString(string(bs), `"type":"$1","nullable":true`) // unfold nullable type:[<type>,"null"] -> type:<type>,nullable:true
|
|
||||||
v1beta1Schema := &apiextensionsv1beta1.JSONSchemaProps{}
|
v1beta1Schema := &apiextensionsv1beta1.JSONSchemaProps{}
|
||||||
err = json.Unmarshal([]byte(str), v1beta1Schema)
|
err = json.Unmarshal(bs, v1beta1Schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -110,7 +97,7 @@ func TestStructuralRoundtrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(orig, s) {
|
if !reflect.DeepEqual(orig, s) {
|
||||||
t.Fatalf("original and result differ: %v", diff.ObjectDiff(orig, s))
|
t.Fatalf("original and result differ: %v", diff.ObjectGoPrintDiff(orig, s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
|
|||||||
// Convert CRD schema to openapi schema
|
// Convert CRD schema to openapi schema
|
||||||
openapiSchema := &spec.Schema{}
|
openapiSchema := &spec.Schema{}
|
||||||
if customResourceValidation != nil {
|
if customResourceValidation != nil {
|
||||||
// WARNING: do not replace this with Structural.ToGoOpenAPI until it supports nullable.
|
// TODO: replace with NewStructural(...).ToGoOpenAPI
|
||||||
if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
|
if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -76,9 +76,7 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
|
|||||||
out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
|
out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
|
||||||
out.Type = spec.StringOrArray{"integer", "string"}
|
out.Type = spec.StringOrArray{"integer", "string"}
|
||||||
}
|
}
|
||||||
if out.Type != nil && in.Nullable {
|
out.Nullable = in.Nullable
|
||||||
out.Type = append(out.Type, "null")
|
|
||||||
}
|
|
||||||
out.Format = in.Format
|
out.Format = in.Format
|
||||||
out.Title = in.Title
|
out.Title = in.Title
|
||||||
out.Maximum = in.Maximum
|
out.Maximum = in.Maximum
|
||||||
|
@ -72,7 +72,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
if err := json.Unmarshal(openAPIJSON, &j); err != nil {
|
if err := json.Unmarshal(openAPIJSON, &j); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
j = convertNullTypeToNullable(j)
|
|
||||||
j = stripIntOrStringType(j)
|
j = stripIntOrStringType(j)
|
||||||
openAPIJSON, err = json.Marshal(j)
|
openAPIJSON, err = json.Marshal(j)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -97,49 +96,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertNullTypeToNullable(x interface{}) interface{} {
|
|
||||||
switch x := x.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
if t, found := x["type"]; found {
|
|
||||||
switch t := t.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
for i, typ := range t {
|
|
||||||
if s, ok := typ.(string); !ok || s != "null" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t = append(t[:i], t[i+1:]...)
|
|
||||||
switch len(t) {
|
|
||||||
case 0:
|
|
||||||
delete(x, "type")
|
|
||||||
case 1:
|
|
||||||
x["type"] = t[0]
|
|
||||||
default:
|
|
||||||
x["type"] = t
|
|
||||||
}
|
|
||||||
x["nullable"] = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
if t == "null" {
|
|
||||||
delete(x, "type")
|
|
||||||
x["nullable"] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k := range x {
|
|
||||||
x[k] = convertNullTypeToNullable(x[k])
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
case []interface{}:
|
|
||||||
for i := range x {
|
|
||||||
x[i] = convertNullTypeToNullable(x[i])
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
default:
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripIntOrStringType(x interface{}) interface{} {
|
func stripIntOrStringType(x interface{}) interface{} {
|
||||||
switch x := x.(type) {
|
switch x := x.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
|
Loading…
Reference in New Issue
Block a user