Merge pull request #77554 from sttts/sttts-structural-publishing

apiextensions: publish (only) structural OpenAPI schemas
This commit is contained in:
Kubernetes Prow Robot 2019-05-20 18:38:39 -07:00 committed by GitHub
commit 938041694c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 621 additions and 330 deletions

View File

@ -16,6 +16,7 @@ require (
github.com/go-openapi/strfmt v0.17.0 github.com/go-openapi/strfmt v0.17.0
github.com/go-openapi/validate v0.18.0 github.com/go-openapi/validate v0.18.0
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415
github.com/google/go-cmp v0.3.0
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect

View File

@ -7,6 +7,7 @@ go_library(
"convert.go", "convert.go",
"goopenapi.go", "goopenapi.go",
"structural.go", "structural.go",
"unfold.go",
"validation.go", "validation.go",
"visitor.go", "visitor.go",
"zz_generated.deepcopy.go", "zz_generated.deepcopy.go",

View File

@ -0,0 +1,63 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package schema
// Unfold expands vendor extensions of a structural schema.
// It mutates the receiver.
func (s *Structural) Unfold() *Structural {
if s == nil {
return nil
}
mapper := Visitor{
Structural: func(s *Structural) bool {
if !s.XIntOrString {
return false
}
skipAnyOf := isIntOrStringAnyOfPattern(s)
skipFirstAllOfAnyOf := isIntOrStringAllOfPattern(s)
if skipAnyOf || skipFirstAllOfAnyOf {
return false
}
if s.AnyOf == nil {
s.AnyOf = []NestedValueValidation{
{ForbiddenGenerics: Generic{Type: "integer"}},
{ForbiddenGenerics: Generic{Type: "string"}},
}
} else {
s.AllOf = append([]NestedValueValidation{
{
ValueValidation: ValueValidation{
AnyOf: []NestedValueValidation{
{ForbiddenGenerics: Generic{Type: "integer"}},
{ForbiddenGenerics: Generic{Type: "string"}},
},
},
},
}, s.AllOf...)
}
return true
},
NestedValueValidation: nil, // x-kubernetes-int-or-string cannot be set in nested value validation
}
mapper.Visit(s)
return s
}

View File

@ -97,15 +97,8 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
// - type: integer // - type: integer
// - type: string // - type: string
// - ... zero or more // - ... zero or more
skipAnyOf := false skipAnyOf := isIntOrStringAnyOfPattern(s)
skipFirstAllOfAnyOf := false skipFirstAllOfAnyOf := isIntOrStringAllOfPattern(s)
if s.XIntOrString && s.ValueValidation != nil {
if len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf) {
skipAnyOf = true
} else if len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf) {
skipFirstAllOfAnyOf = true
}
}
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...) allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
@ -157,6 +150,20 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
return allErrs return allErrs
} }
func isIntOrStringAnyOfPattern(s *Structural) bool {
if s == nil || s.ValueValidation == nil {
return false
}
return len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf)
}
func isIntOrStringAllOfPattern(s *Structural) bool {
if s == nil || s.ValueValidation == nil {
return false
}
return len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf)
}
// validateGeneric checks the generic fields of a structural schema. // validateGeneric checks the generic fields of a structural schema.
func validateGeneric(g *Generic, lvl level, fldPath *field.Path) field.ErrorList { func validateGeneric(g *Generic, lvl level, fldPath *field.Path) field.ErrorList {
if g == nil { if g == nil {

View File

@ -14,7 +14,7 @@ go_library(
deps = [ deps = [
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library", "//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/generated/openapi:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/generated/openapi:go_default_library",
@ -49,10 +49,12 @@ go_test(
deps = [ deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json: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", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library", "//vendor/github.com/google/gofuzz:go_default_library",
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library", "//vendor/github.com/googleapis/gnostic/compiler:go_default_library",

View File

@ -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 {
schema = ToStructuralOpenAPIV2(schema)
}
ret = schema.ToGoOpenAPI()
ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"])) WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
addTypeMetaProperties(schema) addTypeMetaProperties(ret)
} }
schema.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{ 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

View File

@ -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.
if _, found := got.schema.Properties["kind"]; found { for _, metaField := range []string{"kind", "apiVersion", "metadata"} {
got.schema.Properties["kind"] = spec.Schema{} if _, found := got.schema.Properties["kind"]; found {
} prop := got.schema.Properties[metaField]
if _, found := got.schema.Properties["apiVersion"]; found { prop.Description = ""
got.schema.Properties["apiVersion"] = spec.Schema{} got.schema.Properties[metaField] = prop
} }
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) {

View File

@ -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
p.Type = nil
case len(p.Type) == 1:
switch p.Type[0] {
case "null":
// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-ce77fea74b9dd098045004410023e0c3R219
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-62afddb578e9db18fb32ffb6b7802d92R184
if p.Items == nil || (p.Items.Schema == nil && len(p.Items.Schemas) != 1) {
p.Type = nil
p.Items = nil
} }
} }
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 // https://github.com/kubernetes/kube-openapi/pull/143/files#diff-ce77fea74b9dd098045004410023e0c3R219
if p.Items != nil && len(p.Items.Schemas) == 1 { if s.Nullable {
p.Items = &spec.SchemaOrArray{Schema: &p.Items.Schemas[0]} s.Type = ""
} s.Nullable = false
// general fixups not supported by gnostic // untyped values break if items or properties are set in kubectl
p.ID = "" // https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R183
p.Schema = "" s.Items = nil
p.Definitions = nil s.Properties = 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 changed = true
}) }
// restore root level type in input, and remove it in output if we had added it return changed
// TODO: remove with Kubernetes 1.15 },
in.Type = oldRootType // we drop all junctors above, and hence, never reach nested value validations
if len(oldRootType) == 0 { NestedValueValidation: nil,
out.Type = nil
} }
mapper.Visit(out)
return out, nil return out
} }

View File

@ -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)
@ -115,41 +120,32 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaFuzzing(t *testing.T) {
testApiextensionsJSON := apiextensions.JSON(testStr) testApiextensionsJSON := apiextensions.JSON(testStr)
tests := []struct { tests := []struct {
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,115 +584,127 @@ 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 !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)) 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")
}
} }
}) })
} }
} }
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("iteration %d with seed %d", i, seed)
t.Logf("seed = %d", 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
if c.RandBool() { if c.RandBool() {
url = fmt.Sprintf("http://%d", c.Intn(100000)) url = fmt.Sprintf("http://%d", c.Intn(100000))
} else { } else {
url = "#/definitions/test" url = "#/definitions/test"
} }
r, err := spec.NewRef(url) r, err := spec.NewRef(url)
if err != nil {
t.Fatalf("failed to fuzz ref: %v", err)
}
*ref = r
})
// create go-openapi object and fuzz it (we start here because we have the powerful fuzzer already
s := &spec.Schema{}
f.Fuzz(s)
// convert to apiextensions v1beta1
bs, err := json.Marshal(s)
if err != nil { if err != nil {
t.Fatal(err) t.Fatalf("failed to fuzz ref: %v", err)
}
t.Log(string(bs))
var schema *apiextensionsv1beta1.JSONSchemaProps
if err := json.Unmarshal(bs, &schema); err != nil {
t.Fatalf("failed to unmarshal JSON into apiextensions/v1beta1: %v", err)
}
// convert to internal
internalSchema := &apiextensions.JSONSchemaProps{}
if err := apiextensionsv1beta1.Convert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(schema, internalSchema, nil); err != nil {
t.Fatalf("failed to convert from apiextensions/v1beta1 to internal: %v", err)
}
// apply the filter
filtered, err := ConvertJSONSchemaPropsToOpenAPIv2Schema(internalSchema)
if err != nil {
t.Fatalf("failed to filter: %v", err)
}
// create a doc out of it
filteredSwagger := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Definitions: spec.Definitions{
"test": *filtered,
},
Info: &spec.Info{
InfoProps: spec.InfoProps{
Description: "test",
Version: "test",
Title: "test",
},
},
Swagger: "2.0",
},
}
// convert to JSON
bs, err = json.Marshal(filteredSwagger)
if err != nil {
t.Fatalf("failed to encode filtered to JSON: %v", err)
}
// unmarshal as yaml
var yml yaml.MapSlice
if err := yaml.Unmarshal(bs, &yml); err != nil {
t.Fatalf("failed to decode filtered JSON by into memory: %v", err)
}
// create gnostic doc
doc, err := openapi_v2.NewDocument(yml, compiler.NewContext("$root", nil))
if err != nil {
t.Fatalf("failed to create gnostic doc: %v", err)
}
// load with kube-openapi/pkg/util/proto
if _, err := proto.NewOpenAPIData(doc); err != nil {
t.Fatalf("failed to convert to kube-openapi/pkg/util/proto model: %v", err)
} }
*ref = r
}) })
// create go-openapi object and fuzz it (we start here because we have the powerful fuzzer already
s := &spec.Schema{}
f.Fuzz(s)
// convert to apiextensions v1beta1
bs, err := json.Marshal(s)
if err != nil {
t.Fatal(err)
}
t.Log(string(bs))
var schema *apiextensionsv1beta1.JSONSchemaProps
if err := json.Unmarshal(bs, &schema); err != nil {
t.Fatalf("failed to unmarshal JSON into apiextensions/v1beta1: %v", err)
}
// convert to internal
internalSchema := &apiextensions.JSONSchemaProps{}
if err := apiextensionsv1beta1.Convert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(schema, internalSchema, nil); err != nil {
t.Fatalf("failed to convert from apiextensions/v1beta1 to internal: %v", err)
}
// apply the filter
ss, err := structuralschema.NewStructural(internalSchema)
if err != nil {
t.Fatal(err)
}
filtered := ToStructuralOpenAPIV2(ss).ToGoOpenAPI()
// create a doc out of it
filteredSwagger := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Definitions: spec.Definitions{
"test": *filtered,
},
Info: &spec.Info{
InfoProps: spec.InfoProps{
Description: "test",
Version: "test",
Title: "test",
},
},
Swagger: "2.0",
},
}
// convert to JSON
bs, err = json.Marshal(filteredSwagger)
if err != nil {
t.Fatalf("failed to encode filtered to JSON: %v", err)
}
// unmarshal as yaml
var yml yaml.MapSlice
if err := yaml.Unmarshal(bs, &yml); err != nil {
t.Fatalf("failed to decode filtered JSON by into memory: %v", err)
}
// create gnostic doc
doc, err := openapi_v2.NewDocument(yml, compiler.NewContext("$root", nil))
if err != nil {
t.Fatalf("failed to create gnostic doc: %v", err)
}
// load with kube-openapi/pkg/util/proto
if _, err := proto.NewOpenAPIData(doc); err != nil {
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.Type = nil
}
s := apiextensionsv1beta1.JSONSchemaProps{} s.ID = ""
if reflect.TypeOf(s.Type).String() != "string" { s.Ref = spec.Ref{}
panic(fmt.Errorf("this simplifaction is outdated: apiextensions/v1beta1 types not a single string anymore, but %T", s.Type)) 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

View File

@ -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"},
}, },