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