1
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
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,
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,
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},
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,
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 )
556
View as plain text