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 (
"github.com/go-openapi/spec"
v1 "k8s.io/api/autoscaling/v1"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
......@@ -58,22 +59,24 @@ var namer *openapi.DefinitionNamer
// BuildSwagger builds swagger for the given crd in the given version
func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (*spec.Swagger, error) {
var schema *spec.Schema
var schema *structuralschema.Structural
s, err := apiextensions.GetSchemaForVersion(crd, version)
if err != nil {
return nil, err
}
if s != nil && s.OpenAPIV3Schema != nil {
schema, err = ConvertJSONSchemaPropsToOpenAPIv2Schema(s.OpenAPIV3Schema)
if err != nil {
return nil, err
ss, err := structuralschema.NewStructural(s.OpenAPIV3Schema)
if err == nil && len(structuralschema.ValidateStructural(ss, nil)) == 0 {
// skip non-structural schemas
schema = ss.Unfold()
}
}
// TODO(roycaihw): remove the WebService templating below. The following logic
// comes from function registerResourceHandlers() in k8s.io/apiserver.
// Alternatives are either (ideally) refactoring registerResourceHandlers() to
// 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 := &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
// 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
// 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.
// TODO: fix kubectl to understand additionalProperties=true
if schema == nil {
schema = &spec.Schema{
ret = &spec.Schema{
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
}
// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about
// unknown fields for anything 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"]))
addTypeMetaProperties(schema)
addTypeMetaProperties(ret)
}
schema.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
map[string]interface{}{
"group": b.group,
"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
......@@ -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{
schema: &spec.Schema{
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
b.schema = b.buildKubeNative(schema)
b.schema = b.buildKubeNative(schema, v2)
b.listSchema = b.buildListSchema()
return b
......
......@@ -17,106 +17,60 @@ limitations under the License.
package openapi
import (
"strings"
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// ConvertJSONSchemaPropsToOpenAPIv2Schema converts our internal OpenAPI v3 schema
// (*apiextensions.JSONSchemaProps) to an OpenAPI v2 schema (*spec.Schema).
func ConvertJSONSchemaPropsToOpenAPIv2Schema(in *apiextensions.JSONSchemaProps) (*spec.Schema, error) {
// ToStructuralOpenAPIV2 converts our internal OpenAPI v3 structural schema to
// to a v2 compatible schema.
func ToStructuralOpenAPIV2(in *structuralschema.Structural) *structuralschema.Structural {
if in == nil {
return nil, nil
return nil
}
// dirty hack to temporarily set the type at the root. See continuation at the func bottom.
// TODO: remove for Kubernetes 1.15
oldRootType := in.Type
if len(in.Type) == 0 {
in.Type = "object"
}
out := in.DeepCopy()
// Remove unsupported fields in OpenAPI v2 recursively
out := new(spec.Schema)
validation.ConvertJSONSchemaPropsWithPostProcess(in, out, func(p *spec.Schema) error {
p.OneOf = nil
// TODO(roycaihw): preserve cases where we only have one subtree in AnyOf, same for OneOf
p.AnyOf = nil
p.Not = nil
// 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 p.Ref.String() != "" {
// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R95
p.Properties = nil
// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R99
p.Type = nil
// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R104
if !strings.HasPrefix(p.Ref.String(), "#/definitions/") {
p.Ref = spec.Ref{}
mapper := structuralschema.Visitor{
Structural: func(s *structuralschema.Structural) bool {
changed := false
if s.ValueValidation != nil {
if s.ValueValidation.AllOf != nil {
s.ValueValidation.AllOf = nil
changed = true
}
if s.ValueValidation.OneOf != nil {
s.ValueValidation.OneOf = nil
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
if s.ValueValidation.AnyOf != nil {
s.ValueValidation.AnyOf = nil
changed = true
}
if s.ValueValidation.Not != nil {
s.ValueValidation.Not = 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
if p.Items != nil && len(p.Items.Schemas) == 1 {
p.Items = &spec.SchemaOrArray{Schema: &p.Items.Schemas[0]}
}
// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-ce77fea74b9dd098045004410023e0c3R219
if s.Nullable {
s.Type = ""
s.Nullable = false
// general fixups not supported by gnostic
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
}
// untyped values break if items or properties are set in kubectl
// https://github.com/kubernetes/kube-openapi/pull/143/files#diff-62afddb578e9db18fb32ffb6b7802d92R183
s.Items = nil
s.Properties = nil
return nil
})
changed = true
}
// 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 changed
},
// we drop all junctors above, and hence, never reach nested value validations
NestedValueValidation: nil,
}
mapper.Visit(out)
return out, nil
return out
}
......@@ -160,6 +160,7 @@ func TestCRDOpenAPI(t *testing.T) {
},
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"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