2023-05-29 21:03:29 +00:00
|
|
|
/*
|
|
|
|
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 handler
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/sha512"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/NYTimes/gziphandler"
|
|
|
|
"github.com/emicklei/go-restful/v3"
|
|
|
|
"github.com/golang/protobuf/proto"
|
2023-08-17 05:15:28 +00:00
|
|
|
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
2023-05-29 21:03:29 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/munnerz/goautoneg"
|
2023-12-18 20:31:00 +00:00
|
|
|
|
2023-05-29 21:03:29 +00:00
|
|
|
klog "k8s.io/klog/v2"
|
|
|
|
"k8s.io/kube-openapi/pkg/builder"
|
|
|
|
"k8s.io/kube-openapi/pkg/cached"
|
|
|
|
"k8s.io/kube-openapi/pkg/common"
|
|
|
|
"k8s.io/kube-openapi/pkg/common/restfuladapter"
|
|
|
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
subTypeProtobufDeprecated = "com.github.proto-openapi.spec.v2@v1.0+protobuf"
|
|
|
|
subTypeProtobuf = "com.github.proto-openapi.spec.v2.v1.0+protobuf"
|
|
|
|
subTypeJSON = "json"
|
|
|
|
)
|
|
|
|
|
|
|
|
func computeETag(data []byte) string {
|
|
|
|
if data == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%X", sha512.Sum512(data))
|
|
|
|
}
|
|
|
|
|
|
|
|
type timedSpec struct {
|
|
|
|
spec []byte
|
|
|
|
lastModified time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// OpenAPIService is the service responsible for serving OpenAPI spec. It has
|
|
|
|
// the ability to safely change the spec while serving it.
|
|
|
|
type OpenAPIService struct {
|
2023-12-18 20:31:00 +00:00
|
|
|
specCache cached.LastSuccess[*spec.Swagger]
|
|
|
|
jsonCache cached.Value[timedSpec]
|
|
|
|
protoCache cached.Value[timedSpec]
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewOpenAPIService builds an OpenAPIService starting with the given spec.
|
|
|
|
func NewOpenAPIService(swagger *spec.Swagger) *OpenAPIService {
|
2023-12-18 20:31:00 +00:00
|
|
|
return NewOpenAPIServiceLazy(cached.Static(swagger, uuid.New().String()))
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewOpenAPIServiceLazy builds an OpenAPIService from lazy spec.
|
2023-12-18 20:31:00 +00:00
|
|
|
func NewOpenAPIServiceLazy(swagger cached.Value[*spec.Swagger]) *OpenAPIService {
|
2023-05-29 21:03:29 +00:00
|
|
|
o := &OpenAPIService{}
|
|
|
|
o.UpdateSpecLazy(swagger)
|
|
|
|
|
2023-12-18 20:31:00 +00:00
|
|
|
o.jsonCache = cached.Transform[*spec.Swagger](func(spec *spec.Swagger, etag string, err error) (timedSpec, string, error) {
|
|
|
|
if err != nil {
|
|
|
|
return timedSpec{}, "", err
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
2023-12-18 20:31:00 +00:00
|
|
|
json, err := spec.MarshalJSON()
|
2023-05-29 21:03:29 +00:00
|
|
|
if err != nil {
|
2023-12-18 20:31:00 +00:00
|
|
|
return timedSpec{}, "", err
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
2023-12-18 20:31:00 +00:00
|
|
|
return timedSpec{spec: json, lastModified: time.Now()}, computeETag(json), nil
|
2023-05-29 21:03:29 +00:00
|
|
|
}, &o.specCache)
|
2023-12-18 20:31:00 +00:00
|
|
|
o.protoCache = cached.Transform(func(ts timedSpec, etag string, err error) (timedSpec, string, error) {
|
|
|
|
if err != nil {
|
|
|
|
return timedSpec{}, "", err
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
2023-12-18 20:31:00 +00:00
|
|
|
proto, err := ToProtoBinary(ts.spec)
|
2023-05-29 21:03:29 +00:00
|
|
|
if err != nil {
|
2023-12-18 20:31:00 +00:00
|
|
|
return timedSpec{}, "", err
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
|
|
|
// We can re-use the same etag as json because of the Vary header.
|
2023-12-18 20:31:00 +00:00
|
|
|
return timedSpec{spec: proto, lastModified: ts.lastModified}, etag, nil
|
2023-05-29 21:03:29 +00:00
|
|
|
}, o.jsonCache)
|
|
|
|
return o
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *OpenAPIService) UpdateSpec(swagger *spec.Swagger) error {
|
2023-12-18 20:31:00 +00:00
|
|
|
o.UpdateSpecLazy(cached.Static(swagger, uuid.New().String()))
|
2023-05-29 21:03:29 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-12-18 20:31:00 +00:00
|
|
|
func (o *OpenAPIService) UpdateSpecLazy(swagger cached.Value[*spec.Swagger]) {
|
|
|
|
o.specCache.Store(swagger)
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func ToProtoBinary(json []byte) ([]byte, error) {
|
|
|
|
document, err := openapi_v2.ParseDocument(json)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return proto.Marshal(document)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
|
|
|
|
//
|
|
|
|
// Deprecated: use OpenAPIService.RegisterOpenAPIVersionedService instead.
|
2023-08-17 05:15:28 +00:00
|
|
|
func RegisterOpenAPIVersionedService(spec *spec.Swagger, servePath string, handler common.PathHandler) *OpenAPIService {
|
2023-05-29 21:03:29 +00:00
|
|
|
o := NewOpenAPIService(spec)
|
2023-08-17 05:15:28 +00:00
|
|
|
o.RegisterOpenAPIVersionedService(servePath, handler)
|
|
|
|
return o
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
|
2023-08-17 05:15:28 +00:00
|
|
|
func (o *OpenAPIService) RegisterOpenAPIVersionedService(servePath string, handler common.PathHandler) {
|
2023-05-29 21:03:29 +00:00
|
|
|
accepted := []struct {
|
|
|
|
Type string
|
|
|
|
SubType string
|
|
|
|
ReturnedContentType string
|
2023-12-18 20:31:00 +00:00
|
|
|
GetDataAndEtag cached.Value[timedSpec]
|
2023-05-29 21:03:29 +00:00
|
|
|
}{
|
|
|
|
{"application", subTypeJSON, "application/" + subTypeJSON, o.jsonCache},
|
|
|
|
{"application", subTypeProtobufDeprecated, "application/" + subTypeProtobuf, o.protoCache},
|
|
|
|
{"application", subTypeProtobuf, "application/" + subTypeProtobuf, o.protoCache},
|
|
|
|
}
|
|
|
|
|
|
|
|
handler.Handle(servePath, gziphandler.GzipHandler(http.HandlerFunc(
|
|
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
decipherableFormats := r.Header.Get("Accept")
|
|
|
|
if decipherableFormats == "" {
|
|
|
|
decipherableFormats = "*/*"
|
|
|
|
}
|
|
|
|
clauses := goautoneg.ParseAccept(decipherableFormats)
|
|
|
|
w.Header().Add("Vary", "Accept")
|
|
|
|
for _, clause := range clauses {
|
|
|
|
for _, accepts := range accepted {
|
|
|
|
if clause.Type != accepts.Type && clause.Type != "*" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if clause.SubType != accepts.SubType && clause.SubType != "*" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// serve the first matching media type in the sorted clause list
|
2023-12-18 20:31:00 +00:00
|
|
|
ts, etag, err := accepts.GetDataAndEtag.Get()
|
|
|
|
if err != nil {
|
|
|
|
klog.Errorf("Error in OpenAPI handler: %s", err)
|
2023-05-29 21:03:29 +00:00
|
|
|
// only return a 503 if we have no older cache data to serve
|
2023-12-18 20:31:00 +00:00
|
|
|
if ts.spec == nil {
|
2023-05-29 21:03:29 +00:00
|
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Set Content-Type header in the reponse
|
|
|
|
w.Header().Set("Content-Type", accepts.ReturnedContentType)
|
|
|
|
|
|
|
|
// ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
2023-12-18 20:31:00 +00:00
|
|
|
w.Header().Set("Etag", strconv.Quote(etag))
|
2023-05-29 21:03:29 +00:00
|
|
|
// ServeContent will take care of caching using eTag.
|
2023-12-18 20:31:00 +00:00
|
|
|
http.ServeContent(w, r, servePath, ts.lastModified, bytes.NewReader(ts.spec))
|
2023-05-29 21:03:29 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Return 406 for not acceptable format
|
|
|
|
w.WriteHeader(406)
|
|
|
|
return
|
|
|
|
}),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
// BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provide access to it.
|
|
|
|
// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
|
|
|
|
//
|
|
|
|
// Deprecated: BuildAndRegisterOpenAPIVersionedServiceFromRoutes should be used instead.
|
|
|
|
func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
|
|
|
|
return BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath, restfuladapter.AdaptWebServices(webServices), config, handler)
|
|
|
|
}
|
|
|
|
|
|
|
|
// BuildAndRegisterOpenAPIVersionedServiceFromRoutes builds the spec and registers a handler to provide access to it.
|
|
|
|
// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
|
|
|
|
func BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath string, routeContainers []common.RouteContainer, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
|
|
|
|
spec, err := builder.BuildOpenAPISpecFromRoutes(routeContainers, config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
o := NewOpenAPIService(spec)
|
2023-08-17 05:15:28 +00:00
|
|
|
o.RegisterOpenAPIVersionedService(servePath, handler)
|
|
|
|
return o, nil
|
2023-05-29 21:03:29 +00:00
|
|
|
}
|