k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/examples/apiserver_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 apiserver 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "net" 24 "net/http" 25 "net/http/httptest" 26 "net/url" 27 "os" 28 "path" 29 "reflect" 30 "sort" 31 "strings" 32 "testing" 33 "time" 34 35 "github.com/stretchr/testify/assert" 36 37 corev1 "k8s.io/api/core/v1" 38 apierrors "k8s.io/apimachinery/pkg/api/errors" 39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 "k8s.io/apimachinery/pkg/util/wait" 41 "k8s.io/apiserver/pkg/server/dynamiccertificates" 42 genericapiserveroptions "k8s.io/apiserver/pkg/server/options" 43 client "k8s.io/client-go/kubernetes" 44 "k8s.io/client-go/rest" 45 "k8s.io/client-go/tools/clientcmd" 46 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 47 "k8s.io/client-go/util/cert" 48 apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" 49 aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" 50 "k8s.io/kubernetes/cmd/kube-apiserver/app" 51 kastesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 52 "k8s.io/kubernetes/test/integration" 53 "k8s.io/kubernetes/test/integration/framework" 54 wardlev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1" 55 wardlev1beta1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1" 56 sampleserver "k8s.io/sample-apiserver/pkg/cmd/server" 57 wardlev1alpha1client "k8s.io/sample-apiserver/pkg/generated/clientset/versioned/typed/wardle/v1alpha1" 58 netutils "k8s.io/utils/net" 59 ) 60 61 func TestAPIServiceWaitOnStart(t *testing.T) { 62 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 63 t.Cleanup(cancel) 64 65 etcdConfig := framework.SharedEtcd() 66 67 etcd3Client, _, err := integration.GetEtcdClients(etcdConfig.Transport) 68 if err != nil { 69 t.Fatal(err) 70 } 71 t.Cleanup(func() { etcd3Client.Close() }) 72 73 t.Log("Pollute CRD path in etcd so CRD lists cannot succeed and the informer cannot sync") 74 bogusCRDEtcdPath := path.Join("/", etcdConfig.Prefix, "apiextensions.k8s.io/customresourcedefinitions/bogus") 75 if _, err := etcd3Client.KV.Put(ctx, bogusCRDEtcdPath, `bogus data`); err != nil { 76 t.Fatal(err) 77 } 78 79 t.Log("Populate a valid CRD and managed APIService in etcd") 80 if _, err := etcd3Client.KV.Put( 81 ctx, 82 path.Join("/", etcdConfig.Prefix, "apiextensions.k8s.io/customresourcedefinitions/widgets.valid.example.com"), 83 `{ 84 "apiVersion":"apiextensions.k8s.io/v1beta1", 85 "kind":"CustomResourceDefinition", 86 "metadata":{ 87 "name":"widgets.valid.example.com", 88 "uid":"mycrd", 89 "creationTimestamp": "2022-06-08T23:46:32Z" 90 }, 91 "spec":{ 92 "scope": "Namespaced", 93 "group":"valid.example.com", 94 "version":"v1", 95 "names":{ 96 "kind": "Widget", 97 "listKind": "WidgetList", 98 "plural": "widgets", 99 "singular": "widget" 100 } 101 }, 102 "status": { 103 "acceptedNames": { 104 "kind": "Widget", 105 "listKind": "WidgetList", 106 "plural": "widgets", 107 "singular": "widget" 108 }, 109 "conditions": [ 110 { 111 "lastTransitionTime": "2023-05-18T15:03:57Z", 112 "message": "no conflicts found", 113 "reason": "NoConflicts", 114 "status": "True", 115 "type": "NamesAccepted" 116 }, 117 { 118 "lastTransitionTime": "2023-05-18T15:03:57Z", 119 "message": "the initial names have been accepted", 120 "reason": "InitialNamesAccepted", 121 "status": "True", 122 "type": "Established" 123 } 124 ], 125 "storedVersions": [ 126 "v1" 127 ] 128 } 129 }`); err != nil { 130 t.Fatal(err) 131 } 132 if _, err := etcd3Client.KV.Put( 133 ctx, 134 path.Join("/", etcdConfig.Prefix, "apiregistration.k8s.io/apiservices/v1.valid.example.com"), 135 `{ 136 "apiVersion":"apiregistration.k8s.io/v1", 137 "kind":"APIService", 138 "metadata": { 139 "name": "v1.valid.example.com", 140 "uid":"foo", 141 "creationTimestamp": "2022-06-08T23:46:32Z", 142 "labels":{"kube-aggregator.kubernetes.io/automanaged":"true"} 143 }, 144 "spec": { 145 "group": "valid.example.com", 146 "version": "v1", 147 "groupPriorityMinimum":100, 148 "versionPriority":10 149 } 150 }`, 151 ); err != nil { 152 t.Fatal(err) 153 } 154 155 t.Log("Populate a stale managed APIService in etcd") 156 if _, err := etcd3Client.KV.Put( 157 ctx, 158 path.Join("/", etcdConfig.Prefix, "apiregistration.k8s.io/apiservices/v1.stale.example.com"), 159 `{ 160 "apiVersion":"apiregistration.k8s.io/v1", 161 "kind":"APIService", 162 "metadata": { 163 "name": "v1.stale.example.com", 164 "uid":"foo", 165 "creationTimestamp": "2022-06-08T23:46:32Z", 166 "labels":{"kube-aggregator.kubernetes.io/automanaged":"true"} 167 }, 168 "spec": { 169 "group": "stale.example.com", 170 "version": "v1", 171 "groupPriorityMinimum":100, 172 "versionPriority":10 173 } 174 }`, 175 ); err != nil { 176 t.Fatal(err) 177 } 178 179 t.Log("Starting server") 180 options := kastesting.NewDefaultTestServerOptions() 181 options.SkipHealthzCheck = true 182 testServer := kastesting.StartTestServerOrDie(t, options, nil, etcdConfig) 183 defer testServer.TearDownFn() 184 185 kubeClientConfig := rest.CopyConfig(testServer.ClientConfig) 186 aggregatorClient := aggregatorclient.NewForConfigOrDie(kubeClientConfig) 187 188 t.Log("Ensure both APIService objects remain") 189 for i := 0; i < 10; i++ { 190 if _, err := aggregatorClient.ApiregistrationV1().APIServices().Get(ctx, "v1.valid.example.com", metav1.GetOptions{}); err != nil { 191 t.Fatal(err) 192 } 193 if _, err := aggregatorClient.ApiregistrationV1().APIServices().Get(ctx, "v1.stale.example.com", metav1.GetOptions{}); err != nil { 194 t.Fatal(err) 195 } 196 time.Sleep(time.Second) 197 } 198 199 t.Log("Clear the bogus CRD data so the informer can sync") 200 if _, err := etcd3Client.KV.Delete(ctx, bogusCRDEtcdPath); err != nil { 201 t.Fatal(err) 202 } 203 204 t.Log("Ensure the stale APIService object is cleaned up") 205 if err := wait.Poll(time.Second, wait.ForeverTestTimeout, func() (bool, error) { 206 _, err := aggregatorClient.ApiregistrationV1().APIServices().Get(ctx, "v1.stale.example.com", metav1.GetOptions{}) 207 if err == nil { 208 t.Log("stale APIService still exists, waiting...") 209 return false, nil 210 } 211 if !apierrors.IsNotFound(err) { 212 return false, err 213 } 214 return true, nil 215 }); err != nil { 216 t.Fatal(err) 217 } 218 219 t.Log("Ensure the valid APIService object remains") 220 for i := 0; i < 5; i++ { 221 time.Sleep(time.Second) 222 if _, err := aggregatorClient.ApiregistrationV1().APIServices().Get(ctx, "v1.valid.example.com", metav1.GetOptions{}); err != nil { 223 t.Fatal(err) 224 } 225 } 226 } 227 228 func TestAggregatedAPIServer(t *testing.T) { 229 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 230 t.Cleanup(cancel) 231 232 // makes the kube-apiserver very responsive. it's normally a minute 233 dynamiccertificates.FileRefreshDuration = 1 * time.Second 234 235 // we need the wardle port information first to set up the service resolver 236 listener, wardlePort, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0", net.ListenConfig{}) 237 if err != nil { 238 t.Fatal(err) 239 } 240 // endpoints cannot have loopback IPs so we need to override the resolver itself 241 t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://127.0.0.1:%d", wardlePort)))) 242 243 testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: true}, nil, framework.SharedEtcd()) 244 defer testServer.TearDownFn() 245 kubeClientConfig := rest.CopyConfig(testServer.ClientConfig) 246 // force json because everything speaks it 247 kubeClientConfig.ContentType = "" 248 kubeClientConfig.AcceptContentTypes = "" 249 kubeClient := client.NewForConfigOrDie(kubeClientConfig) 250 aggregatorClient := aggregatorclient.NewForConfigOrDie(kubeClientConfig) 251 wardleClient := wardlev1alpha1client.NewForConfigOrDie(kubeClientConfig) 252 253 // create the bare minimum resources required to be able to get the API service into an available state 254 _, err = kubeClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ 255 ObjectMeta: metav1.ObjectMeta{ 256 Name: "kube-wardle", 257 }, 258 }, metav1.CreateOptions{}) 259 if err != nil { 260 t.Fatal(err) 261 } 262 _, err = kubeClient.CoreV1().Services("kube-wardle").Create(ctx, &corev1.Service{ 263 ObjectMeta: metav1.ObjectMeta{ 264 Name: "api", 265 }, 266 Spec: corev1.ServiceSpec{ 267 ExternalName: "needs-to-be-non-empty", 268 Type: corev1.ServiceTypeExternalName, 269 }, 270 }, metav1.CreateOptions{}) 271 if err != nil { 272 t.Fatal(err) 273 } 274 275 // start the wardle server to prove we can aggregate it 276 wardleToKASKubeConfigFile := writeKubeConfigForWardleServerToKASConnection(t, rest.CopyConfig(kubeClientConfig)) 277 defer os.Remove(wardleToKASKubeConfigFile) 278 wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server") 279 defer os.RemoveAll(wardleCertDir) 280 go func() { 281 o := sampleserver.NewWardleServerOptions(os.Stdout, os.Stderr) 282 // ensure this is a SAN on the generated cert for service FQDN 283 o.AlternateDNS = []string{ 284 "api.kube-wardle.svc", 285 } 286 o.RecommendedOptions.SecureServing.Listener = listener 287 o.RecommendedOptions.SecureServing.BindAddress = netutils.ParseIPSloppy("127.0.0.1") 288 wardleCmd := sampleserver.NewCommandStartWardleServer(ctx, o) 289 wardleCmd.SetArgs([]string{ 290 "--authentication-kubeconfig", wardleToKASKubeConfigFile, 291 "--authorization-kubeconfig", wardleToKASKubeConfigFile, 292 "--etcd-servers", framework.GetEtcdURL(), 293 "--cert-dir", wardleCertDir, 294 "--kubeconfig", wardleToKASKubeConfigFile, 295 }) 296 if err := wardleCmd.Execute(); err != nil { 297 t.Error(err) 298 } 299 }() 300 directWardleClientConfig, err := waitForWardleRunning(ctx, t, kubeClientConfig, wardleCertDir, wardlePort) 301 if err != nil { 302 t.Fatal(err) 303 } 304 305 // now we're finally ready to test. These are what's run by default now 306 wardleDirectClient := client.NewForConfigOrDie(directWardleClientConfig) 307 testAPIGroupList(ctx, t, wardleDirectClient.Discovery().RESTClient()) 308 testAPIGroup(ctx, t, wardleDirectClient.Discovery().RESTClient()) 309 testAPIResourceList(ctx, t, wardleDirectClient.Discovery().RESTClient()) 310 311 wardleCA, err := os.ReadFile(directWardleClientConfig.CAFile) 312 if err != nil { 313 t.Fatal(err) 314 } 315 _, err = aggregatorClient.ApiregistrationV1().APIServices().Create(ctx, &apiregistrationv1.APIService{ 316 ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.wardle.example.com"}, 317 Spec: apiregistrationv1.APIServiceSpec{ 318 Service: &apiregistrationv1.ServiceReference{ 319 Namespace: "kube-wardle", 320 Name: "api", 321 }, 322 Group: "wardle.example.com", 323 Version: "v1alpha1", 324 CABundle: wardleCA, 325 GroupPriorityMinimum: 200, 326 VersionPriority: 200, 327 }, 328 }, metav1.CreateOptions{}) 329 if err != nil { 330 t.Fatal(err) 331 } 332 333 // wait for the API service to be available 334 err = wait.Poll(time.Second, wait.ForeverTestTimeout, func() (done bool, err error) { 335 apiService, err := aggregatorClient.ApiregistrationV1().APIServices().Get(ctx, "v1alpha1.wardle.example.com", metav1.GetOptions{}) 336 if err != nil { 337 return false, err 338 } 339 var available bool 340 for _, condition := range apiService.Status.Conditions { 341 if condition.Type == apiregistrationv1.Available && condition.Status == apiregistrationv1.ConditionTrue { 342 available = true 343 break 344 } 345 } 346 if !available { 347 t.Log("api service is not available", apiService.Status.Conditions) 348 return false, nil 349 } 350 351 // make sure discovery is healthy overall 352 _, _, err = kubeClient.Discovery().ServerGroupsAndResources() 353 if err != nil { 354 t.Log("discovery failed", err) 355 return false, nil 356 } 357 358 // make sure we have the wardle resources in discovery 359 apiResources, err := kubeClient.Discovery().ServerResourcesForGroupVersion("wardle.example.com/v1alpha1") 360 if err != nil { 361 t.Log("wardle discovery failed", err) 362 return false, nil 363 } 364 if len(apiResources.APIResources) != 2 { 365 t.Log("wardle discovery has wrong resources", apiResources.APIResources) 366 return false, nil 367 } 368 resources := make([]string, 0, 2) 369 for _, resource := range apiResources.APIResources { 370 resource := resource 371 resources = append(resources, resource.Name) 372 } 373 sort.Strings(resources) 374 if !reflect.DeepEqual([]string{"fischers", "flunders"}, resources) { 375 return false, fmt.Errorf("unexpected resources: %v", resources) 376 } 377 378 return true, nil 379 }) 380 if err != nil { 381 t.Fatal(err) 382 } 383 384 // perform simple CRUD operations against the wardle resources 385 _, err = wardleClient.Fischers().Create(ctx, &wardlev1alpha1.Fischer{ 386 ObjectMeta: metav1.ObjectMeta{ 387 Name: "panda", 388 }, 389 }, metav1.CreateOptions{}) 390 if err != nil { 391 t.Fatal(err) 392 } 393 fischersList, err := wardleClient.Fischers().List(ctx, metav1.ListOptions{}) 394 if err != nil { 395 t.Fatal(err) 396 } 397 if len(fischersList.Items) != 1 { 398 t.Errorf("expected one fischer: %#v", fischersList.Items) 399 } 400 if len(fischersList.ResourceVersion) == 0 { 401 t.Error("expected non-empty resource version for fischer list") 402 } 403 404 _, err = wardleClient.Flunders(metav1.NamespaceSystem).Create(ctx, &wardlev1alpha1.Flunder{ 405 ObjectMeta: metav1.ObjectMeta{ 406 Name: "panda", 407 }, 408 }, metav1.CreateOptions{}) 409 flunderList, err := wardleClient.Flunders(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{}) 410 if err != nil { 411 t.Fatal(err) 412 } 413 if len(flunderList.Items) != 1 { 414 t.Errorf("expected one flunder: %#v", flunderList.Items) 415 } 416 if len(flunderList.ResourceVersion) == 0 { 417 t.Error("expected non-empty resource version for flunder list") 418 } 419 420 // Since ClientCAs are provided by "client-ca::kube-system::extension-apiserver-authentication::client-ca-file" controller 421 // we need to wait until it picks up the configmap (via a lister) otherwise the response might contain an empty result. 422 // The following code waits up to ForeverTestTimeout seconds for ClientCA to show up otherwise it fails 423 // maybe in the future this could be wired into the /readyz EP 424 425 // Now we want to verify that the client CA bundles properly reflect the values for the cluster-authentication 426 var firstKubeCANames []string 427 err = wait.Poll(1*time.Second, wait.ForeverTestTimeout, func() (done bool, err error) { 428 firstKubeCANames, err = cert.GetClientCANamesForURL(kubeClientConfig.Host) 429 if err != nil { 430 return false, err 431 } 432 return len(firstKubeCANames) != 0, nil 433 }) 434 if err != nil { 435 t.Fatal(err) 436 } 437 t.Log(firstKubeCANames) 438 var firstWardleCANames []string 439 err = wait.Poll(1*time.Second, wait.ForeverTestTimeout, func() (done bool, err error) { 440 firstWardleCANames, err = cert.GetClientCANamesForURL(directWardleClientConfig.Host) 441 if err != nil { 442 return false, err 443 } 444 return len(firstWardleCANames) != 0, nil 445 }) 446 if err != nil { 447 t.Fatal(err) 448 } 449 t.Log(firstWardleCANames) 450 // Now we want to verify that the client CA bundles properly reflect the values for the cluster-authentication 451 if !reflect.DeepEqual(firstKubeCANames, firstWardleCANames) { 452 t.Fatal("names don't match") 453 } 454 455 // now we update the client-ca nd request-header-client-ca-file and the kas will consume it, update the configmap 456 // and then the wardle server will detect and update too. 457 if err := os.WriteFile(path.Join(testServer.TmpDir, "client-ca.crt"), differentClientCA, 0644); err != nil { 458 t.Fatal(err) 459 } 460 if err := os.WriteFile(path.Join(testServer.TmpDir, "proxy-ca.crt"), differentFrontProxyCA, 0644); err != nil { 461 t.Fatal(err) 462 } 463 // wait for it to be picked up. there's a test in certreload_test.go that ensure this works 464 time.Sleep(4 * time.Second) 465 466 // Now we want to verify that the client CA bundles properly updated to reflect the new values written for the kube-apiserver 467 secondKubeCANames, err := cert.GetClientCANamesForURL(kubeClientConfig.Host) 468 if err != nil { 469 t.Fatal(err) 470 } 471 t.Log(secondKubeCANames) 472 for i := range firstKubeCANames { 473 if firstKubeCANames[i] == secondKubeCANames[i] { 474 t.Errorf("ca bundles should change") 475 } 476 } 477 secondWardleCANames, err := cert.GetClientCANamesForURL(directWardleClientConfig.Host) 478 if err != nil { 479 t.Fatal(err) 480 } 481 t.Log(secondWardleCANames) 482 483 // second wardle should contain all the certs, first and last 484 numMatches := 0 485 for _, needle := range firstKubeCANames { 486 for _, haystack := range secondWardleCANames { 487 if needle == haystack { 488 numMatches++ 489 break 490 } 491 } 492 } 493 for _, needle := range secondKubeCANames { 494 for _, haystack := range secondWardleCANames { 495 if needle == haystack { 496 numMatches++ 497 break 498 } 499 } 500 } 501 if numMatches != 4 { 502 t.Fatal("names don't match") 503 } 504 } 505 506 func waitForWardleRunning(ctx context.Context, t *testing.T, wardleToKASKubeConfig *rest.Config, wardleCertDir string, wardlePort int) (*rest.Config, error) { 507 directWardleClientConfig := rest.AnonymousClientConfig(rest.CopyConfig(wardleToKASKubeConfig)) 508 directWardleClientConfig.CAFile = path.Join(wardleCertDir, "apiserver.crt") 509 directWardleClientConfig.CAData = nil 510 directWardleClientConfig.ServerName = "" 511 directWardleClientConfig.BearerToken = wardleToKASKubeConfig.BearerToken 512 var wardleClient client.Interface 513 lastHealthContent := []byte{} 514 var lastHealthErr error 515 err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) { 516 if _, err := os.Stat(directWardleClientConfig.CAFile); os.IsNotExist(err) { // wait until the file trust is created 517 lastHealthErr = err 518 return false, nil 519 } 520 directWardleClientConfig.Host = fmt.Sprintf("https://127.0.0.1:%d", wardlePort) 521 wardleClient, err = client.NewForConfig(directWardleClientConfig) 522 if err != nil { 523 // this happens because we race the API server start 524 t.Log(err) 525 return false, nil 526 } 527 healthStatus := 0 528 result := wardleClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do(ctx).StatusCode(&healthStatus) 529 lastHealthContent, lastHealthErr = result.Raw() 530 if healthStatus != http.StatusOK { 531 return false, nil 532 } 533 return true, nil 534 }) 535 if err != nil { 536 t.Log(string(lastHealthContent)) 537 t.Log(lastHealthErr) 538 return nil, err 539 } 540 541 return directWardleClientConfig, nil 542 } 543 544 func TestAggregatedAPIServerRejectRedirectResponse(t *testing.T) { 545 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 546 t.Cleanup(cancel) 547 548 backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 549 w.WriteHeader(http.StatusOK) 550 if strings.HasSuffix(r.URL.Path, "redirectTarget") { 551 t.Errorf("backend called unexpectedly") 552 } 553 })) 554 defer backendServer.Close() 555 556 redirectedURL := backendServer.URL 557 redirectServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 558 if strings.HasSuffix(r.URL.Path, "tryRedirect") { 559 http.Redirect(w, r, redirectedURL+"/redirectTarget", http.StatusMovedPermanently) 560 } else { 561 w.WriteHeader(http.StatusOK) 562 } 563 })) 564 defer redirectServer.Close() 565 566 // endpoints cannot have loopback IPs so we need to override the resolver itself 567 t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://%s", redirectServer.Listener.Addr().String())))) 568 569 // start the server after resolver is overwritten 570 redirectServer.StartTLS() 571 572 testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: false}, nil, framework.SharedEtcd()) 573 defer testServer.TearDownFn() 574 kubeClientConfig := rest.CopyConfig(testServer.ClientConfig) 575 // force json because everything speaks it 576 kubeClientConfig.ContentType = "" 577 kubeClientConfig.AcceptContentTypes = "" 578 kubeClient := client.NewForConfigOrDie(kubeClientConfig) 579 aggregatorClient := aggregatorclient.NewForConfigOrDie(kubeClientConfig) 580 581 // create the bare minimum resources required to be able to get the API service into an available state 582 _, err := kubeClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ 583 ObjectMeta: metav1.ObjectMeta{ 584 Name: "kube-redirect", 585 }, 586 }, metav1.CreateOptions{}) 587 if err != nil { 588 t.Fatal(err) 589 } 590 _, err = kubeClient.CoreV1().Services("kube-redirect").Create(ctx, &corev1.Service{ 591 ObjectMeta: metav1.ObjectMeta{ 592 Name: "api", 593 }, 594 Spec: corev1.ServiceSpec{ 595 ExternalName: "needs-to-be-non-empty", 596 Type: corev1.ServiceTypeExternalName, 597 }, 598 }, metav1.CreateOptions{}) 599 if err != nil { 600 t.Fatal(err) 601 } 602 603 _, err = aggregatorClient.ApiregistrationV1().APIServices().Create(ctx, &apiregistrationv1.APIService{ 604 ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.reject.redirect.example.com"}, 605 Spec: apiregistrationv1.APIServiceSpec{ 606 Service: &apiregistrationv1.ServiceReference{ 607 Namespace: "kube-redirect", 608 Name: "api", 609 }, 610 Group: "reject.redirect.example.com", 611 Version: "v1alpha1", 612 GroupPriorityMinimum: 200, 613 VersionPriority: 200, 614 InsecureSkipTLSVerify: true, 615 }, 616 }, metav1.CreateOptions{}) 617 if err != nil { 618 t.Fatal(err) 619 } 620 621 // wait for the API service to be available 622 err = wait.Poll(time.Second, wait.ForeverTestTimeout, func() (done bool, err error) { 623 apiService, err := aggregatorClient.ApiregistrationV1().APIServices().Get(ctx, "v1alpha1.reject.redirect.example.com", metav1.GetOptions{}) 624 if err != nil { 625 return false, err 626 } 627 var available bool 628 for _, condition := range apiService.Status.Conditions { 629 if condition.Type == apiregistrationv1.Available && condition.Status == apiregistrationv1.ConditionTrue { 630 available = true 631 break 632 } 633 } 634 if !available { 635 t.Log("api service is not available", apiService.Status.Conditions) 636 return false, nil 637 } 638 return available, nil 639 }) 640 if err != nil { 641 t.Errorf("%v", err) 642 } 643 644 // get raw response to check the original error and msg 645 expectedMsg := "the backend attempted to redirect this request, which is not permitted" 646 // add specific request path suffix to discriminate between request from client and generic pings from the aggregator 647 url := url.URL{ 648 Path: "/apis/reject.redirect.example.com/v1alpha1/tryRedirect", 649 } 650 bytes, err := kubeClient.RESTClient().Get().AbsPath(url.String()).DoRaw(context.TODO()) 651 if err == nil { 652 t.Errorf("expect server to reject redirect response, but forwarded") 653 } else if !strings.Contains(string(bytes), expectedMsg) { 654 t.Errorf("expect response contains %s, got %s", expectedMsg, string(bytes)) 655 } 656 } 657 658 func writeKubeConfigForWardleServerToKASConnection(t *testing.T, kubeClientConfig *rest.Config) string { 659 // write a kubeconfig out for starting other API servers with delegated auth. remember, no in-cluster config 660 // the loopback client config uses a loopback cert with different SNI. We need to use the "real" 661 // cert, so we'll hope we aren't hacked during a unit test and instead load it from the server we started. 662 wardleToKASKubeClientConfig := rest.CopyConfig(kubeClientConfig) 663 664 servingCerts, _, err := cert.GetServingCertificatesForURL(wardleToKASKubeClientConfig.Host, "") 665 if err != nil { 666 t.Fatal(err) 667 } 668 encodedServing, err := cert.EncodeCertificates(servingCerts...) 669 if err != nil { 670 t.Fatal(err) 671 } 672 wardleToKASKubeClientConfig.CAData = encodedServing 673 674 for _, v := range servingCerts { 675 t.Logf("Client: Server public key is %v\n", dynamiccertificates.GetHumanCertDetail(v)) 676 } 677 certs, err := cert.ParseCertsPEM(wardleToKASKubeClientConfig.CAData) 678 if err != nil { 679 t.Fatal(err) 680 } 681 for _, curr := range certs { 682 t.Logf("CA bundle %v\n", dynamiccertificates.GetHumanCertDetail(curr)) 683 } 684 685 adminKubeConfig := createKubeConfig(wardleToKASKubeClientConfig) 686 wardleToKASKubeConfigFile, _ := os.CreateTemp("", "") 687 if err := clientcmd.WriteToFile(*adminKubeConfig, wardleToKASKubeConfigFile.Name()); err != nil { 688 t.Fatal(err) 689 } 690 691 defer wardleToKASKubeConfigFile.Close() 692 return wardleToKASKubeConfigFile.Name() 693 } 694 695 func createKubeConfig(clientCfg *rest.Config) *clientcmdapi.Config { 696 clusterNick := "cluster" 697 userNick := "user" 698 contextNick := "context" 699 700 config := clientcmdapi.NewConfig() 701 702 credentials := clientcmdapi.NewAuthInfo() 703 credentials.Token = clientCfg.BearerToken 704 credentials.ClientCertificate = clientCfg.TLSClientConfig.CertFile 705 if len(credentials.ClientCertificate) == 0 { 706 credentials.ClientCertificateData = clientCfg.TLSClientConfig.CertData 707 } 708 credentials.ClientKey = clientCfg.TLSClientConfig.KeyFile 709 if len(credentials.ClientKey) == 0 { 710 credentials.ClientKeyData = clientCfg.TLSClientConfig.KeyData 711 } 712 config.AuthInfos[userNick] = credentials 713 714 cluster := clientcmdapi.NewCluster() 715 cluster.Server = clientCfg.Host 716 cluster.CertificateAuthority = clientCfg.CAFile 717 if len(cluster.CertificateAuthority) == 0 { 718 cluster.CertificateAuthorityData = clientCfg.CAData 719 } 720 cluster.InsecureSkipTLSVerify = clientCfg.Insecure 721 config.Clusters[clusterNick] = cluster 722 723 context := clientcmdapi.NewContext() 724 context.Cluster = clusterNick 725 context.AuthInfo = userNick 726 config.Contexts[contextNick] = context 727 config.CurrentContext = contextNick 728 729 return config 730 } 731 732 func readResponse(ctx context.Context, client rest.Interface, location string) ([]byte, error) { 733 return client.Get().AbsPath(location).DoRaw(ctx) 734 } 735 736 func testAPIGroupList(ctx context.Context, t *testing.T, client rest.Interface) { 737 contents, err := readResponse(ctx, client, "/apis") 738 if err != nil { 739 t.Fatalf("%v", err) 740 } 741 t.Log(string(contents)) 742 var apiGroupList metav1.APIGroupList 743 err = json.Unmarshal(contents, &apiGroupList) 744 if err != nil { 745 t.Fatalf("Error in unmarshalling response from server %s: %v", "/apis", err) 746 } 747 assert.Equal(t, 1, len(apiGroupList.Groups)) 748 assert.Equal(t, wardlev1alpha1.GroupName, apiGroupList.Groups[0].Name) 749 assert.Equal(t, 2, len(apiGroupList.Groups[0].Versions)) 750 751 v1alpha1 := metav1.GroupVersionForDiscovery{ 752 GroupVersion: wardlev1alpha1.SchemeGroupVersion.String(), 753 Version: wardlev1alpha1.SchemeGroupVersion.Version, 754 } 755 v1beta1 := metav1.GroupVersionForDiscovery{ 756 GroupVersion: wardlev1beta1.SchemeGroupVersion.String(), 757 Version: wardlev1beta1.SchemeGroupVersion.Version, 758 } 759 760 assert.Equal(t, v1beta1, apiGroupList.Groups[0].Versions[0]) 761 assert.Equal(t, v1alpha1, apiGroupList.Groups[0].Versions[1]) 762 assert.Equal(t, v1beta1, apiGroupList.Groups[0].PreferredVersion) 763 } 764 765 func testAPIGroup(ctx context.Context, t *testing.T, client rest.Interface) { 766 contents, err := readResponse(ctx, client, "/apis/wardle.example.com") 767 if err != nil { 768 t.Fatalf("%v", err) 769 } 770 t.Log(string(contents)) 771 var apiGroup metav1.APIGroup 772 err = json.Unmarshal(contents, &apiGroup) 773 if err != nil { 774 t.Fatalf("Error in unmarshalling response from server %s: %v", "/apis/wardle.example.com", err) 775 } 776 assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.Group, apiGroup.Name) 777 assert.Equal(t, 2, len(apiGroup.Versions)) 778 assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.String(), apiGroup.Versions[1].GroupVersion) 779 assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.Version, apiGroup.Versions[1].Version) 780 assert.Equal(t, apiGroup.PreferredVersion, apiGroup.Versions[0]) 781 } 782 783 func testAPIResourceList(ctx context.Context, t *testing.T, client rest.Interface) { 784 contents, err := readResponse(ctx, client, "/apis/wardle.example.com/v1alpha1") 785 if err != nil { 786 t.Fatalf("%v", err) 787 } 788 t.Log(string(contents)) 789 var apiResourceList metav1.APIResourceList 790 err = json.Unmarshal(contents, &apiResourceList) 791 if err != nil { 792 t.Fatalf("Error in unmarshalling response from server %s: %v", "/apis/wardle.example.com/v1alpha1", err) 793 } 794 assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.String(), apiResourceList.GroupVersion) 795 assert.Equal(t, 2, len(apiResourceList.APIResources)) 796 assert.Equal(t, "fischers", apiResourceList.APIResources[0].Name) 797 assert.False(t, apiResourceList.APIResources[0].Namespaced) 798 assert.Equal(t, "flunders", apiResourceList.APIResources[1].Name) 799 assert.True(t, apiResourceList.APIResources[1].Namespaced) 800 } 801 802 var ( 803 // I have no idea what these certs are, they just need to be different 804 differentClientCA = []byte(`-----BEGIN CERTIFICATE----- 805 MIIDQDCCAiigAwIBAgIJANWw74P5KJk2MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV 806 BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX 807 DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjAjMSEwHwYDVQQDExh3ZWJo 808 b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 809 AoIBAQDXd/nQ89a5H8ifEsigmMd01Ib6NVR3bkJjtkvYnTbdfYEBj7UzqOQtHoLa 810 dIVmefny5uIHvj93WD8WDVPB3jX2JHrXkDTXd/6o6jIXHcsUfFTVLp6/bZ+Anqe0 811 r/7hAPkzA2A7APyTWM3ZbEeo1afXogXhOJ1u/wz0DflgcB21gNho4kKTONXO3NHD 812 XLpspFqSkxfEfKVDJaYAoMnYZJtFNsa2OvsmLnhYF8bjeT3i07lfwrhUZvP+7Gsp 813 7UgUwc06WuNHjfx1s5e6ySzH0QioMD1rjYneqOvk0pKrMIhuAEWXqq7jlXcDtx1E 814 j+wnYbVqqVYheHZ8BCJoVAAQGs9/AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P 815 BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg 816 hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD 817 ggEBAD/GKSPNyQuAOw/jsYZesb+RMedbkzs18sSwlxAJQMUrrXwlVdHrA8q5WhE6 818 ABLqU1b8lQ8AWun07R8k5tqTmNvCARrAPRUqls/ryER+3Y9YEcxEaTc3jKNZFLbc 819 T6YtcnkdhxsiO136wtiuatpYL91RgCmuSpR8+7jEHhuFU01iaASu7ypFrUzrKHTF 820 bKwiLRQi1cMzVcLErq5CDEKiKhUkoDucyARFszrGt9vNIl/YCcBOkcNvM3c05Hn3 821 M++C29JwS3Hwbubg6WO3wjFjoEhpCwU6qRYUz3MRp4tHO4kxKXx+oQnUiFnR7vW0 822 YkNtGc1RUDHwecCTFpJtPb7Yu/E= 823 -----END CERTIFICATE----- 824 `) 825 differentFrontProxyCA = []byte(`-----BEGIN CERTIFICATE----- 826 MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw 827 GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx 828 MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB 829 BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb 830 KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC 831 BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU 832 K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q 833 a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 834 MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps= 835 -----END CERTIFICATE----- 836 837 `) 838 ) 839 840 type staticURLServiceResolver string 841 842 func (u staticURLServiceResolver) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) { 843 return url.Parse(string(u)) 844 }