/* 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) }