k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/handler/handler.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package handler 18 19 import ( 20 "bytes" 21 "crypto/sha512" 22 "fmt" 23 "net/http" 24 "strconv" 25 "time" 26 27 "github.com/NYTimes/gziphandler" 28 "github.com/emicklei/go-restful/v3" 29 "github.com/golang/protobuf/proto" 30 openapi_v2 "github.com/google/gnostic-models/openapiv2" 31 "github.com/google/uuid" 32 "github.com/munnerz/goautoneg" 33 34 klog "k8s.io/klog/v2" 35 "k8s.io/kube-openapi/pkg/builder" 36 "k8s.io/kube-openapi/pkg/cached" 37 "k8s.io/kube-openapi/pkg/common" 38 "k8s.io/kube-openapi/pkg/common/restfuladapter" 39 "k8s.io/kube-openapi/pkg/validation/spec" 40 ) 41 42 const ( 43 subTypeProtobufDeprecated = "com.github.proto-openapi.spec.v2@v1.0+protobuf" 44 subTypeProtobuf = "com.github.proto-openapi.spec.v2.v1.0+protobuf" 45 subTypeJSON = "json" 46 ) 47 48 func computeETag(data []byte) string { 49 if data == nil { 50 return "" 51 } 52 return fmt.Sprintf("%X", sha512.Sum512(data)) 53 } 54 55 type timedSpec struct { 56 spec []byte 57 lastModified time.Time 58 } 59 60 // OpenAPIService is the service responsible for serving OpenAPI spec. It has 61 // the ability to safely change the spec while serving it. 62 type OpenAPIService struct { 63 specCache cached.LastSuccess[*spec.Swagger] 64 jsonCache cached.Value[timedSpec] 65 protoCache cached.Value[timedSpec] 66 } 67 68 // NewOpenAPIService builds an OpenAPIService starting with the given spec. 69 func NewOpenAPIService(swagger *spec.Swagger) *OpenAPIService { 70 return NewOpenAPIServiceLazy(cached.Static(swagger, uuid.New().String())) 71 } 72 73 // NewOpenAPIServiceLazy builds an OpenAPIService from lazy spec. 74 func NewOpenAPIServiceLazy(swagger cached.Value[*spec.Swagger]) *OpenAPIService { 75 o := &OpenAPIService{} 76 o.UpdateSpecLazy(swagger) 77 78 o.jsonCache = cached.Transform[*spec.Swagger](func(spec *spec.Swagger, etag string, err error) (timedSpec, string, error) { 79 if err != nil { 80 return timedSpec{}, "", err 81 } 82 json, err := spec.MarshalJSON() 83 if err != nil { 84 return timedSpec{}, "", err 85 } 86 return timedSpec{spec: json, lastModified: time.Now()}, computeETag(json), nil 87 }, &o.specCache) 88 o.protoCache = cached.Transform(func(ts timedSpec, etag string, err error) (timedSpec, string, error) { 89 if err != nil { 90 return timedSpec{}, "", err 91 } 92 proto, err := ToProtoBinary(ts.spec) 93 if err != nil { 94 return timedSpec{}, "", err 95 } 96 // We can re-use the same etag as json because of the Vary header. 97 return timedSpec{spec: proto, lastModified: ts.lastModified}, etag, nil 98 }, o.jsonCache) 99 return o 100 } 101 102 func (o *OpenAPIService) UpdateSpec(swagger *spec.Swagger) error { 103 o.UpdateSpecLazy(cached.Static(swagger, uuid.New().String())) 104 return nil 105 } 106 107 func (o *OpenAPIService) UpdateSpecLazy(swagger cached.Value[*spec.Swagger]) { 108 o.specCache.Store(swagger) 109 } 110 111 func ToProtoBinary(json []byte) ([]byte, error) { 112 document, err := openapi_v2.ParseDocument(json) 113 if err != nil { 114 return nil, err 115 } 116 return proto.Marshal(document) 117 } 118 119 // RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec. 120 // 121 // Deprecated: use OpenAPIService.RegisterOpenAPIVersionedService instead. 122 func RegisterOpenAPIVersionedService(spec *spec.Swagger, servePath string, handler common.PathHandler) *OpenAPIService { 123 o := NewOpenAPIService(spec) 124 o.RegisterOpenAPIVersionedService(servePath, handler) 125 return o 126 } 127 128 // RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec. 129 func (o *OpenAPIService) RegisterOpenAPIVersionedService(servePath string, handler common.PathHandler) { 130 accepted := []struct { 131 Type string 132 SubType string 133 ReturnedContentType string 134 GetDataAndEtag cached.Value[timedSpec] 135 }{ 136 {"application", subTypeJSON, "application/" + subTypeJSON, o.jsonCache}, 137 {"application", subTypeProtobufDeprecated, "application/" + subTypeProtobuf, o.protoCache}, 138 {"application", subTypeProtobuf, "application/" + subTypeProtobuf, o.protoCache}, 139 } 140 141 handler.Handle(servePath, gziphandler.GzipHandler(http.HandlerFunc( 142 func(w http.ResponseWriter, r *http.Request) { 143 decipherableFormats := r.Header.Get("Accept") 144 if decipherableFormats == "" { 145 decipherableFormats = "*/*" 146 } 147 clauses := goautoneg.ParseAccept(decipherableFormats) 148 w.Header().Add("Vary", "Accept") 149 for _, clause := range clauses { 150 for _, accepts := range accepted { 151 if clause.Type != accepts.Type && clause.Type != "*" { 152 continue 153 } 154 if clause.SubType != accepts.SubType && clause.SubType != "*" { 155 continue 156 } 157 // serve the first matching media type in the sorted clause list 158 ts, etag, err := accepts.GetDataAndEtag.Get() 159 if err != nil { 160 klog.Errorf("Error in OpenAPI handler: %s", err) 161 // only return a 503 if we have no older cache data to serve 162 if ts.spec == nil { 163 w.WriteHeader(http.StatusServiceUnavailable) 164 return 165 } 166 } 167 // Set Content-Type header in the reponse 168 w.Header().Set("Content-Type", accepts.ReturnedContentType) 169 170 // ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag 171 w.Header().Set("Etag", strconv.Quote(etag)) 172 // ServeContent will take care of caching using eTag. 173 http.ServeContent(w, r, servePath, ts.lastModified, bytes.NewReader(ts.spec)) 174 return 175 } 176 } 177 // Return 406 for not acceptable format 178 w.WriteHeader(406) 179 return 180 }), 181 )) 182 } 183 184 // BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provide access to it. 185 // Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService. 186 // 187 // Deprecated: BuildAndRegisterOpenAPIVersionedServiceFromRoutes should be used instead. 188 func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) { 189 return BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath, restfuladapter.AdaptWebServices(webServices), config, handler) 190 } 191 192 // BuildAndRegisterOpenAPIVersionedServiceFromRoutes builds the spec and registers a handler to provide access to it. 193 // Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService. 194 func BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath string, routeContainers []common.RouteContainer, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) { 195 spec, err := builder.BuildOpenAPISpecFromRoutes(routeContainers, config) 196 if err != nil { 197 return nil, err 198 } 199 o := NewOpenAPIService(spec) 200 o.RegisterOpenAPIVersionedService(servePath, handler) 201 return o, nil 202 }