k8s.io/client-go@v0.31.1/discovery/cached/memory/memcache_test.go (about)

     1  /*
     2  Copyright 2017 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 memory
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"reflect"
    26  	"sync"
    27  	"testing"
    28  
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  	apidiscovery "k8s.io/api/apidiscovery/v2"
    32  	errorsutil "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/util/sets"
    37  	"k8s.io/client-go/discovery"
    38  	"k8s.io/client-go/discovery/fake"
    39  	"k8s.io/client-go/openapi"
    40  	"k8s.io/client-go/rest"
    41  	testutil "k8s.io/client-go/util/testing"
    42  )
    43  
    44  type resourceMapEntry struct {
    45  	list *metav1.APIResourceList
    46  	err  error
    47  }
    48  
    49  type fakeDiscovery struct {
    50  	*fake.FakeDiscovery
    51  
    52  	lock         sync.Mutex
    53  	groupList    *metav1.APIGroupList
    54  	groupListErr error
    55  	resourceMap  map[string]*resourceMapEntry
    56  }
    57  
    58  func (c *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
    59  	c.lock.Lock()
    60  	defer c.lock.Unlock()
    61  	if rl, ok := c.resourceMap[groupVersion]; ok {
    62  		return rl.list, rl.err
    63  	}
    64  	return nil, errors.New("doesn't exist")
    65  }
    66  
    67  func (c *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
    68  	c.lock.Lock()
    69  	defer c.lock.Unlock()
    70  	if c.groupList == nil {
    71  		return nil, errors.New("doesn't exist")
    72  	}
    73  	return c.groupList, c.groupListErr
    74  }
    75  
    76  func TestClient(t *testing.T) {
    77  	fake := &fakeDiscovery{
    78  		groupList: &metav1.APIGroupList{
    79  			Groups: []metav1.APIGroup{{
    80  				Name: "astronomy",
    81  				Versions: []metav1.GroupVersionForDiscovery{{
    82  					GroupVersion: "astronomy/v8beta1",
    83  					Version:      "v8beta1",
    84  				}},
    85  			}},
    86  		},
    87  		resourceMap: map[string]*resourceMapEntry{
    88  			"astronomy/v8beta1": {
    89  				list: &metav1.APIResourceList{
    90  					GroupVersion: "astronomy/v8beta1",
    91  					APIResources: []metav1.APIResource{{
    92  						Name:         "dwarfplanets",
    93  						SingularName: "dwarfplanet",
    94  						Namespaced:   true,
    95  						Kind:         "DwarfPlanet",
    96  						ShortNames:   []string{"dp"},
    97  					}},
    98  				},
    99  			},
   100  		},
   101  	}
   102  
   103  	c := NewMemCacheClient(fake)
   104  	if c.Fresh() {
   105  		t.Errorf("Expected not fresh.")
   106  	}
   107  	g, err := c.ServerGroups()
   108  	if err != nil {
   109  		t.Errorf("Unexpected error: %v", err)
   110  	}
   111  	if e, a := fake.groupList, g; !reflect.DeepEqual(e, a) {
   112  		t.Errorf("Expected %#v, got %#v", e, a)
   113  	}
   114  	if !c.Fresh() {
   115  		t.Errorf("Expected fresh.")
   116  	}
   117  	c.Invalidate()
   118  	if c.Fresh() {
   119  		t.Errorf("Expected not fresh.")
   120  	}
   121  
   122  	g, err = c.ServerGroups()
   123  	if err != nil {
   124  		t.Errorf("Unexpected error: %v", err)
   125  	}
   126  	if e, a := fake.groupList, g; !reflect.DeepEqual(e, a) {
   127  		t.Errorf("Expected %#v, got %#v", e, a)
   128  	}
   129  	if !c.Fresh() {
   130  		t.Errorf("Expected fresh.")
   131  	}
   132  	r, err := c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   133  	if err != nil {
   134  		t.Errorf("Unexpected error: %v", err)
   135  	}
   136  	if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   137  		t.Errorf("Expected %#v, got %#v", e, a)
   138  	}
   139  
   140  	fake.lock.Lock()
   141  	fake.resourceMap = map[string]*resourceMapEntry{
   142  		"astronomy/v8beta1": {
   143  			list: &metav1.APIResourceList{
   144  				GroupVersion: "astronomy/v8beta1",
   145  				APIResources: []metav1.APIResource{{
   146  					Name:         "stars",
   147  					SingularName: "star",
   148  					Namespaced:   true,
   149  					Kind:         "Star",
   150  					ShortNames:   []string{"s"},
   151  				}},
   152  			},
   153  		},
   154  	}
   155  	fake.lock.Unlock()
   156  
   157  	c.Invalidate()
   158  	r, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   159  	if err != nil {
   160  		t.Errorf("Unexpected error: %v", err)
   161  	}
   162  	if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   163  		t.Errorf("Expected %#v, got %#v", e, a)
   164  	}
   165  }
   166  
   167  func TestServerGroupsFails(t *testing.T) {
   168  	fake := &fakeDiscovery{
   169  		groupList: &metav1.APIGroupList{
   170  			Groups: []metav1.APIGroup{{
   171  				Name: "astronomy",
   172  				Versions: []metav1.GroupVersionForDiscovery{{
   173  					GroupVersion: "astronomy/v8beta1",
   174  					Version:      "v8beta1",
   175  				}},
   176  			}},
   177  		},
   178  		groupListErr: errors.New("some error"),
   179  		resourceMap: map[string]*resourceMapEntry{
   180  			"astronomy/v8beta1": {
   181  				list: &metav1.APIResourceList{
   182  					GroupVersion: "astronomy/v8beta1",
   183  					APIResources: []metav1.APIResource{{
   184  						Name:         "dwarfplanets",
   185  						SingularName: "dwarfplanet",
   186  						Namespaced:   true,
   187  						Kind:         "DwarfPlanet",
   188  						ShortNames:   []string{"dp"},
   189  					}},
   190  				},
   191  			},
   192  		},
   193  	}
   194  
   195  	c := NewMemCacheClient(fake)
   196  	if c.Fresh() {
   197  		t.Errorf("Expected not fresh.")
   198  	}
   199  	_, err := c.ServerGroups()
   200  	if err == nil {
   201  		t.Errorf("Expected error")
   202  	}
   203  	if c.Fresh() {
   204  		t.Errorf("Expected not fresh.")
   205  	}
   206  	fake.lock.Lock()
   207  	fake.groupListErr = nil
   208  	fake.lock.Unlock()
   209  	r, err := c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   210  	if err != nil {
   211  		t.Errorf("Unexpected error: %v", err)
   212  	}
   213  	if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   214  		t.Errorf("Expected %#v, got %#v", e, a)
   215  	}
   216  	if !c.Fresh() {
   217  		t.Errorf("Expected not fresh.")
   218  	}
   219  }
   220  
   221  func TestPartialPermanentFailure(t *testing.T) {
   222  	fake := &fakeDiscovery{
   223  		groupList: &metav1.APIGroupList{
   224  			Groups: []metav1.APIGroup{
   225  				{
   226  					Name: "astronomy",
   227  					Versions: []metav1.GroupVersionForDiscovery{{
   228  						GroupVersion: "astronomy/v8beta1",
   229  						Version:      "v8beta1",
   230  					}},
   231  				},
   232  				{
   233  					Name: "astronomy2",
   234  					Versions: []metav1.GroupVersionForDiscovery{{
   235  						GroupVersion: "astronomy2/v8beta1",
   236  						Version:      "v8beta1",
   237  					}},
   238  				},
   239  			},
   240  		},
   241  		resourceMap: map[string]*resourceMapEntry{
   242  			"astronomy/v8beta1": {
   243  				err: errors.New("some permanent error"),
   244  			},
   245  			"astronomy2/v8beta1": {
   246  				list: &metav1.APIResourceList{
   247  					GroupVersion: "astronomy2/v8beta1",
   248  					APIResources: []metav1.APIResource{{
   249  						Name:         "dwarfplanets",
   250  						SingularName: "dwarfplanet",
   251  						Namespaced:   true,
   252  						Kind:         "DwarfPlanet",
   253  						ShortNames:   []string{"dp"},
   254  					}},
   255  				},
   256  			},
   257  		},
   258  	}
   259  
   260  	c := NewMemCacheClient(fake)
   261  	if c.Fresh() {
   262  		t.Errorf("Expected not fresh.")
   263  	}
   264  	r, err := c.ServerResourcesForGroupVersion("astronomy2/v8beta1")
   265  	if err != nil {
   266  		t.Errorf("Unexpected error: %v", err)
   267  	}
   268  	if e, a := fake.resourceMap["astronomy2/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   269  		t.Errorf("Expected %#v, got %#v", e, a)
   270  	}
   271  	_, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   272  	if err == nil {
   273  		t.Errorf("Expected error, got nil")
   274  	}
   275  
   276  	fake.lock.Lock()
   277  	fake.resourceMap["astronomy/v8beta1"] = &resourceMapEntry{
   278  		list: &metav1.APIResourceList{
   279  			GroupVersion: "astronomy/v8beta1",
   280  			APIResources: []metav1.APIResource{{
   281  				Name:         "dwarfplanets",
   282  				SingularName: "dwarfplanet",
   283  				Namespaced:   true,
   284  				Kind:         "DwarfPlanet",
   285  				ShortNames:   []string{"dp"},
   286  			}},
   287  		},
   288  		err: nil,
   289  	}
   290  	fake.lock.Unlock()
   291  	// We don't retry permanent errors, so it should fail.
   292  	_, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   293  	if err == nil {
   294  		t.Errorf("Expected error, got nil")
   295  	}
   296  	c.Invalidate()
   297  
   298  	// After Invalidate, we should retry.
   299  	r, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   300  	if err != nil {
   301  		t.Errorf("Unexpected error: %v", err)
   302  	}
   303  	if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   304  		t.Errorf("Expected %#v, got %#v", e, a)
   305  	}
   306  }
   307  
   308  func TestPartialRetryableFailure(t *testing.T) {
   309  	fake := &fakeDiscovery{
   310  		groupList: &metav1.APIGroupList{
   311  			Groups: []metav1.APIGroup{
   312  				{
   313  					Name: "astronomy",
   314  					Versions: []metav1.GroupVersionForDiscovery{{
   315  						GroupVersion: "astronomy/v8beta1",
   316  						Version:      "v8beta1",
   317  					}},
   318  				},
   319  				{
   320  					Name: "astronomy2",
   321  					Versions: []metav1.GroupVersionForDiscovery{{
   322  						GroupVersion: "astronomy2/v8beta1",
   323  						Version:      "v8beta1",
   324  					}},
   325  				},
   326  			},
   327  		},
   328  		resourceMap: map[string]*resourceMapEntry{
   329  			"astronomy/v8beta1": {
   330  				err: &errorsutil.StatusError{
   331  					ErrStatus: metav1.Status{
   332  						Message: "Some retryable error",
   333  						Code:    int32(http.StatusServiceUnavailable),
   334  						Reason:  metav1.StatusReasonServiceUnavailable,
   335  					},
   336  				},
   337  			},
   338  			"astronomy2/v8beta1": {
   339  				list: &metav1.APIResourceList{
   340  					GroupVersion: "astronomy2/v8beta1",
   341  					APIResources: []metav1.APIResource{{
   342  						Name:         "dwarfplanets",
   343  						SingularName: "dwarfplanet",
   344  						Namespaced:   true,
   345  						Kind:         "DwarfPlanet",
   346  						ShortNames:   []string{"dp"},
   347  					}},
   348  				},
   349  			},
   350  		},
   351  	}
   352  
   353  	c := NewMemCacheClient(fake)
   354  	if c.Fresh() {
   355  		t.Errorf("Expected not fresh.")
   356  	}
   357  	r, err := c.ServerResourcesForGroupVersion("astronomy2/v8beta1")
   358  	if err != nil {
   359  		t.Errorf("Unexpected error: %v", err)
   360  	}
   361  	if e, a := fake.resourceMap["astronomy2/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   362  		t.Errorf("Expected %#v, got %#v", e, a)
   363  	}
   364  	_, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   365  	if err == nil {
   366  		t.Errorf("Expected error, got nil")
   367  	}
   368  
   369  	fake.lock.Lock()
   370  	fake.resourceMap["astronomy/v8beta1"] = &resourceMapEntry{
   371  		list: &metav1.APIResourceList{
   372  			GroupVersion: "astronomy/v8beta1",
   373  			APIResources: []metav1.APIResource{{
   374  				Name:         "dwarfplanets",
   375  				SingularName: "dwarfplanet",
   376  				Namespaced:   true,
   377  				Kind:         "DwarfPlanet",
   378  				ShortNames:   []string{"dp"},
   379  			}},
   380  		},
   381  		err: nil,
   382  	}
   383  	fake.lock.Unlock()
   384  	// We should retry retryable error even without Invalidate() being called,
   385  	// so no error is expected.
   386  	r, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   387  	if err != nil {
   388  		t.Errorf("Expected no error, got %v", err)
   389  	}
   390  	if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   391  		t.Errorf("Expected %#v, got %#v", e, a)
   392  	}
   393  
   394  	// Check that the last result was cached and we don't retry further.
   395  	fake.lock.Lock()
   396  	fake.resourceMap["astronomy/v8beta1"].err = errors.New("some permanent error")
   397  	fake.lock.Unlock()
   398  	r, err = c.ServerResourcesForGroupVersion("astronomy/v8beta1")
   399  	if err != nil {
   400  		t.Errorf("Expected no error, got %v", err)
   401  	}
   402  	if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
   403  		t.Errorf("Expected %#v, got %#v", e, a)
   404  	}
   405  }
   406  
   407  // Tests that schema instances returned by openapi cached and returned after
   408  // successive calls
   409  func TestOpenAPIMemCache(t *testing.T) {
   410  	fakeServer, err := testutil.NewFakeOpenAPIV3Server("../../testdata")
   411  	require.NoError(t, err)
   412  	defer fakeServer.HttpServer.Close()
   413  
   414  	require.NotEmpty(t, fakeServer.ServedDocuments)
   415  
   416  	client := NewMemCacheClient(
   417  		discovery.NewDiscoveryClientForConfigOrDie(
   418  			&rest.Config{Host: fakeServer.HttpServer.URL},
   419  		),
   420  	)
   421  	openapiClient := client.OpenAPIV3()
   422  
   423  	paths, err := openapiClient.Paths()
   424  	require.NoError(t, err)
   425  
   426  	contentTypes := []string{
   427  		runtime.ContentTypeJSON, openapi.ContentTypeOpenAPIV3PB,
   428  	}
   429  
   430  	for _, contentType := range contentTypes {
   431  		t.Run(contentType, func(t *testing.T) {
   432  			for k, v := range paths {
   433  				original, err := v.Schema(contentType)
   434  				if !assert.NoError(t, err) {
   435  					continue
   436  				}
   437  
   438  				pathsAgain, err := openapiClient.Paths()
   439  				if !assert.NoError(t, err) {
   440  					continue
   441  				}
   442  
   443  				schemaAgain, err := pathsAgain[k].Schema(contentType)
   444  				if !assert.NoError(t, err) {
   445  					continue
   446  				}
   447  
   448  				assert.True(t, reflect.ValueOf(paths).Pointer() == reflect.ValueOf(pathsAgain).Pointer())
   449  				assert.True(t, reflect.ValueOf(original).Pointer() == reflect.ValueOf(schemaAgain).Pointer())
   450  
   451  				// Invalidate and try again. This time pointers should not be equal
   452  				client.Invalidate()
   453  
   454  				pathsAgain, err = client.OpenAPIV3().Paths()
   455  				if !assert.NoError(t, err) {
   456  					continue
   457  				}
   458  
   459  				schemaAgain, err = pathsAgain[k].Schema(contentType)
   460  				if !assert.NoError(t, err) {
   461  					continue
   462  				}
   463  
   464  				assert.True(t, reflect.ValueOf(paths).Pointer() != reflect.ValueOf(pathsAgain).Pointer())
   465  				assert.True(t, reflect.ValueOf(original).Pointer() != reflect.ValueOf(schemaAgain).Pointer())
   466  				assert.Equal(t, original, schemaAgain)
   467  			}
   468  		})
   469  	}
   470  }
   471  
   472  // Tests function "GroupsAndMaybeResources" when the "unaggregated" discovery is returned.
   473  func TestMemCacheGroupsAndMaybeResources(t *testing.T) {
   474  	tests := []struct {
   475  		name                  string
   476  		corev1                *metav1.APIVersions
   477  		apis                  *metav1.APIGroupList
   478  		expectedGroupNames    []string
   479  		expectedGroupVersions []string
   480  	}{
   481  		{
   482  			name: "Legacy discovery format: 1 version at /api, 1 group at /apis",
   483  			corev1: &metav1.APIVersions{
   484  				Versions: []string{
   485  					"v1",
   486  				},
   487  			},
   488  			apis: &metav1.APIGroupList{
   489  				Groups: []metav1.APIGroup{
   490  					{
   491  						Name: "extensions",
   492  						Versions: []metav1.GroupVersionForDiscovery{
   493  							{GroupVersion: "extensions/v1beta1"},
   494  						},
   495  					},
   496  				},
   497  			},
   498  			expectedGroupNames:    []string{"", "extensions"},
   499  			expectedGroupVersions: []string{"v1", "extensions/v1beta1"},
   500  		},
   501  		{
   502  			name: "Legacy discovery format: 1 version at /api, 2 groups/1 version at /apis",
   503  			corev1: &metav1.APIVersions{
   504  				Versions: []string{
   505  					"v1",
   506  				},
   507  			},
   508  			apis: &metav1.APIGroupList{
   509  				Groups: []metav1.APIGroup{
   510  					{
   511  						Name: "apps",
   512  						Versions: []metav1.GroupVersionForDiscovery{
   513  							{GroupVersion: "apps/v1"},
   514  						},
   515  					},
   516  					{
   517  						Name: "extensions",
   518  						Versions: []metav1.GroupVersionForDiscovery{
   519  							{GroupVersion: "extensions/v1beta1"},
   520  						},
   521  					},
   522  				},
   523  			},
   524  			expectedGroupNames:    []string{"", "apps", "extensions"},
   525  			expectedGroupVersions: []string{"v1", "apps/v1", "extensions/v1beta1"},
   526  		},
   527  		{
   528  			name: "Legacy discovery format: 1 version at /api, 2 groups/2 versions at /apis",
   529  			corev1: &metav1.APIVersions{
   530  				Versions: []string{
   531  					"v1",
   532  				},
   533  			},
   534  			apis: &metav1.APIGroupList{
   535  				Groups: []metav1.APIGroup{
   536  					{
   537  						Name: "batch",
   538  						Versions: []metav1.GroupVersionForDiscovery{
   539  							{GroupVersion: "batch/v1"},
   540  						},
   541  					},
   542  					{
   543  						Name: "batch",
   544  						Versions: []metav1.GroupVersionForDiscovery{
   545  							{GroupVersion: "batch/v1beta1"},
   546  						},
   547  					},
   548  					{
   549  						Name: "extensions",
   550  						Versions: []metav1.GroupVersionForDiscovery{
   551  							{GroupVersion: "extensions/v1beta1"},
   552  						},
   553  					},
   554  					{
   555  						Name: "extensions",
   556  						Versions: []metav1.GroupVersionForDiscovery{
   557  							{GroupVersion: "extensions/v1alpha1"},
   558  						},
   559  					},
   560  				},
   561  			},
   562  			expectedGroupNames: []string{
   563  				"",
   564  				"batch",
   565  				"extensions",
   566  			},
   567  			expectedGroupVersions: []string{
   568  				"v1",
   569  				"batch/v1",
   570  				"batch/v1beta1",
   571  				"extensions/v1beta1",
   572  				"extensions/v1alpha1",
   573  			},
   574  		},
   575  	}
   576  
   577  	for _, test := range tests {
   578  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   579  			var body interface{}
   580  			switch req.URL.Path {
   581  			case "/api":
   582  				body = test.corev1
   583  			case "/apis":
   584  				body = test.apis
   585  			default:
   586  				w.WriteHeader(http.StatusNotFound)
   587  				return
   588  			}
   589  			output, err := json.Marshal(body)
   590  			require.NoError(t, err)
   591  			// Content-type is "unaggregated" discovery format -- no resources returned.
   592  			w.Header().Set("Content-Type", discovery.AcceptV1)
   593  			w.WriteHeader(http.StatusOK)
   594  			w.Write(output)
   595  		}))
   596  		defer server.Close()
   597  		client := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: server.URL})
   598  		memClient := memCacheClient{
   599  			delegate:               client,
   600  			groupToServerResources: map[string]*cacheEntry{},
   601  		}
   602  		assert.False(t, memClient.Fresh())
   603  		apiGroupList, resourcesMap, failedGVs, err := memClient.GroupsAndMaybeResources()
   604  		require.NoError(t, err)
   605  		// "Unaggregated" discovery always returns nil for resources.
   606  		assert.Nil(t, resourcesMap)
   607  		assert.Emptyf(t, failedGVs, "expected empty failed GroupVersions, got (%d)", len(failedGVs))
   608  		assert.False(t, memClient.receivedAggregatedDiscovery)
   609  		assert.True(t, memClient.Fresh())
   610  		// Test the expected groups are returned for the aggregated format.
   611  		expectedGroupNames := sets.NewString(test.expectedGroupNames...)
   612  		actualGroupNames := sets.NewString(groupNamesFromList(apiGroupList)...)
   613  		assert.True(t, expectedGroupNames.Equal(actualGroupNames),
   614  			"%s: Expected groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List())
   615  		// Test the expected group versions for the aggregated discovery is correct.
   616  		expectedGroupVersions := sets.NewString(test.expectedGroupVersions...)
   617  		actualGroupVersions := sets.NewString(groupVersionsFromGroups(apiGroupList)...)
   618  		assert.True(t, expectedGroupVersions.Equal(actualGroupVersions),
   619  			"%s: Expected group/versions (%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List())
   620  		// Invalidate the cache and retrieve the server groups and resources again.
   621  		memClient.Invalidate()
   622  		assert.False(t, memClient.Fresh())
   623  		apiGroupList, resourcesMap, _, err = memClient.GroupsAndMaybeResources()
   624  		require.NoError(t, err)
   625  		assert.Nil(t, resourcesMap)
   626  		assert.False(t, memClient.receivedAggregatedDiscovery)
   627  		// Test the expected groups are returned for the aggregated format.
   628  		actualGroupNames = sets.NewString(groupNamesFromList(apiGroupList)...)
   629  		assert.True(t, expectedGroupNames.Equal(actualGroupNames),
   630  			"%s: Expected after invalidation groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List())
   631  	}
   632  }
   633  
   634  // Tests function "GroupsAndMaybeResources" when the "aggregated" discovery is returned.
   635  func TestAggregatedMemCacheGroupsAndMaybeResources(t *testing.T) {
   636  	tests := []struct {
   637  		name                  string
   638  		corev1                *apidiscovery.APIGroupDiscoveryList
   639  		apis                  *apidiscovery.APIGroupDiscoveryList
   640  		expectedGroupNames    []string
   641  		expectedGroupVersions []string
   642  		expectedGVKs          []string
   643  		expectedFailedGVs     []string
   644  	}{
   645  		{
   646  			name: "Aggregated discovery: 1 group/1 resources at /api, 1 group/1 resources at /apis",
   647  			corev1: &apidiscovery.APIGroupDiscoveryList{
   648  				Items: []apidiscovery.APIGroupDiscovery{
   649  					{
   650  						Versions: []apidiscovery.APIVersionDiscovery{
   651  							{
   652  								Version: "v1",
   653  								Resources: []apidiscovery.APIResourceDiscovery{
   654  									{
   655  										Resource: "pods",
   656  										ResponseKind: &metav1.GroupVersionKind{
   657  											Group:   "",
   658  											Version: "v1",
   659  											Kind:    "Pod",
   660  										},
   661  										Scope: apidiscovery.ScopeNamespace,
   662  									},
   663  								},
   664  							},
   665  						},
   666  					},
   667  				},
   668  			},
   669  			apis: &apidiscovery.APIGroupDiscoveryList{
   670  				Items: []apidiscovery.APIGroupDiscovery{
   671  					{
   672  						ObjectMeta: metav1.ObjectMeta{
   673  							Name: "apps",
   674  						},
   675  						Versions: []apidiscovery.APIVersionDiscovery{
   676  							{
   677  								Version: "v1",
   678  								Resources: []apidiscovery.APIResourceDiscovery{
   679  									{
   680  										Resource: "deployments",
   681  										ResponseKind: &metav1.GroupVersionKind{
   682  											Group:   "apps",
   683  											Version: "v1",
   684  											Kind:    "Deployment",
   685  										},
   686  										Scope: apidiscovery.ScopeNamespace,
   687  									},
   688  								},
   689  							},
   690  						},
   691  					},
   692  				},
   693  			},
   694  			expectedGroupNames:    []string{"", "apps"},
   695  			expectedGroupVersions: []string{"v1", "apps/v1"},
   696  			expectedGVKs: []string{
   697  				"/v1/Pod",
   698  				"apps/v1/Deployment",
   699  			},
   700  			expectedFailedGVs: []string{},
   701  		},
   702  		{
   703  			name: "Aggregated discovery: 1 group/1 resources at /api, 1 group/2 versions/1 resources/1 stale GV at /apis",
   704  			corev1: &apidiscovery.APIGroupDiscoveryList{
   705  				Items: []apidiscovery.APIGroupDiscovery{
   706  					{
   707  						Versions: []apidiscovery.APIVersionDiscovery{
   708  							{
   709  								Version: "v1",
   710  								Resources: []apidiscovery.APIResourceDiscovery{
   711  									{
   712  										Resource: "pods",
   713  										ResponseKind: &metav1.GroupVersionKind{
   714  											Group:   "",
   715  											Version: "v1",
   716  											Kind:    "Pod",
   717  										},
   718  										Scope: apidiscovery.ScopeNamespace,
   719  									},
   720  								},
   721  							},
   722  						},
   723  					},
   724  				},
   725  			},
   726  			apis: &apidiscovery.APIGroupDiscoveryList{
   727  				Items: []apidiscovery.APIGroupDiscovery{
   728  					{
   729  						ObjectMeta: metav1.ObjectMeta{
   730  							Name: "apps",
   731  						},
   732  						Versions: []apidiscovery.APIVersionDiscovery{
   733  							{
   734  								Version: "v1",
   735  								Resources: []apidiscovery.APIResourceDiscovery{
   736  									{
   737  										Resource: "deployments",
   738  										ResponseKind: &metav1.GroupVersionKind{
   739  											Group:   "apps",
   740  											Version: "v1",
   741  											Kind:    "Deployment",
   742  										},
   743  										Scope: apidiscovery.ScopeNamespace,
   744  									},
   745  								},
   746  							},
   747  							{
   748  								// Stale Version is not included in discovery.
   749  								Version: "v2",
   750  								Resources: []apidiscovery.APIResourceDiscovery{
   751  									{
   752  										Resource: "deployments",
   753  										ResponseKind: &metav1.GroupVersionKind{
   754  											Group:   "apps",
   755  											Version: "v2",
   756  											Kind:    "Deployment",
   757  										},
   758  										Scope: apidiscovery.ScopeNamespace,
   759  									},
   760  								},
   761  								Freshness: apidiscovery.DiscoveryFreshnessStale,
   762  							},
   763  						},
   764  					},
   765  				},
   766  			},
   767  			expectedGroupNames:    []string{"", "apps"},
   768  			expectedGroupVersions: []string{"v1", "apps/v1"},
   769  			expectedGVKs: []string{
   770  				"/v1/Pod",
   771  				"apps/v1/Deployment",
   772  			},
   773  			expectedFailedGVs: []string{"apps/v2"},
   774  		},
   775  		{
   776  			name: "Aggregated discovery: 1 group/2 resources at /api, 1 group/2 resources at /apis",
   777  			corev1: &apidiscovery.APIGroupDiscoveryList{
   778  				Items: []apidiscovery.APIGroupDiscovery{
   779  					{
   780  						Versions: []apidiscovery.APIVersionDiscovery{
   781  							{
   782  								Version: "v1",
   783  								Resources: []apidiscovery.APIResourceDiscovery{
   784  									{
   785  										Resource: "pods",
   786  										ResponseKind: &metav1.GroupVersionKind{
   787  											Group:   "",
   788  											Version: "v1",
   789  											Kind:    "Pod",
   790  										},
   791  										Scope: apidiscovery.ScopeNamespace,
   792  									},
   793  									{
   794  										Resource: "services",
   795  										ResponseKind: &metav1.GroupVersionKind{
   796  											Group:   "",
   797  											Version: "v1",
   798  											Kind:    "Service",
   799  										},
   800  										Scope: apidiscovery.ScopeNamespace,
   801  									},
   802  								},
   803  							},
   804  						},
   805  					},
   806  				},
   807  			},
   808  			apis: &apidiscovery.APIGroupDiscoveryList{
   809  				Items: []apidiscovery.APIGroupDiscovery{
   810  					{
   811  						ObjectMeta: metav1.ObjectMeta{
   812  							Name: "apps",
   813  						},
   814  						Versions: []apidiscovery.APIVersionDiscovery{
   815  							{
   816  								Version: "v1",
   817  								Resources: []apidiscovery.APIResourceDiscovery{
   818  									{
   819  										Resource: "deployments",
   820  										ResponseKind: &metav1.GroupVersionKind{
   821  											Group:   "apps",
   822  											Version: "v1",
   823  											Kind:    "Deployment",
   824  										},
   825  										Scope: apidiscovery.ScopeNamespace,
   826  									},
   827  									{
   828  										Resource: "statefulsets",
   829  										ResponseKind: &metav1.GroupVersionKind{
   830  											Group:   "apps",
   831  											Version: "v1",
   832  											Kind:    "StatefulSet",
   833  										},
   834  										Scope: apidiscovery.ScopeNamespace,
   835  									},
   836  								},
   837  							},
   838  						},
   839  					},
   840  				},
   841  			},
   842  			expectedGroupNames:    []string{"", "apps"},
   843  			expectedGroupVersions: []string{"v1", "apps/v1"},
   844  			expectedGVKs: []string{
   845  				"/v1/Pod",
   846  				"/v1/Service",
   847  				"apps/v1/Deployment",
   848  				"apps/v1/StatefulSet",
   849  			},
   850  			expectedFailedGVs: []string{},
   851  		},
   852  		{
   853  			name: "Aggregated discovery: 1 group/2 resources at /api, 2 group/2 resources/1 stale GV at /apis",
   854  			corev1: &apidiscovery.APIGroupDiscoveryList{
   855  				Items: []apidiscovery.APIGroupDiscovery{
   856  					{
   857  						Versions: []apidiscovery.APIVersionDiscovery{
   858  							{
   859  								Version: "v1",
   860  								Resources: []apidiscovery.APIResourceDiscovery{
   861  									{
   862  										Resource: "pods",
   863  										ResponseKind: &metav1.GroupVersionKind{
   864  											Group:   "",
   865  											Version: "v1",
   866  											Kind:    "Pod",
   867  										},
   868  										Scope: apidiscovery.ScopeNamespace,
   869  									},
   870  									{
   871  										Resource: "services",
   872  										ResponseKind: &metav1.GroupVersionKind{
   873  											Group:   "",
   874  											Version: "v1",
   875  											Kind:    "Service",
   876  										},
   877  										Scope: apidiscovery.ScopeNamespace,
   878  									},
   879  								},
   880  							},
   881  						},
   882  					},
   883  				},
   884  			},
   885  			apis: &apidiscovery.APIGroupDiscoveryList{
   886  				Items: []apidiscovery.APIGroupDiscovery{
   887  					{
   888  						ObjectMeta: metav1.ObjectMeta{
   889  							Name: "apps",
   890  						},
   891  						Versions: []apidiscovery.APIVersionDiscovery{
   892  							{
   893  								Version: "v1",
   894  								Resources: []apidiscovery.APIResourceDiscovery{
   895  									{
   896  										Resource: "deployments",
   897  										ResponseKind: &metav1.GroupVersionKind{
   898  											Group:   "apps",
   899  											Version: "v1",
   900  											Kind:    "Deployment",
   901  										},
   902  										Scope: apidiscovery.ScopeNamespace,
   903  									},
   904  									{
   905  										Resource: "statefulsets",
   906  										ResponseKind: &metav1.GroupVersionKind{
   907  											Group:   "apps",
   908  											Version: "v1",
   909  											Kind:    "StatefulSet",
   910  										},
   911  										Scope: apidiscovery.ScopeNamespace,
   912  									},
   913  								},
   914  							},
   915  							{
   916  								// Stale version is not included in discovery.
   917  								Version: "v1beta1",
   918  								Resources: []apidiscovery.APIResourceDiscovery{
   919  									{
   920  										Resource: "deployments",
   921  										ResponseKind: &metav1.GroupVersionKind{
   922  											Group:   "apps",
   923  											Version: "v1beta1",
   924  											Kind:    "Deployment",
   925  										},
   926  										Scope: apidiscovery.ScopeNamespace,
   927  									},
   928  									{
   929  										Resource: "statefulsets",
   930  										ResponseKind: &metav1.GroupVersionKind{
   931  											Group:   "apps",
   932  											Version: "v1beta1",
   933  											Kind:    "StatefulSet",
   934  										},
   935  										Scope: apidiscovery.ScopeNamespace,
   936  									},
   937  								},
   938  								Freshness: apidiscovery.DiscoveryFreshnessStale,
   939  							},
   940  						},
   941  					},
   942  					{
   943  						ObjectMeta: metav1.ObjectMeta{
   944  							Name: "batch",
   945  						},
   946  						Versions: []apidiscovery.APIVersionDiscovery{
   947  							{
   948  								Version: "v1",
   949  								Resources: []apidiscovery.APIResourceDiscovery{
   950  									{
   951  										Resource: "jobs",
   952  										ResponseKind: &metav1.GroupVersionKind{
   953  											Group:   "batch",
   954  											Version: "v1",
   955  											Kind:    "Job",
   956  										},
   957  										Scope: apidiscovery.ScopeNamespace,
   958  									},
   959  									{
   960  										Resource: "cronjobs",
   961  										ResponseKind: &metav1.GroupVersionKind{
   962  											Group:   "batch",
   963  											Version: "v1",
   964  											Kind:    "CronJob",
   965  										},
   966  										Scope: apidiscovery.ScopeNamespace,
   967  									},
   968  								},
   969  							},
   970  						},
   971  					},
   972  				},
   973  			},
   974  			expectedGroupNames:    []string{"", "apps", "batch"},
   975  			expectedGroupVersions: []string{"v1", "apps/v1", "batch/v1"},
   976  			expectedGVKs: []string{
   977  				"/v1/Pod",
   978  				"/v1/Service",
   979  				"apps/v1/Deployment",
   980  				"apps/v1/StatefulSet",
   981  				"batch/v1/Job",
   982  				"batch/v1/CronJob",
   983  			},
   984  			expectedFailedGVs: []string{"apps/v1beta1"},
   985  		},
   986  		{
   987  			name:   "Aggregated discovery: /api returns nothing, 2 groups/2 resources/2 stale GV at /apis",
   988  			corev1: &apidiscovery.APIGroupDiscoveryList{},
   989  			apis: &apidiscovery.APIGroupDiscoveryList{
   990  				Items: []apidiscovery.APIGroupDiscovery{
   991  					{
   992  						ObjectMeta: metav1.ObjectMeta{
   993  							Name: "apps",
   994  						},
   995  						Versions: []apidiscovery.APIVersionDiscovery{
   996  							// Statel "v1" Version is not included in discovery.
   997  							{
   998  								Version: "v1",
   999  								Resources: []apidiscovery.APIResourceDiscovery{
  1000  									{
  1001  										Resource: "deployments",
  1002  										ResponseKind: &metav1.GroupVersionKind{
  1003  											Group:   "apps",
  1004  											Version: "v1",
  1005  											Kind:    "Deployment",
  1006  										},
  1007  										Scope: apidiscovery.ScopeNamespace,
  1008  									},
  1009  									{
  1010  										Resource: "statefulsets",
  1011  										ResponseKind: &metav1.GroupVersionKind{
  1012  											Group:   "apps",
  1013  											Version: "v1",
  1014  											Kind:    "StatefulSet",
  1015  										},
  1016  										Scope: apidiscovery.ScopeNamespace,
  1017  									},
  1018  								},
  1019  								Freshness: apidiscovery.DiscoveryFreshnessStale,
  1020  							},
  1021  							{
  1022  								Version: "v1beta1",
  1023  								Resources: []apidiscovery.APIResourceDiscovery{
  1024  									{
  1025  										Resource: "deployments",
  1026  										ResponseKind: &metav1.GroupVersionKind{
  1027  											Group:   "apps",
  1028  											Version: "v1beta1",
  1029  											Kind:    "Deployment",
  1030  										},
  1031  										Scope: apidiscovery.ScopeNamespace,
  1032  									},
  1033  									{
  1034  										Resource: "statefulsets",
  1035  										ResponseKind: &metav1.GroupVersionKind{
  1036  											Group:   "apps",
  1037  											Version: "v1beta1",
  1038  											Kind:    "StatefulSet",
  1039  										},
  1040  										Scope: apidiscovery.ScopeNamespace,
  1041  									},
  1042  								},
  1043  							},
  1044  						},
  1045  					},
  1046  					{
  1047  						ObjectMeta: metav1.ObjectMeta{
  1048  							Name: "batch",
  1049  						},
  1050  						Versions: []apidiscovery.APIVersionDiscovery{
  1051  							{
  1052  								Version: "v1",
  1053  								Resources: []apidiscovery.APIResourceDiscovery{
  1054  									{
  1055  										Resource: "jobs",
  1056  										ResponseKind: &metav1.GroupVersionKind{
  1057  											Group:   "batch",
  1058  											Version: "v1",
  1059  											Kind:    "Job",
  1060  										},
  1061  										Scope: apidiscovery.ScopeNamespace,
  1062  									},
  1063  									{
  1064  										Resource: "cronjobs",
  1065  										ResponseKind: &metav1.GroupVersionKind{
  1066  											Group:   "batch",
  1067  											Version: "v1",
  1068  											Kind:    "CronJob",
  1069  										},
  1070  										Scope: apidiscovery.ScopeNamespace,
  1071  									},
  1072  								},
  1073  							},
  1074  							{
  1075  								// Stale Version is not included in discovery.
  1076  								Version: "v1beta1",
  1077  								Resources: []apidiscovery.APIResourceDiscovery{
  1078  									{
  1079  										Resource: "jobs",
  1080  										ResponseKind: &metav1.GroupVersionKind{
  1081  											Group:   "batch",
  1082  											Version: "v1beta1",
  1083  											Kind:    "Job",
  1084  										},
  1085  										Scope: apidiscovery.ScopeNamespace,
  1086  									},
  1087  								},
  1088  								Freshness: apidiscovery.DiscoveryFreshnessStale,
  1089  							},
  1090  						},
  1091  					},
  1092  				},
  1093  			},
  1094  			expectedGroupNames:    []string{"apps", "batch"},
  1095  			expectedGroupVersions: []string{"apps/v1beta1", "batch/v1"},
  1096  			expectedGVKs: []string{
  1097  				"apps/v1beta1/Deployment",
  1098  				"apps/v1beta1/StatefulSet",
  1099  				"batch/v1/Job",
  1100  				"batch/v1/CronJob",
  1101  			},
  1102  			expectedFailedGVs: []string{"apps/v1", "batch/v1beta1"},
  1103  		},
  1104  	}
  1105  
  1106  	for _, test := range tests {
  1107  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
  1108  			var agg *apidiscovery.APIGroupDiscoveryList
  1109  			switch req.URL.Path {
  1110  			case "/api":
  1111  				agg = test.corev1
  1112  			case "/apis":
  1113  				agg = test.apis
  1114  			default:
  1115  				w.WriteHeader(http.StatusNotFound)
  1116  				return
  1117  			}
  1118  			output, err := json.Marshal(agg)
  1119  			require.NoError(t, err)
  1120  			// Content-type is "aggregated" discovery format.
  1121  			w.Header().Set("Content-Type", discovery.AcceptV2)
  1122  			w.WriteHeader(http.StatusOK)
  1123  			w.Write(output)
  1124  		}))
  1125  		defer server.Close()
  1126  		client := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: server.URL})
  1127  		memClient := memCacheClient{
  1128  			delegate:               client,
  1129  			groupToServerResources: map[string]*cacheEntry{},
  1130  		}
  1131  		assert.False(t, memClient.Fresh())
  1132  		apiGroupList, resourcesMap, failedGVs, err := memClient.GroupsAndMaybeResources()
  1133  		require.NoError(t, err)
  1134  		assert.True(t, memClient.receivedAggregatedDiscovery)
  1135  		assert.True(t, memClient.Fresh())
  1136  		// Test the expected groups are returned for the aggregated format.
  1137  		expectedGroupNames := sets.NewString(test.expectedGroupNames...)
  1138  		actualGroupNames := sets.NewString(groupNamesFromList(apiGroupList)...)
  1139  		assert.True(t, expectedGroupNames.Equal(actualGroupNames),
  1140  			"%s: Expected groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List())
  1141  		// Test the expected group versions for the aggregated discovery is correct.
  1142  		expectedGroupVersions := sets.NewString(test.expectedGroupVersions...)
  1143  		actualGroupVersions := sets.NewString(groupVersionsFromGroups(apiGroupList)...)
  1144  		assert.True(t, expectedGroupVersions.Equal(actualGroupVersions),
  1145  			"%s: Expected group/versions (%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List())
  1146  		// Test the resources are correct.
  1147  		expectedGVKs := sets.NewString(test.expectedGVKs...)
  1148  		resources := []*metav1.APIResourceList{}
  1149  		for _, resourceList := range resourcesMap {
  1150  			resources = append(resources, resourceList)
  1151  		}
  1152  		actualGVKs := sets.NewString(groupVersionKinds(resources)...)
  1153  		assert.True(t, expectedGVKs.Equal(actualGVKs),
  1154  			"%s: Expected GVKs (%s), got (%s)", test.name, expectedGVKs.List(), actualGVKs.List())
  1155  		// Test the returned failed GroupVersions are correct.
  1156  		expectedFailedGVs := sets.NewString(test.expectedFailedGVs...)
  1157  		actualFailedGVs := sets.NewString(failedGroupVersions(failedGVs)...)
  1158  		assert.True(t, expectedFailedGVs.Equal(actualFailedGVs),
  1159  			"%s: Expected Failed GroupVersions (%s), got (%s)", test.name, expectedFailedGVs.List(), actualFailedGVs.List())
  1160  		// Invalidate the cache and retrieve the server groups again.
  1161  		memClient.Invalidate()
  1162  		assert.False(t, memClient.Fresh())
  1163  		apiGroupList, _, _, err = memClient.GroupsAndMaybeResources()
  1164  
  1165  		require.NoError(t, err)
  1166  		// Test the expected groups are returned for the aggregated format.
  1167  		actualGroupNames = sets.NewString(groupNamesFromList(apiGroupList)...)
  1168  		assert.True(t, expectedGroupNames.Equal(actualGroupNames),
  1169  			"%s: Expected after invalidation groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List())
  1170  	}
  1171  }
  1172  
  1173  // Tests function "ServerGroups" when the "aggregated" discovery is returned.
  1174  func TestMemCacheAggregatedServerGroups(t *testing.T) {
  1175  	tests := []struct {
  1176  		name                      string
  1177  		corev1                    *apidiscovery.APIGroupDiscoveryList
  1178  		apis                      *apidiscovery.APIGroupDiscoveryList
  1179  		expectedGroupNames        []string
  1180  		expectedGroupVersions     []string
  1181  		expectedPreferredVersions []string
  1182  	}{
  1183  		{
  1184  			name: "Aggregated discovery: 1 group/1 version at /api, 1 group/1 version at /apis",
  1185  			corev1: &apidiscovery.APIGroupDiscoveryList{
  1186  				Items: []apidiscovery.APIGroupDiscovery{
  1187  					{
  1188  						Versions: []apidiscovery.APIVersionDiscovery{
  1189  							{
  1190  								Version: "v1",
  1191  								Resources: []apidiscovery.APIResourceDiscovery{
  1192  									{
  1193  										Resource: "pods",
  1194  										ResponseKind: &metav1.GroupVersionKind{
  1195  											Group:   "",
  1196  											Version: "v1",
  1197  											Kind:    "Pod",
  1198  										},
  1199  										Scope: apidiscovery.ScopeNamespace,
  1200  									},
  1201  								},
  1202  							},
  1203  						},
  1204  					},
  1205  				},
  1206  			},
  1207  			apis: &apidiscovery.APIGroupDiscoveryList{
  1208  				Items: []apidiscovery.APIGroupDiscovery{
  1209  					{
  1210  						ObjectMeta: metav1.ObjectMeta{
  1211  							Name: "apps",
  1212  						},
  1213  						Versions: []apidiscovery.APIVersionDiscovery{
  1214  							{
  1215  								Version: "v1",
  1216  								Resources: []apidiscovery.APIResourceDiscovery{
  1217  									{
  1218  										Resource: "deployments",
  1219  										ResponseKind: &metav1.GroupVersionKind{
  1220  											Group:   "apps",
  1221  											Version: "v1",
  1222  											Kind:    "Deployment",
  1223  										},
  1224  										Scope: apidiscovery.ScopeNamespace,
  1225  									},
  1226  								},
  1227  							},
  1228  						},
  1229  					},
  1230  				},
  1231  			},
  1232  			expectedGroupNames:        []string{"", "apps"},
  1233  			expectedGroupVersions:     []string{"v1", "apps/v1"},
  1234  			expectedPreferredVersions: []string{"v1", "apps/v1"},
  1235  		},
  1236  		{
  1237  			name: "Aggregated discovery: 1 group/1 version at /api, 1 group/2 versions at /apis",
  1238  			corev1: &apidiscovery.APIGroupDiscoveryList{
  1239  				Items: []apidiscovery.APIGroupDiscovery{
  1240  					{
  1241  						ObjectMeta: metav1.ObjectMeta{
  1242  							Name: "",
  1243  						},
  1244  						Versions: []apidiscovery.APIVersionDiscovery{
  1245  							{
  1246  								Version: "v1",
  1247  								Resources: []apidiscovery.APIResourceDiscovery{
  1248  									{
  1249  										Resource: "pods",
  1250  										ResponseKind: &metav1.GroupVersionKind{
  1251  											Group:   "",
  1252  											Version: "v1",
  1253  											Kind:    "Pod",
  1254  										},
  1255  										Scope: apidiscovery.ScopeNamespace,
  1256  									},
  1257  								},
  1258  							},
  1259  						},
  1260  					},
  1261  				},
  1262  			},
  1263  			apis: &apidiscovery.APIGroupDiscoveryList{
  1264  				Items: []apidiscovery.APIGroupDiscovery{
  1265  					{
  1266  						ObjectMeta: metav1.ObjectMeta{
  1267  							Name: "apps",
  1268  						},
  1269  						Versions: []apidiscovery.APIVersionDiscovery{
  1270  							// v2 is preferred since it is first
  1271  							{
  1272  								Version: "v2",
  1273  								Resources: []apidiscovery.APIResourceDiscovery{
  1274  									{
  1275  										Resource: "deployments",
  1276  										ResponseKind: &metav1.GroupVersionKind{
  1277  											Group:   "apps",
  1278  											Version: "v2",
  1279  											Kind:    "Deployment",
  1280  										},
  1281  										Scope: apidiscovery.ScopeNamespace,
  1282  									},
  1283  								},
  1284  							},
  1285  							{
  1286  								Version: "v1",
  1287  								Resources: []apidiscovery.APIResourceDiscovery{
  1288  									{
  1289  										Resource: "deployments",
  1290  										ResponseKind: &metav1.GroupVersionKind{
  1291  											Group:   "apps",
  1292  											Version: "v1",
  1293  											Kind:    "Deployment",
  1294  										},
  1295  										Scope: apidiscovery.ScopeNamespace,
  1296  									},
  1297  								},
  1298  							},
  1299  						},
  1300  					},
  1301  				},
  1302  			},
  1303  			expectedGroupNames:        []string{"", "apps"},
  1304  			expectedGroupVersions:     []string{"v1", "apps/v1", "apps/v2"},
  1305  			expectedPreferredVersions: []string{"v1", "apps/v2"},
  1306  		},
  1307  		{
  1308  			name:   "Aggregated discovery: /api returns nothing, 2 groups at /apis",
  1309  			corev1: &apidiscovery.APIGroupDiscoveryList{},
  1310  			apis: &apidiscovery.APIGroupDiscoveryList{
  1311  				Items: []apidiscovery.APIGroupDiscovery{
  1312  					{
  1313  						ObjectMeta: metav1.ObjectMeta{
  1314  							Name: "apps",
  1315  						},
  1316  						Versions: []apidiscovery.APIVersionDiscovery{
  1317  							{
  1318  								Version: "v1",
  1319  								Resources: []apidiscovery.APIResourceDiscovery{
  1320  									{
  1321  										Resource: "deployments",
  1322  										ResponseKind: &metav1.GroupVersionKind{
  1323  											Group:   "apps",
  1324  											Version: "v1",
  1325  											Kind:    "Deployment",
  1326  										},
  1327  										Scope: apidiscovery.ScopeNamespace,
  1328  									},
  1329  									{
  1330  										Resource: "statefulsets",
  1331  										ResponseKind: &metav1.GroupVersionKind{
  1332  											Group:   "apps",
  1333  											Version: "v1",
  1334  											Kind:    "StatefulSet",
  1335  										},
  1336  										Scope: apidiscovery.ScopeNamespace,
  1337  									},
  1338  								},
  1339  							},
  1340  						},
  1341  					},
  1342  					{
  1343  						ObjectMeta: metav1.ObjectMeta{
  1344  							Name: "batch",
  1345  						},
  1346  						Versions: []apidiscovery.APIVersionDiscovery{
  1347  							// v1 is preferred since it is first
  1348  							{
  1349  								Version: "v1",
  1350  								Resources: []apidiscovery.APIResourceDiscovery{
  1351  									{
  1352  										Resource: "jobs",
  1353  										ResponseKind: &metav1.GroupVersionKind{
  1354  											Group:   "batch",
  1355  											Version: "v1",
  1356  											Kind:    "Job",
  1357  										},
  1358  										Scope: apidiscovery.ScopeNamespace,
  1359  									},
  1360  									{
  1361  										Resource: "cronjobs",
  1362  										ResponseKind: &metav1.GroupVersionKind{
  1363  											Group:   "batch",
  1364  											Version: "v1",
  1365  											Kind:    "CronJob",
  1366  										},
  1367  										Scope: apidiscovery.ScopeNamespace,
  1368  									},
  1369  								},
  1370  							},
  1371  							{
  1372  								Version: "v1beta1",
  1373  								Resources: []apidiscovery.APIResourceDiscovery{
  1374  									{
  1375  										Resource: "jobs",
  1376  										ResponseKind: &metav1.GroupVersionKind{
  1377  											Group:   "batch",
  1378  											Version: "v1beta1",
  1379  											Kind:    "Job",
  1380  										},
  1381  										Scope: apidiscovery.ScopeNamespace,
  1382  									},
  1383  									{
  1384  										Resource: "cronjobs",
  1385  										ResponseKind: &metav1.GroupVersionKind{
  1386  											Group:   "batch",
  1387  											Version: "v1beta1",
  1388  											Kind:    "CronJob",
  1389  										},
  1390  										Scope: apidiscovery.ScopeNamespace,
  1391  									},
  1392  								},
  1393  							},
  1394  						},
  1395  					},
  1396  				},
  1397  			},
  1398  			expectedGroupNames:        []string{"apps", "batch"},
  1399  			expectedGroupVersions:     []string{"apps/v1", "batch/v1", "batch/v1beta1"},
  1400  			expectedPreferredVersions: []string{"apps/v1", "batch/v1"},
  1401  		},
  1402  	}
  1403  
  1404  	for _, test := range tests {
  1405  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
  1406  			var agg *apidiscovery.APIGroupDiscoveryList
  1407  			switch req.URL.Path {
  1408  			case "/api":
  1409  				agg = test.corev1
  1410  			case "/apis":
  1411  				agg = test.apis
  1412  			default:
  1413  				w.WriteHeader(http.StatusNotFound)
  1414  				return
  1415  			}
  1416  			output, err := json.Marshal(agg)
  1417  			require.NoError(t, err)
  1418  			// Content-type is "aggregated" discovery format.
  1419  			w.Header().Set("Content-Type", discovery.AcceptV2Beta1)
  1420  			w.WriteHeader(http.StatusOK)
  1421  			w.Write(output)
  1422  		}))
  1423  		defer server.Close()
  1424  		client := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: server.URL})
  1425  		memCacheClient := NewMemCacheClient(client)
  1426  		assert.False(t, memCacheClient.Fresh())
  1427  		apiGroupList, err := memCacheClient.ServerGroups()
  1428  		require.NoError(t, err)
  1429  		assert.True(t, memCacheClient.Fresh())
  1430  		// Test the expected groups are returned for the aggregated format.
  1431  		expectedGroupNames := sets.NewString(test.expectedGroupNames...)
  1432  		actualGroupNames := sets.NewString(groupNamesFromList(apiGroupList)...)
  1433  		assert.True(t, expectedGroupNames.Equal(actualGroupNames),
  1434  			"%s: Expected groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List())
  1435  		// Test the expected group versions for the aggregated discovery is correct.
  1436  		expectedGroupVersions := sets.NewString(test.expectedGroupVersions...)
  1437  		actualGroupVersions := sets.NewString(groupVersionsFromGroups(apiGroupList)...)
  1438  		assert.True(t, expectedGroupVersions.Equal(actualGroupVersions),
  1439  			"%s: Expected group/versions (%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List())
  1440  		// Test the groups preferred version is correct.
  1441  		expectedPreferredVersions := sets.NewString(test.expectedPreferredVersions...)
  1442  		actualPreferredVersions := sets.NewString(preferredVersionsFromList(apiGroupList)...)
  1443  		assert.True(t, expectedPreferredVersions.Equal(actualPreferredVersions),
  1444  			"%s: Expected preferred group/version (%s), got (%s)", test.name, expectedPreferredVersions.List(), actualPreferredVersions.List())
  1445  		// Invalidate the cache and retrieve the server groups again.
  1446  		memCacheClient.Invalidate()
  1447  		assert.False(t, memCacheClient.Fresh())
  1448  		apiGroupList, err = memCacheClient.ServerGroups()
  1449  		require.NoError(t, err)
  1450  		// Test the expected groups are returned for the aggregated format.
  1451  		actualGroupNames = sets.NewString(groupNamesFromList(apiGroupList)...)
  1452  		assert.True(t, expectedGroupNames.Equal(actualGroupNames),
  1453  			"%s: Expected after invalidation groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List())
  1454  	}
  1455  }
  1456  
  1457  func groupNamesFromList(groups *metav1.APIGroupList) []string {
  1458  	result := []string{}
  1459  	for _, group := range groups.Groups {
  1460  		result = append(result, group.Name)
  1461  	}
  1462  	return result
  1463  }
  1464  
  1465  func preferredVersionsFromList(groups *metav1.APIGroupList) []string {
  1466  	result := []string{}
  1467  	for _, group := range groups.Groups {
  1468  		preferredGV := group.PreferredVersion.GroupVersion
  1469  		result = append(result, preferredGV)
  1470  	}
  1471  	return result
  1472  }
  1473  
  1474  func groupVersionsFromGroups(groups *metav1.APIGroupList) []string {
  1475  	result := []string{}
  1476  	for _, group := range groups.Groups {
  1477  		for _, version := range group.Versions {
  1478  			result = append(result, version.GroupVersion)
  1479  		}
  1480  	}
  1481  	return result
  1482  }
  1483  
  1484  func groupVersionKinds(resources []*metav1.APIResourceList) []string {
  1485  	result := []string{}
  1486  	for _, resourceList := range resources {
  1487  		for _, resource := range resourceList.APIResources {
  1488  			gvk := fmt.Sprintf("%s/%s/%s", resource.Group, resource.Version, resource.Kind)
  1489  			result = append(result, gvk)
  1490  		}
  1491  	}
  1492  	return result
  1493  }
  1494  
  1495  func failedGroupVersions(gvs map[schema.GroupVersion]error) []string {
  1496  	result := []string{}
  1497  	for gv := range gvs {
  1498  		result = append(result, gv.String())
  1499  	}
  1500  	return result
  1501  }