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  )