k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/apiserver/discovery/discovery_test.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package discovery 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "net/http" 24 "reflect" 25 "strings" 26 "testing" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/stretchr/testify/require" 30 31 apidiscoveryv2 "k8s.io/api/apidiscovery/v2" 32 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 33 apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 34 "k8s.io/apimachinery/pkg/api/meta" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" 39 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 40 "k8s.io/apimachinery/pkg/util/sets" 41 discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" 42 genericfeatures "k8s.io/apiserver/pkg/features" 43 utilfeature "k8s.io/apiserver/pkg/util/feature" 44 "k8s.io/client-go/discovery" 45 "k8s.io/client-go/dynamic" 46 kubernetes "k8s.io/client-go/kubernetes" 47 k8sscheme "k8s.io/client-go/kubernetes/scheme" 48 featuregatetesting "k8s.io/component-base/featuregate/testing" 49 apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" 50 aggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" 51 aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 52 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 53 54 "k8s.io/kubernetes/test/integration/framework" 55 ) 56 57 type kubeClientSet = kubernetes.Interface 58 59 type aggegatorClientSet = aggregator.Interface 60 61 type apiextensionsClientSet = apiextensions.Interface 62 63 type dynamicClientset = dynamic.Interface 64 type testClientSet struct { 65 kubeClientSet 66 aggegatorClientSet 67 apiextensionsClientSet 68 dynamicClientset 69 } 70 71 var _ testClient = testClientSet{} 72 73 func (t testClientSet) Discovery() discovery.DiscoveryInterface { 74 return t.kubeClientSet.Discovery() 75 } 76 77 var ( 78 scheme = runtime.NewScheme() 79 codecs = runtimeserializer.NewCodecFactory(scheme) 80 serialize runtime.NegotiatedSerializer 81 82 basicTestGroup = apidiscoveryv2.APIGroupDiscovery{ 83 ObjectMeta: metav1.ObjectMeta{ 84 Name: "stable.example.com", 85 }, 86 Versions: []apidiscoveryv2.APIVersionDiscovery{ 87 { 88 Version: "v1", 89 Resources: []apidiscoveryv2.APIResourceDiscovery{ 90 { 91 Resource: "jobs", 92 Verbs: []string{"create", "list", "watch", "delete"}, 93 ShortNames: []string{"jz"}, 94 Categories: []string{"all"}, 95 }, 96 }, 97 Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, 98 }, 99 }, 100 } 101 102 basicTestGroupWithFixup = apidiscoveryv2.APIGroupDiscovery{ 103 ObjectMeta: metav1.ObjectMeta{ 104 Name: "stable.example.com", 105 }, 106 Versions: []apidiscoveryv2.APIVersionDiscovery{ 107 { 108 Version: "v1", 109 Resources: []apidiscoveryv2.APIResourceDiscovery{ 110 { 111 Resource: "jobs", 112 Verbs: []string{"create", "list", "watch", "delete"}, 113 ShortNames: []string{"jz"}, 114 Categories: []string{"all"}, 115 // aggregator will populate this with a non-nil value 116 ResponseKind: &metav1.GroupVersionKind{}, 117 }, 118 }, 119 Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, 120 }, 121 }, 122 } 123 124 basicTestGroupStale = apidiscoveryv2.APIGroupDiscovery{ 125 ObjectMeta: metav1.ObjectMeta{ 126 Name: "stable.example.com", 127 }, 128 Versions: []apidiscoveryv2.APIVersionDiscovery{ 129 { 130 Version: "v1", 131 Freshness: apidiscoveryv2.DiscoveryFreshnessStale, 132 }, 133 }, 134 } 135 136 stableGroup = "stable.example.com" 137 stableV1 = metav1.GroupVersion{Group: stableGroup, Version: "v1"} 138 stableV1alpha1 = metav1.GroupVersion{Group: stableGroup, Version: "v1alpha1"} 139 stableV1alpha2 = metav1.GroupVersion{Group: stableGroup, Version: "v1alpha2"} 140 stableV1beta1 = metav1.GroupVersion{Group: stableGroup, Version: "v1beta1"} 141 stableV2 = metav1.GroupVersion{Group: stableGroup, Version: "v2"} 142 ) 143 144 func init() { 145 // Add all builtin types to scheme 146 utilruntime.Must(k8sscheme.AddToScheme(scheme)) 147 utilruntime.Must(aggregatorclientsetscheme.AddToScheme(scheme)) 148 utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) 149 150 info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) 151 if !ok { 152 panic("failed to create serializer info") 153 } 154 155 serialize = runtime.NewSimpleNegotiatedSerializer(info) 156 } 157 158 // Spins up an api server which is cleaned up at the end up the test 159 // Returns some kubernetes clients 160 func setup(t *testing.T) (context.Context, testClientSet, context.CancelFunc) { 161 ctx, cancelCtx := context.WithCancel(context.Background()) 162 163 server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd()) 164 t.Cleanup(server.TearDownFn) 165 166 kubeClientSet, err := kubernetes.NewForConfig(server.ClientConfig) 167 require.NoError(t, err) 168 169 aggegatorClientSet, err := aggregator.NewForConfig(server.ClientConfig) 170 require.NoError(t, err) 171 172 apiextensionsClientSet, err := apiextensions.NewForConfig(server.ClientConfig) 173 require.NoError(t, err) 174 175 dynamicClientset, err := dynamic.NewForConfig(server.ClientConfig) 176 require.NoError(t, err) 177 178 client := testClientSet{ 179 kubeClientSet: kubeClientSet, 180 aggegatorClientSet: aggegatorClientSet, 181 apiextensionsClientSet: apiextensionsClientSet, 182 dynamicClientset: dynamicClientset, 183 } 184 return ctx, client, cancelCtx 185 } 186 187 func TestReadinessAggregatedAPIServiceDiscovery(t *testing.T) { 188 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true) 189 190 // Keep any goroutines spawned from running past the execution of this test 191 ctx, client, cleanup := setup(t) 192 defer cleanup() 193 194 // Create a resource manager whichs serves our GroupVersion 195 resourceManager := discoveryendpoint.NewResourceManager("apis") 196 resourceManager.SetGroups([]apidiscoveryv2.APIGroupDiscovery{basicTestGroup}) 197 198 apiServiceWaitCh := make(chan struct{}) 199 200 // Install our ResourceManager as an Aggregated APIService to the 201 // test server 202 service := NewFakeService("test-server", client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 203 if strings.HasPrefix(r.URL.Path, "/apis/stable.example.com") { 204 // Return invalid response so APIService can be marked as "available" 205 w.WriteHeader(http.StatusOK) 206 } else if strings.HasPrefix(r.URL.Path, "/apis") { 207 select { 208 case <-apiServiceWaitCh: 209 // Hang responding to discovery until aggregated discovery document contains the aggregated group marked as Stale. 210 resourceManager.ServeHTTP(w, r) 211 case <-ctx.Done(): 212 return 213 } 214 } else { 215 // reject openapi/v2, openapi/v3, apis/<group>/<version> 216 w.WriteHeader(http.StatusNotFound) 217 } 218 })) 219 go func() { 220 require.NoError(t, service.Run(ctx)) 221 }() 222 require.NoError(t, service.WaitForReady(ctx)) 223 224 // For each groupversion served by our resourcemanager, create an APIService 225 // object connected to our fake APIServer 226 for _, versionInfo := range basicTestGroup.Versions { 227 groupVersion := metav1.GroupVersion{ 228 Group: basicTestGroup.Name, 229 Version: versionInfo.Version, 230 } 231 232 require.NoError(t, registerAPIService(ctx, client, groupVersion, service)) 233 } 234 235 // Keep repeatedly fetching document from aggregator. 236 // Check to see if it initially contains the aggregated group as stale 237 require.NoError(t, WaitForGroups(ctx, client, basicTestGroupStale)) 238 require.NoError(t, WaitForRootPaths(t, ctx, client, sets.New("/apis/"+basicTestGroup.Name), nil)) 239 240 // Allow the APIService to start responding and ensure that Freshness is updated when the APIService is reacheable. 241 close(apiServiceWaitCh) 242 require.NoError(t, WaitForGroups(ctx, client, basicTestGroupWithFixup)) 243 } 244 245 func registerAPIService(ctx context.Context, client aggregator.Interface, gv metav1.GroupVersion, service FakeService) error { 246 port := service.Port() 247 if port == nil { 248 return errors.New("service not yet started") 249 } 250 // Register the APIService 251 patch := apiregistrationv1.APIService{ 252 ObjectMeta: metav1.ObjectMeta{ 253 Name: gv.Version + "." + gv.Group, 254 }, 255 TypeMeta: metav1.TypeMeta{ 256 Kind: "APIService", 257 APIVersion: "apiregistration.k8s.io/v1", 258 }, 259 Spec: apiregistrationv1.APIServiceSpec{ 260 Group: gv.Group, 261 Version: gv.Version, 262 InsecureSkipTLSVerify: true, 263 GroupPriorityMinimum: 1000, 264 VersionPriority: 15, 265 Service: &apiregistrationv1.ServiceReference{ 266 Namespace: "default", 267 Name: service.Name(), 268 Port: port, 269 }, 270 }, 271 } 272 273 _, err := client. 274 ApiregistrationV1(). 275 APIServices(). 276 Create(context.TODO(), &patch, metav1.CreateOptions{FieldManager: "test-manager"}) 277 return err 278 } 279 280 func unregisterAPIService(ctx context.Context, client aggregator.Interface, gv metav1.GroupVersion) error { 281 return client.ApiregistrationV1().APIServices().Delete(ctx, gv.Version+"."+gv.Group, metav1.DeleteOptions{}) 282 } 283 284 func TestAggregatedAPIServiceDiscovery(t *testing.T) { 285 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true) 286 287 // Keep any goroutines spawned from running past the execution of this test 288 ctx, client, cleanup := setup(t) 289 defer cleanup() 290 291 // Create a resource manager whichs serves our GroupVersion 292 resourceManager := discoveryendpoint.NewResourceManager("apis") 293 resourceManager.SetGroups([]apidiscoveryv2.APIGroupDiscovery{basicTestGroup}) 294 295 // Install our ResourceManager as an Aggregated APIService to the 296 // test server 297 service := NewFakeService("test-server", client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 298 if strings.HasPrefix(r.URL.Path, "/apis") { 299 resourceManager.ServeHTTP(w, r) 300 } else if strings.HasPrefix(r.URL.Path, "/apis/stable.example.com") { 301 // Return invalid response so APIService can be marked as "available" 302 w.WriteHeader(http.StatusOK) 303 } else { 304 // reject openapi/v2, openapi/v3, apis/<group>/<version> 305 w.WriteHeader(http.StatusNotFound) 306 } 307 })) 308 go func() { 309 require.NoError(t, service.Run(ctx)) 310 }() 311 require.NoError(t, service.WaitForReady(ctx)) 312 313 // For each groupversion served by our resourcemanager, create an APIService 314 // object connected to our fake APIServer 315 var groupVersions []metav1.GroupVersion 316 for _, versionInfo := range basicTestGroup.Versions { 317 groupVersion := metav1.GroupVersion{ 318 Group: basicTestGroup.Name, 319 Version: versionInfo.Version, 320 } 321 322 require.NoError(t, registerAPIService(ctx, client, groupVersion, service)) 323 groupVersions = append(groupVersions, groupVersion) 324 } 325 326 // Keep repeatedly fetching document from aggregator. 327 // Check to see if it contains our service within a reasonable amount of time 328 require.NoError(t, WaitForGroups(ctx, client, basicTestGroupWithFixup)) 329 require.NoError(t, WaitForRootPaths(t, ctx, client, sets.New("/apis/"+basicTestGroup.Name), nil)) 330 331 // Unregister and ensure the group gets dropped from root paths 332 for _, groupVersion := range groupVersions { 333 require.NoError(t, unregisterAPIService(ctx, client, groupVersion)) 334 } 335 require.NoError(t, WaitForRootPaths(t, ctx, client, nil, sets.New("/apis/"+basicTestGroup.Name))) 336 } 337 338 func runTestCases(t *testing.T, cases []testCase) { 339 // Keep any goroutines spawned from running past the execution of this test 340 ctx, client, cleanup := setup(t) 341 defer cleanup() 342 343 // Fetch the original discovery information so we can wait for it to 344 // reset between tests 345 originalV1, err := FetchV1DiscoveryGroups(ctx, client) 346 require.NoError(t, err) 347 348 originalV2, err := FetchV2Discovery(ctx, client) 349 require.NoError(t, err) 350 351 for _, c := range cases { 352 t.Run(c.Name, func(t *testing.T) { 353 func() { 354 testContext, testDone := context.WithCancel(ctx) 355 defer testDone() 356 357 for i, a := range c.Actions { 358 if cleaning, ok := a.(cleaningAction); ok { 359 defer func() { 360 require.NoError(t, cleaning.Cleanup(testContext, client), "cleanup after \"%T\" step %v", a, i) 361 }() 362 } 363 require.NoError(t, a.Do(testContext, client), "running \"%T\" step %v", a, i) 364 } 365 }() 366 367 var diff string 368 err := WaitForV1GroupsWithCondition(ctx, client, func(result metav1.APIGroupList) bool { 369 diff = cmp.Diff(originalV1, result) 370 return reflect.DeepEqual(result, originalV1) 371 }) 372 require.NoError(t, err, "v1 discovery must reset between tests: "+diff) 373 374 err = WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { 375 diff = cmp.Diff(originalV2, result) 376 return reflect.DeepEqual(result, originalV2) 377 }) 378 require.NoError(t, err, "v2 discovery must reset between tests: "+diff) 379 }) 380 } 381 } 382 383 // Declarative tests targeting CRD integration 384 func TestCRD(t *testing.T) { 385 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true) 386 387 runTestCases(t, []testCase{ 388 { 389 // Show that when a CRD is added it gets included on the discovery doc 390 // within a reasonable amount of time 391 Name: "CRDInclusion", 392 Actions: []testAction{ 393 applyCRD(makeCRDSpec(stableGroup, "Foo", false, []string{"v1", "v1alpha1", "v1beta1", "v2"})), 394 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 395 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 396 waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 397 }, 398 }, 399 { 400 // Show that a CRD added to the discovery doc can also be removed 401 Name: "CRDRemoval", 402 Actions: []testAction{ 403 applyCRD(makeCRDSpec(stableGroup, "Foo", false, []string{"v1", "v1alpha1", "v1beta1", "v2"})), 404 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 405 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 406 waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 407 deleteObject{ 408 GroupVersionResource: metav1.GroupVersionResource(apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions")), 409 Name: "foos.stable.example.com", 410 }, 411 waitForAbsentGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 412 waitForAbsentGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 413 }, 414 }, 415 { 416 // Show that if CRD and APIService share a groupversion, and the 417 // APIService is deleted, and CRD updated, the APIService remains in 418 // discovery. 419 // This test simulates a resync of CRD controler to show that eventually 420 // APIService is recreated 421 Name: "CRDAPIServiceOverlap", 422 Actions: []testAction{ 423 applyAPIService( 424 apiregistrationv1.APIServiceSpec{ 425 Group: stableGroup, 426 Version: "v1", 427 InsecureSkipTLSVerify: true, 428 GroupPriorityMinimum: int32(1000), 429 VersionPriority: int32(15), 430 Service: &apiregistrationv1.ServiceReference{ 431 Name: "unused", 432 Namespace: "default", 433 }, 434 }, 435 ), 436 437 // Wait for GV to appear in both discovery documents 438 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1}), 439 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1}), 440 441 applyCRD(makeCRDSpec(stableGroup, "Bar", false, []string{"v1", "v2"})), 442 443 // Show that we have v1 and v2 but v1 is stale 444 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV2}), 445 waitForStaleGroupVersionsV2([]metav1.GroupVersion{stableV1}), 446 waitForFreshGroupVersionsV2([]metav1.GroupVersion{stableV2}), 447 448 // Delete APIService shared by the aggregated apiservice and 449 // CRD 450 deleteObject{ 451 GroupVersionResource: metav1.GroupVersionResource(apiregistrationv1.SchemeGroupVersion.WithResource("apiservices")), 452 Name: "v1.stable.example.com", 453 }, 454 455 // Update CRD to trigger a resync by adding a category and new groupversion 456 applyCRD(makeCRDSpec(stableGroup, "Bar", false, []string{"v1", "v2", "v1alpha1"}, "all")), 457 458 // Show that the groupversion is re-added back 459 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV2, stableV1alpha1}), 460 waitForFreshGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV2, stableV1alpha1}), 461 }, 462 }, 463 { 464 // Show that if CRD and Aggregated APIservice share a groupversiom, 465 // The aggregated apiservice's discovery information is shown in both 466 // v1 and v2 discovery 467 Name: "CRDAPIServiceSameGroupDifferentVersions", 468 Actions: []testAction{ 469 // Wait for CRD to apply 470 applyCRD(makeCRDSpec(stableGroup, "Bar", false, []string{"v2", "v1alpha1"})), 471 // Wait for GV to appear in both discovery documents 472 waitForGroupVersionsV1([]metav1.GroupVersion{stableV2, stableV1alpha1}), 473 waitForGroupVersionsV2([]metav1.GroupVersion{stableV2, stableV1alpha1}), 474 waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV2, stableV1alpha1}), 475 476 applyAPIService( 477 apiregistrationv1.APIServiceSpec{ 478 Group: stableGroup, 479 Version: "v1", 480 InsecureSkipTLSVerify: true, 481 GroupPriorityMinimum: int32(1000), 482 VersionPriority: int32(100), 483 Service: &apiregistrationv1.ServiceReference{ 484 Name: "unused", 485 Namespace: "default", 486 }, 487 }, 488 ), 489 490 // We should now have stable v1 available 491 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1}), 492 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1}), 493 waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1}), 494 495 // The CRD group-versions not served by the aggregated 496 // apiservice should still be availablee 497 waitForGroupVersionsV1([]metav1.GroupVersion{stableV2, stableV1alpha1}), 498 waitForGroupVersionsV2([]metav1.GroupVersion{stableV2, stableV1alpha1}), 499 waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV2, stableV1alpha1}), 500 501 // Remove API service. Show we have switched to CRD 502 deleteObject{ 503 GroupVersionResource: metav1.GroupVersionResource(apiregistrationv1.SchemeGroupVersion.WithResource("apiservices")), 504 Name: "v1.stable.example.com", 505 }, 506 507 // Show that we still have stable v1 since it is in the CRD 508 waitForGroupVersionsV1([]metav1.GroupVersion{stableV2, stableV1alpha1}), 509 waitForGroupVersionsV2([]metav1.GroupVersion{stableV2, stableV1alpha1}), 510 waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV2, stableV1alpha1}), 511 512 waitForAbsentGroupVersionsV1([]metav1.GroupVersion{stableV1}), 513 waitForAbsentGroupVersionsV2([]metav1.GroupVersion{stableV1}), 514 waitForAbsentGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1}), 515 }, 516 }, 517 { 518 // Show that if CRD and a builtin share a group version, 519 // the builtin takes precedence in both versions of discovery 520 Name: "CRDBuiltinOverlapPrecence", 521 Actions: []testAction{ 522 // Create CRD that overrides a builtin 523 applyCRD(makeCRDSpec("apiextensions.k8s.io", "Bar", true, []string{"v1", "v2", "vfake"})), 524 525 waitForGroupVersionsV1([]metav1.GroupVersion{{Group: "apiextensions.k8s.io", Version: "vfake"}}), 526 waitForGroupVersionsV2([]metav1.GroupVersion{{Group: "apiextensions.k8s.io", Version: "vfake"}}), 527 528 // Show that the builtin group-version is still used for V1 529 // By showing presence of v1.CustomResourceDefinition 530 // and absence of v1.Bar 531 waitForResourcesV1([]metav1.GroupVersionResource{ 532 { 533 Group: "apiextensions.k8s.io", 534 Version: "v1", 535 Resource: "customresourcedefinitions", 536 }, 537 { 538 Group: "apiextensions.k8s.io", 539 Version: "vfake", 540 Resource: "bars", 541 }, 542 }), 543 waitForResourcesV2([]metav1.GroupVersionResource{ 544 { 545 Group: "apiextensions.k8s.io", 546 Version: "v1", 547 Resource: "customresourcedefinitions", 548 }, 549 { 550 Group: "apiextensions.k8s.io", 551 Version: "vfake", 552 Resource: "bars", 553 }, 554 }), 555 556 waitForResourcesAbsentV1([]metav1.GroupVersionResource{ 557 { 558 Group: "apiextensions.k8s.io", 559 Version: "v1", 560 Resource: "bars", 561 }, 562 }), 563 waitForResourcesAbsentV2([]metav1.GroupVersionResource{ 564 { 565 Group: "apiextensions.k8s.io", 566 Version: "v1", 567 Resource: "bars", 568 }, 569 }), 570 }, 571 }, 572 { 573 // Tests that a race discovered during alpha phase of the feature is fixed. 574 // Rare race would occur if a CRD was synced before the removal of an aggregated 575 // APIService could be synced. 576 // To test this we: 577 // 1. Add CRD to apiserver 578 // 2. Wait for it to sync 579 // 3. Add aggregated APIService with same groupversion 580 // 4. Remove aggregated apiservice 581 // 5. Check that we have CRD GVs in discovery document 582 // Show that if CRD and APIService share a groupversion, and the 583 // APIService is deleted, and CRD updated, the groupversion from 584 // the CRD remains in discovery. 585 Name: "Race", 586 Actions: []testAction{ 587 // Create CRD with the same GV as the aggregated APIService 588 applyCRD(makeCRDSpec(stableGroup, "Bar", false, []string{"v1", "v2"})), 589 590 // only CRD has stable v2, this will show that CRD has been synced 591 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV2}), 592 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV2}), 593 594 // Add Aggregated APIService that overlaps the CRD. 595 applyAPIService( 596 apiregistrationv1.APIServiceSpec{ 597 Group: stableGroup, 598 Version: "v1", 599 InsecureSkipTLSVerify: true, 600 GroupPriorityMinimum: int32(1000), 601 VersionPriority: int32(100), 602 Service: &apiregistrationv1.ServiceReference{ 603 Name: "fake", 604 Namespace: "default", 605 }, 606 }, 607 ), 608 609 // Delete APIService shared by the aggregated apiservice and 610 // CRD 611 deleteObject{ 612 GroupVersionResource: metav1.GroupVersionResource(apiregistrationv1.SchemeGroupVersion.WithResource("apiservices")), 613 Name: "v1.stable.example.com", 614 }, 615 616 // Show the CRD (with stablev2) is the one which is now advertised 617 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV2}), 618 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV2}), 619 }, 620 }, 621 }) 622 } 623 624 func TestFreshness(t *testing.T) { 625 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true) 626 627 requireStaleGVs := func(gvs ...metav1.GroupVersion) inlineAction { 628 return inlineAction(func(ctx context.Context, client testClient) error { 629 document, err := FetchV2Discovery(ctx, client) 630 if err != nil { 631 return nil 632 } 633 634 // Track the stale gvs in array for nice diff output upon test failure 635 staleGVs := []metav1.GroupVersion{} 636 637 // Iterate through input so order does not matter 638 for _, targetGv := range gvs { 639 entry := FindGroupVersionV2(document, targetGv) 640 if entry == nil { 641 continue 642 } 643 644 switch entry.Freshness { 645 case apidiscoveryv2.DiscoveryFreshnessCurrent: 646 // Skip 647 case apidiscoveryv2.DiscoveryFreshnessStale: 648 staleGVs = append(staleGVs, targetGv) 649 default: 650 return fmt.Errorf("unrecognized freshness '%v' on gv '%v'", entry.Freshness, targetGv) 651 } 652 } 653 654 if !(len(staleGVs) == 0 && len(gvs) == 0) && !reflect.DeepEqual(staleGVs, gvs) { 655 diff := cmp.Diff(staleGVs, gvs) 656 return fmt.Errorf("expected sets of stale gvs to be equal:\n%v", diff) 657 } 658 659 return nil 660 }) 661 } 662 663 runTestCases(t, []testCase{ 664 { 665 Name: "BuiltinsFresh", 666 Actions: []testAction{ 667 // Wait for discovery ready 668 waitForGroupVersionsV2{metav1.GroupVersion(apiregistrationv1.SchemeGroupVersion)}, 669 // Require there are no stale groupversions and no unrecognized 670 // GVs 671 requireStaleGVs(), 672 }, 673 }, 674 { 675 // CRD freshness is always current 676 Name: "CRDFresh", 677 Actions: []testAction{ 678 // Add a CRD and wait for it to appear in discovery 679 applyCRD(makeCRDSpec(stableGroup, "Foo", false, []string{"v1", "v1alpha1", "v1beta1", "v2"})), 680 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 681 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), 682 683 // Test CRD is current by requiring there is nothing stale 684 requireStaleGVs(), 685 }, 686 }, 687 { 688 // Make an aggregated APIService that's unreachable and show 689 // that its groupversion is included in the discovery document as 690 // stale 691 Name: "AggregatedUnreachable", 692 Actions: []testAction{ 693 applyAPIService{ 694 Group: stableGroup, 695 Version: "v1", 696 GroupPriorityMinimum: 1000, 697 VersionPriority: 15, 698 Service: &apiregistrationv1.ServiceReference{ 699 Name: "doesnt-exist", 700 Namespace: "default", 701 }, 702 }, 703 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1}), 704 // Require there is one and only one stale GV and it is stableV1 705 requireStaleGVs(stableV1), 706 }, 707 }, 708 }) 709 710 } 711 712 // Shows a group for which multiple APIServices specify a GroupPriorityMinimum, 713 // it is sorted the same in both versions of discovery 714 func TestGroupPriority(t *testing.T) { 715 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true) 716 717 makeApiServiceSpec := func(gv metav1.GroupVersion, groupPriorityMin, versionPriority int) apiregistrationv1.APIServiceSpec { 718 return apiregistrationv1.APIServiceSpec{ 719 Group: gv.Group, 720 Version: gv.Version, 721 InsecureSkipTLSVerify: true, 722 GroupPriorityMinimum: int32(groupPriorityMin), 723 VersionPriority: int32(versionPriority), 724 Service: &apiregistrationv1.ServiceReference{ 725 Name: "unused", 726 Namespace: "default", 727 }, 728 } 729 } 730 731 checkGVOrder := inlineAction(func(ctx context.Context, client testClient) (err error) { 732 // Fetch v1 document and v2 document, and ensure they have 733 // equal orderings of groupversions. and nothing missing or 734 // extra. 735 v1GroupsAndVersions, err := FetchV1DiscoveryGroups(ctx, client) 736 if err != nil { 737 return err 738 } 739 v2GroupsAndVersions, err := FetchV2Discovery(ctx, client) 740 if err != nil { 741 return err 742 } 743 744 v1Gvs := []metav1.GroupVersion{} 745 v2Gvs := []metav1.GroupVersion{} 746 747 for _, group := range v1GroupsAndVersions.Groups { 748 for _, version := range group.Versions { 749 v1Gvs = append(v1Gvs, metav1.GroupVersion{ 750 Group: group.Name, 751 Version: version.Version, 752 }) 753 } 754 } 755 756 for _, group := range v2GroupsAndVersions.Items { 757 for _, version := range group.Versions { 758 v2Gvs = append(v2Gvs, metav1.GroupVersion{ 759 Group: group.Name, 760 Version: version.Version, 761 }) 762 } 763 } 764 765 if !reflect.DeepEqual(v1Gvs, v2Gvs) { 766 return fmt.Errorf("expected equal orderings and lists of groupversions in both v1 and v2 discovery:\n%v", cmp.Diff(v1Gvs, v2Gvs)) 767 } 768 769 return nil 770 }) 771 772 runTestCases(t, []testCase{ 773 { 774 // Show that the legacy and aggregated discovery docs have the same 775 // set of builtin groupversions 776 Name: "BuiltinsAndOrdering", 777 Actions: []testAction{ 778 waitForGroupVersionsV1{metav1.GroupVersion(apiregistrationv1.SchemeGroupVersion)}, 779 waitForGroupVersionsV2{metav1.GroupVersion(apiregistrationv1.SchemeGroupVersion)}, 780 checkGVOrder, 781 }, 782 }, 783 { 784 // Show that a very high priority group is sorted first (below apiregistration v1) 785 // Also show the ordering is same for both v1 and v2 discovery apis 786 // Does not vary version priority 787 Name: "HighGroupPriority", 788 Actions: []testAction{ 789 // A VERY high priority which should take precedence 790 // 20000 is highest possible priority 791 applyAPIService(makeApiServiceSpec(stableV1, 20000, 15)), 792 // A VERY low priority which should be ignored 793 applyAPIService(makeApiServiceSpec(stableV1alpha1, 1, 15)), 794 // A medium-high priority (that conflicts with k8s) which should be ignored 795 applyAPIService(makeApiServiceSpec(stableV1alpha2, 17300, 15)), 796 // Wait for all the added group-versions to appear in both discovery documents 797 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1alpha2}), 798 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1alpha2}), 799 // Check that both v1 and v2 endpoints have exactly the same 800 // sets of groupversions 801 checkGVOrder, 802 // Check that the first group-version is the one with the highest 803 // priority 804 inlineAction(func(ctx context.Context, client testClient) error { 805 v2GroupsAndVersions, err := FetchV2Discovery(ctx, client) 806 if err != nil { 807 return err 808 } 809 810 // First group should always be apiregistration.k8s.io 811 secondGV := metav1.GroupVersion{ 812 Group: v2GroupsAndVersions.Items[1].Name, 813 Version: v2GroupsAndVersions.Items[1].Versions[0].Version, 814 } 815 816 if !reflect.DeepEqual(&stableV1, &secondGV) { 817 return fmt.Errorf("expected second group's first version to be %v, not %v", stableV1, secondGV) 818 } 819 820 return nil 821 }), 822 }, 823 }, 824 { 825 // Show that a very low group priority is ordered last 826 Name: "LowGroupPriority", 827 Actions: []testAction{ 828 // A minimal priority 829 applyAPIService(makeApiServiceSpec(stableV1alpha1, 1, 15)), 830 // Wait for all the added group-versions to appear in v2 discovery 831 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1alpha1}), 832 // Check that the last group-version is the one with the lowest 833 // priority 834 inlineAction(func(ctx context.Context, client testClient) error { 835 v2GroupsAndVersions, err := FetchV2Discovery(ctx, client) 836 if err != nil { 837 return err 838 } 839 lastGroup := v2GroupsAndVersions.Items[len(v2GroupsAndVersions.Items)-1] 840 841 lastGV := metav1.GroupVersion{ 842 Group: lastGroup.Name, 843 Version: lastGroup.Versions[0].Version, 844 } 845 846 if !reflect.DeepEqual(&stableV1alpha1, &lastGV) { 847 return fmt.Errorf("expected last group to be %v, not %v", stableV1alpha1, lastGV) 848 } 849 850 return nil 851 }), 852 // Wait for all the added group-versions to appear in both discovery documents 853 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1alpha1}), 854 // Check that both v1 and v2 endpoints have exactly the same 855 // sets of groupversions 856 checkGVOrder, 857 }, 858 }, 859 { 860 // Show that versions within a group are sorted by priority 861 Name: "VersionPriority", 862 Actions: []testAction{ 863 applyAPIService(makeApiServiceSpec(stableV1, 1000, 2)), 864 applyAPIService(makeApiServiceSpec(stableV1alpha1, 1000, 1)), 865 applyAPIService(makeApiServiceSpec(stableV1alpha2, 1000, 3)), 866 // Wait for all the added group-versions to appear in both discovery documents 867 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1alpha2}), 868 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1alpha2}), 869 // Check that both v1 and v2 endpoints have exactly the same 870 // sets of groupversions 871 checkGVOrder, 872 inlineAction(func(ctx context.Context, client testClient) error { 873 // Find the entry for stable.example.com 874 // and show the versions are ordered how we expect 875 v2GroupsAndVersions, err := FetchV2Discovery(ctx, client) 876 if err != nil { 877 return err 878 } 879 880 // Should be ordered last for this test 881 group := v2GroupsAndVersions.Items[len(v2GroupsAndVersions.Items)-1] 882 if group.Name != stableGroup { 883 return fmt.Errorf("group is not where we expect: found %v, expected %v", group.Name, stableGroup) 884 } 885 886 versionOrder := []string{} 887 for _, version := range group.Versions { 888 versionOrder = append(versionOrder, version.Version) 889 } 890 891 expectedOrder := []string{ 892 stableV1alpha2.Version, 893 stableV1.Version, 894 stableV1alpha1.Version, 895 } 896 897 if !reflect.DeepEqual(expectedOrder, versionOrder) { 898 return fmt.Errorf("version in wrong order: %v", cmp.Diff(expectedOrder, versionOrder)) 899 } 900 901 return nil 902 }), 903 }, 904 }, 905 { 906 // Show that versions within a group are sorted by priority 907 // and that equal versions will be sorted by a kube-aware version 908 // comparator 909 Name: "VersionPriorityTiebreaker", 910 Actions: []testAction{ 911 applyAPIService(makeApiServiceSpec(stableV1, 1000, 15)), 912 applyAPIService(makeApiServiceSpec(stableV1alpha1, 1000, 15)), 913 applyAPIService(makeApiServiceSpec(stableV1alpha2, 1000, 15)), 914 applyAPIService(makeApiServiceSpec(stableV1beta1, 1000, 15)), 915 applyAPIService(makeApiServiceSpec(stableV2, 1000, 15)), 916 // Wait for all the added group-versions to appear in both discovery documents 917 waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1alpha2, stableV1beta1, stableV2}), 918 waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1alpha2, stableV1beta1, stableV2}), 919 // Check that both v1 and v2 endpoints have exactly the same 920 // sets of groupversions 921 checkGVOrder, 922 inlineAction(func(ctx context.Context, client testClient) error { 923 // Find the entry for stable.example.com 924 // and show the versions are ordered how we expect 925 v2GroupsAndVersions, err := FetchV2Discovery(ctx, client) 926 if err != nil { 927 return err 928 } 929 930 // Should be ordered last for this test 931 group := v2GroupsAndVersions.Items[len(v2GroupsAndVersions.Items)-1] 932 if group.Name != stableGroup { 933 return fmt.Errorf("group is not where we expect: found %v, expected %v", group.Name, stableGroup) 934 } 935 936 versionOrder := []string{} 937 for _, version := range group.Versions { 938 versionOrder = append(versionOrder, version.Version) 939 } 940 941 expectedOrder := []string{ 942 stableV2.Version, 943 stableV1.Version, 944 stableV1beta1.Version, 945 stableV1alpha2.Version, 946 stableV1alpha1.Version, 947 } 948 949 if !reflect.DeepEqual(expectedOrder, versionOrder) { 950 return fmt.Errorf("version in wrong order: %v", cmp.Diff(expectedOrder, versionOrder)) 951 } 952 953 return nil 954 }), 955 }, 956 }, 957 }) 958 } 959 960 func TestSingularNames(t *testing.T) { 961 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--runtime-config=api/all=true"}, framework.SharedEtcd()) 962 t.Cleanup(server.TearDownFn) 963 964 kubeClientSet, err := kubernetes.NewForConfig(server.ClientConfig) 965 require.NoError(t, err) 966 967 _, resources, err := kubeClientSet.Discovery().ServerGroupsAndResources() 968 require.NoError(t, err) 969 970 for _, rr := range resources { 971 for _, r := range rr.APIResources { 972 if strings.Contains(r.Name, "/") { 973 continue 974 } 975 if r.SingularName == "" { 976 t.Errorf("missing singularName for resource %q in %q", r.Name, rr.GroupVersion) 977 continue 978 } 979 if r.SingularName != strings.ToLower(r.Kind) { 980 t.Errorf("expected singularName for resource %q in %q to be %q, got %q", r.Name, rr.GroupVersion, strings.ToLower(r.Kind), r.SingularName) 981 continue 982 } 983 } 984 } 985 } 986 987 func makeCRDSpec(group string, kind string, namespaced bool, versions []string, categories ...string) apiextensionsv1.CustomResourceDefinitionSpec { 988 scope := apiextensionsv1.NamespaceScoped 989 if !namespaced { 990 scope = apiextensionsv1.ClusterScoped 991 } 992 993 plural, singular := meta.UnsafeGuessKindToResource(schema.GroupVersionKind{Kind: kind}) 994 res := apiextensionsv1.CustomResourceDefinitionSpec{ 995 Group: group, 996 Scope: scope, 997 Names: apiextensionsv1.CustomResourceDefinitionNames{ 998 Plural: plural.Resource, 999 Singular: singular.Resource, 1000 Kind: kind, 1001 Categories: categories, 1002 }, 1003 } 1004 1005 for i, version := range versions { 1006 res.Versions = append(res.Versions, apiextensionsv1.CustomResourceDefinitionVersion{ 1007 Name: version, 1008 Served: true, 1009 Storage: i == 0, 1010 Schema: &apiextensionsv1.CustomResourceValidation{ 1011 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1012 Type: "object", 1013 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1014 "data": { 1015 Type: "string", 1016 }, 1017 }, 1018 }, 1019 }, 1020 }) 1021 } 1022 return res 1023 }