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  }