From 0a886ffaf8b9de97ef8134a4182b719ba2c6f22f Mon Sep 17 00:00:00 2001 From: mbohlool Date: Wed, 31 May 2017 22:28:40 -0700 Subject: [PATCH] Separate Build and Serving parts of OpenAPI spec handler --- .../apiserver/pkg/server/openapi/openapi.go | 99 +---------- .../pkg/server/openapi/openapi_handler.go | 159 ++++++++++++++++++ .../apiserver/pkg/server/routes/openapi.go | 11 +- 3 files changed, 175 insertions(+), 94 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/server/openapi/openapi_handler.go diff --git a/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi.go b/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi.go index b434dfadf6..bb8c30f2b9 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi.go +++ b/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi.go @@ -17,26 +17,17 @@ limitations under the License. package openapi import ( - "bytes" - "compress/gzip" "crypto/sha512" "encoding/json" "fmt" - "gopkg.in/yaml.v2" - "mime" "net/http" "reflect" "strings" - "time" "github.com/emicklei/go-restful" "github.com/go-openapi/spec" - "github.com/golang/protobuf/proto" - "github.com/googleapis/gnostic/OpenAPIv2" - "github.com/googleapis/gnostic/compiler" "k8s.io/apimachinery/pkg/openapi" - genericmux "k8s.io/apiserver/pkg/server/mux" "k8s.io/apiserver/pkg/util/trie" ) @@ -55,10 +46,6 @@ const ( type openAPI struct { config *openapi.Config swagger *spec.Swagger - swaggerBytes []byte - swaggerPb []byte - swaggerPbGz []byte - lastModified time.Time protocolList []string definitions map[string]openapi.OpenAPIDefinition } @@ -67,17 +54,9 @@ func computeEtag(data []byte) string { return fmt.Sprintf("\"%X\"", sha512.Sum512(data)) } -// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification. -func RegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *openapi.Config, mux *genericmux.PathRecorderMux) (err error) { - - if !strings.HasSuffix(servePath, JSON_EXT) { - return fmt.Errorf("Serving path must ends with \"%s\".", JSON_EXT) - } - - servePathBase := servePath[:len(servePath)-len(JSON_EXT)] - +func BuildSwaggerSpec(webServices []*restful.WebService, config *openapi.Config) (*spec.Swagger, error) { o := openAPI{ - config: config, + config: config, swagger: &spec.Swagger{ SwaggerProps: spec.SwaggerProps{ Swagger: OpenAPIVersion, @@ -88,44 +67,12 @@ func RegisterOpenAPIService(servePath string, webServices []*restful.WebService, }, } - err = o.init(webServices) + err := o.init(webServices) if err != nil { - return err + return nil, err } - mime.AddExtensionType(".json", MIME_JSON) - mime.AddExtensionType(".pb-v1", MIME_PB) - mime.AddExtensionType(".gz", MIME_PB_GZ) - - type fileInfo struct { - ext string - data []byte - } - - files := []fileInfo{ - {".json", o.swaggerBytes}, - {"-2.0.0.json", o.swaggerBytes}, - {"-2.0.0.pb-v1", o.swaggerPb}, - {"-2.0.0.pb-v1.gz", o.swaggerPbGz}, - } - - for _, file := range files { - path := servePathBase + file.ext - data := file.data - etag := computeEtag(file.data) - mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != path { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("Path not found!")) - return - } - w.Header().Set("Etag", etag) - // ServeContent will take care of caching using eTag. - http.ServeContent(w, r, path, o.lastModified, bytes.NewReader(data)) - }) - } - - return nil + return o.swagger, nil } func (o *openAPI) init(webServices []*restful.WebService) error { @@ -141,7 +88,7 @@ func (o *openAPI) init(webServices []*restful.WebService) error { } o.definitions = o.config.GetDefinitions(func(name string) spec.Ref { defName, _ := o.config.GetDefinitionName(name) - return spec.MustCreateRef("#/definitions/" + openapi.EscapeJsonPointer(defName)) + return spec.MustCreateRef(DEFINITION_PREFIX + openapi.EscapeJsonPointer(defName)) }) if o.config.CommonResponses == nil { o.config.CommonResponses = map[int]spec.Response{} @@ -161,41 +108,9 @@ func (o *openAPI) init(webServices []*restful.WebService) error { } } - o.swaggerBytes, err = json.MarshalIndent(o.swagger, " ", " ") - if err != nil { - return err - } - o.swaggerPb, err = toProtoBinary(o.swaggerBytes) - if err != nil { - return err - } - o.swaggerPbGz = toGzip(o.swaggerPb) - o.lastModified = time.Now() - return nil } -func toProtoBinary(spec []byte) ([]byte, error) { - var info yaml.MapSlice - err := yaml.Unmarshal(spec, &info) - if err != nil { - return nil, err - } - document, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil)) - if err != nil { - return nil, err - } - return proto.Marshal(document) -} - -func toGzip(data []byte) []byte { - var buf bytes.Buffer - zw := gzip.NewWriter(&buf) - zw.Write(data) - zw.Close() - return buf.Bytes() -} - func getCanonicalizeTypeName(t reflect.Type) string { if t.PkgPath() == "" { return t.Name() @@ -251,7 +166,7 @@ func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) { return "", err } defName, _ := o.config.GetDefinitionName(name) - return "#/definitions/" + openapi.EscapeJsonPointer(defName), nil + return DEFINITION_PREFIX + openapi.EscapeJsonPointer(defName), nil } // buildPaths builds OpenAPI paths using go-restful's web services. diff --git a/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi_handler.go b/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi_handler.go new file mode 100644 index 0000000000..1ed6e9a347 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi_handler.go @@ -0,0 +1,159 @@ +/* +Copyright 2017 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 ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "mime" + "net/http" + "strings" + "time" + + "github.com/go-openapi/spec" + "github.com/golang/protobuf/proto" + "github.com/googleapis/gnostic/OpenAPIv2" + "github.com/googleapis/gnostic/compiler" + "gopkg.in/yaml.v2" + genericmux "k8s.io/apiserver/pkg/server/mux" +) + +type OpenAPIService struct { + orgSpec *spec.Swagger + specBytes []byte + specPb []byte + specPbGz []byte + lastModified time.Time + updateHooks []func(*http.Request) +} + +// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification. +func RegisterOpenAPIService(openapiSpec *spec.Swagger, servePath string, mux *genericmux.PathRecorderMux) (*OpenAPIService, error) { + if !strings.HasSuffix(servePath, JSON_EXT) { + return nil, fmt.Errorf("Serving path must ends with \"%s\".", JSON_EXT) + } + + servePathBase := servePath[:len(servePath)-len(JSON_EXT)] + + o := OpenAPIService{} + if err := o.UpdateSpec(openapiSpec); err != nil { + return nil, err + } + + mime.AddExtensionType(".json", MIME_JSON) + mime.AddExtensionType(".pb-v1", MIME_PB) + mime.AddExtensionType(".gz", MIME_PB_GZ) + + type fileInfo struct { + ext string + getData func() []byte + } + + files := []fileInfo{ + {".json", o.getSwaggerBytes}, + {"-2.0.0.json", o.getSwaggerBytes}, + {"-2.0.0.pb-v1", o.getSwaggerPbBytes}, + {"-2.0.0.pb-v1.gz", o.getSwaggerPbGzBytes}, + } + + for _, file := range files { + path := servePathBase + file.ext + getData := file.getData + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != path { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Path not found!")) + return + } + o.update(r) + data := getData() + etag := computeEtag(data) + w.Header().Set("Etag", etag) + // ServeContent will take care of caching using eTag. + http.ServeContent(w, r, path, o.lastModified, bytes.NewReader(data)) + }) + } + + return &o, nil +} + +func (o *OpenAPIService) getSwaggerBytes() []byte { + return o.specBytes +} + +func (o *OpenAPIService) getSwaggerPbBytes() []byte { + return o.specPb +} + +func (o *OpenAPIService) getSwaggerPbGzBytes() []byte { + return o.specPbGz +} + +func (o *OpenAPIService) GetSpec() *spec.Swagger { + return o.orgSpec +} + +func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) { + o.orgSpec = openapiSpec + o.specBytes, err = json.MarshalIndent(openapiSpec, " ", " ") + if err != nil { + return err + } + o.specPb, err = toProtoBinary(o.specBytes) + if err != nil { + return err + } + o.specPbGz = toGzip(o.specPb) + o.lastModified = time.Now() + + return nil +} + +func toProtoBinary(spec []byte) ([]byte, error) { + var info yaml.MapSlice + err := yaml.Unmarshal(spec, &info) + if err != nil { + return nil, err + } + document, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil)) + if err != nil { + return nil, err + } + return proto.Marshal(document) +} + +func toGzip(data []byte) []byte { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + zw.Write(data) + zw.Close() + return buf.Bytes() +} + +// Adds an update hook to be called on each spec request. The hook is responsible +// to call UpdateSpec method. +func (o *OpenAPIService) AddUpdateHook(hook func(*http.Request)) { + o.updateHooks = append(o.updateHooks, hook) +} + +func (o *OpenAPIService) update(r *http.Request) { + for _, h := range o.updateHooks { + h(r) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/routes/openapi.go b/staging/src/k8s.io/apiserver/pkg/server/routes/openapi.go index 9ced93008f..5626c7f9ba 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/routes/openapi.go +++ b/staging/src/k8s.io/apiserver/pkg/server/routes/openapi.go @@ -32,9 +32,16 @@ type OpenAPI struct { } // Install adds the SwaggerUI webservice to the given mux. -func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) { - err := apiserveropenapi.RegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux) +func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) *apiserveropenapi.OpenAPIService { + openapiSpec, err := apiserveropenapi.BuildSwaggerSpec(c.RegisteredWebServices(), oa.Config) if err != nil { glog.Fatalf("Failed to register open api spec for root: %v", err) + return nil } + service, err := apiserveropenapi.RegisterOpenAPIService(openapiSpec, "/swagger.json", mux) + if err != nil { + glog.Fatalf("Failed to register open api spec for root: %v", err) + return nil + } + return service }