...

Source file src/sigs.k8s.io/controller-runtime/pkg/client/apiutil/restmapper.go

Documentation: sigs.k8s.io/controller-runtime/pkg/client/apiutil

     1  /*
     2  Copyright 2023 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 apiutil
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"sync"
    23  
    24  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/client-go/discovery"
    29  	"k8s.io/client-go/rest"
    30  	"k8s.io/client-go/restmapper"
    31  )
    32  
    33  // NewDynamicRESTMapper returns a dynamic RESTMapper for cfg. The dynamic
    34  // RESTMapper dynamically discovers resource types at runtime.
    35  func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) {
    36  	if httpClient == nil {
    37  		return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
    38  	}
    39  
    40  	client, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, httpClient)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	return &mapper{
    45  		mapper:      restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
    46  		client:      client,
    47  		knownGroups: map[string]*restmapper.APIGroupResources{},
    48  		apiGroups:   map[string]*metav1.APIGroup{},
    49  	}, nil
    50  }
    51  
    52  // mapper is a RESTMapper that will lazily query the provided
    53  // client for discovery information to do REST mappings.
    54  type mapper struct {
    55  	mapper      meta.RESTMapper
    56  	client      discovery.DiscoveryInterface
    57  	knownGroups map[string]*restmapper.APIGroupResources
    58  	apiGroups   map[string]*metav1.APIGroup
    59  
    60  	// mutex to provide thread-safe mapper reloading.
    61  	mu sync.RWMutex
    62  }
    63  
    64  // KindFor implements Mapper.KindFor.
    65  func (m *mapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
    66  	res, err := m.getMapper().KindFor(resource)
    67  	if meta.IsNoMatchError(err) {
    68  		if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
    69  			return schema.GroupVersionKind{}, err
    70  		}
    71  		res, err = m.getMapper().KindFor(resource)
    72  	}
    73  
    74  	return res, err
    75  }
    76  
    77  // KindsFor implements Mapper.KindsFor.
    78  func (m *mapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
    79  	res, err := m.getMapper().KindsFor(resource)
    80  	if meta.IsNoMatchError(err) {
    81  		if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
    82  			return nil, err
    83  		}
    84  		res, err = m.getMapper().KindsFor(resource)
    85  	}
    86  
    87  	return res, err
    88  }
    89  
    90  // ResourceFor implements Mapper.ResourceFor.
    91  func (m *mapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
    92  	res, err := m.getMapper().ResourceFor(input)
    93  	if meta.IsNoMatchError(err) {
    94  		if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
    95  			return schema.GroupVersionResource{}, err
    96  		}
    97  		res, err = m.getMapper().ResourceFor(input)
    98  	}
    99  
   100  	return res, err
   101  }
   102  
   103  // ResourcesFor implements Mapper.ResourcesFor.
   104  func (m *mapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
   105  	res, err := m.getMapper().ResourcesFor(input)
   106  	if meta.IsNoMatchError(err) {
   107  		if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
   108  			return nil, err
   109  		}
   110  		res, err = m.getMapper().ResourcesFor(input)
   111  	}
   112  
   113  	return res, err
   114  }
   115  
   116  // RESTMapping implements Mapper.RESTMapping.
   117  func (m *mapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
   118  	res, err := m.getMapper().RESTMapping(gk, versions...)
   119  	if meta.IsNoMatchError(err) {
   120  		if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
   121  			return nil, err
   122  		}
   123  		res, err = m.getMapper().RESTMapping(gk, versions...)
   124  	}
   125  
   126  	return res, err
   127  }
   128  
   129  // RESTMappings implements Mapper.RESTMappings.
   130  func (m *mapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
   131  	res, err := m.getMapper().RESTMappings(gk, versions...)
   132  	if meta.IsNoMatchError(err) {
   133  		if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
   134  			return nil, err
   135  		}
   136  		res, err = m.getMapper().RESTMappings(gk, versions...)
   137  	}
   138  
   139  	return res, err
   140  }
   141  
   142  // ResourceSingularizer implements Mapper.ResourceSingularizer.
   143  func (m *mapper) ResourceSingularizer(resource string) (string, error) {
   144  	return m.getMapper().ResourceSingularizer(resource)
   145  }
   146  
   147  func (m *mapper) getMapper() meta.RESTMapper {
   148  	m.mu.RLock()
   149  	defer m.mu.RUnlock()
   150  	return m.mapper
   151  }
   152  
   153  // addKnownGroupAndReload reloads the mapper with updated information about missing API group.
   154  // versions can be specified for partial updates, for instance for v1beta1 version only.
   155  func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) error {
   156  	// versions will here be [""] if the forwarded Version value of
   157  	// GroupVersionResource (in calling method) was not specified.
   158  	if len(versions) == 1 && versions[0] == "" {
   159  		versions = nil
   160  	}
   161  
   162  	// If no specific versions are set by user, we will scan all available ones for the API group.
   163  	// This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
   164  	// this data will be taken from cache.
   165  	if len(versions) == 0 {
   166  		apiGroup, err := m.findAPIGroupByName(groupName)
   167  		if err != nil {
   168  			return err
   169  		}
   170  		if apiGroup != nil {
   171  			for _, version := range apiGroup.Versions {
   172  				versions = append(versions, version.Version)
   173  			}
   174  		}
   175  	}
   176  
   177  	m.mu.Lock()
   178  	defer m.mu.Unlock()
   179  
   180  	// Create or fetch group resources from cache.
   181  	groupResources := &restmapper.APIGroupResources{
   182  		Group:              metav1.APIGroup{Name: groupName},
   183  		VersionedResources: make(map[string][]metav1.APIResource),
   184  	}
   185  
   186  	// Update information for group resources about versioned resources.
   187  	// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
   188  	// If we encounter a missing API version (NotFound error), we will remove the group from
   189  	// the m.apiGroups and m.knownGroups caches.
   190  	// If this happens, in the next call the group will be added back to apiGroups
   191  	// and only the existing versions will be loaded in knownGroups.
   192  	groupVersionResources, err := m.fetchGroupVersionResourcesLocked(groupName, versions...)
   193  	if err != nil {
   194  		return fmt.Errorf("failed to get API group resources: %w", err)
   195  	}
   196  
   197  	if _, ok := m.knownGroups[groupName]; ok {
   198  		groupResources = m.knownGroups[groupName]
   199  	}
   200  
   201  	// Update information for group resources about the API group by adding new versions.
   202  	// Ignore the versions that are already registered.
   203  	for groupVersion, resources := range groupVersionResources {
   204  		version := groupVersion.Version
   205  
   206  		groupResources.VersionedResources[version] = resources.APIResources
   207  		found := false
   208  		for _, v := range groupResources.Group.Versions {
   209  			if v.Version == version {
   210  				found = true
   211  				break
   212  			}
   213  		}
   214  
   215  		if !found {
   216  			groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{
   217  				GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(),
   218  				Version:      version,
   219  			})
   220  		}
   221  	}
   222  
   223  	// Update data in the cache.
   224  	m.knownGroups[groupName] = groupResources
   225  
   226  	// Finally, update the group with received information and regenerate the mapper.
   227  	updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
   228  	for _, agr := range m.knownGroups {
   229  		updatedGroupResources = append(updatedGroupResources, agr)
   230  	}
   231  
   232  	m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
   233  	return nil
   234  }
   235  
   236  // findAPIGroupByNameLocked returns API group by its name.
   237  func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error) {
   238  	// Looking in the cache first.
   239  	{
   240  		m.mu.RLock()
   241  		group, ok := m.apiGroups[groupName]
   242  		m.mu.RUnlock()
   243  		if ok {
   244  			return group, nil
   245  		}
   246  	}
   247  
   248  	// Update the cache if nothing was found.
   249  	apiGroups, err := m.client.ServerGroups()
   250  	if err != nil {
   251  		return nil, fmt.Errorf("failed to get server groups: %w", err)
   252  	}
   253  	if len(apiGroups.Groups) == 0 {
   254  		return nil, fmt.Errorf("received an empty API groups list")
   255  	}
   256  
   257  	m.mu.Lock()
   258  	for i := range apiGroups.Groups {
   259  		group := &apiGroups.Groups[i]
   260  		m.apiGroups[group.Name] = group
   261  	}
   262  	m.mu.Unlock()
   263  
   264  	// Looking in the cache again.
   265  	m.mu.RLock()
   266  	defer m.mu.RUnlock()
   267  
   268  	// Don't return an error here if the API group is not present.
   269  	// The reloaded RESTMapper will take care of returning a NoMatchError.
   270  	return m.apiGroups[groupName], nil
   271  }
   272  
   273  // fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions.
   274  // This method might modify the cache so it needs to be called under the lock.
   275  func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
   276  	groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
   277  	failedGroups := make(map[schema.GroupVersion]error)
   278  
   279  	for _, version := range versions {
   280  		groupVersion := schema.GroupVersion{Group: groupName, Version: version}
   281  
   282  		apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
   283  		if apierrors.IsNotFound(err) {
   284  			// If the version is not found, we remove the group from the cache
   285  			// so it gets refreshed on the next call.
   286  			if m.isAPIGroupCached(groupVersion) {
   287  				delete(m.apiGroups, groupName)
   288  			}
   289  			if m.isGroupVersionCached(groupVersion) {
   290  				delete(m.knownGroups, groupName)
   291  			}
   292  			continue
   293  		} else if err != nil {
   294  			failedGroups[groupVersion] = err
   295  		}
   296  
   297  		if apiResourceList != nil {
   298  			// even in case of error, some fallback might have been returned.
   299  			groupVersionResources[groupVersion] = apiResourceList
   300  		}
   301  	}
   302  
   303  	if len(failedGroups) > 0 {
   304  		err := ErrResourceDiscoveryFailed(failedGroups)
   305  		return nil, &err
   306  	}
   307  
   308  	return groupVersionResources, nil
   309  }
   310  
   311  // isGroupVersionCached checks if a version for a group is cached in the known groups cache.
   312  func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool {
   313  	if cachedGroup, ok := m.knownGroups[gv.Group]; ok {
   314  		_, cached := cachedGroup.VersionedResources[gv.Version]
   315  		return cached
   316  	}
   317  
   318  	return false
   319  }
   320  
   321  // isAPIGroupCached checks if a version for a group is cached in the api groups cache.
   322  func (m *mapper) isAPIGroupCached(gv schema.GroupVersion) bool {
   323  	cachedGroup, ok := m.apiGroups[gv.Group]
   324  	if !ok {
   325  		return false
   326  	}
   327  
   328  	for _, version := range cachedGroup.Versions {
   329  		if version.Version == gv.Version {
   330  			return true
   331  		}
   332  	}
   333  
   334  	return false
   335  }
   336  

View as plain text