k8s.io/apiserver@v0.31.1/pkg/endpoints/discovery/aggregated/handler.go (about) 1 /* 2 Copyright 2022 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 aggregated 18 19 import ( 20 "fmt" 21 "net/http" 22 "reflect" 23 "sort" 24 "sync" 25 26 apidiscoveryv2 "k8s.io/api/apidiscovery/v2" 27 apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/runtime/serializer" 30 "k8s.io/apimachinery/pkg/version" 31 apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2" 32 33 "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" 34 35 "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" 36 "k8s.io/apiserver/pkg/endpoints/metrics" 37 38 "sync/atomic" 39 40 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 "k8s.io/apimachinery/pkg/runtime" 42 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 43 "k8s.io/klog/v2" 44 ) 45 46 type Source uint 47 48 // The GroupVersion from the lowest Source takes precedence 49 const ( 50 AggregatorSource Source = 0 51 BuiltinSource Source = 100 52 CRDSource Source = 200 53 ) 54 55 // This handler serves the /apis endpoint for an aggregated list of 56 // api resources indexed by their group version. 57 type ResourceManager interface { 58 // Adds knowledge of the given groupversion to the discovery document 59 // If it was already being tracked, updates the stored APIVersionDiscovery 60 // Thread-safe 61 AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) 62 63 // Sets a priority to be used while sorting a specific group and 64 // group-version. If two versions report different priorities for 65 // the group, the higher one will be used. If the group is not 66 // known, the priority is ignored. The priority for this version 67 // is forgotten once the group-version is forgotten 68 SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int) 69 70 // Removes all group versions for a given group 71 // Thread-safe 72 RemoveGroup(groupName string) 73 74 // Removes a specific groupversion. If all versions of a group have been 75 // removed, then the entire group is unlisted. 76 // Thread-safe 77 RemoveGroupVersion(gv metav1.GroupVersion) 78 79 // Resets the manager's known list of group-versions and replaces them 80 // with the given groups 81 // Thread-Safe 82 SetGroups([]apidiscoveryv2.APIGroupDiscovery) 83 84 // Returns the same resource manager using a different source 85 // The source is used to decide how to de-duplicate groups. 86 // The group from the least-numbered source is used 87 WithSource(source Source) ResourceManager 88 89 http.Handler 90 } 91 92 type resourceManager struct { 93 source Source 94 *resourceDiscoveryManager 95 } 96 97 func (rm resourceManager) AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) { 98 rm.resourceDiscoveryManager.AddGroupVersion(rm.source, groupName, value) 99 } 100 func (rm resourceManager) SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int) { 101 rm.resourceDiscoveryManager.SetGroupVersionPriority(rm.source, gv, grouppriority, versionpriority) 102 } 103 func (rm resourceManager) RemoveGroup(groupName string) { 104 rm.resourceDiscoveryManager.RemoveGroup(rm.source, groupName) 105 } 106 func (rm resourceManager) RemoveGroupVersion(gv metav1.GroupVersion) { 107 rm.resourceDiscoveryManager.RemoveGroupVersion(rm.source, gv) 108 } 109 func (rm resourceManager) SetGroups(groups []apidiscoveryv2.APIGroupDiscovery) { 110 rm.resourceDiscoveryManager.SetGroups(rm.source, groups) 111 } 112 113 func (rm resourceManager) WithSource(source Source) ResourceManager { 114 return resourceManager{ 115 source: source, 116 resourceDiscoveryManager: rm.resourceDiscoveryManager, 117 } 118 } 119 120 type groupKey struct { 121 name string 122 123 // Source identifies where this group came from and dictates which group 124 // among duplicates is chosen to be used for discovery. 125 source Source 126 } 127 128 type groupVersionKey struct { 129 metav1.GroupVersion 130 source Source 131 } 132 133 type resourceDiscoveryManager struct { 134 serializer runtime.NegotiatedSerializer 135 // cache is an atomic pointer to avoid the use of locks 136 cache atomic.Pointer[cachedGroupList] 137 138 serveHTTPFunc http.HandlerFunc 139 140 // Writes protected by the lock. 141 // List of all apigroups & resources indexed by the resource manager 142 lock sync.RWMutex 143 apiGroups map[groupKey]*apidiscoveryv2.APIGroupDiscovery 144 versionPriorities map[groupVersionKey]priorityInfo 145 } 146 147 type priorityInfo struct { 148 GroupPriorityMinimum int 149 VersionPriority int 150 } 151 152 func NewResourceManager(path string) ResourceManager { 153 scheme := runtime.NewScheme() 154 utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) 155 utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme)) 156 // Register conversion for apidiscovery 157 utilruntime.Must(apidiscoveryv2conversion.RegisterConversions(scheme)) 158 159 codecs := serializer.NewCodecFactory(scheme) 160 rdm := &resourceDiscoveryManager{ 161 serializer: codecs, 162 versionPriorities: make(map[groupVersionKey]priorityInfo), 163 } 164 rdm.serveHTTPFunc = metrics.InstrumentHandlerFunc("GET", 165 /* group = */ "", 166 /* version = */ "", 167 /* resource = */ "", 168 /* subresource = */ path, 169 /* scope = */ "", 170 /* component = */ metrics.APIServerComponent, 171 /* deprecated */ false, 172 /* removedRelease */ "", 173 rdm.serveHTTP) 174 return resourceManager{ 175 source: BuiltinSource, 176 resourceDiscoveryManager: rdm, 177 } 178 } 179 180 func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(source Source, gv metav1.GroupVersion, groupPriorityMinimum, versionPriority int) { 181 rdm.lock.Lock() 182 defer rdm.lock.Unlock() 183 184 key := groupVersionKey{ 185 GroupVersion: gv, 186 source: source, 187 } 188 rdm.versionPriorities[key] = priorityInfo{ 189 GroupPriorityMinimum: groupPriorityMinimum, 190 VersionPriority: versionPriority, 191 } 192 rdm.cache.Store(nil) 193 } 194 195 func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscoveryv2.APIGroupDiscovery) { 196 rdm.lock.Lock() 197 defer rdm.lock.Unlock() 198 199 rdm.apiGroups = nil 200 rdm.cache.Store(nil) 201 202 for _, group := range groups { 203 for _, version := range group.Versions { 204 rdm.addGroupVersionLocked(source, group.Name, version) 205 } 206 } 207 208 // Filter unused out priority entries 209 for gv := range rdm.versionPriorities { 210 key := groupKey{ 211 source: source, 212 name: gv.Group, 213 } 214 entry, exists := rdm.apiGroups[key] 215 if !exists { 216 delete(rdm.versionPriorities, gv) 217 continue 218 } 219 220 containsVersion := false 221 222 for _, v := range entry.Versions { 223 if v.Version == gv.Version { 224 containsVersion = true 225 break 226 } 227 } 228 229 if !containsVersion { 230 delete(rdm.versionPriorities, gv) 231 } 232 } 233 } 234 235 func (rdm *resourceDiscoveryManager) AddGroupVersion(source Source, groupName string, value apidiscoveryv2.APIVersionDiscovery) { 236 rdm.lock.Lock() 237 defer rdm.lock.Unlock() 238 239 rdm.addGroupVersionLocked(source, groupName, value) 240 } 241 242 func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupName string, value apidiscoveryv2.APIVersionDiscovery) { 243 244 if rdm.apiGroups == nil { 245 rdm.apiGroups = make(map[groupKey]*apidiscoveryv2.APIGroupDiscovery) 246 } 247 248 key := groupKey{ 249 source: source, 250 name: groupName, 251 } 252 253 if existing, groupExists := rdm.apiGroups[key]; groupExists { 254 // If this version already exists, replace it 255 versionExists := false 256 257 // Not very efficient, but in practice there are generally not many versions 258 for i := range existing.Versions { 259 if existing.Versions[i].Version == value.Version { 260 // The new gv is the exact same as what is already in 261 // the map. This is a noop and cache should not be 262 // invalidated. 263 if reflect.DeepEqual(existing.Versions[i], value) { 264 return 265 } 266 267 existing.Versions[i] = value 268 versionExists = true 269 break 270 } 271 } 272 273 if !versionExists { 274 existing.Versions = append(existing.Versions, value) 275 } 276 277 } else { 278 group := &apidiscoveryv2.APIGroupDiscovery{ 279 ObjectMeta: metav1.ObjectMeta{ 280 Name: groupName, 281 }, 282 Versions: []apidiscoveryv2.APIVersionDiscovery{value}, 283 } 284 rdm.apiGroups[key] = group 285 } 286 klog.Infof("Adding GroupVersion %s %s to ResourceManager", groupName, value.Version) 287 288 gv := metav1.GroupVersion{Group: groupName, Version: value.Version} 289 gvKey := groupVersionKey{ 290 GroupVersion: gv, 291 source: source, 292 } 293 if _, ok := rdm.versionPriorities[gvKey]; !ok { 294 rdm.versionPriorities[gvKey] = priorityInfo{ 295 GroupPriorityMinimum: 1000, 296 VersionPriority: 15, 297 } 298 } 299 300 // Reset response document so it is recreated lazily 301 rdm.cache.Store(nil) 302 } 303 304 func (rdm *resourceDiscoveryManager) RemoveGroupVersion(source Source, apiGroup metav1.GroupVersion) { 305 rdm.lock.Lock() 306 defer rdm.lock.Unlock() 307 308 key := groupKey{ 309 source: source, 310 name: apiGroup.Group, 311 } 312 313 group, exists := rdm.apiGroups[key] 314 if !exists { 315 return 316 } 317 318 modified := false 319 for i := range group.Versions { 320 if group.Versions[i].Version == apiGroup.Version { 321 group.Versions = append(group.Versions[:i], group.Versions[i+1:]...) 322 modified = true 323 break 324 } 325 } 326 // If no modification was done, cache does not need to be cleared 327 if !modified { 328 return 329 } 330 331 gvKey := groupVersionKey{ 332 GroupVersion: apiGroup, 333 source: source, 334 } 335 336 delete(rdm.versionPriorities, gvKey) 337 if len(group.Versions) == 0 { 338 delete(rdm.apiGroups, key) 339 } 340 341 // Reset response document so it is recreated lazily 342 rdm.cache.Store(nil) 343 } 344 345 func (rdm *resourceDiscoveryManager) RemoveGroup(source Source, groupName string) { 346 rdm.lock.Lock() 347 defer rdm.lock.Unlock() 348 349 key := groupKey{ 350 source: source, 351 name: groupName, 352 } 353 354 delete(rdm.apiGroups, key) 355 356 for k := range rdm.versionPriorities { 357 if k.Group == groupName && k.source == source { 358 delete(rdm.versionPriorities, k) 359 } 360 } 361 362 // Reset response document so it is recreated lazily 363 rdm.cache.Store(nil) 364 } 365 366 // Prepares the api group list for serving by converting them from map into 367 // list and sorting them according to insertion order 368 func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2.APIGroupDiscovery { 369 regenerationCounter.Inc() 370 // Re-order the apiGroups by their priority. 371 groups := []apidiscoveryv2.APIGroupDiscovery{} 372 373 groupsToUse := map[string]apidiscoveryv2.APIGroupDiscovery{} 374 sourcesUsed := map[metav1.GroupVersion]Source{} 375 376 for key, group := range rdm.apiGroups { 377 if existing, ok := groupsToUse[key.name]; ok { 378 for _, v := range group.Versions { 379 gv := metav1.GroupVersion{Group: key.name, Version: v.Version} 380 381 // Skip groupversions we've already seen before. Only DefaultSource 382 // takes precedence 383 if usedSource, seen := sourcesUsed[gv]; seen && key.source >= usedSource { 384 continue 385 } else if seen { 386 // Find the index of the duplicate version and replace 387 for i := 0; i < len(existing.Versions); i++ { 388 if existing.Versions[i].Version == v.Version { 389 existing.Versions[i] = v 390 break 391 } 392 } 393 394 } else { 395 // New group-version, just append 396 existing.Versions = append(existing.Versions, v) 397 } 398 399 sourcesUsed[gv] = key.source 400 groupsToUse[key.name] = existing 401 } 402 // Check to see if we have overlapping versions. If we do, take the one 403 // with highest source precedence 404 } else { 405 groupsToUse[key.name] = *group.DeepCopy() 406 for _, v := range group.Versions { 407 gv := metav1.GroupVersion{Group: key.name, Version: v.Version} 408 sourcesUsed[gv] = key.source 409 } 410 } 411 } 412 413 for _, group := range groupsToUse { 414 415 // Re-order versions based on their priority. Use kube-aware string 416 // comparison as a tie breaker 417 sort.SliceStable(group.Versions, func(i, j int) bool { 418 iVersion := group.Versions[i].Version 419 jVersion := group.Versions[j].Version 420 421 iGV := metav1.GroupVersion{Group: group.Name, Version: iVersion} 422 jGV := metav1.GroupVersion{Group: group.Name, Version: jVersion} 423 424 iSource := sourcesUsed[iGV] 425 jSource := sourcesUsed[jGV] 426 427 iPriority := rdm.versionPriorities[groupVersionKey{iGV, iSource}].VersionPriority 428 jPriority := rdm.versionPriorities[groupVersionKey{jGV, jSource}].VersionPriority 429 430 // Sort by version string comparator if priority is equal 431 if iPriority == jPriority { 432 return version.CompareKubeAwareVersionStrings(iVersion, jVersion) > 0 433 } 434 435 // i sorts before j if it has a higher priority 436 return iPriority > jPriority 437 }) 438 439 groups = append(groups, group) 440 } 441 442 // For each group, determine the highest minimum group priority and use that 443 priorities := map[string]int{} 444 for gv, info := range rdm.versionPriorities { 445 if source := sourcesUsed[gv.GroupVersion]; source != gv.source { 446 continue 447 } 448 449 if existing, exists := priorities[gv.Group]; exists { 450 if existing < info.GroupPriorityMinimum { 451 priorities[gv.Group] = info.GroupPriorityMinimum 452 } 453 } else { 454 priorities[gv.Group] = info.GroupPriorityMinimum 455 } 456 } 457 458 sort.SliceStable(groups, func(i, j int) bool { 459 iName := groups[i].Name 460 jName := groups[j].Name 461 462 // Default to 0 priority by default 463 iPriority := priorities[iName] 464 jPriority := priorities[jName] 465 466 // Sort discovery based on apiservice priority. 467 // Duplicated from staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helpers.go 468 if iPriority == jPriority { 469 // Equal priority uses name to break ties 470 return iName < jName 471 } 472 473 // i sorts before j if it has a higher priority 474 return iPriority > jPriority 475 }) 476 477 return groups 478 } 479 480 // Fetches from cache if it exists. If cache is empty, create it. 481 func (rdm *resourceDiscoveryManager) fetchFromCache() *cachedGroupList { 482 rdm.lock.RLock() 483 defer rdm.lock.RUnlock() 484 485 cacheLoad := rdm.cache.Load() 486 if cacheLoad != nil { 487 return cacheLoad 488 } 489 response := apidiscoveryv2.APIGroupDiscoveryList{ 490 Items: rdm.calculateAPIGroupsLocked(), 491 } 492 etag, err := calculateETag(response) 493 if err != nil { 494 klog.Errorf("failed to calculate etag for discovery document: %s", etag) 495 etag = "" 496 } 497 cached := &cachedGroupList{ 498 cachedResponse: response, 499 cachedResponseETag: etag, 500 } 501 rdm.cache.Store(cached) 502 return cached 503 } 504 505 type cachedGroupList struct { 506 cachedResponse apidiscoveryv2.APIGroupDiscoveryList 507 // etag is calculated based on a SHA hash of only the JSON object. 508 // A response via different Accept encodings (eg: protobuf, json) will 509 // yield the same etag. This is okay because Accept is part of the Vary header. 510 // Per RFC7231 a client must only cache a response etag pair if the header field 511 // matches as indicated by the Vary field. Thus, protobuf and json and other Accept 512 // encodings will not be cached as the same response despite having the same etag. 513 cachedResponseETag string 514 } 515 516 func (rdm *resourceDiscoveryManager) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 517 rdm.serveHTTPFunc(resp, req) 518 } 519 520 func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *http.Request) { 521 cache := rdm.fetchFromCache() 522 response := cache.cachedResponse 523 etag := cache.cachedResponseETag 524 525 mediaType, _, err := negotiation.NegotiateOutputMediaType(req, rdm.serializer, DiscoveryEndpointRestrictions) 526 if err != nil { 527 // Should never happen. wrapper.go will only proxy requests to this 528 // handler if the media type passes DiscoveryEndpointRestrictions 529 utilruntime.HandleError(err) 530 resp.WriteHeader(http.StatusInternalServerError) 531 return 532 } 533 var targetGV schema.GroupVersion 534 if mediaType.Convert == nil || 535 (mediaType.Convert.GroupVersion() != apidiscoveryv2.SchemeGroupVersion && 536 mediaType.Convert.GroupVersion() != apidiscoveryv2beta1.SchemeGroupVersion) { 537 utilruntime.HandleError(fmt.Errorf("expected aggregated discovery group version, got group: %s, version %s", mediaType.Convert.Group, mediaType.Convert.Version)) 538 resp.WriteHeader(http.StatusInternalServerError) 539 return 540 } 541 targetGV = mediaType.Convert.GroupVersion() 542 543 if len(etag) > 0 { 544 // Use proper e-tag headers if one is available 545 ServeHTTPWithETag( 546 &response, 547 etag, 548 targetGV, 549 rdm.serializer, 550 resp, 551 req, 552 ) 553 } else { 554 // Default to normal response in rare case etag is 555 // not cached with the object for some reason. 556 responsewriters.WriteObjectNegotiated( 557 rdm.serializer, 558 DiscoveryEndpointRestrictions, 559 targetGV, 560 resp, 561 req, 562 http.StatusOK, 563 &response, 564 true, 565 ) 566 } 567 }