k8s.io/client-go@v0.31.1/restmapper/discovery.go (about) 1 /* 2 Copyright 2016 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 restmapper 18 19 import ( 20 "fmt" 21 "strings" 22 "sync" 23 24 "k8s.io/apimachinery/pkg/api/meta" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime/schema" 27 "k8s.io/client-go/discovery" 28 29 "k8s.io/klog/v2" 30 ) 31 32 // APIGroupResources is an API group with a mapping of versions to 33 // resources. 34 type APIGroupResources struct { 35 Group metav1.APIGroup 36 // A mapping of version string to a slice of APIResources for 37 // that version. 38 VersionedResources map[string][]metav1.APIResource 39 } 40 41 // NewDiscoveryRESTMapper returns a PriorityRESTMapper based on the discovered 42 // groups and resources passed in. 43 func NewDiscoveryRESTMapper(groupResources []*APIGroupResources) meta.RESTMapper { 44 unionMapper := meta.MultiRESTMapper{} 45 46 var groupPriority []string 47 // /v1 is special. It should always come first 48 resourcePriority := []schema.GroupVersionResource{{Group: "", Version: "v1", Resource: meta.AnyResource}} 49 kindPriority := []schema.GroupVersionKind{{Group: "", Version: "v1", Kind: meta.AnyKind}} 50 51 for _, group := range groupResources { 52 groupPriority = append(groupPriority, group.Group.Name) 53 54 // Make sure the preferred version comes first 55 if len(group.Group.PreferredVersion.Version) != 0 { 56 preferred := group.Group.PreferredVersion.Version 57 if _, ok := group.VersionedResources[preferred]; ok { 58 resourcePriority = append(resourcePriority, schema.GroupVersionResource{ 59 Group: group.Group.Name, 60 Version: group.Group.PreferredVersion.Version, 61 Resource: meta.AnyResource, 62 }) 63 64 kindPriority = append(kindPriority, schema.GroupVersionKind{ 65 Group: group.Group.Name, 66 Version: group.Group.PreferredVersion.Version, 67 Kind: meta.AnyKind, 68 }) 69 } 70 } 71 72 for _, discoveryVersion := range group.Group.Versions { 73 resources, ok := group.VersionedResources[discoveryVersion.Version] 74 if !ok { 75 continue 76 } 77 78 // Add non-preferred versions after the preferred version, in case there are resources that only exist in those versions 79 if discoveryVersion.Version != group.Group.PreferredVersion.Version { 80 resourcePriority = append(resourcePriority, schema.GroupVersionResource{ 81 Group: group.Group.Name, 82 Version: discoveryVersion.Version, 83 Resource: meta.AnyResource, 84 }) 85 86 kindPriority = append(kindPriority, schema.GroupVersionKind{ 87 Group: group.Group.Name, 88 Version: discoveryVersion.Version, 89 Kind: meta.AnyKind, 90 }) 91 } 92 93 gv := schema.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version} 94 versionMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{gv}) 95 96 for _, resource := range resources { 97 scope := meta.RESTScopeNamespace 98 if !resource.Namespaced { 99 scope = meta.RESTScopeRoot 100 } 101 102 // if we have a slash, then this is a subresource and we shouldn't create mappings for those. 103 if strings.Contains(resource.Name, "/") { 104 continue 105 } 106 107 plural := gv.WithResource(resource.Name) 108 singular := gv.WithResource(resource.SingularName) 109 // this is for legacy resources and servers which don't list singular forms. For those we must still guess. 110 if len(resource.SingularName) == 0 { 111 _, singular = meta.UnsafeGuessKindToResource(gv.WithKind(resource.Kind)) 112 } 113 114 versionMapper.AddSpecific(gv.WithKind(strings.ToLower(resource.Kind)), plural, singular, scope) 115 versionMapper.AddSpecific(gv.WithKind(resource.Kind), plural, singular, scope) 116 // TODO this is producing unsafe guesses that don't actually work, but it matches previous behavior 117 versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope) 118 } 119 // TODO why is this type not in discovery (at least for "v1") 120 versionMapper.Add(gv.WithKind("List"), meta.RESTScopeRoot) 121 unionMapper = append(unionMapper, versionMapper) 122 } 123 } 124 125 for _, group := range groupPriority { 126 resourcePriority = append(resourcePriority, schema.GroupVersionResource{ 127 Group: group, 128 Version: meta.AnyVersion, 129 Resource: meta.AnyResource, 130 }) 131 kindPriority = append(kindPriority, schema.GroupVersionKind{ 132 Group: group, 133 Version: meta.AnyVersion, 134 Kind: meta.AnyKind, 135 }) 136 } 137 138 return meta.PriorityRESTMapper{ 139 Delegate: unionMapper, 140 ResourcePriority: resourcePriority, 141 KindPriority: kindPriority, 142 } 143 } 144 145 // GetAPIGroupResources uses the provided discovery client to gather 146 // discovery information and populate a slice of APIGroupResources. 147 func GetAPIGroupResources(cl discovery.DiscoveryInterface) ([]*APIGroupResources, error) { 148 gs, rs, err := cl.ServerGroupsAndResources() 149 if rs == nil || gs == nil { 150 return nil, err 151 // TODO track the errors and update callers to handle partial errors. 152 } 153 rsm := map[string]*metav1.APIResourceList{} 154 for _, r := range rs { 155 rsm[r.GroupVersion] = r 156 } 157 158 var result []*APIGroupResources 159 for _, group := range gs { 160 groupResources := &APIGroupResources{ 161 Group: *group, 162 VersionedResources: make(map[string][]metav1.APIResource), 163 } 164 for _, version := range group.Versions { 165 resources, ok := rsm[version.GroupVersion] 166 if !ok { 167 continue 168 } 169 groupResources.VersionedResources[version.Version] = resources.APIResources 170 } 171 result = append(result, groupResources) 172 } 173 return result, nil 174 } 175 176 // DeferredDiscoveryRESTMapper is a RESTMapper that will defer 177 // initialization of the RESTMapper until the first mapping is 178 // requested. 179 type DeferredDiscoveryRESTMapper struct { 180 initMu sync.Mutex 181 delegate meta.RESTMapper 182 cl discovery.CachedDiscoveryInterface 183 } 184 185 // NewDeferredDiscoveryRESTMapper returns a 186 // DeferredDiscoveryRESTMapper that will lazily query the provided 187 // client for discovery information to do REST mappings. 188 func NewDeferredDiscoveryRESTMapper(cl discovery.CachedDiscoveryInterface) *DeferredDiscoveryRESTMapper { 189 return &DeferredDiscoveryRESTMapper{ 190 cl: cl, 191 } 192 } 193 194 func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) { 195 d.initMu.Lock() 196 defer d.initMu.Unlock() 197 198 if d.delegate != nil { 199 return d.delegate, nil 200 } 201 202 groupResources, err := GetAPIGroupResources(d.cl) 203 if err != nil { 204 return nil, err 205 } 206 207 d.delegate = NewDiscoveryRESTMapper(groupResources) 208 return d.delegate, nil 209 } 210 211 // Reset resets the internally cached Discovery information and will 212 // cause the next mapping request to re-discover. 213 func (d *DeferredDiscoveryRESTMapper) Reset() { 214 klog.V(5).Info("Invalidating discovery information") 215 216 d.initMu.Lock() 217 defer d.initMu.Unlock() 218 219 d.cl.Invalidate() 220 d.delegate = nil 221 } 222 223 // KindFor takes a partial resource and returns back the single match. 224 // It returns an error if there are multiple matches. 225 func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) { 226 del, err := d.getDelegate() 227 if err != nil { 228 return schema.GroupVersionKind{}, err 229 } 230 gvk, err = del.KindFor(resource) 231 if err != nil && !d.cl.Fresh() { 232 d.Reset() 233 gvk, err = d.KindFor(resource) 234 } 235 return 236 } 237 238 // KindsFor takes a partial resource and returns back the list of 239 // potential kinds in priority order. 240 func (d *DeferredDiscoveryRESTMapper) KindsFor(resource schema.GroupVersionResource) (gvks []schema.GroupVersionKind, err error) { 241 del, err := d.getDelegate() 242 if err != nil { 243 return nil, err 244 } 245 gvks, err = del.KindsFor(resource) 246 if len(gvks) == 0 && !d.cl.Fresh() { 247 d.Reset() 248 gvks, err = d.KindsFor(resource) 249 } 250 return 251 } 252 253 // ResourceFor takes a partial resource and returns back the single 254 // match. It returns an error if there are multiple matches. 255 func (d *DeferredDiscoveryRESTMapper) ResourceFor(input schema.GroupVersionResource) (gvr schema.GroupVersionResource, err error) { 256 del, err := d.getDelegate() 257 if err != nil { 258 return schema.GroupVersionResource{}, err 259 } 260 gvr, err = del.ResourceFor(input) 261 if err != nil && !d.cl.Fresh() { 262 d.Reset() 263 gvr, err = d.ResourceFor(input) 264 } 265 return 266 } 267 268 // ResourcesFor takes a partial resource and returns back the list of 269 // potential resource in priority order. 270 func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input schema.GroupVersionResource) (gvrs []schema.GroupVersionResource, err error) { 271 del, err := d.getDelegate() 272 if err != nil { 273 return nil, err 274 } 275 gvrs, err = del.ResourcesFor(input) 276 if len(gvrs) == 0 && !d.cl.Fresh() { 277 d.Reset() 278 gvrs, err = d.ResourcesFor(input) 279 } 280 return 281 } 282 283 // RESTMapping identifies a preferred resource mapping for the 284 // provided group kind. 285 func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (m *meta.RESTMapping, err error) { 286 del, err := d.getDelegate() 287 if err != nil { 288 return nil, err 289 } 290 m, err = del.RESTMapping(gk, versions...) 291 if err != nil && !d.cl.Fresh() { 292 d.Reset() 293 m, err = d.RESTMapping(gk, versions...) 294 } 295 return 296 } 297 298 // RESTMappings returns the RESTMappings for the provided group kind 299 // in a rough internal preferred order. If no kind is found, it will 300 // return a NoResourceMatchError. 301 func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) (ms []*meta.RESTMapping, err error) { 302 del, err := d.getDelegate() 303 if err != nil { 304 return nil, err 305 } 306 ms, err = del.RESTMappings(gk, versions...) 307 if len(ms) == 0 && !d.cl.Fresh() { 308 d.Reset() 309 ms, err = d.RESTMappings(gk, versions...) 310 } 311 return 312 } 313 314 // ResourceSingularizer converts a resource name from plural to 315 // singular (e.g., from pods to pod). 316 func (d *DeferredDiscoveryRESTMapper) ResourceSingularizer(resource string) (singular string, err error) { 317 del, err := d.getDelegate() 318 if err != nil { 319 return resource, err 320 } 321 singular, err = del.ResourceSingularizer(resource) 322 if err != nil && !d.cl.Fresh() { 323 d.Reset() 324 singular, err = d.ResourceSingularizer(resource) 325 } 326 return 327 } 328 329 func (d *DeferredDiscoveryRESTMapper) String() string { 330 del, err := d.getDelegate() 331 if err != nil { 332 return fmt.Sprintf("DeferredDiscoveryRESTMapper{%v}", err) 333 } 334 return fmt.Sprintf("DeferredDiscoveryRESTMapper{\n\t%v\n}", del) 335 } 336 337 // Make sure it satisfies the interface 338 var _ meta.ResettableRESTMapper = &DeferredDiscoveryRESTMapper{}