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"
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
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
|
2024-02-19 20:58:29 +00:00
|
|
|
client discovery.DiscoveryInterface
|
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
|
|
|
|
|
|
|
// mutex to provide thread-safe mapper reloading.
|
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
|
|
|
|
|
|
|
// 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.
|
|
|
|
if len(versions) == 0 {
|
2023-06-01 17:01:19 +00:00
|
|
|
apiGroup, err := m.findAPIGroupByName(groupName)
|
2023-02-06 20:04:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-02-01 13:08:54 +00:00
|
|
|
if apiGroup != nil {
|
|
|
|
for _, version := range apiGroup.Versions {
|
|
|
|
versions = append(versions, version.Version)
|
|
|
|
}
|
2023-02-06 20:04:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
m.mu.Lock()
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
2023-02-06 20:04:43 +00:00
|
|
|
// Create or fetch group resources from cache.
|
|
|
|
groupResources := &restmapper.APIGroupResources{
|
|
|
|
Group: metav1.APIGroup{Name: groupName},
|
|
|
|
VersionedResources: make(map[string][]metav1.APIResource),
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
if _, ok := m.knownGroups[groupName]; ok {
|
|
|
|
groupResources = m.knownGroups[groupName]
|
2023-02-06 20:04:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update information for group resources about the API group by adding new versions.
|
2023-04-18 08:08:00 +00:00
|
|
|
// Ignore the versions that are already registered.
|
2024-02-12 20:25:59 +00:00
|
|
|
for groupVersion, resources := range groupVersionResources {
|
|
|
|
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 {
|
|
|
|
groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{
|
|
|
|
GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(),
|
|
|
|
Version: version,
|
|
|
|
})
|
|
|
|
}
|
2023-02-06 20:04:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update data in the cache.
|
|
|
|
m.knownGroups[groupName] = groupResources
|
|
|
|
|
|
|
|
// Finally, update the group with received information and regenerate the mapper.
|
|
|
|
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
|
|
|
|
for _, agr := range m.knownGroups {
|
|
|
|
updatedGroupResources = append(updatedGroupResources, agr)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-18 08:08:00 +00:00
|
|
|
// findAPIGroupByNameLocked returns API group by its name.
|
2023-06-01 17:01:19 +00:00
|
|
|
func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error) {
|
2023-04-18 08:08:00 +00:00
|
|
|
// Looking in the cache first.
|
2023-06-01 17:01:19 +00:00
|
|
|
{
|
|
|
|
m.mu.RLock()
|
|
|
|
group, ok := m.apiGroups[groupName]
|
|
|
|
m.mu.RUnlock()
|
|
|
|
if ok {
|
|
|
|
return group, nil
|
2023-02-06 20:04:43 +00:00
|
|
|
}
|
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.
|
|
|
|
apiGroups, err := m.client.ServerGroups()
|
|
|
|
if err != nil {
|
2023-06-01 17:01:19 +00:00
|
|
|
return nil, 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 {
|
2023-06-01 17:01:19 +00:00
|
|
|
return nil, fmt.Errorf("received an empty API groups list")
|
2023-04-18 08:08:00 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 17:01:19 +00:00
|
|
|
m.mu.Lock()
|
|
|
|
for i := range apiGroups.Groups {
|
|
|
|
group := &apiGroups.Groups[i]
|
|
|
|
m.apiGroups[group.Name] = group
|
|
|
|
}
|
|
|
|
m.mu.Unlock()
|
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
|
|
|
m.mu.RLock()
|
|
|
|
defer m.mu.RUnlock()
|
2023-02-06 20:04:43 +00:00
|
|
|
|
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.
|
|
|
|
return m.apiGroups[groupName], 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.
|
2024-02-19 20:58:29 +00:00
|
|
|
if m.isAPIGroupCached(groupVersion) {
|
|
|
|
delete(m.apiGroups, groupName)
|
|
|
|
}
|
|
|
|
if m.isGroupVersionCached(groupVersion) {
|
|
|
|
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
|
|
|
|
|
|
|
// isGroupVersionCached checks if a version for a group is cached in the known groups cache.
|
|
|
|
func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool {
|
|
|
|
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
|
|
|
|
|
|
|
// isAPIGroupCached checks if a version for a group is cached in the api groups cache.
|
|
|
|
func (m *mapper) isAPIGroupCached(gv schema.GroupVersion) bool {
|
|
|
|
cachedGroup, ok := m.apiGroups[gv.Group]
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, version := range cachedGroup.Versions {
|
|
|
|
if version.Version == gv.Version {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|