k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/handler3/handler.go (about) 1 /* 2 Copyright 2021 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 handler3 18 19 import ( 20 "bytes" 21 "crypto/sha512" 22 "encoding/json" 23 "fmt" 24 "net/http" 25 "net/url" 26 "path" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 32 "github.com/golang/protobuf/proto" 33 openapi_v3 "github.com/google/gnostic-models/openapiv3" 34 "github.com/google/uuid" 35 "github.com/munnerz/goautoneg" 36 37 "k8s.io/klog/v2" 38 "k8s.io/kube-openapi/pkg/cached" 39 "k8s.io/kube-openapi/pkg/common" 40 "k8s.io/kube-openapi/pkg/spec3" 41 ) 42 43 const ( 44 subTypeProtobufDeprecated = "com.github.proto-openapi.spec.v3@v1.0+protobuf" 45 subTypeProtobuf = "com.github.proto-openapi.spec.v3.v1.0+protobuf" 46 subTypeJSON = "json" 47 ) 48 49 // OpenAPIV3Discovery is the format of the Discovery document for OpenAPI V3 50 // It maps Discovery paths to their corresponding URLs with a hash parameter included 51 type OpenAPIV3Discovery struct { 52 Paths map[string]OpenAPIV3DiscoveryGroupVersion `json:"paths"` 53 } 54 55 // OpenAPIV3DiscoveryGroupVersion includes information about a group version and URL 56 // for accessing the OpenAPI. The URL includes a hash parameter to support client side caching 57 type OpenAPIV3DiscoveryGroupVersion struct { 58 // Path is an absolute path of an OpenAPI V3 document in the form of /openapi/v3/apis/apps/v1?hash=014fbff9a07c 59 ServerRelativeURL string `json:"serverRelativeURL"` 60 } 61 62 func ToV3ProtoBinary(json []byte) ([]byte, error) { 63 document, err := openapi_v3.ParseDocument(json) 64 if err != nil { 65 return nil, err 66 } 67 return proto.Marshal(document) 68 } 69 70 type timedSpec struct { 71 spec []byte 72 lastModified time.Time 73 } 74 75 // This type is protected by the lock on OpenAPIService. 76 type openAPIV3Group struct { 77 specCache cached.LastSuccess[*spec3.OpenAPI] 78 pbCache cached.Value[timedSpec] 79 jsonCache cached.Value[timedSpec] 80 } 81 82 func newOpenAPIV3Group() *openAPIV3Group { 83 o := &openAPIV3Group{} 84 o.jsonCache = cached.Transform[*spec3.OpenAPI](func(spec *spec3.OpenAPI, etag string, err error) (timedSpec, string, error) { 85 if err != nil { 86 return timedSpec{}, "", err 87 } 88 json, err := json.Marshal(spec) 89 if err != nil { 90 return timedSpec{}, "", err 91 } 92 return timedSpec{spec: json, lastModified: time.Now()}, computeETag(json), nil 93 }, &o.specCache) 94 o.pbCache = cached.Transform(func(ts timedSpec, etag string, err error) (timedSpec, string, error) { 95 if err != nil { 96 return timedSpec{}, "", err 97 } 98 proto, err := ToV3ProtoBinary(ts.spec) 99 if err != nil { 100 return timedSpec{}, "", err 101 } 102 return timedSpec{spec: proto, lastModified: ts.lastModified}, etag, nil 103 }, o.jsonCache) 104 return o 105 } 106 107 func (o *openAPIV3Group) UpdateSpec(openapi cached.Value[*spec3.OpenAPI]) { 108 o.specCache.Store(openapi) 109 } 110 111 // OpenAPIService is the service responsible for serving OpenAPI spec. It has 112 // the ability to safely change the spec while serving it. 113 type OpenAPIService struct { 114 // Mutex protects the schema map. 115 mutex sync.Mutex 116 v3Schema map[string]*openAPIV3Group 117 118 discoveryCache cached.LastSuccess[timedSpec] 119 } 120 121 func computeETag(data []byte) string { 122 if data == nil { 123 return "" 124 } 125 return fmt.Sprintf("%X", sha512.Sum512(data)) 126 } 127 128 func constructServerRelativeURL(gvString, etag string) string { 129 u := url.URL{Path: path.Join("/openapi/v3", gvString)} 130 query := url.Values{} 131 query.Set("hash", etag) 132 u.RawQuery = query.Encode() 133 return u.String() 134 } 135 136 // NewOpenAPIService builds an OpenAPIService starting with the given spec. 137 func NewOpenAPIService() *OpenAPIService { 138 o := &OpenAPIService{} 139 o.v3Schema = make(map[string]*openAPIV3Group) 140 // We're not locked because we haven't shared the structure yet. 141 o.discoveryCache.Store(o.buildDiscoveryCacheLocked()) 142 return o 143 } 144 145 func (o *OpenAPIService) buildDiscoveryCacheLocked() cached.Value[timedSpec] { 146 caches := make(map[string]cached.Value[timedSpec], len(o.v3Schema)) 147 for gvName, group := range o.v3Schema { 148 caches[gvName] = group.jsonCache 149 } 150 return cached.Merge(func(results map[string]cached.Result[timedSpec]) (timedSpec, string, error) { 151 discovery := &OpenAPIV3Discovery{Paths: make(map[string]OpenAPIV3DiscoveryGroupVersion)} 152 for gvName, result := range results { 153 if result.Err != nil { 154 return timedSpec{}, "", result.Err 155 } 156 discovery.Paths[gvName] = OpenAPIV3DiscoveryGroupVersion{ 157 ServerRelativeURL: constructServerRelativeURL(gvName, result.Etag), 158 } 159 } 160 j, err := json.Marshal(discovery) 161 if err != nil { 162 return timedSpec{}, "", err 163 } 164 return timedSpec{spec: j, lastModified: time.Now()}, computeETag(j), nil 165 }, caches) 166 } 167 168 func (o *OpenAPIService) getSingleGroupBytes(getType string, group string) ([]byte, string, time.Time, error) { 169 o.mutex.Lock() 170 defer o.mutex.Unlock() 171 v, ok := o.v3Schema[group] 172 if !ok { 173 return nil, "", time.Now(), fmt.Errorf("Cannot find CRD group %s", group) 174 } 175 switch getType { 176 case subTypeJSON: 177 ts, etag, err := v.jsonCache.Get() 178 return ts.spec, etag, ts.lastModified, err 179 case subTypeProtobuf, subTypeProtobufDeprecated: 180 ts, etag, err := v.pbCache.Get() 181 return ts.spec, etag, ts.lastModified, err 182 default: 183 return nil, "", time.Now(), fmt.Errorf("Invalid accept clause %s", getType) 184 } 185 } 186 187 // UpdateGroupVersionLazy adds or updates an existing group with the new cached. 188 func (o *OpenAPIService) UpdateGroupVersionLazy(group string, openapi cached.Value[*spec3.OpenAPI]) { 189 o.mutex.Lock() 190 defer o.mutex.Unlock() 191 if _, ok := o.v3Schema[group]; !ok { 192 o.v3Schema[group] = newOpenAPIV3Group() 193 // Since there is a new item, we need to re-build the cache map. 194 o.discoveryCache.Store(o.buildDiscoveryCacheLocked()) 195 } 196 o.v3Schema[group].UpdateSpec(openapi) 197 } 198 199 func (o *OpenAPIService) UpdateGroupVersion(group string, openapi *spec3.OpenAPI) { 200 o.UpdateGroupVersionLazy(group, cached.Static(openapi, uuid.New().String())) 201 } 202 203 func (o *OpenAPIService) DeleteGroupVersion(group string) { 204 o.mutex.Lock() 205 defer o.mutex.Unlock() 206 delete(o.v3Schema, group) 207 // Rebuild the merge cache map since the items have changed. 208 o.discoveryCache.Store(o.buildDiscoveryCacheLocked()) 209 } 210 211 func (o *OpenAPIService) HandleDiscovery(w http.ResponseWriter, r *http.Request) { 212 ts, etag, err := o.discoveryCache.Get() 213 if err != nil { 214 klog.Errorf("Error serving discovery: %s", err) 215 w.WriteHeader(http.StatusInternalServerError) 216 return 217 } 218 w.Header().Set("Etag", strconv.Quote(etag)) 219 w.Header().Set("Content-Type", "application/json") 220 http.ServeContent(w, r, "/openapi/v3", ts.lastModified, bytes.NewReader(ts.spec)) 221 } 222 223 func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Request) { 224 url := strings.SplitAfterN(r.URL.Path, "/", 4) 225 group := url[3] 226 227 decipherableFormats := r.Header.Get("Accept") 228 if decipherableFormats == "" { 229 decipherableFormats = "*/*" 230 } 231 clauses := goautoneg.ParseAccept(decipherableFormats) 232 w.Header().Add("Vary", "Accept") 233 234 if len(clauses) == 0 { 235 return 236 } 237 238 accepted := []struct { 239 Type string 240 SubType string 241 ReturnedContentType string 242 }{ 243 {"application", subTypeJSON, "application/" + subTypeJSON}, 244 {"application", subTypeProtobuf, "application/" + subTypeProtobuf}, 245 {"application", subTypeProtobufDeprecated, "application/" + subTypeProtobuf}, 246 } 247 248 for _, clause := range clauses { 249 for _, accepts := range accepted { 250 if clause.Type != accepts.Type && clause.Type != "*" { 251 continue 252 } 253 if clause.SubType != accepts.SubType && clause.SubType != "*" { 254 continue 255 } 256 data, etag, lastModified, err := o.getSingleGroupBytes(accepts.SubType, group) 257 if err != nil { 258 return 259 } 260 // Set Content-Type header in the reponse 261 w.Header().Set("Content-Type", accepts.ReturnedContentType) 262 263 // ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag 264 w.Header().Set("Etag", strconv.Quote(etag)) 265 266 if hash := r.URL.Query().Get("hash"); hash != "" { 267 if hash != etag { 268 u := constructServerRelativeURL(group, etag) 269 http.Redirect(w, r, u, 301) 270 return 271 } 272 // The Vary header is required because the Accept header can 273 // change the contents returned. This prevents clients from caching 274 // protobuf as JSON and vice versa. 275 w.Header().Set("Vary", "Accept") 276 277 // Only set these headers when a hash is given. 278 w.Header().Set("Cache-Control", "public, immutable") 279 // Set the Expires directive to the maximum value of one year from the request, 280 // effectively indicating that the cache never expires. 281 w.Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(time.RFC1123)) 282 } 283 http.ServeContent(w, r, "", lastModified, bytes.NewReader(data)) 284 return 285 } 286 } 287 w.WriteHeader(406) 288 return 289 } 290 291 func (o *OpenAPIService) RegisterOpenAPIV3VersionedService(servePath string, handler common.PathHandlerByGroupVersion) error { 292 handler.Handle(servePath, http.HandlerFunc(o.HandleDiscovery)) 293 handler.HandlePrefix(servePath+"/", http.HandlerFunc(o.HandleGroupVersion)) 294 return nil 295 }