apiextensions: switch OpenAPI pubilshing to structural schema
This commit is contained in:
		| @@ -26,6 +26,7 @@ import ( | |||||||
| 	"github.com/go-openapi/spec" | 	"github.com/go-openapi/spec" | ||||||
|  |  | ||||||
| 	v1 "k8s.io/api/autoscaling/v1" | 	v1 "k8s.io/api/autoscaling/v1" | ||||||
|  | 	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" | 	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
| @@ -58,22 +59,24 @@ var namer *openapi.DefinitionNamer | |||||||
|  |  | ||||||
| // BuildSwagger builds swagger for the given crd in the given version | // BuildSwagger builds swagger for the given crd in the given version | ||||||
| func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (*spec.Swagger, error) { | func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (*spec.Swagger, error) { | ||||||
| 	var schema *spec.Schema | 	var schema *structuralschema.Structural | ||||||
| 	s, err := apiextensions.GetSchemaForVersion(crd, version) | 	s, err := apiextensions.GetSchemaForVersion(crd, version) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if s != nil && s.OpenAPIV3Schema != nil { | 	if s != nil && s.OpenAPIV3Schema != nil { | ||||||
| 		schema, err = ConvertJSONSchemaPropsToOpenAPIv2Schema(s.OpenAPIV3Schema) | 		ss, err := structuralschema.NewStructural(s.OpenAPIV3Schema) | ||||||
| 		if err != nil { | 		if err == nil && len(structuralschema.ValidateStructural(ss, nil)) == 0 { | ||||||
| 			return nil, err | 			// skip non-structural schemas | ||||||
|  | 			schema = ss.Unfold() | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// TODO(roycaihw): remove the WebService templating below. The following logic | 	// TODO(roycaihw): remove the WebService templating below. The following logic | ||||||
| 	// comes from function registerResourceHandlers() in k8s.io/apiserver. | 	// comes from function registerResourceHandlers() in k8s.io/apiserver. | ||||||
| 	// Alternatives are either (ideally) refactoring registerResourceHandlers() to | 	// Alternatives are either (ideally) refactoring registerResourceHandlers() to | ||||||
| 	// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers(). | 	// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers(). | ||||||
| 	b := newBuilder(crd, version, schema) | 	b := newBuilder(crd, version, schema, true) | ||||||
|  |  | ||||||
| 	// Sample response types for building web service | 	// Sample response types for building web service | ||||||
| 	sample := &CRDCanonicalTypeNamer{ | 	sample := &CRDCanonicalTypeNamer{ | ||||||
| @@ -288,23 +291,27 @@ func (b *builder) buildRoute(root, path, action, verb string, sample interface{} | |||||||
|  |  | ||||||
| // buildKubeNative builds input schema with Kubernetes' native object meta, type meta and | // buildKubeNative builds input schema with Kubernetes' native object meta, type meta and | ||||||
| // extensions | // extensions | ||||||
| func (b *builder) buildKubeNative(schema *spec.Schema) *spec.Schema { | func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool) (ret *spec.Schema) { | ||||||
| 	// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false | 	// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false | ||||||
| 	// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by | 	// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by | ||||||
| 	// adding additionalProperties=true support to explicitly allow additional fields. | 	// adding additionalProperties=true support to explicitly allow additional fields. | ||||||
| 	// TODO: fix kubectl to understand additionalProperties=true | 	// TODO: fix kubectl to understand additionalProperties=true | ||||||
| 	if schema == nil { | 	if schema == nil { | ||||||
| 		schema = &spec.Schema{ | 		ret = &spec.Schema{ | ||||||
| 			SchemaProps: spec.SchemaProps{Type: []string{"object"}}, | 			SchemaProps: spec.SchemaProps{Type: []string{"object"}}, | ||||||
| 		} | 		} | ||||||
| 		// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about | 		// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about | ||||||
| 		// unknown fields for anything else. | 		// unknown fields for anything else. | ||||||
| 	} else { | 	} else { | ||||||
| 		schema.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef). | 		if v2 { | ||||||
| 			WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"])) | 			schema = ToStructuralOpenAPIV2(schema) | ||||||
| 		addTypeMetaProperties(schema) |  | ||||||
| 		} | 		} | ||||||
| 	schema.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{ | 		ret = schema.ToGoOpenAPI() | ||||||
|  | 		ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef). | ||||||
|  | 			WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"])) | ||||||
|  | 		addTypeMetaProperties(ret) | ||||||
|  | 	} | ||||||
|  | 	ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{ | ||||||
| 		map[string]interface{}{ | 		map[string]interface{}{ | ||||||
| 			"group":   b.group, | 			"group":   b.group, | ||||||
| 			"version": b.version, | 			"version": b.version, | ||||||
| @@ -312,7 +319,7 @@ func (b *builder) buildKubeNative(schema *spec.Schema) *spec.Schema { | |||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	return schema | 	return ret | ||||||
| } | } | ||||||
|  |  | ||||||
| // getDefinition gets definition for given Kubernetes type. This function is extracted from | // getDefinition gets definition for given Kubernetes type. This function is extracted from | ||||||
| @@ -391,7 +398,7 @@ func (b *builder) getOpenAPIConfig() *common.Config { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, schema *spec.Schema) *builder { | func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, schema *structuralschema.Structural, v2 bool) *builder { | ||||||
| 	b := &builder{ | 	b := &builder{ | ||||||
| 		schema: &spec.Schema{ | 		schema: &spec.Schema{ | ||||||
| 			SchemaProps: spec.SchemaProps{Type: []string{"object"}}, | 			SchemaProps: spec.SchemaProps{Type: []string{"object"}}, | ||||||
| @@ -410,7 +417,7 @@ func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, sch | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Pre-build schema with Kubernetes native properties | 	// Pre-build schema with Kubernetes native properties | ||||||
| 	b.schema = b.buildKubeNative(schema) | 	b.schema = b.buildKubeNative(schema, v2) | ||||||
| 	b.listSchema = b.buildListSchema() | 	b.listSchema = b.buildListSchema() | ||||||
|  |  | ||||||
| 	return b | 	return b | ||||||
|   | |||||||
| @@ -22,14 +22,14 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/go-openapi/spec" | 	"github.com/go-openapi/spec" | ||||||
| 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" | 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" | ||||||
|  | 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" | ||||||
|  | 	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" | ||||||
| 	"k8s.io/apimachinery/pkg/util/diff" | 	"k8s.io/apimachinery/pkg/util/diff" | ||||||
| 	"k8s.io/apimachinery/pkg/util/json" | 	"k8s.io/apimachinery/pkg/util/json" | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestNewBuilder(t *testing.T) { | func TestNewBuilder(t *testing.T) { | ||||||
| 	type args struct { |  | ||||||
| 	} |  | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		name string | 		name string | ||||||
|  |  | ||||||
| @@ -37,41 +37,302 @@ func TestNewBuilder(t *testing.T) { | |||||||
|  |  | ||||||
| 		wantedSchema      string | 		wantedSchema      string | ||||||
| 		wantedItemsSchema string | 		wantedItemsSchema string | ||||||
|  |  | ||||||
|  | 		v2 bool // produce OpenAPIv2 | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			"nil", | 			"nil", | ||||||
| 			"", | 			"", | ||||||
| 			`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | 			`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | ||||||
|  | 			true, | ||||||
| 		}, | 		}, | ||||||
| 		{"empty", | 		{"with properties", | ||||||
| 			"{}", | 			`{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`, | ||||||
| 			`{"properties":{"apiVersion":{},"kind":{},"metadata":{}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, | 			`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, | ||||||
| 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | ||||||
|  | 			true, | ||||||
| 		}, | 		}, | ||||||
| 		{"empty properties", | 		{"type only", | ||||||
| 			`{"properties":{"spec":{},"status":{}}}`, |  | ||||||
| 			`{"properties":{"apiVersion":{},"kind":{},"metadata":{},"spec":{},"status":{}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, |  | ||||||
| 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, |  | ||||||
| 		}, |  | ||||||
| 		{"filled properties", |  | ||||||
| 			`{"properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`, |  | ||||||
| 			`{"properties":{"apiVersion":{},"kind":{},"metadata":{},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, |  | ||||||
| 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, |  | ||||||
| 		}, |  | ||||||
| 		{"type", |  | ||||||
| 			`{"type":"object"}`, | 			`{"type":"object"}`, | ||||||
| 			`{"properties":{"apiVersion":{},"kind":{},"metadata":{}},"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, | 			`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, | ||||||
| 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | ||||||
|  | 			true, | ||||||
|  | 		}, | ||||||
|  | 		{"with extensions", | ||||||
|  | 			` | ||||||
|  | { | ||||||
|  |   "type":"object",  | ||||||
|  |   "properties": { | ||||||
|  |     "int-or-string-1": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-2": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"type":"integer"}, | ||||||
|  |           {"type":"string"} | ||||||
|  |         ] | ||||||
|  |       }, { | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"minimum": 42.0} | ||||||
|  |         ] | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-3": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ], | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"minimum": 42.0} | ||||||
|  |         ] | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-4": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-5": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ], | ||||||
|  |       "allOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-6": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "preserve-unknown-fields": { | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true | ||||||
|  |     }, | ||||||
|  |     "embedded-object": { | ||||||
|  |       "x-kubernetes-embedded-resource": true, | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true, | ||||||
|  |       "type": "object" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }`, | ||||||
|  | 			` | ||||||
|  | { | ||||||
|  |   "type":"object", | ||||||
|  |   "properties": { | ||||||
|  |     "apiVersion": {"type":"string"}, | ||||||
|  |     "kind": {"type":"string"}, | ||||||
|  |     "metadata": {"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}, | ||||||
|  |     "int-or-string-1": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "int-or-string-2": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "int-or-string-3": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "int-or-string-4": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "int-or-string-5": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "int-or-string-6": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "preserve-unknown-fields": { | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true | ||||||
|  |     }, | ||||||
|  |     "embedded-object": { | ||||||
|  |       "x-kubernetes-embedded-resource": true, | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true, | ||||||
|  |       "type": "object" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}] | ||||||
|  | }`, | ||||||
|  | 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | ||||||
|  | 			true, | ||||||
|  | 		}, | ||||||
|  | 		{"with extensions as v3 schema", | ||||||
|  | 			` | ||||||
|  | { | ||||||
|  |   "type":"object",  | ||||||
|  |   "properties": { | ||||||
|  |     "int-or-string-1": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-2": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"type":"integer"}, | ||||||
|  |           {"type":"string"} | ||||||
|  |         ] | ||||||
|  |       }, { | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"minimum": 42.0} | ||||||
|  |         ] | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-3": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ], | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"minimum": 42.0} | ||||||
|  |         ] | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-4": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-5": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ], | ||||||
|  |       "allOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-6": { | ||||||
|  |       "x-kubernetes-int-or-string": true | ||||||
|  |     }, | ||||||
|  |     "preserve-unknown-fields": { | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true | ||||||
|  |     }, | ||||||
|  |     "embedded-object": { | ||||||
|  |       "x-kubernetes-embedded-resource": true, | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true, | ||||||
|  |       "type": "object" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }`, | ||||||
|  | 			` | ||||||
|  | { | ||||||
|  |   "type":"object", | ||||||
|  |   "properties": { | ||||||
|  |     "apiVersion": {"type":"string"}, | ||||||
|  |     "kind": {"type":"string"}, | ||||||
|  |     "metadata": {"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}, | ||||||
|  |     "int-or-string-1": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-2": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"type":"integer"}, | ||||||
|  |           {"type":"string"} | ||||||
|  |         ] | ||||||
|  |       }, { | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"minimum": 42.0} | ||||||
|  |         ] | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-3": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ], | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"minimum": 42.0} | ||||||
|  |         ] | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-4": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"type":"integer"}, | ||||||
|  |           {"type":"string"} | ||||||
|  |         ] | ||||||
|  |       }], | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-5": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"minimum": 42.0} | ||||||
|  |       ], | ||||||
|  |       "allOf": [{ | ||||||
|  |         "anyOf": [ | ||||||
|  |           {"type":"integer"}, | ||||||
|  |           {"type":"string"} | ||||||
|  |         ] | ||||||
|  |       }, { | ||||||
|  |         "minimum": 42.0 | ||||||
|  |       }] | ||||||
|  |     }, | ||||||
|  |     "int-or-string-6": { | ||||||
|  |       "x-kubernetes-int-or-string": true, | ||||||
|  |       "anyOf": [ | ||||||
|  |         {"type":"integer"}, | ||||||
|  |         {"type":"string"} | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "preserve-unknown-fields": { | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true | ||||||
|  |     }, | ||||||
|  |     "embedded-object": { | ||||||
|  |       "x-kubernetes-embedded-resource": true, | ||||||
|  |       "x-kubernetes-preserve-unknown-fields": true, | ||||||
|  |       "type": "object" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}] | ||||||
|  | }`, | ||||||
|  | 			`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`, | ||||||
|  | 			false, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			var schema *spec.Schema | 			var schema *structuralschema.Structural | ||||||
| 			if len(tt.schema) > 0 { | 			if len(tt.schema) > 0 { | ||||||
| 				schema = &spec.Schema{} | 				v1beta1Schema := &v1beta1.JSONSchemaProps{} | ||||||
| 				if err := json.Unmarshal([]byte(tt.schema), schema); err != nil { | 				if err := json.Unmarshal([]byte(tt.schema), &v1beta1Schema); err != nil { | ||||||
| 					t.Fatal(err) | 					t.Fatal(err) | ||||||
| 				} | 				} | ||||||
|  | 				internalSchema := &apiextensions.JSONSchemaProps{} | ||||||
|  | 				v1beta1.Convert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v1beta1Schema, internalSchema, nil) | ||||||
|  | 				var err error | ||||||
|  | 				schema, err = structuralschema.NewStructural(internalSchema) | ||||||
|  | 				if err != nil { | ||||||
|  | 					t.Fatalf("structural schema error: %v", err) | ||||||
|  | 				} | ||||||
|  | 				if errs := structuralschema.ValidateStructural(schema, nil); len(errs) > 0 { | ||||||
|  | 					t.Fatalf("structural schema validation error: %v", errs.ToAggregate()) | ||||||
|  | 				} | ||||||
|  | 				schema = schema.Unfold() | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			got := newBuilder(&apiextensions.CustomResourceDefinition{ | 			got := newBuilder(&apiextensions.CustomResourceDefinition{ | ||||||
| @@ -86,7 +347,7 @@ func TestNewBuilder(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 					Scope: apiextensions.NamespaceScoped, | 					Scope: apiextensions.NamespaceScoped, | ||||||
| 				}, | 				}, | ||||||
| 			}, "v1", schema) | 			}, "v1", schema, tt.v2) | ||||||
|  |  | ||||||
| 			var wantedSchema, wantedItemsSchema spec.Schema | 			var wantedSchema, wantedItemsSchema spec.Schema | ||||||
| 			if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil { | 			if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil { | ||||||
| @@ -103,14 +364,12 @@ func TestNewBuilder(t *testing.T) { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// wipe out TypeMeta/ObjectMeta content, with those many lines of descriptions. We trust that they match here. | 			// wipe out TypeMeta/ObjectMeta content, with those many lines of descriptions. We trust that they match here. | ||||||
|  | 			for _, metaField := range []string{"kind", "apiVersion", "metadata"} { | ||||||
| 				if _, found := got.schema.Properties["kind"]; found { | 				if _, found := got.schema.Properties["kind"]; found { | ||||||
| 				got.schema.Properties["kind"] = spec.Schema{} | 					prop := got.schema.Properties[metaField] | ||||||
|  | 					prop.Description = "" | ||||||
|  | 					got.schema.Properties[metaField] = prop | ||||||
| 				} | 				} | ||||||
| 			if _, found := got.schema.Properties["apiVersion"]; found { |  | ||||||
| 				got.schema.Properties["apiVersion"] = spec.Schema{} |  | ||||||
| 			} |  | ||||||
| 			if _, found := got.schema.Properties["metadata"]; found { |  | ||||||
| 				got.schema.Properties["metadata"] = spec.Schema{} |  | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !reflect.DeepEqual(&wantedSchema, got.schema) { | 			if !reflect.DeepEqual(&wantedSchema, got.schema) { | ||||||
|   | |||||||
| @@ -17,106 +17,60 @@ limitations under the License. | |||||||
| package openapi | package openapi | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"strings" | 	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" | ||||||
|  |  | ||||||
| 	"github.com/go-openapi/spec" |  | ||||||
|  |  | ||||||
| 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" |  | ||||||
| 	"k8s.io/apiextensions-apiserver/pkg/apiserver/validation" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ConvertJSONSchemaPropsToOpenAPIv2Schema converts our internal OpenAPI v3 schema | // ToStructuralOpenAPIV2 converts our internal OpenAPI v3 structural schema to | ||||||
| // (*apiextensions.JSONSchemaProps) to an OpenAPI v2 schema (*spec.Schema). | // to a v2 compatible schema. | ||||||
| func ConvertJSONSchemaPropsToOpenAPIv2Schema(in *apiextensions.JSONSchemaProps) (*spec.Schema, error) { | func ToStructuralOpenAPIV2(in *structuralschema.Structural) *structuralschema.Structural { | ||||||
| 	if in == nil { | 	if in == nil { | ||||||
| 		return nil, nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// dirty hack to temporarily set the type at the root. See continuation at the func bottom. | 	out := in.DeepCopy() | ||||||
| 	// TODO: remove for Kubernetes 1.15 |  | ||||||
| 	oldRootType := in.Type |  | ||||||
| 	if len(in.Type) == 0 { |  | ||||||
| 		in.Type = "object" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Remove unsupported fields in OpenAPI v2 recursively | 	// Remove unsupported fields in OpenAPI v2 recursively | ||||||
| 	out := new(spec.Schema) | 	mapper := structuralschema.Visitor{ | ||||||
| 	validation.ConvertJSONSchemaPropsWithPostProcess(in, out, func(p *spec.Schema) error { | 		Structural: func(s *structuralschema.Structural) bool { | ||||||
| 		p.OneOf = nil | 			changed := false | ||||||
| 		// TODO(roycaihw): preserve cases where we only have one subtree in AnyOf, same for OneOf | 			if s.ValueValidation != nil { | ||||||
| 		p.AnyOf = nil | 				if s.ValueValidation.AllOf != nil { | ||||||
| 		p.Not = nil | 					s.ValueValidation.AllOf = nil | ||||||
|  | 					changed = true | ||||||
| 		// TODO: drop everything below in 1.15 when we have passed one version skew towards kube-openapi in <1.14, which rejects valid openapi schemata | 				} | ||||||
|  | 				if s.ValueValidation.OneOf != nil { | ||||||
| 		if p.Ref.String() != "" { | 					s.ValueValidation.OneOf = nil | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R95 | 					changed = true | ||||||
| 			p.Properties = nil | 				} | ||||||
|  | 				if s.ValueValidation.AnyOf != nil { | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R99 | 					s.ValueValidation.AnyOf = nil | ||||||
| 			p.Type = nil | 					changed = true | ||||||
|  | 				} | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R104 | 				if s.ValueValidation.Not != nil { | ||||||
| 			if !strings.HasPrefix(p.Ref.String(), "#/definitions/") { | 					s.ValueValidation.Not = nil | ||||||
| 				p.Ref = spec.Ref{} | 					changed = true | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 		switch { |  | ||||||
| 		case len(p.Type) == 2 && (p.Type[0] == "null" || p.Type[1] == "null"): |  | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-ce77fea74b9dd098045004410023e0c3R219 | 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-ce77fea74b9dd098045004410023e0c3R219 | ||||||
| 			p.Type = nil | 			if s.Nullable { | ||||||
| 		case len(p.Type) == 1: | 				s.Type = "" | ||||||
| 			switch p.Type[0] { | 				s.Nullable = false | ||||||
| 			case "null": |  | ||||||
| 				// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-ce77fea74b9dd098045004410023e0c3R219 | 				// untyped values break if items or properties are set in kubectl | ||||||
| 				p.Type = nil |  | ||||||
| 			case "array": |  | ||||||
| 				// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R183 | 				// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R183 | ||||||
| 				// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R184 | 				s.Items = nil | ||||||
| 				if p.Items == nil || (p.Items.Schema == nil && len(p.Items.Schemas) != 1) { | 				s.Properties = nil | ||||||
| 					p.Type = nil |  | ||||||
| 					p.Items = nil | 				changed = true | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		case len(p.Type) > 1: |  | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R272 |  | ||||||
| 			// We also set Properties to null to enforce parseArbitrary at https://github.com/kubernetes/kube-openapi/blob/814a8073653e40e0e324205d093770d4e7bb811f/pkg/util/proto/document.go#L247 |  | ||||||
| 			p.Type = nil |  | ||||||
| 			p.Properties = nil |  | ||||||
| 		default: |  | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R248 |  | ||||||
| 			p.Properties = nil |  | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 		// normalize items | 			return changed | ||||||
| 		if p.Items != nil && len(p.Items.Schemas) == 1 { | 		}, | ||||||
| 			p.Items = &spec.SchemaOrArray{Schema: &p.Items.Schemas[0]} | 		// we drop all junctors above, and hence, never reach nested value validations | ||||||
|  | 		NestedValueValidation: nil, | ||||||
| 	} | 	} | ||||||
|  | 	mapper.Visit(out) | ||||||
|  |  | ||||||
| 		// general fixups not supported by gnostic | 	return out | ||||||
| 		p.ID = "" |  | ||||||
| 		p.Schema = "" |  | ||||||
| 		p.Definitions = nil |  | ||||||
| 		p.AdditionalItems = nil |  | ||||||
| 		p.Dependencies = nil |  | ||||||
| 		p.PatternProperties = nil |  | ||||||
| 		if p.ExternalDocs != nil && len(p.ExternalDocs.URL) == 0 { |  | ||||||
| 			p.ExternalDocs = nil |  | ||||||
| 		} |  | ||||||
| 		if p.Items != nil && p.Items.Schemas != nil { |  | ||||||
| 			p.Items = nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	// restore root level type in input, and remove it in output if we had added it |  | ||||||
| 	// TODO: remove with Kubernetes 1.15 |  | ||||||
| 	in.Type = oldRootType |  | ||||||
| 	if len(oldRootType) == 0 { |  | ||||||
| 		out.Type = nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return out, nil |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,13 +26,15 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/go-openapi/spec" | 	"github.com/go-openapi/spec" | ||||||
| 	"github.com/google/gofuzz" | 	"github.com/google/go-cmp/cmp" | ||||||
| 	"github.com/googleapis/gnostic/OpenAPIv2" | 	fuzz "github.com/google/gofuzz" | ||||||
|  | 	openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2" | ||||||
| 	"github.com/googleapis/gnostic/compiler" | 	"github.com/googleapis/gnostic/compiler" | ||||||
| 	"gopkg.in/yaml.v2" | 	"gopkg.in/yaml.v2" | ||||||
|  |  | ||||||
| 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" | 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" | ||||||
| 	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" | 	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" | ||||||
| 	"k8s.io/apimachinery/pkg/util/diff" | 	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" | ||||||
| 	"k8s.io/kube-openapi/pkg/util/proto" | 	"k8s.io/kube-openapi/pkg/util/proto" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -94,11 +96,14 @@ properties: | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	schema, err := ConvertJSONSchemaPropsToOpenAPIv2Schema(&specInternal) | 	ss, err := structuralschema.NewStructural(&specInternal) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	ssV2 := ToStructuralOpenAPIV2(ss) | ||||||
|  | 	schema := ssV2.ToGoOpenAPI() | ||||||
|  |  | ||||||
| 	if _, found := schema.Properties["spec"]; !found { | 	if _, found := schema.Properties["spec"]; !found { | ||||||
| 		t.Errorf("spec not found") | 		t.Errorf("spec not found") | ||||||
| 	} | 	} | ||||||
| @@ -107,7 +112,7 @@ properties: | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaByType(t *testing.T) { | ||||||
| 	testStr := "test" | 	testStr := "test" | ||||||
| 	testStr2 := "test2" | 	testStr2 := "test2" | ||||||
| 	testFloat64 := float64(6.4) | 	testFloat64 := float64(6.4) | ||||||
| @@ -118,38 +123,29 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 		name        string | 		name        string | ||||||
| 		in          *apiextensions.JSONSchemaProps | 		in          *apiextensions.JSONSchemaProps | ||||||
| 		expected    *spec.Schema | 		expected    *spec.Schema | ||||||
|  | 		expectError bool | ||||||
|  | 		expectDiff  bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "id", | 			name: "id", | ||||||
| 			in: &apiextensions.JSONSchemaProps{ | 			in: &apiextensions.JSONSchemaProps{ | ||||||
| 				ID: testStr, | 				ID: testStr, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: new(spec.Schema). |  | ||||||
| 			// 	WithID(testStr), |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "$schema", | 			name: "$schema", | ||||||
| 			in: &apiextensions.JSONSchemaProps{ | 			in: &apiextensions.JSONSchemaProps{ | ||||||
| 				Schema: "test", | 				Schema: "test", | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		Schema: "test", |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "$ref", | 			name: "$ref", | ||||||
| 			in: &apiextensions.JSONSchemaProps{ | 			in: &apiextensions.JSONSchemaProps{ | ||||||
| 				Ref: &testStr, | 				Ref: &testStr, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R104 |  | ||||||
| 			// expected: spec.RefSchema(testStr), |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "description", | 			name: "description", | ||||||
| @@ -168,6 +164,14 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 			expected: new(spec.Schema). | 			expected: new(spec.Schema). | ||||||
| 				Typed(testStr, testStr2), | 				Typed(testStr, testStr2), | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "nullable", | ||||||
|  | 			in: &apiextensions.JSONSchemaProps{ | ||||||
|  | 				Type:     "object", | ||||||
|  | 				Nullable: true, | ||||||
|  | 			}, | ||||||
|  | 			expected: new(spec.Schema), | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "title", | 			name: "title", | ||||||
| 			in: &apiextensions.JSONSchemaProps{ | 			in: &apiextensions.JSONSchemaProps{ | ||||||
| @@ -317,18 +321,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R272 |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		Items: &spec.SchemaOrArray{ |  | ||||||
| 			// 			Schemas: []spec.Schema{ |  | ||||||
| 			// 				*spec.BooleanProperty(), |  | ||||||
| 			// 				*spec.StringProperty(), |  | ||||||
| 			// 			}, |  | ||||||
| 			// 		}, |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "allOf", | 			name: "allOf", | ||||||
| @@ -338,8 +331,10 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					{Type: "string"}, | 					{Type: "string"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema). | 			expected: new(spec.Schema), | ||||||
| 				WithAllOf(*spec.BooleanProperty(), *spec.StringProperty()), | 			// intentionally not exported in v2 | ||||||
|  | 			// expected: new(spec.Schema). | ||||||
|  | 			//   WithAllOf(*spec.BooleanProperty(), *spec.StringProperty()), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "oneOf", | 			name: "oneOf", | ||||||
| @@ -471,8 +466,10 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema). | 			expected: new(spec.Schema), | ||||||
| 				WithAllOf(spec.Schema{}, spec.Schema{}, spec.Schema{}, *spec.StringProperty()), | 			// not supported by OpenAPI v2 + allOf intentionally not exported | ||||||
|  | 			// expected: new(spec.Schema). | ||||||
|  | 			//	WithAllOf(spec.Schema{}, spec.Schema{}, spec.Schema{}, *spec.StringProperty()), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "properties", | 			name: "properties", | ||||||
| @@ -485,22 +482,39 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 				SetProperty(testStr, *spec.BooleanProperty()), | 				SetProperty(testStr, *spec.BooleanProperty()), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "additionalProperties", | 			name: "additionalProperties schema", | ||||||
| 			in: &apiextensions.JSONSchemaProps{ | 			in: &apiextensions.JSONSchemaProps{ | ||||||
| 				AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{ | 				AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{ | ||||||
| 					Allows: true, | 					Allows: false, | ||||||
| 					Schema: &apiextensions.JSONSchemaProps{Type: "boolean"}, | 					Schema: &apiextensions.JSONSchemaProps{Type: "boolean"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: &spec.Schema{ | 			expected: &spec.Schema{ | ||||||
| 				SchemaProps: spec.SchemaProps{ | 				SchemaProps: spec.SchemaProps{ | ||||||
| 					AdditionalProperties: &spec.SchemaOrBool{ | 					AdditionalProperties: &spec.SchemaOrBool{ | ||||||
| 						Allows: true, | 						Allows: false, | ||||||
| 						Schema: spec.BooleanProperty(), | 						Schema: spec.BooleanProperty(), | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "additionalProperties bool", | ||||||
|  | 			in: &apiextensions.JSONSchemaProps{ | ||||||
|  | 				AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{ | ||||||
|  | 					Allows: true, | ||||||
|  | 					Schema: nil, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			expected: &spec.Schema{ | ||||||
|  | 				SchemaProps: spec.SchemaProps{ | ||||||
|  | 					AdditionalProperties: &spec.SchemaOrBool{ | ||||||
|  | 						Allows: true, | ||||||
|  | 						Schema: nil, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "patternProperties", | 			name: "patternProperties", | ||||||
| 			in: &apiextensions.JSONSchemaProps{ | 			in: &apiextensions.JSONSchemaProps{ | ||||||
| @@ -508,15 +522,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					testStr: {Type: "boolean"}, | 					testStr: {Type: "boolean"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		PatternProperties: map[string]spec.Schema{ |  | ||||||
| 			// 			testStr: *spec.BooleanProperty(), |  | ||||||
| 			// 		}, |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "dependencies schema", | 			name: "dependencies schema", | ||||||
| @@ -527,17 +533,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		Dependencies: spec.Dependencies{ |  | ||||||
| 			// 			testStr: spec.SchemaOrStringArray{ |  | ||||||
| 			// 				Schema: spec.BooleanProperty(), |  | ||||||
| 			// 			}, |  | ||||||
| 			// 		}, |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "dependencies string array", | 			name: "dependencies string array", | ||||||
| @@ -548,17 +544,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		Dependencies: spec.Dependencies{ |  | ||||||
| 			// 			testStr: spec.SchemaOrStringArray{ |  | ||||||
| 			// 				Property: []string{testStr2}, |  | ||||||
| 			// 			}, |  | ||||||
| 			// 		}, |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "additionalItems", | 			name: "additionalItems", | ||||||
| @@ -568,16 +554,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					Schema: &apiextensions.JSONSchemaProps{Type: "boolean"}, | 					Schema: &apiextensions.JSONSchemaProps{Type: "boolean"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		AdditionalItems: &spec.SchemaOrBool{ |  | ||||||
| 			// 			Allows: true, |  | ||||||
| 			// 			Schema: spec.BooleanProperty(), |  | ||||||
| 			// 		}, |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "definitions", | 			name: "definitions", | ||||||
| @@ -586,15 +563,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 					testStr: apiextensions.JSONSchemaProps{Type: "boolean"}, | 					testStr: apiextensions.JSONSchemaProps{Type: "boolean"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema), | 			expectError: true, // rejected by kube validation and NewStructural | ||||||
| 			// not supported by gnostic |  | ||||||
| 			// expected: &spec.Schema{ |  | ||||||
| 			// 	SchemaProps: spec.SchemaProps{ |  | ||||||
| 			// 		Definitions: spec.Definitions{ |  | ||||||
| 			// 			testStr: *spec.BooleanProperty(), |  | ||||||
| 			// 		}, |  | ||||||
| 			// 	}, |  | ||||||
| 			// }, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "externalDocs", | 			name: "externalDocs", | ||||||
| @@ -606,6 +575,7 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema). | 			expected: new(spec.Schema). | ||||||
| 				WithExternalDocs(testStr, testStr2), | 				WithExternalDocs(testStr, testStr2), | ||||||
|  | 			expectDiff: true, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "example", | 			name: "example", | ||||||
| @@ -614,32 +584,44 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			expected: new(spec.Schema). | 			expected: new(spec.Schema). | ||||||
| 				WithExample(testStr), | 				WithExample(testStr), | ||||||
|  | 			expectDiff: true, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, test := range tests { | 	for _, test := range tests { | ||||||
| 		t.Run(test.name, func(t *testing.T) { | 		t.Run(test.name, func(t *testing.T) { | ||||||
| 			out, err := ConvertJSONSchemaPropsToOpenAPIv2Schema(test.in) | 			ss, err := structuralschema.NewStructural(test.in) | ||||||
| 			if err != nil { | 			if err != nil && !test.expectError { | ||||||
| 				t.Fatalf("unexpected error in converting openapi schema: %v", err) | 				t.Fatalf("structural schema error: %v", err) | ||||||
|  | 			} else if err == nil && test.expectError { | ||||||
|  | 				t.Fatalf("expected NewStructural error, but didn't get any") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !test.expectError { | ||||||
|  | 				out := ToStructuralOpenAPIV2(ss).ToGoOpenAPI() | ||||||
|  | 				if equal := reflect.DeepEqual(*out, *test.expected); !equal && !test.expectDiff { | ||||||
|  | 					t.Errorf("unexpected result:\n  want=%v\n   got=%v\n\n%s", *test.expected, *out, cmp.Diff(*test.expected, *out, cmp.Comparer(refEqual))) | ||||||
|  | 				} else if equal && test.expectDiff { | ||||||
|  | 					t.Errorf("expected diff, but didn't get any") | ||||||
| 				} | 				} | ||||||
| 			if !reflect.DeepEqual(*out, *test.expected) { |  | ||||||
| 				t.Errorf("unexpected result:\n  want=%v\n   got=%v\n\n%s", *test.expected, *out, diff.ObjectDiff(*test.expected, *out)) |  | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func refEqual(x spec.Ref, y spec.Ref) bool { | ||||||
|  | 	return x.String() == y.String() | ||||||
|  | } | ||||||
|  |  | ||||||
| // TestKubeOpenapiRejectionFiltering tests that the CRD openapi schema filtering leads to a spec that the | // TestKubeOpenapiRejectionFiltering tests that the CRD openapi schema filtering leads to a spec that the | ||||||
| // kube-openapi/pkg/util/proto model code support in version used in Kubernetes 1.13. | // kube-openapi/pkg/util/proto model code support in version used in Kubernetes 1.13. | ||||||
| func TestKubeOpenapiRejectionFiltering(t *testing.T) { | func TestKubeOpenapiRejectionFiltering(t *testing.T) { | ||||||
| 	for i := 0; i < 10000; i++ { | 	for i := 0; i < 10000; i++ { | ||||||
| 		t.Run(fmt.Sprintf("iteration %d", i), func(t *testing.T) { |  | ||||||
| 		f := fuzz.New() | 		f := fuzz.New() | ||||||
| 		seed := time.Now().UnixNano() | 		seed := time.Now().UnixNano() | ||||||
| 		randSource := rand.New(rand.NewSource(seed)) | 		randSource := rand.New(rand.NewSource(seed)) | ||||||
| 		f.RandSource(randSource) | 		f.RandSource(randSource) | ||||||
| 			t.Logf("seed = %d", seed) | 		t.Logf("iteration %d with seed %d", i, seed) | ||||||
|  |  | ||||||
| 		fuzzFuncs(f, func(ref *spec.Ref, c fuzz.Continue, visible bool) { | 		fuzzFuncs(f, func(ref *spec.Ref, c fuzz.Continue, visible bool) { | ||||||
| 			var url string | 			var url string | ||||||
| @@ -678,10 +660,11 @@ func TestKubeOpenapiRejectionFiltering(t *testing.T) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// apply the filter | 		// apply the filter | ||||||
| 			filtered, err := ConvertJSONSchemaPropsToOpenAPIv2Schema(internalSchema) | 		ss, err := structuralschema.NewStructural(internalSchema) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				t.Fatalf("failed to filter: %v", err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | 		filtered := ToStructuralOpenAPIV2(ss).ToGoOpenAPI() | ||||||
|  |  | ||||||
| 		// create a doc out of it | 		// create a doc out of it | ||||||
| 		filteredSwagger := &spec.Swagger{ | 		filteredSwagger := &spec.Swagger{ | ||||||
| @@ -722,7 +705,6 @@ func TestKubeOpenapiRejectionFiltering(t *testing.T) { | |||||||
| 		if _, err := proto.NewOpenAPIData(doc); err != nil { | 		if _, err := proto.NewOpenAPIData(doc); err != nil { | ||||||
| 			t.Fatalf("failed to convert to kube-openapi/pkg/util/proto model: %v", err) | 			t.Fatalf("failed to convert to kube-openapi/pkg/util/proto model: %v", err) | ||||||
| 		} | 		} | ||||||
| 		}) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -794,9 +776,11 @@ func fuzzFuncs(f *fuzz.Fuzzer, refFunc func(ref *spec.Ref, c fuzz.Continue, visi | |||||||
| 			if p.Default != nil { | 			if p.Default != nil { | ||||||
| 				p.Default = "42" | 				p.Default = "42" | ||||||
| 			} | 			} | ||||||
| 			if p.Example != nil { | 			p.Example = nil | ||||||
| 				p.Example = "42" | 		}, | ||||||
| 			} | 		func(s *spec.SwaggerSchemaProps, c fuzz.Continue) { | ||||||
|  | 			// nothing allowed | ||||||
|  | 			*s = spec.SwaggerSchemaProps{} | ||||||
| 		}, | 		}, | ||||||
| 		func(s *spec.SchemaProps, c fuzz.Continue) { | 		func(s *spec.SchemaProps, c fuzz.Continue) { | ||||||
| 			// gofuzz is broken and calls this even for *SchemaProps fields, ignoring NilChance, leading to infinite recursion | 			// gofuzz is broken and calls this even for *SchemaProps fields, ignoring NilChance, leading to infinite recursion | ||||||
| @@ -809,14 +793,26 @@ func fuzzFuncs(f *fuzz.Fuzzer, refFunc func(ref *spec.Ref, c fuzz.Continue, visi | |||||||
|  |  | ||||||
| 			c.FuzzNoCustom(s) | 			c.FuzzNoCustom(s) | ||||||
|  |  | ||||||
| 			// we don't support multi-type schema props yet in apiextensions/v1beta1 | 			if c.RandBool() { | ||||||
| 			if len(s.Type) > 1 { | 				types := []string{"object", "array", "boolean", "string", "integer", "number"} | ||||||
| 				s.Type = s.Type[:1] | 				s.Type = []string{types[c.Intn(len(types))]} | ||||||
|  | 			} else { | ||||||
| 				s := apiextensionsv1beta1.JSONSchemaProps{} | 				s.Type = nil | ||||||
| 				if reflect.TypeOf(s.Type).String() != "string" { |  | ||||||
| 					panic(fmt.Errorf("this simplifaction is outdated: apiextensions/v1beta1 types not a single string anymore, but %T", s.Type)) |  | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			s.ID = "" | ||||||
|  | 			s.Ref = spec.Ref{} | ||||||
|  | 			s.AdditionalItems = nil | ||||||
|  | 			s.Dependencies = nil | ||||||
|  | 			s.Schema = "" | ||||||
|  | 			s.PatternProperties = nil | ||||||
|  | 			s.Definitions = nil | ||||||
|  |  | ||||||
|  | 			if len(s.Type) == 1 && s.Type[0] == "array" { | ||||||
|  | 				s.Items = &spec.SchemaOrArray{Schema: &spec.Schema{}} | ||||||
|  | 				c.Fuzz(s.Items.Schema) | ||||||
|  | 			} else { | ||||||
|  | 				s.Items = nil | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// reset JSON fields to some correct JSON | 			// reset JSON fields to some correct JSON | ||||||
|   | |||||||
| @@ -160,6 +160,7 @@ func TestCRDOpenAPI(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			Validation: &apiextensionsv1beta1.CustomResourceValidation{ | 			Validation: &apiextensionsv1beta1.CustomResourceValidation{ | ||||||
| 				OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ | 				OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ | ||||||
|  | 					Type: "object", | ||||||
| 					Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ | 					Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ | ||||||
| 						"foo": {Type: "string"}, | 						"foo": {Type: "string"}, | ||||||
| 					}, | 					}, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Dr. Stefan Schimanski
					Dr. Stefan Schimanski