Commit bc86aeba authored by Dr. Stefan Schimanski's avatar Dr. Stefan Schimanski

apiextensions: switch OpenAPI pubilshing to structural schema

parent 6acd9215
...@@ -26,6 +26,7 @@ import ( ...@@ -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 ...@@ -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{} ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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
......
...@@ -17,106 +17,60 @@ limitations under the License. ...@@ -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
} }
...@@ -160,6 +160,7 @@ func TestCRDOpenAPI(t *testing.T) { ...@@ -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"},
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment