k8s.io/apiserver@v0.31.1/pkg/endpoints/discovery/aggregated/handler_test.go (about)

     1  /*
     2  Copyright 2022 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 aggregated_test
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"math/rand"
    23  	"net/http"
    24  	"net/http/httptest"
    25  
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"testing"
    31  
    32  	fuzz "github.com/google/gofuzz"
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  
    36  	apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
    37  	apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
    38  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    39  	"k8s.io/apimachinery/pkg/runtime"
    40  	"k8s.io/apimachinery/pkg/runtime/schema"
    41  	runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
    42  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    43  	"k8s.io/apimachinery/pkg/version"
    44  	apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2"
    45  	discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
    46  )
    47  
    48  var scheme = runtime.NewScheme()
    49  var codecs = runtimeserializer.NewCodecFactory(scheme)
    50  
    51  const discoveryPath = "/apis"
    52  
    53  func init() {
    54  	utilruntime.Must(apidiscoveryv2.AddToScheme(scheme))
    55  	utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
    56  	// Register conversion for apidiscovery
    57  	utilruntime.Must(apidiscoveryv2conversion.RegisterConversions(scheme))
    58  	codecs = runtimeserializer.NewCodecFactory(scheme)
    59  }
    60  
    61  func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv2.APIGroupDiscoveryList {
    62  	fuzzer := fuzz.NewWithSeed(seed)
    63  	fuzzer.NumElements(atLeastNumGroups, maxNumGroups)
    64  	fuzzer.NilChance(0)
    65  	fuzzer.Funcs(func(o *apidiscoveryv2.APIGroupDiscovery, c fuzz.Continue) {
    66  		c.FuzzNoCustom(o)
    67  
    68  		// The ResourceManager will just not serve the group if its versions
    69  		// list is empty
    70  		atLeastOne := apidiscoveryv2.APIVersionDiscovery{}
    71  		c.Fuzz(&atLeastOne)
    72  		o.Versions = append(o.Versions, atLeastOne)
    73  		sort.Slice(o.Versions[:], func(i, j int) bool {
    74  			return version.CompareKubeAwareVersionStrings(o.Versions[i].Version, o.Versions[j].Version) > 0
    75  		})
    76  
    77  		o.TypeMeta = metav1.TypeMeta{}
    78  		var name string
    79  		c.Fuzz(&name)
    80  		o.ObjectMeta = metav1.ObjectMeta{
    81  			Name: name,
    82  		}
    83  	})
    84  
    85  	var apis []apidiscoveryv2.APIGroupDiscovery
    86  	fuzzer.Fuzz(&apis)
    87  	sort.Slice(apis[:], func(i, j int) bool {
    88  		return apis[i].Name < apis[j].Name
    89  	})
    90  
    91  	return apidiscoveryv2.APIGroupDiscoveryList{
    92  		TypeMeta: metav1.TypeMeta{
    93  			Kind:       "APIGroupDiscoveryList",
    94  			APIVersion: "apidiscovery.k8s.io/v2",
    95  		},
    96  		Items: apis,
    97  	}
    98  }
    99  
   100  func fetchPathV2Beta1(handler http.Handler, acceptPrefix string, path string, etag string) (*http.Response, []byte, *apidiscoveryv2beta1.APIGroupDiscoveryList) {
   101  	acceptSuffix := ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList"
   102  	r, bytes := fetchPathHelper(handler, acceptPrefix+acceptSuffix, path, etag)
   103  	var decoded *apidiscoveryv2beta1.APIGroupDiscoveryList
   104  	if len(bytes) > 0 {
   105  		decoded = &apidiscoveryv2beta1.APIGroupDiscoveryList{}
   106  		err := runtime.DecodeInto(codecs.UniversalDecoder(), bytes, decoded)
   107  		if err != nil {
   108  			panic(fmt.Sprintf("failed to decode response: %v", err))
   109  		}
   110  
   111  	}
   112  	return r, bytes, decoded
   113  }
   114  
   115  func fetchPath(handler http.Handler, acceptPrefix string, path string, etag string) (*http.Response, []byte, *apidiscoveryv2.APIGroupDiscoveryList) {
   116  	acceptSuffix := ";g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,"
   117  	acceptSuffixV2Beta1 := ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,"
   118  	r, bytes := fetchPathHelper(handler, acceptPrefix+acceptSuffix+","+acceptPrefix+acceptSuffixV2Beta1, path, etag)
   119  	var decoded *apidiscoveryv2.APIGroupDiscoveryList
   120  	if len(bytes) > 0 {
   121  		decoded = &apidiscoveryv2.APIGroupDiscoveryList{}
   122  		err := runtime.DecodeInto(codecs.UniversalDecoder(), bytes, decoded)
   123  		if err != nil {
   124  			panic(fmt.Sprintf("failed to decode response: %v", err))
   125  		}
   126  	}
   127  	return r, bytes, decoded
   128  }
   129  
   130  func fetchPathHelper(handler http.Handler, accept string, path string, etag string) (*http.Response, []byte) {
   131  	// Expect json-formatted apis group list
   132  	w := httptest.NewRecorder()
   133  	req := httptest.NewRequest("GET", discoveryPath, nil)
   134  
   135  	// Ask for JSON response
   136  	req.Header.Set("Accept", accept)
   137  
   138  	if etag != "" {
   139  		// Quote provided etag if unquoted
   140  		quoted := etag
   141  		if !strings.HasPrefix(etag, "\"") {
   142  			quoted = strconv.Quote(etag)
   143  		}
   144  		req.Header.Set("If-None-Match", quoted)
   145  	}
   146  
   147  	handler.ServeHTTP(w, req)
   148  
   149  	bytes := w.Body.Bytes()
   150  	return w.Result(), bytes
   151  }
   152  
   153  // Add all builtin APIServices to the manager and check the output
   154  func TestBasicResponse(t *testing.T) {
   155  	manager := discoveryendpoint.NewResourceManager("apis")
   156  
   157  	apis := fuzzAPIGroups(1, 3, 10)
   158  	manager.SetGroups(apis.Items)
   159  
   160  	response, body, decoded := fetchPath(manager, "application/json", discoveryPath, "")
   161  
   162  	jsonFormatted, err := json.Marshal(&apis)
   163  	require.NoError(t, err, "json marshal should always succeed")
   164  
   165  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   166  	assert.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported")
   167  	assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set")
   168  
   169  	assert.NoError(t, err, "decode should always succeed")
   170  	assert.EqualValues(t, &apis, decoded, "decoded value should equal input")
   171  	assert.Equal(t, string(jsonFormatted)+"\n", string(body), "response should be the api group list")
   172  }
   173  
   174  // Test that protobuf is outputted correctly
   175  func TestBasicResponseProtobuf(t *testing.T) {
   176  	manager := discoveryendpoint.NewResourceManager("apis")
   177  
   178  	apis := fuzzAPIGroups(1, 3, 10)
   179  	manager.SetGroups(apis.Items)
   180  
   181  	response, _, decoded := fetchPath(manager, "application/vnd.kubernetes.protobuf", discoveryPath, "")
   182  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   183  	assert.Equal(t, "application/vnd.kubernetes.protobuf;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported")
   184  	assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set")
   185  	assert.EqualValues(t, &apis, decoded, "decoded value should equal input")
   186  }
   187  
   188  // V2Beta1 should still be served
   189  func TestV2Beta1SkewSupport(t *testing.T) {
   190  	manager := discoveryendpoint.NewResourceManager("apis")
   191  
   192  	apis := fuzzAPIGroups(1, 3, 10)
   193  	manager.SetGroups(apis.Items)
   194  
   195  	converted, err := scheme.ConvertToVersion(&apis, &schema.GroupVersion{Group: "apidiscovery.k8s.io", Version: "v2beta1"})
   196  	if err != nil {
   197  		t.Fatal(err)
   198  	}
   199  
   200  	v2beta1apis := converted.(*apidiscoveryv2beta1.APIGroupDiscoveryList)
   201  
   202  	response, body, decoded := fetchPathV2Beta1(manager, "application/json", discoveryPath, "")
   203  
   204  	jsonFormatted, err := json.Marshal(v2beta1apis)
   205  	require.NoError(t, err, "json marshal should always succeed")
   206  
   207  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   208  	assert.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported")
   209  	assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set")
   210  
   211  	assert.NoError(t, err, "decode should always succeed")
   212  	assert.EqualValues(t, v2beta1apis, decoded, "decoded value should equal input")
   213  	assert.Equal(t, string(jsonFormatted)+"\n", string(body), "response should be the api group list")
   214  }
   215  
   216  // Test that an etag associated with the service only depends on the apiresources
   217  // e.g.: Multiple services with the same contents should have the same etag.
   218  func TestEtagConsistent(t *testing.T) {
   219  	// Create 2 managers, add a bunch of services to each
   220  	manager1 := discoveryendpoint.NewResourceManager("apis")
   221  	manager2 := discoveryendpoint.NewResourceManager("apis")
   222  
   223  	apis := fuzzAPIGroups(1, 3, 11)
   224  	manager1.SetGroups(apis.Items)
   225  	manager2.SetGroups(apis.Items)
   226  
   227  	// Make sure etag of each is the same
   228  	res1_initial, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
   229  	res2_initial, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
   230  
   231  	assert.NotEmpty(t, res1_initial.Header.Get("ETag"), "Etag should be populated")
   232  	assert.NotEmpty(t, res2_initial.Header.Get("ETag"), "Etag should be populated")
   233  	assert.Equal(t, res1_initial.Header.Get("ETag"), res2_initial.Header.Get("ETag"), "etag should be deterministic")
   234  
   235  	// Then add one service to only one.
   236  	// Make sure etag is changed, but other is the same
   237  	apis = fuzzAPIGroups(1, 1, 11)
   238  	for _, group := range apis.Items {
   239  		for _, version := range group.Versions {
   240  			manager1.AddGroupVersion(group.Name, version)
   241  		}
   242  	}
   243  
   244  	res1_addedToOne, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
   245  	res2_addedToOne, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
   246  
   247  	assert.NotEmpty(t, res1_addedToOne.Header.Get("ETag"), "Etag should be populated")
   248  	assert.NotEmpty(t, res2_addedToOne.Header.Get("ETag"), "Etag should be populated")
   249  	assert.NotEqual(t, res1_initial.Header.Get("ETag"), res1_addedToOne.Header.Get("ETag"), "ETag should be changed since version was added")
   250  	assert.Equal(t, res2_initial.Header.Get("ETag"), res2_addedToOne.Header.Get("ETag"), "ETag should be unchanged since data was unchanged")
   251  
   252  	// Then add service to other one
   253  	// Make sure etag is the same
   254  	for _, group := range apis.Items {
   255  		for _, version := range group.Versions {
   256  			manager2.AddGroupVersion(group.Name, version)
   257  		}
   258  	}
   259  
   260  	res1_addedToBoth, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
   261  	res2_addedToBoth, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
   262  
   263  	assert.NotEmpty(t, res1_addedToOne.Header.Get("ETag"), "Etag should be populated")
   264  	assert.NotEmpty(t, res2_addedToOne.Header.Get("ETag"), "Etag should be populated")
   265  	assert.Equal(t, res1_addedToBoth.Header.Get("ETag"), res2_addedToBoth.Header.Get("ETag"), "ETags should be equal since content is equal")
   266  	assert.NotEqual(t, res2_initial.Header.Get("ETag"), res2_addedToBoth.Header.Get("ETag"), "ETag should be changed since data was changed")
   267  
   268  	// Remove the group version from both. Initial E-Tag should be restored
   269  	for _, group := range apis.Items {
   270  		for _, version := range group.Versions {
   271  			manager1.RemoveGroupVersion(metav1.GroupVersion{
   272  				Group:   group.Name,
   273  				Version: version.Version,
   274  			})
   275  			manager2.RemoveGroupVersion(metav1.GroupVersion{
   276  				Group:   group.Name,
   277  				Version: version.Version,
   278  			})
   279  		}
   280  	}
   281  
   282  	res1_removeFromBoth, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
   283  	res2_removeFromBoth, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
   284  
   285  	assert.NotEmpty(t, res1_addedToOne.Header.Get("ETag"), "Etag should be populated")
   286  	assert.NotEmpty(t, res2_addedToOne.Header.Get("ETag"), "Etag should be populated")
   287  	assert.Equal(t, res1_removeFromBoth.Header.Get("ETag"), res2_removeFromBoth.Header.Get("ETag"), "ETags should be equal since content is equal")
   288  	assert.Equal(t, res1_initial.Header.Get("ETag"), res1_removeFromBoth.Header.Get("ETag"), "ETag should be equal to initial value since added content was removed")
   289  }
   290  
   291  // Test that if a request comes in with an If-None-Match header with an incorrect
   292  // E-Tag, that fresh content is returned.
   293  func TestEtagNonMatching(t *testing.T) {
   294  	manager := discoveryendpoint.NewResourceManager("apis")
   295  	apis := fuzzAPIGroups(1, 3, 12)
   296  	manager.SetGroups(apis.Items)
   297  
   298  	// fetch the document once
   299  	initial, _, _ := fetchPath(manager, "application/json", discoveryPath, "")
   300  	assert.NotEmpty(t, initial.Header.Get("ETag"), "ETag should be populated")
   301  
   302  	// Send another request with a wrong e-tag. The same response should
   303  	// get sent again
   304  	second, _, _ := fetchPath(manager, "application/json", discoveryPath, "wrongetag")
   305  
   306  	assert.Equal(t, http.StatusOK, initial.StatusCode, "response should be 200 OK")
   307  	assert.Equal(t, http.StatusOK, second.StatusCode, "response should be 200 OK")
   308  	assert.Equal(t, initial.Header.Get("ETag"), second.Header.Get("ETag"), "ETag of both requests should be equal")
   309  }
   310  
   311  // Test that if a request comes in with an If-None-Match header with a correct
   312  // E-Tag, that 304 Not Modified is returned
   313  func TestEtagMatching(t *testing.T) {
   314  	manager := discoveryendpoint.NewResourceManager("apis")
   315  	apis := fuzzAPIGroups(1, 3, 12)
   316  	manager.SetGroups(apis.Items)
   317  
   318  	// fetch the document once
   319  	initial, initialBody, _ := fetchPath(manager, "application/json", discoveryPath, "")
   320  	assert.NotEmpty(t, initial.Header.Get("ETag"), "ETag should be populated")
   321  	assert.NotEmpty(t, initialBody, "body should not be empty")
   322  
   323  	// Send another request with a wrong e-tag. The same response should
   324  	// get sent again
   325  	second, secondBody, _ := fetchPath(manager, "application/json", discoveryPath, initial.Header.Get("ETag"))
   326  
   327  	assert.Equal(t, http.StatusOK, initial.StatusCode, "initial response should be 200 OK")
   328  	assert.Equal(t, http.StatusNotModified, second.StatusCode, "second response should be 304 Not Modified")
   329  	assert.Equal(t, initial.Header.Get("ETag"), second.Header.Get("ETag"), "ETag of both requests should be equal")
   330  	assert.Empty(t, secondBody, "body should be empty when returning 304 Not Modified")
   331  }
   332  
   333  // Test that if a request comes in with an If-None-Match header with an old
   334  // E-Tag, that fresh content is returned
   335  func TestEtagOutdated(t *testing.T) {
   336  	manager := discoveryendpoint.NewResourceManager("apis")
   337  	apis := fuzzAPIGroups(1, 3, 15)
   338  	manager.SetGroups(apis.Items)
   339  
   340  	// fetch the document once
   341  	initial, initialBody, _ := fetchPath(manager, "application/json", discoveryPath, "")
   342  	assert.NotEmpty(t, initial.Header.Get("ETag"), "ETag should be populated")
   343  	assert.NotEmpty(t, initialBody, "body should not be empty")
   344  
   345  	// Then add some services so the etag changes
   346  	apis = fuzzAPIGroups(1, 3, 14)
   347  	for _, group := range apis.Items {
   348  		for _, version := range group.Versions {
   349  			manager.AddGroupVersion(group.Name, version)
   350  		}
   351  	}
   352  
   353  	// Send another request with the old e-tag. Response should not be 304 Not Modified
   354  	second, secondBody, _ := fetchPath(manager, "application/json", discoveryPath, initial.Header.Get("ETag"))
   355  
   356  	assert.Equal(t, http.StatusOK, initial.StatusCode, "initial response should be 200 OK")
   357  	assert.Equal(t, http.StatusOK, second.StatusCode, "second response should be 304 Not Modified")
   358  	assert.NotEqual(t, initial.Header.Get("ETag"), second.Header.Get("ETag"), "ETag of both requests should be unequal since contents differ")
   359  	assert.NotEmpty(t, secondBody, "body should be not empty when returning 304 Not Modified")
   360  }
   361  
   362  // Test that an api service can be added or removed
   363  func TestAddRemove(t *testing.T) {
   364  	manager := discoveryendpoint.NewResourceManager("apis")
   365  	apis := fuzzAPIGroups(1, 3, 15)
   366  	for _, group := range apis.Items {
   367  		for _, version := range group.Versions {
   368  			manager.AddGroupVersion(group.Name, version)
   369  		}
   370  	}
   371  
   372  	_, _, initialDocument := fetchPath(manager, "application/json", discoveryPath, "")
   373  
   374  	for _, group := range apis.Items {
   375  		for _, version := range group.Versions {
   376  			manager.RemoveGroupVersion(metav1.GroupVersion{
   377  				Group:   group.Name,
   378  				Version: version.Version,
   379  			})
   380  		}
   381  	}
   382  
   383  	_, _, secondDocument := fetchPath(manager, "application/json", discoveryPath, "")
   384  
   385  	require.NotNil(t, initialDocument, "initial document should parse")
   386  	require.NotNil(t, secondDocument, "second document should parse")
   387  	assert.Len(t, initialDocument.Items, len(apis.Items), "initial document should have set number of groups")
   388  	assert.Empty(t, secondDocument.Items, "second document should have no groups")
   389  }
   390  
   391  // Show that updating an existing service replaces and does not add the entry
   392  // and instead replaces it
   393  func TestUpdateService(t *testing.T) {
   394  	manager := discoveryendpoint.NewResourceManager("apis")
   395  	apis := fuzzAPIGroups(1, 3, 15)
   396  	for _, group := range apis.Items {
   397  		for _, version := range group.Versions {
   398  			manager.AddGroupVersion(group.Name, version)
   399  		}
   400  	}
   401  
   402  	_, _, initialDocument := fetchPath(manager, "application/json", discoveryPath, "")
   403  
   404  	assert.Equal(t, initialDocument, &apis, "should have returned expected document")
   405  
   406  	b, err := json.Marshal(apis)
   407  	if err != nil {
   408  		t.Error(err)
   409  	}
   410  	var newapis apidiscoveryv2.APIGroupDiscoveryList
   411  	err = json.Unmarshal(b, &newapis)
   412  	if err != nil {
   413  		t.Error(err)
   414  	}
   415  
   416  	newapis.Items[0].Versions[0].Resources[0].Resource = "changed a resource name!"
   417  	for _, group := range newapis.Items {
   418  		for _, version := range group.Versions {
   419  			manager.AddGroupVersion(group.Name, version)
   420  		}
   421  	}
   422  
   423  	_, _, secondDocument := fetchPath(manager, "application/json", discoveryPath, "")
   424  	assert.Equal(t, secondDocument, &newapis, "should have returned expected document")
   425  	assert.NotEqual(t, secondDocument, initialDocument, "should have returned expected document")
   426  }
   427  
   428  func TestMultipleSources(t *testing.T) {
   429  	type pair struct {
   430  		manager discoveryendpoint.ResourceManager
   431  		apis    apidiscoveryv2.APIGroupDiscoveryList
   432  	}
   433  
   434  	pairs := []pair{}
   435  
   436  	defaultManager := discoveryendpoint.NewResourceManager("apis")
   437  	for i := 0; i < 10; i++ {
   438  		name := discoveryendpoint.Source(100 * i)
   439  		manager := defaultManager.WithSource(name)
   440  		apis := fuzzAPIGroups(1, 3, int64(15+i))
   441  
   442  		// Give the groups deterministic names
   443  		for i := range apis.Items {
   444  			apis.Items[i].Name = fmt.Sprintf("%v.%v.com", i, name)
   445  		}
   446  
   447  		pairs = append(pairs, pair{manager, apis})
   448  	}
   449  
   450  	expectedResult := []apidiscoveryv2.APIGroupDiscovery{}
   451  
   452  	groupCounter := 0
   453  	for _, p := range pairs {
   454  		for gi, g := range p.apis.Items {
   455  			for vi, v := range g.Versions {
   456  				p.manager.AddGroupVersion(g.Name, v)
   457  
   458  				// Use index for priority so we dont have to do any sorting
   459  				// Use negative index since it is sorted descending
   460  				p.manager.SetGroupVersionPriority(metav1.GroupVersion{Group: g.Name, Version: v.Version}, -gi-groupCounter, -vi)
   461  			}
   462  
   463  			expectedResult = append(expectedResult, g)
   464  		}
   465  
   466  		groupCounter += len(p.apis.Items)
   467  	}
   468  
   469  	// Show discovery document is what we expect
   470  	_, _, initialDocument := fetchPath(defaultManager, "application/json", discoveryPath, "")
   471  
   472  	require.Len(t, initialDocument.Items, len(expectedResult))
   473  	require.Equal(t, initialDocument.Items, expectedResult)
   474  }
   475  
   476  // Shows that if you have multiple sources including Default source using
   477  // with the same group name the groups added by the "Default" source are used
   478  func TestSourcePrecedence(t *testing.T) {
   479  	defaultManager := discoveryendpoint.NewResourceManager("apis")
   480  	otherManager := defaultManager.WithSource(500)
   481  	apis := fuzzAPIGroups(1, 3, int64(15))
   482  	for _, g := range apis.Items {
   483  		for i, v := range g.Versions {
   484  			v.Freshness = apidiscoveryv2.DiscoveryFreshnessCurrent
   485  			g.Versions[i] = v
   486  			otherManager.AddGroupVersion(g.Name, v)
   487  		}
   488  	}
   489  
   490  	_, _, initialDocument := fetchPath(defaultManager, "application/json", discoveryPath, "")
   491  	require.Equal(t, apis.Items, initialDocument.Items)
   492  
   493  	// Add the first groupversion under default.
   494  	// No versions should appear in discovery document except this one
   495  	overrideVersion := initialDocument.Items[0].Versions[0]
   496  	overrideVersion.Freshness = apidiscoveryv2.DiscoveryFreshnessStale
   497  	defaultManager.AddGroupVersion(initialDocument.Items[0].Name, overrideVersion)
   498  
   499  	_, _, maskedDocument := fetchPath(defaultManager, "application/json", discoveryPath, "")
   500  	masked := initialDocument.DeepCopy()
   501  	masked.Items[0].Versions[0].Freshness = apidiscoveryv2.DiscoveryFreshnessStale
   502  
   503  	require.Equal(t, masked.Items, maskedDocument.Items)
   504  
   505  	// Wipe out default group. The other versions from the other group should now
   506  	// appear since the group is not being overridden by defaults ource
   507  	defaultManager.RemoveGroup(apis.Items[0].Name)
   508  
   509  	_, _, resetDocument := fetchPath(defaultManager, "application/json", discoveryPath, "")
   510  	require.Equal(t, resetDocument.Items, initialDocument.Items)
   511  }
   512  
   513  // Show the discovery manager is capable of serving requests to multiple users
   514  // with unchanging data
   515  func TestConcurrentRequests(t *testing.T) {
   516  	manager := discoveryendpoint.NewResourceManager("apis")
   517  	apis := fuzzAPIGroups(1, 3, 15)
   518  	manager.SetGroups(apis.Items)
   519  
   520  	waitGroup := sync.WaitGroup{}
   521  
   522  	numReaders := 100
   523  	numRequestsPerReader := 100
   524  
   525  	// Spawn a bunch of readers that will keep sending requests to the server
   526  	for i := 0; i < numReaders; i++ {
   527  		waitGroup.Add(1)
   528  		go func() {
   529  			defer waitGroup.Done()
   530  			etag := ""
   531  			for j := 0; j < numRequestsPerReader; j++ {
   532  				usedEtag := etag
   533  				if j%2 == 0 {
   534  					// Disable use of etag for every second request
   535  					usedEtag = ""
   536  				}
   537  				response, body, document := fetchPath(manager, "application/json", discoveryPath, usedEtag)
   538  
   539  				if usedEtag != "" {
   540  					assert.Equal(t, http.StatusNotModified, response.StatusCode, "response should be Not Modified if etag was used")
   541  					assert.Empty(t, body, "body should be empty if etag used")
   542  				} else {
   543  					assert.Equal(t, http.StatusOK, response.StatusCode, "response should be OK if etag was unused")
   544  					assert.Equal(t, &apis, document, "document should be equal")
   545  				}
   546  
   547  				etag = response.Header.Get("ETag")
   548  			}
   549  		}()
   550  	}
   551  	waitGroup.Wait()
   552  }
   553  
   554  // Show the handler is capable of serving many concurrent readers and many
   555  // concurrent writers without tripping up. Good to run with go '-race' detector
   556  // since there are not many "correctness" checks
   557  func TestAbuse(t *testing.T) {
   558  	manager := discoveryendpoint.NewResourceManager("apis")
   559  
   560  	numReaders := 100
   561  	numRequestsPerReader := 1000
   562  
   563  	numWriters := 10
   564  	numWritesPerWriter := 1000
   565  
   566  	waitGroup := sync.WaitGroup{}
   567  
   568  	// Spawn a bunch of writers that randomly add groups, remove groups, and
   569  	// reset the list of groups
   570  	for i := 0; i < numWriters; i++ {
   571  		source := rand.NewSource(int64(i))
   572  
   573  		waitGroup.Add(1)
   574  		go func() {
   575  			defer waitGroup.Done()
   576  
   577  			// track list of groups we've added so that we can remove them
   578  			// randomly
   579  			var addedGroups []metav1.GroupVersion
   580  
   581  			for j := 0; j < numWritesPerWriter; j++ {
   582  				switch source.Int63() % 3 {
   583  				case 0:
   584  					// Add a fuzzed group
   585  					apis := fuzzAPIGroups(1, 2, 15)
   586  					for _, group := range apis.Items {
   587  						for _, version := range group.Versions {
   588  							manager.AddGroupVersion(group.Name, version)
   589  							addedGroups = append(addedGroups, metav1.GroupVersion{
   590  								Group:   group.Name,
   591  								Version: version.Version,
   592  							})
   593  						}
   594  					}
   595  				case 1:
   596  					// Remove a group that we have added
   597  					if len(addedGroups) > 0 {
   598  						manager.RemoveGroupVersion(addedGroups[0])
   599  						addedGroups = addedGroups[1:]
   600  					} else {
   601  						// Send a request and try to remove a group someone else
   602  						// might have added
   603  						_, _, document := fetchPath(manager, "application/json", discoveryPath, "")
   604  						assert.NotNil(t, document, "manager should always succeed in returning a document")
   605  
   606  						if len(document.Items) > 0 {
   607  							manager.RemoveGroupVersion(metav1.GroupVersion{
   608  								Group:   document.Items[0].Name,
   609  								Version: document.Items[0].Versions[0].Version,
   610  							})
   611  						}
   612  
   613  					}
   614  				case 2:
   615  					manager.SetGroups(nil)
   616  					addedGroups = nil
   617  				default:
   618  					panic("unreachable")
   619  				}
   620  			}
   621  		}()
   622  	}
   623  
   624  	// Spawn a bunch of readers that will keep sending requests to the server
   625  	// and making sure the response makes sense
   626  	for i := 0; i < numReaders; i++ {
   627  		waitGroup.Add(1)
   628  		go func() {
   629  			defer waitGroup.Done()
   630  
   631  			etag := ""
   632  			for j := 0; j < numRequestsPerReader; j++ {
   633  				response, body, document := fetchPath(manager, "application/json", discoveryPath, etag)
   634  
   635  				if response.StatusCode == http.StatusNotModified {
   636  					assert.Equal(t, etag, response.Header.Get("ETag"))
   637  					assert.Empty(t, body, "body should be empty if etag used")
   638  					assert.Nil(t, document)
   639  				} else {
   640  					assert.Equal(t, http.StatusOK, response.StatusCode, "response should be OK if etag was unused")
   641  					assert.NotNil(t, document)
   642  				}
   643  
   644  				etag = response.Header.Get("ETag")
   645  			}
   646  		}()
   647  	}
   648  
   649  	waitGroup.Wait()
   650  }
   651  
   652  func TestVersionSortingNoPriority(t *testing.T) {
   653  	manager := discoveryendpoint.NewResourceManager("apis")
   654  
   655  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   656  		Version: "v1alpha1",
   657  	})
   658  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   659  		Version: "v2beta1",
   660  	})
   661  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   662  		Version: "v1",
   663  	})
   664  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   665  		Version: "v1beta1",
   666  	})
   667  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   668  		Version: "v2",
   669  	})
   670  
   671  	response, _, decoded := fetchPath(manager, "application/json", discoveryPath, "")
   672  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   673  
   674  	versions := decoded.Items[0].Versions
   675  
   676  	// Ensure that v1 is sorted before v1alpha1
   677  	assert.Equal(t, versions[0].Version, "v2")
   678  	assert.Equal(t, versions[1].Version, "v1")
   679  	assert.Equal(t, versions[2].Version, "v2beta1")
   680  	assert.Equal(t, versions[3].Version, "v1beta1")
   681  	assert.Equal(t, versions[4].Version, "v1alpha1")
   682  }
   683  
   684  func TestVersionSortingWithPriority(t *testing.T) {
   685  	manager := discoveryendpoint.NewResourceManager("apis")
   686  
   687  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   688  		Version: "v1",
   689  	})
   690  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "default", Version: "v1"}, 1000, 100)
   691  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   692  		Version: "v1alpha1",
   693  	})
   694  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "default", Version: "v1alpha1"}, 1000, 200)
   695  
   696  	response, _, decoded := fetchPath(manager, "application/json", discoveryPath, "")
   697  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   698  
   699  	versions := decoded.Items[0].Versions
   700  
   701  	// Ensure that reverse alpha sort order can be overridden by setting group version priorities.
   702  	assert.Equal(t, versions[0].Version, "v1alpha1")
   703  	assert.Equal(t, versions[1].Version, "v1")
   704  }
   705  
   706  // if two apiservices declare conflicting priorities for their group priority, take the higher one.
   707  func TestGroupVersionSortingConflictingPriority(t *testing.T) {
   708  	manager := discoveryendpoint.NewResourceManager("apis")
   709  
   710  	manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{
   711  		Version: "v1",
   712  	})
   713  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "default", Version: "v1"}, 1000, 100)
   714  	manager.AddGroupVersion("test", apidiscoveryv2.APIVersionDiscovery{
   715  		Version: "v1alpha1",
   716  	})
   717  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "test", Version: "v1alpha1"}, 500, 100)
   718  	manager.AddGroupVersion("test", apidiscoveryv2.APIVersionDiscovery{
   719  		Version: "v1alpha2",
   720  	})
   721  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "test", Version: "v1alpha1"}, 2000, 100)
   722  
   723  	response, _, decoded := fetchPath(manager, "application/json", discoveryPath, "")
   724  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   725  
   726  	groups := decoded.Items
   727  
   728  	// Ensure that reverse alpha sort order can be overridden by setting group version priorities.
   729  	assert.Equal(t, groups[0].Name, "test")
   730  	assert.Equal(t, groups[1].Name, "default")
   731  }
   732  
   733  // Show that the GroupPriorityMinimum is not sticky if a higher group version is removed
   734  // after a lower one is added
   735  func TestStatelessGroupPriorityMinimum(t *testing.T) {
   736  	manager := discoveryendpoint.NewResourceManager("apis")
   737  
   738  	stableGroup := "stable.example.com"
   739  	experimentalGroup := "experimental.example.com"
   740  
   741  	manager.AddGroupVersion(stableGroup, apidiscoveryv2.APIVersionDiscovery{
   742  		Version: "v1",
   743  	})
   744  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: stableGroup, Version: "v1"}, 1000, 100)
   745  
   746  	manager.AddGroupVersion(experimentalGroup, apidiscoveryv2.APIVersionDiscovery{
   747  		Version: "v1",
   748  	})
   749  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: experimentalGroup, Version: "v1"}, 100, 100)
   750  
   751  	manager.AddGroupVersion(experimentalGroup, apidiscoveryv2.APIVersionDiscovery{
   752  		Version: "v1alpha1",
   753  	})
   754  	manager.SetGroupVersionPriority(metav1.GroupVersion{Group: experimentalGroup, Version: "v1alpha1"}, 10000, 100)
   755  
   756  	// Expect v1alpha1's group priority to be used and sort it first in the list
   757  	response, _, decoded := fetchPath(manager, "application/json", discoveryPath, "")
   758  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   759  	assert.Equal(t, decoded.Items[0].Name, "experimental.example.com")
   760  	assert.Equal(t, decoded.Items[1].Name, "stable.example.com")
   761  
   762  	// Remove v1alpha1 and expect the new lower priority to take hold
   763  	manager.RemoveGroupVersion(metav1.GroupVersion{Group: experimentalGroup, Version: "v1alpha1"})
   764  
   765  	response, _, decoded = fetchPath(manager, "application/json", discoveryPath, "")
   766  	assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
   767  
   768  	assert.Equal(t, decoded.Items[0].Name, "stable.example.com")
   769  	assert.Equal(t, decoded.Items[1].Name, "experimental.example.com")
   770  }