2023-02-06 20:04:43 +00:00
/ *
Copyright 2023 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 apiutil
import (
"fmt"
2023-06-01 17:01:19 +00:00
"net/http"
2023-02-06 20:04:43 +00:00
"sync"
2024-02-01 13:08:54 +00:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2023-02-06 20:04:43 +00:00
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
2023-06-01 17:01:19 +00:00
"k8s.io/client-go/rest"
2023-02-06 20:04:43 +00:00
"k8s.io/client-go/restmapper"
2025-02-03 07:59:16 +00:00
"k8s.io/utils/ptr"
2023-02-06 20:04:43 +00:00
)
2023-06-01 17:01:19 +00:00
// NewDynamicRESTMapper returns a dynamic RESTMapper for cfg. The dynamic
// RESTMapper dynamically discovers resource types at runtime.
func NewDynamicRESTMapper ( cfg * rest . Config , httpClient * http . Client ) ( meta . RESTMapper , error ) {
if httpClient == nil {
return nil , fmt . Errorf ( "httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client" )
}
client , err := discovery . NewDiscoveryClientForConfigAndClient ( cfg , httpClient )
if err != nil {
return nil , err
}
2025-02-03 07:59:16 +00:00
2023-06-01 17:01:19 +00:00
return & mapper {
mapper : restmapper . NewDiscoveryRESTMapper ( [ ] * restmapper . APIGroupResources { } ) ,
client : client ,
knownGroups : map [ string ] * restmapper . APIGroupResources { } ,
apiGroups : map [ string ] * metav1 . APIGroup { } ,
} , nil
}
// mapper is a RESTMapper that will lazily query the provided
2023-02-06 20:04:43 +00:00
// client for discovery information to do REST mappings.
2023-06-01 17:01:19 +00:00
type mapper struct {
2023-02-06 20:04:43 +00:00
mapper meta . RESTMapper
2025-02-03 07:59:16 +00:00
client discovery . AggregatedDiscoveryInterface
2023-02-06 20:04:43 +00:00
knownGroups map [ string ] * restmapper . APIGroupResources
2023-06-01 17:01:19 +00:00
apiGroups map [ string ] * metav1 . APIGroup
2023-02-06 20:04:43 +00:00
2025-02-03 07:59:16 +00:00
initialDiscoveryDone bool
2023-02-06 20:04:43 +00:00
// mutex to provide thread-safe mapper reloading.
2025-02-03 07:59:16 +00:00
// It protects all fields in the mapper as well as methods
// that have the `Locked` suffix.
2023-06-01 17:01:19 +00:00
mu sync . RWMutex
2023-02-06 20:04:43 +00:00
}
// KindFor implements Mapper.KindFor.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) KindFor ( resource schema . GroupVersionResource ) ( schema . GroupVersionKind , error ) {
res , err := m . getMapper ( ) . KindFor ( resource )
2023-02-06 20:04:43 +00:00
if meta . IsNoMatchError ( err ) {
2023-06-01 17:01:19 +00:00
if err := m . addKnownGroupAndReload ( resource . Group , resource . Version ) ; err != nil {
return schema . GroupVersionKind { } , err
2023-02-06 20:04:43 +00:00
}
2023-06-01 17:01:19 +00:00
res , err = m . getMapper ( ) . KindFor ( resource )
2023-02-06 20:04:43 +00:00
}
return res , err
}
// KindsFor implements Mapper.KindsFor.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) KindsFor ( resource schema . GroupVersionResource ) ( [ ] schema . GroupVersionKind , error ) {
res , err := m . getMapper ( ) . KindsFor ( resource )
2023-02-06 20:04:43 +00:00
if meta . IsNoMatchError ( err ) {
2023-06-01 17:01:19 +00:00
if err := m . addKnownGroupAndReload ( resource . Group , resource . Version ) ; err != nil {
return nil , err
2023-02-06 20:04:43 +00:00
}
2023-06-01 17:01:19 +00:00
res , err = m . getMapper ( ) . KindsFor ( resource )
2023-02-06 20:04:43 +00:00
}
return res , err
}
// ResourceFor implements Mapper.ResourceFor.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) ResourceFor ( input schema . GroupVersionResource ) ( schema . GroupVersionResource , error ) {
res , err := m . getMapper ( ) . ResourceFor ( input )
2023-02-06 20:04:43 +00:00
if meta . IsNoMatchError ( err ) {
2023-06-01 17:01:19 +00:00
if err := m . addKnownGroupAndReload ( input . Group , input . Version ) ; err != nil {
return schema . GroupVersionResource { } , err
2023-02-06 20:04:43 +00:00
}
2023-06-01 17:01:19 +00:00
res , err = m . getMapper ( ) . ResourceFor ( input )
2023-02-06 20:04:43 +00:00
}
return res , err
}
// ResourcesFor implements Mapper.ResourcesFor.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) ResourcesFor ( input schema . GroupVersionResource ) ( [ ] schema . GroupVersionResource , error ) {
res , err := m . getMapper ( ) . ResourcesFor ( input )
2023-02-06 20:04:43 +00:00
if meta . IsNoMatchError ( err ) {
2023-06-01 17:01:19 +00:00
if err := m . addKnownGroupAndReload ( input . Group , input . Version ) ; err != nil {
return nil , err
2023-02-06 20:04:43 +00:00
}
2023-06-01 17:01:19 +00:00
res , err = m . getMapper ( ) . ResourcesFor ( input )
2023-02-06 20:04:43 +00:00
}
return res , err
}
// RESTMapping implements Mapper.RESTMapping.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) RESTMapping ( gk schema . GroupKind , versions ... string ) ( * meta . RESTMapping , error ) {
res , err := m . getMapper ( ) . RESTMapping ( gk , versions ... )
2023-02-06 20:04:43 +00:00
if meta . IsNoMatchError ( err ) {
2023-06-01 17:01:19 +00:00
if err := m . addKnownGroupAndReload ( gk . Group , versions ... ) ; err != nil {
return nil , err
2023-02-06 20:04:43 +00:00
}
2023-06-01 17:01:19 +00:00
res , err = m . getMapper ( ) . RESTMapping ( gk , versions ... )
2023-02-06 20:04:43 +00:00
}
return res , err
}
// RESTMappings implements Mapper.RESTMappings.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) RESTMappings ( gk schema . GroupKind , versions ... string ) ( [ ] * meta . RESTMapping , error ) {
res , err := m . getMapper ( ) . RESTMappings ( gk , versions ... )
2023-02-06 20:04:43 +00:00
if meta . IsNoMatchError ( err ) {
2023-06-01 17:01:19 +00:00
if err := m . addKnownGroupAndReload ( gk . Group , versions ... ) ; err != nil {
return nil , err
2023-02-06 20:04:43 +00:00
}
2023-06-01 17:01:19 +00:00
res , err = m . getMapper ( ) . RESTMappings ( gk , versions ... )
2023-02-06 20:04:43 +00:00
}
return res , err
}
// ResourceSingularizer implements Mapper.ResourceSingularizer.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) ResourceSingularizer ( resource string ) ( string , error ) {
return m . getMapper ( ) . ResourceSingularizer ( resource )
}
func ( m * mapper ) getMapper ( ) meta . RESTMapper {
m . mu . RLock ( )
defer m . mu . RUnlock ( )
return m . mapper
2023-02-06 20:04:43 +00:00
}
// addKnownGroupAndReload reloads the mapper with updated information about missing API group.
// versions can be specified for partial updates, for instance for v1beta1 version only.
2023-06-01 17:01:19 +00:00
func ( m * mapper ) addKnownGroupAndReload ( groupName string , versions ... string ) error {
// versions will here be [""] if the forwarded Version value of
// GroupVersionResource (in calling method) was not specified.
if len ( versions ) == 1 && versions [ 0 ] == "" {
versions = nil
}
2023-02-06 20:04:43 +00:00
2025-02-03 07:59:16 +00:00
m . mu . Lock ( )
defer m . mu . Unlock ( )
2023-02-06 20:04:43 +00:00
// If no specific versions are set by user, we will scan all available ones for the API group.
// This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
// this data will be taken from cache.
2025-02-03 07:59:16 +00:00
//
// We always run this once, because if the server supports aggregated discovery, this will
// load everything with two api calls which we assume is overall cheaper.
if len ( versions ) == 0 || ! m . initialDiscoveryDone {
apiGroup , didAggregatedDiscovery , err := m . findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked ( groupName )
2023-02-06 20:04:43 +00:00
if err != nil {
return err
}
2025-02-03 07:59:16 +00:00
if apiGroup != nil && len ( versions ) == 0 {
2024-02-01 13:08:54 +00:00
for _ , version := range apiGroup . Versions {
versions = append ( versions , version . Version )
}
2023-02-06 20:04:43 +00:00
}
2025-02-03 07:59:16 +00:00
// No need to do anything further if aggregatedDiscovery is supported and we did a lookup
if didAggregatedDiscovery {
failedGroups := make ( map [ schema . GroupVersion ] error )
for _ , version := range versions {
if m . knownGroups [ groupName ] == nil || m . knownGroups [ groupName ] . VersionedResources [ version ] == nil {
failedGroups [ schema . GroupVersion { Group : groupName , Version : version } ] = & meta . NoResourceMatchError {
PartialResource : schema . GroupVersionResource {
Group : groupName ,
Version : version ,
} }
}
}
if len ( failedGroups ) > 0 {
return ptr . To ( ErrResourceDiscoveryFailed ( failedGroups ) )
}
return nil
}
2023-02-06 20:04:43 +00:00
}
// Update information for group resources about versioned resources.
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
2024-02-12 20:25:59 +00:00
// If we encounter a missing API version (NotFound error), we will remove the group from
// the m.apiGroups and m.knownGroups caches.
// If this happens, in the next call the group will be added back to apiGroups
// and only the existing versions will be loaded in knownGroups.
groupVersionResources , err := m . fetchGroupVersionResourcesLocked ( groupName , versions ... )
2023-02-06 20:04:43 +00:00
if err != nil {
return fmt . Errorf ( "failed to get API group resources: %w" , err )
}
2024-02-12 20:25:59 +00:00
2025-02-03 07:59:16 +00:00
m . addGroupVersionResourcesToCacheAndReloadLocked ( groupVersionResources )
return nil
}
2023-02-06 20:04:43 +00:00
2025-02-03 07:59:16 +00:00
// addGroupVersionResourcesToCacheAndReloadLocked does what the name suggests. The mutex must be held when
// calling it.
func ( m * mapper ) addGroupVersionResourcesToCacheAndReloadLocked ( gvr map [ schema . GroupVersion ] * metav1 . APIResourceList ) {
2023-02-06 20:04:43 +00:00
// Update information for group resources about the API group by adding new versions.
2025-02-03 07:59:16 +00:00
// Ignore the versions that are already registered
for groupVersion , resources := range gvr {
var groupResources * restmapper . APIGroupResources
if _ , ok := m . knownGroups [ groupVersion . Group ] ; ok {
groupResources = m . knownGroups [ groupVersion . Group ]
} else {
groupResources = & restmapper . APIGroupResources {
Group : metav1 . APIGroup { Name : groupVersion . Group } ,
VersionedResources : make ( map [ string ] [ ] metav1 . APIResource ) ,
}
}
2024-02-12 20:25:59 +00:00
version := groupVersion . Version
groupResources . VersionedResources [ version ] = resources . APIResources
2023-04-18 08:08:00 +00:00
found := false
for _ , v := range groupResources . Group . Versions {
if v . Version == version {
found = true
break
}
}
if ! found {
2025-03-24 21:06:14 +00:00
gv := metav1 . GroupVersionForDiscovery {
2025-02-03 07:59:16 +00:00
GroupVersion : metav1 . GroupVersion { Group : groupVersion . Group , Version : version } . String ( ) ,
2023-04-18 08:08:00 +00:00
Version : version ,
2025-03-24 21:06:14 +00:00
}
// Prepend if preferred version, else append. The upstream DiscoveryRestMappper assumes
// the first version is the preferred one: https://github.com/kubernetes/kubernetes/blob/ef54ac803b712137871c1a1f8d635d50e69ffa6c/staging/src/k8s.io/apimachinery/pkg/api/meta/restmapper.go#L458-L461
if group , ok := m . apiGroups [ groupVersion . Group ] ; ok && group . PreferredVersion . Version == version {
groupResources . Group . Versions = append ( [ ] metav1 . GroupVersionForDiscovery { gv } , groupResources . Group . Versions ... )
} else {
groupResources . Group . Versions = append ( groupResources . Group . Versions , gv )
}
2023-04-18 08:08:00 +00:00
}
2023-02-06 20:04:43 +00:00
2025-02-03 07:59:16 +00:00
// Update data in the cache.
m . knownGroups [ groupVersion . Group ] = groupResources
}
2023-02-06 20:04:43 +00:00
2025-02-03 07:59:16 +00:00
// Finally, reload the mapper.
2023-02-06 20:04:43 +00:00
updatedGroupResources := make ( [ ] * restmapper . APIGroupResources , 0 , len ( m . knownGroups ) )
for _ , agr := range m . knownGroups {
updatedGroupResources = append ( updatedGroupResources , agr )
}
m . mapper = restmapper . NewDiscoveryRESTMapper ( updatedGroupResources )
}
2025-02-03 07:59:16 +00:00
// findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked tries to find the passed apiGroup.
// If the server supports aggregated discovery, it will always perform that.
func ( m * mapper ) findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked ( groupName string ) ( _ * metav1 . APIGroup , didAggregatedDiscovery bool , _ error ) {
// Looking in the cache first
group , ok := m . apiGroups [ groupName ]
if ok {
return group , false , nil
2023-04-18 08:08:00 +00:00
}
2023-02-06 20:04:43 +00:00
2023-04-18 08:08:00 +00:00
// Update the cache if nothing was found.
2025-02-03 07:59:16 +00:00
apiGroups , maybeResources , _ , err := m . client . GroupsAndMaybeResources ( )
2023-04-18 08:08:00 +00:00
if err != nil {
2025-02-03 07:59:16 +00:00
return nil , false , fmt . Errorf ( "failed to get server groups: %w" , err )
2023-02-06 20:04:43 +00:00
}
2023-04-18 08:08:00 +00:00
if len ( apiGroups . Groups ) == 0 {
2025-02-03 07:59:16 +00:00
return nil , false , fmt . Errorf ( "received an empty API groups list" )
2023-04-18 08:08:00 +00:00
}
2025-02-03 07:59:16 +00:00
m . initialDiscoveryDone = true
2023-06-01 17:01:19 +00:00
for i := range apiGroups . Groups {
group := & apiGroups . Groups [ i ]
m . apiGroups [ group . Name ] = group
}
2025-03-24 21:06:14 +00:00
if len ( maybeResources ) > 0 {
didAggregatedDiscovery = true
m . addGroupVersionResourcesToCacheAndReloadLocked ( maybeResources )
}
2023-02-06 20:04:43 +00:00
2023-04-18 08:08:00 +00:00
// Looking in the cache again.
2024-02-01 13:08:54 +00:00
// Don't return an error here if the API group is not present.
// The reloaded RESTMapper will take care of returning a NoMatchError.
2025-02-03 07:59:16 +00:00
return m . apiGroups [ groupName ] , didAggregatedDiscovery , nil
2023-02-06 20:04:43 +00:00
}
2024-02-12 20:25:59 +00:00
// fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions.
// This method might modify the cache so it needs to be called under the lock.
func ( m * mapper ) fetchGroupVersionResourcesLocked ( groupName string , versions ... string ) ( map [ schema . GroupVersion ] * metav1 . APIResourceList , error ) {
2023-02-06 20:04:43 +00:00
groupVersionResources := make ( map [ schema . GroupVersion ] * metav1 . APIResourceList )
failedGroups := make ( map [ schema . GroupVersion ] error )
for _ , version := range versions {
groupVersion := schema . GroupVersion { Group : groupName , Version : version }
apiResourceList , err := m . client . ServerResourcesForGroupVersion ( groupVersion . String ( ) )
2024-02-19 20:58:29 +00:00
if apierrors . IsNotFound ( err ) {
2024-02-12 20:25:59 +00:00
// If the version is not found, we remove the group from the cache
// so it gets refreshed on the next call.
2025-02-03 07:59:16 +00:00
if m . isAPIGroupCachedLocked ( groupVersion ) {
2024-02-19 20:58:29 +00:00
delete ( m . apiGroups , groupName )
}
2025-02-03 07:59:16 +00:00
if m . isGroupVersionCachedLocked ( groupVersion ) {
2024-02-19 20:58:29 +00:00
delete ( m . knownGroups , groupName )
}
2024-02-12 20:25:59 +00:00
continue
} else if err != nil {
2023-02-06 20:04:43 +00:00
failedGroups [ groupVersion ] = err
}
2024-02-12 20:25:59 +00:00
2023-02-06 20:04:43 +00:00
if apiResourceList != nil {
// even in case of error, some fallback might have been returned.
groupVersionResources [ groupVersion ] = apiResourceList
}
}
if len ( failedGroups ) > 0 {
2023-09-18 12:45:40 +00:00
err := ErrResourceDiscoveryFailed ( failedGroups )
return nil , & err
2023-02-06 20:04:43 +00:00
}
return groupVersionResources , nil
}
2024-02-12 20:25:59 +00:00
2025-02-03 07:59:16 +00:00
// isGroupVersionCachedLocked checks if a version for a group is cached in the known groups cache.
func ( m * mapper ) isGroupVersionCachedLocked ( gv schema . GroupVersion ) bool {
2024-02-12 20:25:59 +00:00
if cachedGroup , ok := m . knownGroups [ gv . Group ] ; ok {
_ , cached := cachedGroup . VersionedResources [ gv . Version ]
return cached
}
return false
}
2024-02-19 20:58:29 +00:00
2025-02-03 07:59:16 +00:00
// isAPIGroupCachedLocked checks if a version for a group is cached in the api groups cache.
func ( m * mapper ) isAPIGroupCachedLocked ( gv schema . GroupVersion ) bool {
2024-02-19 20:58:29 +00:00
cachedGroup , ok := m . apiGroups [ gv . Group ]
if ! ok {
return false
}
for _ , version := range cachedGroup . Versions {
if version . Version == gv . Version {
return true
}
}
return false
}