Separate Build and Serving parts of OpenAPI spec handler

pull/6/head
mbohlool 2017-05-31 22:28:40 -07:00
parent ef8ee84cd0
commit 0a886ffaf8
3 changed files with 175 additions and 94 deletions

View File

@ -17,26 +17,17 @@ limitations under the License.
package openapi package openapi
import ( import (
"bytes"
"compress/gzip"
"crypto/sha512" "crypto/sha512"
"encoding/json" "encoding/json"
"fmt" "fmt"
"gopkg.in/yaml.v2"
"mime"
"net/http" "net/http"
"reflect" "reflect"
"strings" "strings"
"time"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
"github.com/go-openapi/spec" "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" "k8s.io/apimachinery/pkg/openapi"
genericmux "k8s.io/apiserver/pkg/server/mux"
"k8s.io/apiserver/pkg/util/trie" "k8s.io/apiserver/pkg/util/trie"
) )
@ -55,10 +46,6 @@ const (
type openAPI struct { type openAPI struct {
config *openapi.Config config *openapi.Config
swagger *spec.Swagger swagger *spec.Swagger
swaggerBytes []byte
swaggerPb []byte
swaggerPbGz []byte
lastModified time.Time
protocolList []string protocolList []string
definitions map[string]openapi.OpenAPIDefinition definitions map[string]openapi.OpenAPIDefinition
} }
@ -67,17 +54,9 @@ func computeEtag(data []byte) string {
return fmt.Sprintf("\"%X\"", sha512.Sum512(data)) return fmt.Sprintf("\"%X\"", sha512.Sum512(data))
} }
// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification. func BuildSwaggerSpec(webServices []*restful.WebService, config *openapi.Config) (*spec.Swagger, error) {
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)]
o := openAPI{ o := openAPI{
config: config, config: config,
swagger: &spec.Swagger{ swagger: &spec.Swagger{
SwaggerProps: spec.SwaggerProps{ SwaggerProps: spec.SwaggerProps{
Swagger: OpenAPIVersion, Swagger: OpenAPIVersion,
@ -88,44 +67,12 @@ func RegisterOpenAPIService(servePath string, webServices []*restful.WebService,
}, },
} }
err = o.init(webServices) err := o.init(webServices)
if err != nil { if err != nil {
return err return nil, err
} }
mime.AddExtensionType(".json", MIME_JSON) return o.swagger, nil
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
} }
func (o *openAPI) init(webServices []*restful.WebService) error { 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 { o.definitions = o.config.GetDefinitions(func(name string) spec.Ref {
defName, _ := o.config.GetDefinitionName(name) 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 { if o.config.CommonResponses == nil {
o.config.CommonResponses = map[int]spec.Response{} 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 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 { func getCanonicalizeTypeName(t reflect.Type) string {
if t.PkgPath() == "" { if t.PkgPath() == "" {
return t.Name() return t.Name()
@ -251,7 +166,7 @@ func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) {
return "", err return "", err
} }
defName, _ := o.config.GetDefinitionName(name) 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. // buildPaths builds OpenAPI paths using go-restful's web services.

View File

@ -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)
}
}

View File

@ -32,9 +32,16 @@ type OpenAPI struct {
} }
// Install adds the SwaggerUI webservice to the given mux. // Install adds the SwaggerUI webservice to the given mux.
func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) { func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) *apiserveropenapi.OpenAPIService {
err := apiserveropenapi.RegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux) openapiSpec, err := apiserveropenapi.BuildSwaggerSpec(c.RegisteredWebServices(), oa.Config)
if err != nil { if err != nil {
glog.Fatalf("Failed to register open api spec for root: %v", err) 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
} }