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 94b901383c7..f0fcc1311af 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi.go +++ b/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi.go @@ -17,17 +17,26 @@ 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" restful "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" ) @@ -46,6 +55,10 @@ 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 } @@ -54,9 +67,17 @@ func computeEtag(data []byte) string { return fmt.Sprintf("\"%X\"", sha512.Sum512(data)) } -func BuildSwaggerSpec(webServices []*restful.WebService, config *openapi.Config) (*spec.Swagger, error) { +// 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)] + o := openAPI{ - config: config, + config: config, swagger: &spec.Swagger{ SwaggerProps: spec.SwaggerProps{ Swagger: OpenAPIVersion, @@ -67,12 +88,44 @@ func BuildSwaggerSpec(webServices []*restful.WebService, config *openapi.Config) }, } - err := o.init(webServices) + err = o.init(webServices) if err != nil { - return nil, err + return err } - return o.swagger, nil + 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 } func (o *openAPI) init(webServices []*restful.WebService) error { @@ -88,7 +141,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(DEFINITION_PREFIX + openapi.EscapeJsonPointer(defName)) + return spec.MustCreateRef("#/definitions/" + openapi.EscapeJsonPointer(defName)) }) if o.config.CommonResponses == nil { o.config.CommonResponses = map[int]spec.Response{} @@ -108,9 +161,41 @@ 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() @@ -166,7 +251,7 @@ func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) { return "", err } defName, _ := o.config.GetDefinitionName(name) - return DEFINITION_PREFIX + openapi.EscapeJsonPointer(defName), nil + return "#/definitions/" + 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 deleted file mode 100644 index 7280dde08d2..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/server/openapi/openapi_handler.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -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/NYTimes/gziphandler" - "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.Handle(path, gziphandler.GzipHandler(http.HandlerFunc( - 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 5626c7f9ba8..9ced93008f7 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/routes/openapi.go +++ b/staging/src/k8s.io/apiserver/pkg/server/routes/openapi.go @@ -32,16 +32,9 @@ type OpenAPI struct { } // Install adds the SwaggerUI webservice to the given mux. -func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) *apiserveropenapi.OpenAPIService { - openapiSpec, err := apiserveropenapi.BuildSwaggerSpec(c.RegisteredWebServices(), oa.Config) +func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) { + err := apiserveropenapi.RegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux) 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 }