From dee088586a76b876c473418efba8190be7fa6b26 Mon Sep 17 00:00:00 2001 From: jennybuckley Date: Thu, 24 May 2018 09:55:19 -0700 Subject: [PATCH] Expose openapi schema to handlers --- .../apiserver/pkg/endpoints/groupversion.go | 4 + .../apiserver/pkg/endpoints/handlers/rest.go | 2 + .../apiserver/pkg/endpoints/installer.go | 12 ++ .../apiserver/pkg/server/genericapiserver.go | 1 + .../pkg/server/genericapiserver_test.go | 39 ++++- .../apiserver/pkg/util/openapi/proto.go | 142 ++++++++++++++++++ .../apiserver/pkg/util/openapi/proto_test.go | 77 ++++++++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go create mode 100644 staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go b/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go index 7060eb7389..23d13adc3d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go @@ -30,6 +30,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/registry/rest" + openapicommon "k8s.io/kube-openapi/pkg/common" ) // APIGroupVersion is a helper for exposing rest.Storage objects as http.Handlers via go-restful @@ -77,6 +78,9 @@ type APIGroupVersion struct { // EnableAPIResponseCompression indicates whether API Responses should support compression // if the client requests it via Accept-Encoding EnableAPIResponseCompression bool + + // OpenAPIConfig lets the individual handlers build a subset of the OpenAPI schema before they are installed. + OpenAPIConfig *openapicommon.Config } // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 4da38f43b1..942e53483f 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -39,6 +39,7 @@ import ( "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" + openapiproto "k8s.io/kube-openapi/pkg/util/proto" ) // RequestScope encapsulates common fields across all RESTful handler methods. @@ -55,6 +56,7 @@ type RequestScope struct { UnsafeConvertor runtime.ObjectConvertor TableConvertor rest.TableConvertor + OpenAPISchema openapiproto.Schema Resource schema.GroupVersionResource Kind schema.GroupVersionKind diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 0158d28f61..3edd09dcdf 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -39,6 +39,8 @@ import ( "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/registry/rest" genericfilters "k8s.io/apiserver/pkg/server/filters" + utilopenapi "k8s.io/apiserver/pkg/util/openapi" + openapibuilder "k8s.io/kube-openapi/pkg/builder" ) const ( @@ -495,6 +497,16 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag if a.group.MetaGroupVersion != nil { reqScope.MetaGroupVersion = *a.group.MetaGroupVersion } + if a.group.OpenAPIConfig != nil { + openAPIDefinitions, err := openapibuilder.BuildOpenAPIDefinitionsForResource(defaultVersionedObject, a.group.OpenAPIConfig) + if err != nil { + return nil, fmt.Errorf("unable to build openapi definitions for %v: %v", fqKindToRegister, err) + } + reqScope.OpenAPISchema, err = utilopenapi.ToProtoSchema(openAPIDefinitions, fqKindToRegister) + if err != nil { + return nil, fmt.Errorf("unable to get openapi schema for %v: %v", fqKindToRegister, err) + } + } for _, action := range actions { producedObject := storageMeta.ProducesObject(action.Verb) if producedObject == nil { diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go index 10a8ddff6e..9beba735d3 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go @@ -426,6 +426,7 @@ func (s *GenericAPIServer) newAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupV Admit: s.admissionControl, MinRequestTimeout: s.minRequestTimeout, EnableAPIResponseCompression: s.enableAPIResponseCompression, + OpenAPIConfig: s.openAPIConfig, } } diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go index fbe7235c2c..8439d8cf75 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go @@ -31,6 +31,7 @@ import ( "testing" "time" + openapi "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -78,9 +79,45 @@ func init() { examplev1.AddToScheme(scheme) } +func buildTestOpenAPIDefinition() kubeopenapi.OpenAPIDefinition { + return kubeopenapi.OpenAPIDefinition{ + Schema: openapi.Schema{ + SchemaProps: openapi.SchemaProps{ + Description: "Description", + Properties: map[string]openapi.Schema{}, + }, + VendorExtensible: openapi.VendorExtensible{ + Extensions: openapi.Extensions{ + "x-kubernetes-group-version-kind": []map[string]string{ + { + "group": "", + "version": "v1", + "kind": "Getter", + }, + { + "group": "batch", + "version": "v1", + "kind": "Getter", + }, + { + "group": "extensions", + "version": "v1", + "kind": "Getter", + }, + }, + }, + }, + }, + } +} + func testGetOpenAPIDefinitions(_ kubeopenapi.ReferenceCallback) map[string]kubeopenapi.OpenAPIDefinition { return map[string]kubeopenapi.OpenAPIDefinition{ - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Status": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": buildTestOpenAPIDefinition(), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": {}, } } diff --git a/staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go new file mode 100644 index 0000000000..5641d1a141 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go @@ -0,0 +1,142 @@ +/* +Copyright 2018 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 openapi + +import ( + "encoding/json" + "fmt" + + "github.com/go-openapi/spec" + openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2" + "github.com/googleapis/gnostic/compiler" + yaml "gopkg.in/yaml.v2" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/util/proto" +) + +const ( + // groupVersionKindExtensionKey is the key used to lookup the + // GroupVersionKind value for an object definition from the + // definition's "extensions" map. + groupVersionKindExtensionKey = "x-kubernetes-group-version-kind" +) + +// ToProtoSchema builds the proto formatted schema from an OpenAPI spec +func ToProtoSchema(openAPIDefinitions *spec.Definitions, gvk schema.GroupVersionKind) (proto.Schema, error) { + openAPISpec := newMinimalValidOpenAPISpec() + openAPISpec.Definitions = *openAPIDefinitions + + specBytes, err := json.MarshalIndent(openAPISpec, " ", " ") + if err != nil { + return nil, err + } + + var info yaml.MapSlice + err = yaml.Unmarshal(specBytes, &info) + if err != nil { + return nil, err + } + + doc, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil)) + if err != nil { + return nil, err + } + + models, err := proto.NewOpenAPIData(doc) + if err != nil { + return nil, err + } + + for _, modelName := range models.ListModels() { + model := models.LookupModel(modelName) + if model == nil { + return nil, fmt.Errorf("the ListModels function returned a model that can't be looked-up") + } + gvkList := parseGroupVersionKind(model) + for _, modelGVK := range gvkList { + if modelGVK == gvk { + return model, nil + } + } + } + + return nil, fmt.Errorf("no model found with a %v tag matching %v", groupVersionKindExtensionKey, gvk) +} + +// newMinimalValidOpenAPISpec creates a minimal openapi spec with only the required fields filled in +func newMinimalValidOpenAPISpec() *spec.Swagger { + return &spec.Swagger{ + SwaggerProps: spec.SwaggerProps{ + Swagger: "2.0", + Info: &spec.Info{ + InfoProps: spec.InfoProps{ + Title: "Kubernetes", + Version: "0.0.0", + }, + }, + }, + } +} + +// parseGroupVersionKind gets and parses GroupVersionKind from the extension. Returns empty if it doesn't have one. +func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind { + extensions := s.GetExtensions() + + gvkListResult := []schema.GroupVersionKind{} + + // Get the extensions + gvkExtension, ok := extensions[groupVersionKindExtensionKey] + if !ok { + return []schema.GroupVersionKind{} + } + + // gvk extension must be a list of at least 1 element. + gvkList, ok := gvkExtension.([]interface{}) + if !ok { + return []schema.GroupVersionKind{} + } + + for _, gvk := range gvkList { + // gvk extension list must be a map with group, version, and + // kind fields + gvkMap, ok := gvk.(map[interface{}]interface{}) + if !ok { + continue + } + group, ok := gvkMap["group"].(string) + if !ok { + continue + } + version, ok := gvkMap["version"].(string) + if !ok { + continue + } + kind, ok := gvkMap["kind"].(string) + if !ok { + continue + } + + gvkListResult = append(gvkListResult, schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + } + + return gvkListResult +} diff --git a/staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go new file mode 100644 index 0000000000..64421a7ff8 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2018 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 openapi + +import ( + "reflect" + "testing" + + "github.com/go-openapi/spec" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/util/proto" +) + +// TestOpenAPIDefinitionsToProtoSchema tests the openapi parser +func TestOpenAPIDefinitionsToProtoSchema(t *testing.T) { + openAPIDefinitions := &spec.Definitions{ + "io.k8s.api.testgroup.v1.Foo": spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Description of Foos", + Properties: map[string]spec.Schema{}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-group-version-kind": []map[string]string{ + { + "group": "testgroup.k8s.io", + "version": "v1", + "kind": "Foo", + }, + }, + }, + }, + }, + } + gvk := schema.GroupVersionKind{ + Group: "testgroup.k8s.io", + Version: "v1", + Kind: "Foo", + } + expectedSchema := &proto.Arbitrary{ + BaseSchema: proto.BaseSchema{ + Description: "Description of Foos", + Extensions: map[string]interface{}{ + "x-kubernetes-group-version-kind": []interface{}{ + map[interface{}]interface{}{ + "group": "testgroup.k8s.io", + "version": "v1", + "kind": "Foo", + }, + }, + }, + Path: proto.NewPath("io.k8s.api.testgroup.v1.Foo"), + }, + } + actualSchema, err := ToProtoSchema(openAPIDefinitions, gvk) + if err != nil { + t.Fatalf("expected ToProtoSchema not to return an error") + } + if !reflect.DeepEqual(expectedSchema, actualSchema) { + t.Fatalf("expected schema:\n%v\nbut got:\n%v", expectedSchema, actualSchema) + } +}