karmada/pkg/util/restmapper/restmapper.go

161 lines
5.2 KiB
Go

/*
Copyright 2020 The Karmada 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 restmapper
import (
"net/http"
"sync"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
)
// GetGroupVersionResource is a helper to map GVK(schema.GroupVersionKind) to GVR(schema.GroupVersionResource).
func GetGroupVersionResource(restMapper meta.RESTMapper, gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
restMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return schema.GroupVersionResource{}, err
}
return restMapping.Resource, nil
}
// cachedRESTMapper caches the previous result to accelerate subsequent queries.
// Note: now the acceleration applies only to RESTMapping() which is heavily used by Karmada.
type cachedRESTMapper struct {
restMapper meta.RESTMapper
discoveryClient discovery.DiscoveryInterface
gvkToGVR sync.Map
// mu is used to provide thread-safe mapper reloading.
mu sync.RWMutex
}
func (g *cachedRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
return g.getMapper().KindFor(resource)
}
func (g *cachedRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
return g.getMapper().KindsFor(resource)
}
func (g *cachedRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
return g.getMapper().ResourceFor(input)
}
func (g *cachedRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
return g.getMapper().ResourcesFor(input)
}
func (g *cachedRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
return g.getMapper().RESTMappings(gk, versions...)
}
func (g *cachedRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
return g.getMapper().ResourceSingularizer(resource)
}
func (g *cachedRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
// in case of multi-versions or no versions, cachedRESTMapper don't know which is the preferred version,
// so just bypass the cache and consult the underlying mapper.
if len(versions) != 1 {
return g.getMapper().RESTMapping(gk, versions...)
}
gvk := gk.WithVersion(versions[0])
value, ok := g.gvkToGVR.Load(gvk)
if ok { // hit cache, just return
return value.(*meta.RESTMapping), nil
}
// consult underlying mapper and then update cache
restMapping, err := g.getMapper().RESTMapping(gk, versions...)
if meta.IsNoMatchError(err) {
// hit here means a resource might be missing from the current rest mapper,
// probably because a new resource(CRD) has been added, we have to reload
// resource and rebuild the rest mapper.
var groupResources []*restmapper.APIGroupResources
groupResources, err = restmapper.GetAPIGroupResources(g.discoveryClient)
if err != nil {
return nil, err
}
newMapper := restmapper.NewDiscoveryRESTMapper(groupResources)
restMapping, err = newMapper.RESTMapping(gk, versions...)
if err == nil {
// hit here means after reloading, the new rest mapper can recognize
// the resource, we have to replace the mapper and clear cache.
g.mu.Lock()
g.restMapper = newMapper
g.mu.Unlock()
g.gvkToGVR.Range(func(key, _ any) bool {
g.gvkToGVR.Delete(key)
return true
})
}
}
if err != nil {
return restMapping, err
}
g.gvkToGVR.Store(gvk, restMapping)
return restMapping, nil
}
func (g *cachedRESTMapper) getMapper() meta.RESTMapper {
g.mu.RLock()
defer g.mu.RUnlock()
return g.restMapper
}
// NewCachedRESTMapper builds a cachedRESTMapper with a customized underlyingMapper.
// If underlyingMapper is nil, defaults to DiscoveryRESTMapper.
func NewCachedRESTMapper(cfg *rest.Config, underlyingMapper meta.RESTMapper) (meta.RESTMapper, error) {
cachedMapper := cachedRESTMapper{}
// short path, build with customized underlying mapper.
if underlyingMapper != nil {
cachedMapper.restMapper = underlyingMapper
return &cachedMapper, nil
}
client, err := discovery.NewDiscoveryClientForConfig(cfg)
if err != nil {
return nil, err
}
// loading current resources for building a base rest mapper.
groupResources, err := restmapper.GetAPIGroupResources(client)
if err != nil {
return nil, err
}
cachedMapper.restMapper = restmapper.NewDiscoveryRESTMapper(groupResources)
cachedMapper.discoveryClient = client
return &cachedMapper, nil
}
// MapperProvider is a wrapper of cachedRESTMapper which is typically used
// to generate customized RESTMapper for controller-runtime framework.
func MapperProvider(c *rest.Config, _ *http.Client) (meta.RESTMapper, error) {
return NewCachedRESTMapper(c, nil)
}