k8s.io/client-go@v0.31.1/restmapper/shortcut_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 "testing" 21 22 openapi_v2 "github.com/google/gnostic-models/openapiv2" 23 "github.com/google/go-cmp/cmp" 24 25 "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/api/meta" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/version" 30 "k8s.io/client-go/discovery" 31 "k8s.io/client-go/openapi" 32 restclient "k8s.io/client-go/rest" 33 "k8s.io/client-go/rest/fake" 34 ) 35 36 func TestReplaceAliases(t *testing.T) { 37 tests := []struct { 38 name string 39 arg string 40 expected schema.GroupVersionResource 41 srvRes []*metav1.APIResourceList 42 }{ 43 { 44 name: "storageclasses-no-replacement", 45 arg: "storageclasses", 46 expected: schema.GroupVersionResource{Resource: "storageclasses"}, 47 srvRes: []*metav1.APIResourceList{}, 48 }, 49 { 50 name: "hpa-priority", 51 arg: "hpa", 52 expected: schema.GroupVersionResource{Resource: "superhorizontalpodautoscalers", Group: "autoscaling"}, 53 srvRes: []*metav1.APIResourceList{ 54 { 55 GroupVersion: "autoscaling/v1", 56 APIResources: []metav1.APIResource{ 57 { 58 Name: "superhorizontalpodautoscalers", 59 ShortNames: []string{"hpa"}, 60 }, 61 }, 62 }, 63 { 64 GroupVersion: "autoscaling/v1", 65 APIResources: []metav1.APIResource{ 66 { 67 Name: "horizontalpodautoscalers", 68 ShortNames: []string{"hpa"}, 69 }, 70 }, 71 }, 72 }, 73 }, 74 { 75 name: "resource-override", 76 arg: "dpl", 77 expected: schema.GroupVersionResource{Resource: "deployments", Group: "foo"}, 78 srvRes: []*metav1.APIResourceList{ 79 { 80 GroupVersion: "foo/v1", 81 APIResources: []metav1.APIResource{ 82 { 83 Name: "deployments", 84 ShortNames: []string{"dpl"}, 85 }, 86 }, 87 }, 88 { 89 GroupVersion: "extension/v1beta1", 90 APIResources: []metav1.APIResource{ 91 { 92 Name: "deployments", 93 ShortNames: []string{"deploy"}, 94 }, 95 }, 96 }, 97 }, 98 }, 99 { 100 name: "resource-match-preferred", 101 arg: "pods", 102 expected: schema.GroupVersionResource{Resource: "pods", Group: ""}, 103 srvRes: []*metav1.APIResourceList{ 104 { 105 GroupVersion: "v1", 106 APIResources: []metav1.APIResource{{Name: "pods", SingularName: "pod"}}, 107 }, 108 { 109 GroupVersion: "acme.com/v1", 110 APIResources: []metav1.APIResource{{Name: "poddlers", ShortNames: []string{"pods", "pod"}}}, 111 }, 112 }, 113 }, 114 { 115 name: "resource-match-singular-preferred", 116 arg: "pod", 117 expected: schema.GroupVersionResource{Resource: "pod", Group: ""}, 118 srvRes: []*metav1.APIResourceList{ 119 { 120 GroupVersion: "v1", 121 APIResources: []metav1.APIResource{{Name: "pods", SingularName: "pod"}}, 122 }, 123 { 124 GroupVersion: "acme.com/v1", 125 APIResources: []metav1.APIResource{{Name: "poddlers", ShortNames: []string{"pods", "pod"}}}, 126 }, 127 }, 128 }, 129 } 130 131 for _, test := range tests { 132 ds := &fakeDiscoveryClient{} 133 ds.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { 134 return test.srvRes, nil 135 } 136 mapper := NewShortcutExpander(&fakeRESTMapper{}, ds, nil).(shortcutExpander) 137 138 actual := mapper.expandResourceShortcut(schema.GroupVersionResource{Resource: test.arg}) 139 if actual != test.expected { 140 t.Errorf("%s: unexpected argument: expected %s, got %s", test.name, test.expected, actual) 141 } 142 } 143 } 144 145 func TestKindFor(t *testing.T) { 146 tests := []struct { 147 in schema.GroupVersionResource 148 expected schema.GroupVersionResource 149 srvRes []*metav1.APIResourceList 150 }{ 151 { 152 in: schema.GroupVersionResource{Group: "storage.k8s.io", Version: "", Resource: "sc"}, 153 expected: schema.GroupVersionResource{Group: "storage.k8s.io", Version: "", Resource: "storageclasses"}, 154 srvRes: []*metav1.APIResourceList{ 155 { 156 GroupVersion: "storage.k8s.io/v1", 157 APIResources: []metav1.APIResource{ 158 { 159 Name: "storageclasses", 160 ShortNames: []string{"sc"}, 161 }, 162 }, 163 }, 164 }, 165 }, 166 { 167 in: schema.GroupVersionResource{Group: "", Version: "", Resource: "sc"}, 168 expected: schema.GroupVersionResource{Group: "storage.k8s.io", Version: "", Resource: "storageclasses"}, 169 srvRes: []*metav1.APIResourceList{ 170 { 171 GroupVersion: "storage.k8s.io/v1", 172 APIResources: []metav1.APIResource{ 173 { 174 Name: "storageclasses", 175 ShortNames: []string{"sc"}, 176 }, 177 }, 178 }, 179 }, 180 }, 181 } 182 183 for i, test := range tests { 184 ds := &fakeDiscoveryClient{} 185 ds.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { 186 return test.srvRes, nil 187 } 188 189 delegate := &fakeRESTMapper{} 190 mapper := NewShortcutExpander(delegate, ds, func(a string) { 191 t.Fatalf("unexpected warning message %s", a) 192 }) 193 194 mapper.KindFor(test.in) 195 if delegate.kindForInput != test.expected { 196 t.Errorf("%d: unexpected data returned %#v, expected %#v", i, delegate.kindForInput, test.expected) 197 } 198 } 199 } 200 201 func TestKindForWithNewCRDs(t *testing.T) { 202 tests := map[string]struct { 203 in schema.GroupVersionResource 204 expected schema.GroupVersionKind 205 srvRes []*metav1.APIResourceList 206 }{ 207 "": { 208 in: schema.GroupVersionResource{Group: "a", Version: "", Resource: "sc"}, 209 expected: schema.GroupVersionKind{Group: "a", Version: "v1", Kind: "StorageClass"}, 210 srvRes: []*metav1.APIResourceList{ 211 { 212 GroupVersion: "a/v1", 213 APIResources: []metav1.APIResource{ 214 { 215 Name: "storageclasses", 216 ShortNames: []string{"sc"}, 217 Kind: "StorageClass", 218 }, 219 }, 220 }, 221 }, 222 }, 223 } 224 225 for name, test := range tests { 226 t.Run(name, func(t *testing.T) { 227 invalidateCalled := false 228 fakeDiscovery := &fakeDiscoveryClient{} 229 fakeDiscovery.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { 230 if invalidateCalled { 231 return test.srvRes, nil 232 } 233 return []*metav1.APIResourceList{}, nil 234 } 235 fakeCachedDiscovery := &fakeCachedDiscoveryClient{DiscoveryInterface: fakeDiscovery} 236 fakeCachedDiscovery.invalidateHandler = func() { 237 invalidateCalled = true 238 } 239 fakeCachedDiscovery.freshHandler = func() bool { 240 return invalidateCalled 241 } 242 243 // in real world the discovery client is fronted with a cache which 244 // will answer the initial request, only failure to match will trigger 245 // the cache invalidation and live discovery call 246 delegate := NewDeferredDiscoveryRESTMapper(fakeCachedDiscovery) 247 mapper := NewShortcutExpander(delegate, fakeCachedDiscovery, func(a string) { 248 t.Fatalf("unexpected warning message %s", a) 249 }) 250 251 gvk, err := mapper.KindFor(test.in) 252 if err != nil { 253 t.Errorf("unexpected error: %v", err) 254 } 255 if diff := cmp.Equal(gvk, test.expected); !diff { 256 t.Errorf("unexpected data returned %#v, expected %#v", gvk, test.expected) 257 } 258 }) 259 } 260 } 261 262 func TestWarnAmbigious(t *testing.T) { 263 tests := []struct { 264 name string 265 arg string 266 expected schema.GroupVersionResource 267 expectedWarningLogs []string 268 srvRes []*metav1.APIResourceList 269 }{ 270 { 271 name: "warn ambiguity", 272 arg: "hpa", 273 expected: schema.GroupVersionResource{Resource: "superhorizontalpodautoscalers", Group: "autoscaling"}, 274 expectedWarningLogs: []string{`short name "hpa" could also match lower priority resource horizontalpodautoscalers.autoscaling`}, 275 srvRes: []*metav1.APIResourceList{ 276 { 277 GroupVersion: "autoscaling/v1", 278 APIResources: []metav1.APIResource{ 279 { 280 Name: "superhorizontalpodautoscalers", 281 ShortNames: []string{"hpa"}, 282 }, 283 }, 284 }, 285 { 286 GroupVersion: "autoscaling/v1", 287 APIResources: []metav1.APIResource{ 288 { 289 Name: "horizontalpodautoscalers", 290 ShortNames: []string{"hpa"}, 291 }, 292 }, 293 }, 294 }, 295 }, 296 { 297 name: "warn-builtin-shortname-ambugity", 298 arg: "po", 299 expected: schema.GroupVersionResource{Resource: "pods", Group: ""}, 300 expectedWarningLogs: []string{`short name "po" could also match lower priority resource poddlers.acme.com`}, 301 srvRes: []*metav1.APIResourceList{ 302 { 303 GroupVersion: "v1", 304 APIResources: []metav1.APIResource{{Name: "pods", SingularName: "pod", ShortNames: []string{"po"}}}, 305 }, 306 { 307 GroupVersion: "acme.com/v1", 308 APIResources: []metav1.APIResource{{Name: "poddlers", ShortNames: []string{"po"}}}, 309 }, 310 }, 311 }, 312 { 313 name: "warn-builtin-shortname-ambugity-multi-version", 314 arg: "po", 315 expected: schema.GroupVersionResource{Resource: "pods", Group: ""}, 316 expectedWarningLogs: []string{`short name "po" could also match lower priority resource poddlers.acme.com`}, 317 srvRes: []*metav1.APIResourceList{ 318 { 319 GroupVersion: "v1", 320 APIResources: []metav1.APIResource{{Name: "pods", SingularName: "pod", ShortNames: []string{"po"}}}, 321 }, 322 { 323 GroupVersion: "acme.com/v1", 324 APIResources: []metav1.APIResource{{Name: "poddlers", ShortNames: []string{"po"}}}, 325 }, 326 { 327 GroupVersion: "acme.com/v1beta1", 328 APIResources: []metav1.APIResource{{Name: "poddlers", ShortNames: []string{"po"}}}, 329 }, 330 }, 331 }, 332 { 333 name: "resource-match-singular-preferred", 334 arg: "pod", 335 expected: schema.GroupVersionResource{Resource: "pod", Group: ""}, 336 srvRes: []*metav1.APIResourceList{ 337 { 338 GroupVersion: "v1", 339 APIResources: []metav1.APIResource{{Name: "pods", SingularName: "pod"}}, 340 }, 341 { 342 GroupVersion: "acme.com/v1", 343 APIResources: []metav1.APIResource{{Name: "poddlers", ShortNames: []string{"pods", "pod"}}}, 344 }, 345 }, 346 }, 347 { 348 name: "resource-multiple-versions-shortform", 349 arg: "hpa", 350 expected: schema.GroupVersionResource{Resource: "horizontalpodautoscalers", Group: "autoscaling"}, 351 expectedWarningLogs: []string{}, 352 srvRes: []*metav1.APIResourceList{ 353 { 354 GroupVersion: "autoscaling/v1alphav1", 355 APIResources: []metav1.APIResource{ 356 { 357 Name: "horizontalpodautoscalers", 358 ShortNames: []string{"hpa"}, 359 }, 360 }, 361 }, 362 { 363 GroupVersion: "autoscaling/v1", 364 APIResources: []metav1.APIResource{ 365 { 366 Name: "horizontalpodautoscalers", 367 ShortNames: []string{"hpa"}, 368 }, 369 }, 370 }, 371 }, 372 }, 373 { 374 name: "multi-resource-multiple-versions-shortform", 375 arg: "hpa", 376 expected: schema.GroupVersionResource{Resource: "horizontalpodautoscalers", Group: "autoscaling"}, 377 expectedWarningLogs: []string{ 378 `short name "hpa" could also match lower priority resource foo.foo`, 379 `short name "hpa" could also match lower priority resource bar.bar`, 380 }, 381 srvRes: []*metav1.APIResourceList{ 382 { 383 GroupVersion: "autoscaling/v1alphav1", 384 APIResources: []metav1.APIResource{ 385 { 386 Name: "horizontalpodautoscalers", 387 ShortNames: []string{"hpa"}, 388 }, 389 }, 390 }, 391 { 392 GroupVersion: "autoscaling/v1", 393 APIResources: []metav1.APIResource{ 394 { 395 Name: "horizontalpodautoscalers", 396 ShortNames: []string{"hpa"}, 397 }, 398 }, 399 }, 400 { 401 GroupVersion: "foo/v1", 402 APIResources: []metav1.APIResource{ 403 { 404 Name: "foo", 405 ShortNames: []string{"hpa"}, 406 }, 407 }, 408 }, 409 { 410 GroupVersion: "foo/v1beta1", 411 APIResources: []metav1.APIResource{ 412 { 413 Name: "foo", 414 ShortNames: []string{"hpa"}, 415 }, 416 }, 417 }, 418 { 419 GroupVersion: "bar/v1", 420 APIResources: []metav1.APIResource{ 421 { 422 Name: "bar", 423 ShortNames: []string{"hpa"}, 424 }, 425 }, 426 }, 427 }, 428 }, 429 } 430 431 for _, test := range tests { 432 ds := &fakeDiscoveryClient{} 433 ds.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { 434 return test.srvRes, nil 435 } 436 437 var actualWarnings []string 438 mapper := NewShortcutExpander(&fakeRESTMapper{}, ds, func(a string) { 439 actualWarnings = append(actualWarnings, a) 440 }).(shortcutExpander) 441 442 actual := mapper.expandResourceShortcut(schema.GroupVersionResource{Resource: test.arg}) 443 if actual != test.expected { 444 t.Errorf("%s: unexpected argument: expected %s, got %s", test.name, test.expected, actual) 445 } 446 447 if len(actualWarnings) == 0 && len(test.expectedWarningLogs) == 0 { 448 continue 449 } 450 451 if !cmp.Equal(test.expectedWarningLogs, actualWarnings) { 452 t.Fatalf("expected warning message %s but got %s", test.expectedWarningLogs, actualWarnings) 453 } 454 } 455 } 456 457 type fakeRESTMapper struct { 458 kindForInput schema.GroupVersionResource 459 } 460 461 func (f *fakeRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { 462 f.kindForInput = resource 463 return schema.GroupVersionKind{}, nil 464 } 465 466 func (f *fakeRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { 467 return nil, nil 468 } 469 470 func (f *fakeRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { 471 return schema.GroupVersionResource{}, nil 472 } 473 474 func (f *fakeRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { 475 return nil, nil 476 } 477 478 func (f *fakeRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { 479 return nil, nil 480 } 481 482 func (f *fakeRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { 483 return nil, nil 484 } 485 486 func (f *fakeRESTMapper) ResourceSingularizer(resource string) (singular string, err error) { 487 return "", nil 488 } 489 490 type fakeDiscoveryClient struct { 491 serverResourcesHandler func() ([]*metav1.APIResourceList, error) 492 } 493 494 var _ discovery.DiscoveryInterface = &fakeDiscoveryClient{} 495 496 func (c *fakeDiscoveryClient) RESTClient() restclient.Interface { 497 return &fake.RESTClient{} 498 } 499 500 func (c *fakeDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { 501 return &metav1.APIGroupList{ 502 Groups: []metav1.APIGroup{ 503 { 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 }, 517 }, nil 518 } 519 520 func (c *fakeDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { 521 if groupVersion == "a/v1" { 522 return &metav1.APIResourceList{APIResources: []metav1.APIResource{{Name: "widgets", Kind: "Widget"}}}, nil 523 } 524 525 return nil, errors.NewNotFound(schema.GroupResource{}, "") 526 } 527 528 func (c *fakeDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 529 sgs, err := c.ServerGroups() 530 if err != nil { 531 return nil, nil, err 532 } 533 resultGroups := []*metav1.APIGroup{} 534 for i := range sgs.Groups { 535 resultGroups = append(resultGroups, &sgs.Groups[i]) 536 } 537 if c.serverResourcesHandler != nil { 538 rs, err := c.serverResourcesHandler() 539 return resultGroups, rs, err 540 } 541 return resultGroups, []*metav1.APIResourceList{}, nil 542 } 543 544 func (c *fakeDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { 545 return nil, nil 546 } 547 548 func (c *fakeDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { 549 return nil, nil 550 } 551 552 func (c *fakeDiscoveryClient) ServerVersion() (*version.Info, error) { 553 return &version.Info{}, nil 554 } 555 556 func (c *fakeDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { 557 return &openapi_v2.Document{}, nil 558 } 559 560 func (c *fakeDiscoveryClient) OpenAPIV3() openapi.Client { 561 panic("implement me") 562 } 563 564 func (c *fakeDiscoveryClient) WithLegacy() discovery.DiscoveryInterface { 565 panic("implement me") 566 } 567 568 type fakeCachedDiscoveryClient struct { 569 discovery.DiscoveryInterface 570 freshHandler func() bool 571 invalidateHandler func() 572 } 573 574 var _ discovery.CachedDiscoveryInterface = &fakeCachedDiscoveryClient{} 575 576 func (c *fakeCachedDiscoveryClient) Fresh() bool { 577 if c.freshHandler != nil { 578 return c.freshHandler() 579 } 580 return true 581 } 582 583 func (c *fakeCachedDiscoveryClient) Invalidate() { 584 if c.invalidateHandler != nil { 585 c.invalidateHandler() 586 } 587 }