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  }