k8s.io/client-go@v0.31.1/restmapper/discovery_test.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 "reflect" 22 "testing" 23 24 "k8s.io/apimachinery/pkg/api/errors" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime/schema" 27 "k8s.io/apimachinery/pkg/util/dump" 28 "k8s.io/apimachinery/pkg/version" 29 . "k8s.io/client-go/discovery" 30 "k8s.io/client-go/openapi" 31 restclient "k8s.io/client-go/rest" 32 "k8s.io/client-go/rest/fake" 33 34 openapi_v2 "github.com/google/gnostic-models/openapiv2" 35 "github.com/stretchr/testify/assert" 36 ) 37 38 func TestRESTMapper(t *testing.T) { 39 resources := []*APIGroupResources{ 40 { 41 Group: metav1.APIGroup{ 42 Name: "extensions", 43 Versions: []metav1.GroupVersionForDiscovery{ 44 {Version: "v1beta"}, 45 }, 46 PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1beta"}, 47 }, 48 VersionedResources: map[string][]metav1.APIResource{ 49 "v1beta": { 50 {Name: "jobs", Namespaced: true, Kind: "Job"}, 51 {Name: "pods", Namespaced: true, Kind: "Pod"}, 52 }, 53 }, 54 }, 55 { 56 Group: metav1.APIGroup{ 57 Versions: []metav1.GroupVersionForDiscovery{ 58 {Version: "v1"}, 59 {Version: "v2"}, 60 }, 61 PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, 62 }, 63 VersionedResources: map[string][]metav1.APIResource{ 64 "v1": { 65 {Name: "pods", Namespaced: true, Kind: "Pod"}, 66 }, 67 "v2": { 68 {Name: "pods", Namespaced: true, Kind: "Pod"}, 69 }, 70 }, 71 }, 72 73 // This group tests finding and prioritizing resources that only exist in non-preferred versions 74 { 75 Group: metav1.APIGroup{ 76 Name: "unpreferred", 77 Versions: []metav1.GroupVersionForDiscovery{ 78 {Version: "v1"}, 79 {Version: "v2beta1"}, 80 {Version: "v2alpha1"}, 81 }, 82 PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, 83 }, 84 VersionedResources: map[string][]metav1.APIResource{ 85 "v1": { 86 {Name: "broccoli", Namespaced: true, Kind: "Broccoli"}, 87 }, 88 "v2beta1": { 89 {Name: "broccoli", Namespaced: true, Kind: "Broccoli"}, 90 {Name: "peas", Namespaced: true, Kind: "Pea"}, 91 }, 92 "v2alpha1": { 93 {Name: "broccoli", Namespaced: true, Kind: "Broccoli"}, 94 {Name: "peas", Namespaced: true, Kind: "Pea"}, 95 }, 96 }, 97 }, 98 } 99 100 restMapper := NewDiscoveryRESTMapper(resources) 101 102 kindTCs := []struct { 103 input schema.GroupVersionResource 104 want schema.GroupVersionKind 105 }{ 106 { 107 input: schema.GroupVersionResource{ 108 Resource: "pods", 109 }, 110 want: schema.GroupVersionKind{ 111 Version: "v1", 112 Kind: "Pod", 113 }, 114 }, 115 { 116 input: schema.GroupVersionResource{ 117 Version: "v1", 118 Resource: "pods", 119 }, 120 want: schema.GroupVersionKind{ 121 Version: "v1", 122 Kind: "Pod", 123 }, 124 }, 125 { 126 input: schema.GroupVersionResource{ 127 Version: "v2", 128 Resource: "pods", 129 }, 130 want: schema.GroupVersionKind{ 131 Version: "v2", 132 Kind: "Pod", 133 }, 134 }, 135 { 136 input: schema.GroupVersionResource{ 137 Resource: "pods", 138 }, 139 want: schema.GroupVersionKind{ 140 Version: "v1", 141 Kind: "Pod", 142 }, 143 }, 144 { 145 input: schema.GroupVersionResource{ 146 Resource: "jobs", 147 }, 148 want: schema.GroupVersionKind{ 149 Group: "extensions", 150 Version: "v1beta", 151 Kind: "Job", 152 }, 153 }, 154 { 155 input: schema.GroupVersionResource{ 156 Resource: "peas", 157 }, 158 want: schema.GroupVersionKind{ 159 Group: "unpreferred", 160 Version: "v2beta1", 161 Kind: "Pea", 162 }, 163 }, 164 } 165 166 for _, tc := range kindTCs { 167 got, err := restMapper.KindFor(tc.input) 168 if err != nil { 169 t.Errorf("KindFor(%#v) unexpected error: %v", tc.input, err) 170 continue 171 } 172 173 if !reflect.DeepEqual(got, tc.want) { 174 t.Errorf("KindFor(%#v) = %#v, want %#v", tc.input, got, tc.want) 175 } 176 } 177 178 resourceTCs := []struct { 179 input schema.GroupVersionResource 180 want schema.GroupVersionResource 181 }{ 182 { 183 input: schema.GroupVersionResource{ 184 Resource: "pods", 185 }, 186 want: schema.GroupVersionResource{ 187 Version: "v1", 188 Resource: "pods", 189 }, 190 }, 191 { 192 input: schema.GroupVersionResource{ 193 Version: "v1", 194 Resource: "pods", 195 }, 196 want: schema.GroupVersionResource{ 197 Version: "v1", 198 Resource: "pods", 199 }, 200 }, 201 { 202 input: schema.GroupVersionResource{ 203 Version: "v2", 204 Resource: "pods", 205 }, 206 want: schema.GroupVersionResource{ 207 Version: "v2", 208 Resource: "pods", 209 }, 210 }, 211 { 212 input: schema.GroupVersionResource{ 213 Resource: "pods", 214 }, 215 want: schema.GroupVersionResource{ 216 Version: "v1", 217 Resource: "pods", 218 }, 219 }, 220 { 221 input: schema.GroupVersionResource{ 222 Resource: "jobs", 223 }, 224 want: schema.GroupVersionResource{ 225 Group: "extensions", 226 Version: "v1beta", 227 Resource: "jobs", 228 }, 229 }, 230 } 231 232 for _, tc := range resourceTCs { 233 got, err := restMapper.ResourceFor(tc.input) 234 if err != nil { 235 t.Errorf("ResourceFor(%#v) unexpected error: %v", tc.input, err) 236 continue 237 } 238 239 if !reflect.DeepEqual(got, tc.want) { 240 t.Errorf("ResourceFor(%#v) = %#v, want %#v", tc.input, got, tc.want) 241 } 242 } 243 } 244 245 func TestDeferredDiscoveryRESTMapper_CacheMiss(t *testing.T) { 246 assert := assert.New(t) 247 248 cdc := fakeCachedDiscoveryInterface{fresh: false} 249 m := NewDeferredDiscoveryRESTMapper(&cdc) 250 assert.False(cdc.fresh, "should NOT be fresh after instantiation") 251 assert.Zero(cdc.invalidateCalls, "should not have called Invalidate()") 252 253 gvk, err := m.KindFor(schema.GroupVersionResource{ 254 Group: "a", 255 Version: "v1", 256 Resource: "foo", 257 }) 258 assert.NoError(err) 259 assert.True(cdc.fresh, "should be fresh after a cache-miss") 260 assert.Equal(cdc.invalidateCalls, 1, "should have called Invalidate() once") 261 assert.Equal(gvk.Kind, "Foo") 262 263 gvk, err = m.KindFor(schema.GroupVersionResource{ 264 Group: "a", 265 Version: "v1", 266 Resource: "foo", 267 }) 268 assert.NoError(err) 269 assert.Equal(cdc.invalidateCalls, 1, "should NOT have called Invalidate() again") 270 271 gvk, err = m.KindFor(schema.GroupVersionResource{ 272 Group: "a", 273 Version: "v1", 274 Resource: "bar", 275 }) 276 assert.Error(err) 277 assert.Equal(cdc.invalidateCalls, 1, "should NOT have called Invalidate() again after another cache-miss, but with fresh==true") 278 279 cdc.fresh = false 280 gvk, err = m.KindFor(schema.GroupVersionResource{ 281 Group: "a", 282 Version: "v1", 283 Resource: "bar", 284 }) 285 assert.Error(err) 286 assert.Equal(cdc.invalidateCalls, 2, "should HAVE called Invalidate() again after another cache-miss, but with fresh==false") 287 } 288 289 func TestGetAPIGroupResources(t *testing.T) { 290 type Test struct { 291 name string 292 293 discovery DiscoveryInterface 294 295 expected []*APIGroupResources 296 expectedError error 297 } 298 299 for _, test := range []Test{ 300 {"nil", &fakeFailingDiscovery{nil, nil, nil, nil}, nil, nil}, 301 {"normal", 302 &fakeFailingDiscovery{ 303 []metav1.APIGroup{aGroup, bGroup}, nil, 304 map[string]*metav1.APIResourceList{"a/v1": &aResources, "b/v1": &bResources}, nil, 305 }, 306 []*APIGroupResources{ 307 {aGroup, map[string][]metav1.APIResource{"v1": {aFoo}}}, 308 {bGroup, map[string][]metav1.APIResource{"v1": {bBar}}}, 309 }, nil, 310 }, 311 {"groups failed, but has fallback with a only", 312 &fakeFailingDiscovery{ 313 []metav1.APIGroup{aGroup}, fmt.Errorf("error fetching groups"), 314 map[string]*metav1.APIResourceList{"a/v1": &aResources, "b/v1": &bResources}, nil, 315 }, 316 []*APIGroupResources{ 317 {aGroup, map[string][]metav1.APIResource{"v1": {aFoo}}}, 318 }, nil, 319 }, 320 {"groups failed, but has no fallback", 321 &fakeFailingDiscovery{ 322 nil, fmt.Errorf("error fetching groups"), 323 map[string]*metav1.APIResourceList{"a/v1": &aResources, "b/v1": &bResources}, nil, 324 }, 325 nil, fmt.Errorf("error fetching groups"), 326 }, 327 {"a failed, but has fallback", 328 &fakeFailingDiscovery{ 329 []metav1.APIGroup{aGroup, bGroup}, nil, 330 map[string]*metav1.APIResourceList{"a/v1": &aResources, "b/v1": &bResources}, map[string]error{"a/v1": fmt.Errorf("a failed")}, 331 }, 332 []*APIGroupResources{ 333 {aGroup, map[string][]metav1.APIResource{"v1": {aFoo}}}, 334 {bGroup, map[string][]metav1.APIResource{"v1": {bBar}}}, 335 }, nil, // TODO: do we want this? 336 }, 337 {"a failed, but has no fallback", 338 &fakeFailingDiscovery{ 339 []metav1.APIGroup{aGroup, bGroup}, nil, 340 map[string]*metav1.APIResourceList{"b/v1": &bResources}, map[string]error{"a/v1": fmt.Errorf("a failed")}, 341 }, 342 []*APIGroupResources{ 343 {aGroup, map[string][]metav1.APIResource{}}, 344 {bGroup, map[string][]metav1.APIResource{"v1": {bBar}}}, 345 }, nil, // TODO: do we want this? 346 }, 347 {"a and b failed, but have fallbacks", 348 &fakeFailingDiscovery{ 349 []metav1.APIGroup{aGroup, bGroup}, nil, 350 map[string]*metav1.APIResourceList{"a/v1": &aResources, "b/v1": &bResources}, // TODO: both fallbacks are ignored 351 map[string]error{"a/v1": fmt.Errorf("a failed"), "b/v1": fmt.Errorf("b failed")}, 352 }, 353 []*APIGroupResources{ 354 {aGroup, map[string][]metav1.APIResource{"v1": {aFoo}}}, 355 {bGroup, map[string][]metav1.APIResource{"v1": {bBar}}}, 356 }, nil, // TODO: do we want this? 357 }, 358 } { 359 t.Run(test.name, func(t *testing.T) { 360 got, err := GetAPIGroupResources(test.discovery) 361 if err == nil && test.expectedError != nil { 362 t.Fatalf("expected error %q, but got none", test.expectedError) 363 } else if err != nil && test.expectedError == nil { 364 t.Fatalf("unexpected error: %v", err) 365 } 366 if !reflect.DeepEqual(test.expected, got) { 367 t.Errorf("unexpected result:\nexpected = %s\ngot = %s", dump.Pretty(test.expected), dump.Pretty(got)) 368 } 369 }) 370 } 371 372 } 373 374 var _ DiscoveryInterface = &fakeFailingDiscovery{} 375 376 type fakeFailingDiscovery struct { 377 groups []metav1.APIGroup 378 groupsErr error 379 380 resourcesForGroupVersion map[string]*metav1.APIResourceList 381 resourcesForGroupVersionErr map[string]error 382 } 383 384 func (*fakeFailingDiscovery) RESTClient() restclient.Interface { 385 return nil 386 } 387 388 func (d *fakeFailingDiscovery) ServerGroups() (*metav1.APIGroupList, error) { 389 if d.groups == nil && d.groupsErr != nil { 390 return nil, d.groupsErr 391 } 392 return &metav1.APIGroupList{Groups: d.groups}, d.groupsErr 393 } 394 395 func (d *fakeFailingDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 396 return ServerGroupsAndResources(d) 397 } 398 func (d *fakeFailingDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { 399 if rs, found := d.resourcesForGroupVersion[groupVersion]; found { 400 return rs, d.resourcesForGroupVersionErr[groupVersion] 401 } 402 return nil, fmt.Errorf("not found") 403 } 404 405 func (d *fakeFailingDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) { 406 return ServerPreferredResources(d) 407 } 408 409 func (d *fakeFailingDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { 410 return ServerPreferredNamespacedResources(d) 411 } 412 413 func (*fakeFailingDiscovery) ServerVersion() (*version.Info, error) { 414 return &version.Info{}, nil 415 } 416 417 func (*fakeFailingDiscovery) OpenAPISchema() (*openapi_v2.Document, error) { 418 panic("implement me") 419 } 420 421 func (c *fakeFailingDiscovery) OpenAPIV3() openapi.Client { 422 panic("implement me") 423 } 424 425 func (c *fakeFailingDiscovery) WithLegacy() DiscoveryInterface { 426 panic("implement me") 427 } 428 429 type fakeCachedDiscoveryInterface struct { 430 invalidateCalls int 431 fresh bool 432 enabledGroupA bool 433 } 434 435 var _ CachedDiscoveryInterface = &fakeCachedDiscoveryInterface{} 436 437 func (c *fakeCachedDiscoveryInterface) Fresh() bool { 438 return c.fresh 439 } 440 441 func (c *fakeCachedDiscoveryInterface) Invalidate() { 442 c.invalidateCalls = c.invalidateCalls + 1 443 c.fresh = true 444 c.enabledGroupA = true 445 } 446 447 func (c *fakeCachedDiscoveryInterface) RESTClient() restclient.Interface { 448 return &fake.RESTClient{} 449 } 450 451 func (c *fakeCachedDiscoveryInterface) ServerGroups() (*metav1.APIGroupList, error) { 452 if c.enabledGroupA { 453 return &metav1.APIGroupList{ 454 Groups: []metav1.APIGroup{aGroup}, 455 }, nil 456 } 457 return &metav1.APIGroupList{}, nil 458 } 459 460 func (c *fakeCachedDiscoveryInterface) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 461 return ServerGroupsAndResources(c) 462 } 463 464 func (c *fakeCachedDiscoveryInterface) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { 465 if c.enabledGroupA && groupVersion == "a/v1" { 466 return &aResources, nil 467 } 468 469 return nil, errors.NewNotFound(schema.GroupResource{}, "") 470 } 471 472 func (c *fakeCachedDiscoveryInterface) ServerPreferredResources() ([]*metav1.APIResourceList, error) { 473 if c.enabledGroupA { 474 return []*metav1.APIResourceList{ 475 { 476 GroupVersion: "a/v1", 477 APIResources: []metav1.APIResource{ 478 { 479 Name: "foo", 480 Kind: "Foo", 481 Verbs: []string{}, 482 }, 483 }, 484 }, 485 }, nil 486 } 487 return nil, nil 488 } 489 490 func (c *fakeCachedDiscoveryInterface) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { 491 return nil, nil 492 } 493 494 func (c *fakeCachedDiscoveryInterface) ServerVersion() (*version.Info, error) { 495 return &version.Info{}, nil 496 } 497 498 func (c *fakeCachedDiscoveryInterface) OpenAPISchema() (*openapi_v2.Document, error) { 499 return &openapi_v2.Document{}, nil 500 } 501 502 func (c *fakeCachedDiscoveryInterface) OpenAPIV3() openapi.Client { 503 panic("implement me") 504 } 505 506 func (c *fakeCachedDiscoveryInterface) WithLegacy() DiscoveryInterface { 507 panic("implement me") 508 } 509 510 var ( 511 aGroup = metav1.APIGroup{ 512 Name: "a", 513 Versions: []metav1.GroupVersionForDiscovery{ 514 { 515 GroupVersion: "a/v1", 516 Version: "v1", 517 }, 518 }, 519 PreferredVersion: metav1.GroupVersionForDiscovery{ 520 GroupVersion: "a/v1", 521 Version: "v1", 522 }, 523 } 524 bGroup = metav1.APIGroup{ 525 Name: "b", 526 Versions: []metav1.GroupVersionForDiscovery{ 527 { 528 GroupVersion: "b/v1", 529 Version: "v1", 530 }, 531 }, 532 PreferredVersion: metav1.GroupVersionForDiscovery{ 533 GroupVersion: "b/v1", 534 Version: "v1", 535 }, 536 } 537 aResources = metav1.APIResourceList{ 538 GroupVersion: "a/v1", 539 APIResources: []metav1.APIResource{aFoo}, 540 } 541 aFoo = metav1.APIResource{ 542 Name: "foo", 543 Kind: "Foo", 544 Namespaced: false, 545 } 546 bResources = metav1.APIResourceList{ 547 GroupVersion: "b/v1", 548 APIResources: []metav1.APIResource{bBar}, 549 } 550 bBar = metav1.APIResource{ 551 Name: "bar", 552 Kind: "Bar", 553 Namespaced: true, 554 } 555 )