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 }