sigs.k8s.io/controller-runtime@v0.18.2/pkg/client/apiutil/restmapper.go (about) 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 }