k8s.io/kubernetes@v1.29.3/test/integration/apiserver/openapi/openapi_apiservice_test.go (about)

     1  /*
     2  Copyright 2023 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 openapi
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"net/http"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/require"
    28  	apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/util/wait"
    31  	"k8s.io/client-go/discovery"
    32  	"k8s.io/client-go/dynamic"
    33  	kubernetes "k8s.io/client-go/kubernetes"
    34  	apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
    35  	aggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
    36  	"k8s.io/kube-openapi/pkg/validation/spec"
    37  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    38  	testdiscovery "k8s.io/kubernetes/test/integration/apiserver/discovery"
    39  	"k8s.io/kubernetes/test/integration/framework"
    40  )
    41  
    42  func TestSlowAPIServiceOpenAPIDoesNotBlockHealthCheck(t *testing.T) {
    43  	ctx, cancelCtx := context.WithCancel(context.Background())
    44  	defer cancelCtx()
    45  
    46  	etcd := framework.SharedEtcd()
    47  	setupServer := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, etcd)
    48  	client := generateTestClient(t, setupServer)
    49  
    50  	service := testdiscovery.NewFakeService("test-server", client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    51  		if r.URL.Path != "/openapi/v2" {
    52  			return
    53  		}
    54  		// Effectively let the APIService block until request timeout.
    55  		<-ctx.Done()
    56  		openapi := &spec.Swagger{
    57  			SwaggerProps: spec.SwaggerProps{
    58  				Paths: &spec.Paths{
    59  					Paths: map[string]spec.PathItem{
    60  						"/apis/wardle.example.com/v1alpha1": {},
    61  					},
    62  				},
    63  			},
    64  		}
    65  		data, err := openapi.MarshalJSON()
    66  		if err != nil {
    67  			t.Error(err)
    68  		}
    69  		http.ServeContent(w, r, "/openapi/v2", time.Now(), bytes.NewReader(data))
    70  	}))
    71  	go func() {
    72  		require.NoError(t, service.Run(ctx))
    73  	}()
    74  	require.NoError(t, service.WaitForReady(ctx))
    75  
    76  	groupVersion := metav1.GroupVersion{
    77  		Group:   "wardle.example.com",
    78  		Version: "v1alpha1",
    79  	}
    80  
    81  	require.NoError(t, registerAPIService(ctx, client, groupVersion, service))
    82  
    83  	setupServer.TearDownFn()
    84  	server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, etcd)
    85  	t.Cleanup(server.TearDownFn)
    86  	client2 := generateTestClient(t, server)
    87  
    88  	err := wait.PollUntilContextTimeout(context.Background(), 100*time.Millisecond, 1*time.Second, true, func(context.Context) (bool, error) {
    89  		var statusCode int
    90  		client2.AdmissionregistrationV1().RESTClient().Get().AbsPath("/healthz").Do(context.TODO()).StatusCode(&statusCode)
    91  		if statusCode == 200 {
    92  			return true, nil
    93  		}
    94  		return false, nil
    95  	})
    96  	require.NoError(t, err)
    97  }
    98  
    99  func TestFetchingOpenAPIBeforeReady(t *testing.T) {
   100  	ctx, cancelCtx := context.WithCancel(context.Background())
   101  	defer cancelCtx()
   102  
   103  	server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
   104  	t.Cleanup(server.TearDownFn)
   105  	client := generateTestClient(t, server)
   106  
   107  	readyCh := make(chan bool)
   108  	defer close(readyCh)
   109  	go func() {
   110  		select {
   111  		case <-readyCh:
   112  		default:
   113  			_, _ = client.Discovery().RESTClient().Get().AbsPath("/openapi/v2").Do(context.TODO()).Raw()
   114  		}
   115  	}()
   116  
   117  	service := testdiscovery.NewFakeService("test-server", client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   118  		openapi := &spec.Swagger{
   119  			SwaggerProps: spec.SwaggerProps{
   120  				Paths: &spec.Paths{
   121  					Paths: map[string]spec.PathItem{
   122  						"/apis/wardle.example.com/v1alpha1": {},
   123  					},
   124  				},
   125  			},
   126  		}
   127  		data, err := openapi.MarshalJSON()
   128  		if err != nil {
   129  			t.Error(err)
   130  		}
   131  		http.ServeContent(w, r, "/openapi/v2", time.Now(), bytes.NewReader(data))
   132  	}))
   133  	go func() {
   134  		require.NoError(t, service.Run(ctx))
   135  	}()
   136  	require.NoError(t, service.WaitForReady(ctx))
   137  
   138  	groupVersion := metav1.GroupVersion{
   139  		Group:   "wardle.example.com",
   140  		Version: "v1alpha1",
   141  	}
   142  
   143  	require.NoError(t, registerAPIService(ctx, client, groupVersion, service))
   144  	defer func() {
   145  		require.NoError(t, unregisterAPIService(ctx, client, groupVersion))
   146  	}()
   147  
   148  	err := wait.PollUntilContextTimeout(context.Background(), time.Millisecond*10, time.Second, true, func(context.Context) (bool, error) {
   149  		b, err := client.Discovery().RESTClient().Get().AbsPath("/openapi/v2").Do(context.TODO()).Raw()
   150  		require.NoError(t, err)
   151  		var openapi spec.Swagger
   152  		require.NoError(t, openapi.UnmarshalJSON(b))
   153  		if _, ok := openapi.Paths.Paths["/apis/wardle.example.com/v1alpha1"]; ok {
   154  			return true, nil
   155  		}
   156  		return false, nil
   157  	})
   158  	require.NoError(t, err)
   159  
   160  }
   161  
   162  // These definitions were copied from k8s.io/kubernetes/test/integation/apiserver/discovery
   163  // and should be consolidated.
   164  type kubeClientSet = kubernetes.Interface
   165  
   166  type aggegatorClientSet = aggregator.Interface
   167  
   168  type apiextensionsClientSet = apiextensions.Interface
   169  
   170  type dynamicClientset = dynamic.Interface
   171  
   172  type testClientSet struct {
   173  	kubeClientSet
   174  	aggegatorClientSet
   175  	apiextensionsClientSet
   176  	dynamicClientset
   177  }
   178  
   179  type testClient interface {
   180  	kubernetes.Interface
   181  	aggregator.Interface
   182  	apiextensions.Interface
   183  	dynamic.Interface
   184  }
   185  
   186  var _ testClient = testClientSet{}
   187  
   188  func (t testClientSet) Discovery() discovery.DiscoveryInterface {
   189  	return t.kubeClientSet.Discovery()
   190  }
   191  
   192  func generateTestClient(t *testing.T, server *kubeapiservertesting.TestServer) testClient {
   193  	kubeClientSet, err := kubernetes.NewForConfig(server.ClientConfig)
   194  	require.NoError(t, err)
   195  
   196  	aggegatorClientSet, err := aggregator.NewForConfig(server.ClientConfig)
   197  	require.NoError(t, err)
   198  
   199  	apiextensionsClientSet, err := apiextensions.NewForConfig(server.ClientConfig)
   200  	require.NoError(t, err)
   201  
   202  	dynamicClientset, err := dynamic.NewForConfig(server.ClientConfig)
   203  	require.NoError(t, err)
   204  
   205  	client := testClientSet{
   206  		kubeClientSet:          kubeClientSet,
   207  		aggegatorClientSet:     aggegatorClientSet,
   208  		apiextensionsClientSet: apiextensionsClientSet,
   209  		dynamicClientset:       dynamicClientset,
   210  	}
   211  	return client
   212  }
   213  
   214  func registerAPIService(ctx context.Context, client aggregator.Interface, gv metav1.GroupVersion, service testdiscovery.FakeService) error {
   215  	port := service.Port()
   216  	if port == nil {
   217  		return errors.New("service not yet started")
   218  	}
   219  	// Register the APIService
   220  	patch := apiregistrationv1.APIService{
   221  		ObjectMeta: metav1.ObjectMeta{
   222  			Name: gv.Version + "." + gv.Group,
   223  		},
   224  		TypeMeta: metav1.TypeMeta{
   225  			Kind:       "APIService",
   226  			APIVersion: "apiregistration.k8s.io/v1",
   227  		},
   228  		Spec: apiregistrationv1.APIServiceSpec{
   229  			Group:                 gv.Group,
   230  			Version:               gv.Version,
   231  			InsecureSkipTLSVerify: true,
   232  			GroupPriorityMinimum:  1000,
   233  			VersionPriority:       15,
   234  			Service: &apiregistrationv1.ServiceReference{
   235  				Namespace: "default",
   236  				Name:      service.Name(),
   237  				Port:      port,
   238  			},
   239  		},
   240  	}
   241  
   242  	_, err := client.
   243  		ApiregistrationV1().
   244  		APIServices().
   245  		Create(context.TODO(), &patch, metav1.CreateOptions{FieldManager: "test-manager"})
   246  	return err
   247  }
   248  
   249  func unregisterAPIService(ctx context.Context, client aggregator.Interface, gv metav1.GroupVersion) error {
   250  	return client.ApiregistrationV1().APIServices().Delete(ctx, gv.Version+"."+gv.Group, metav1.DeleteOptions{})
   251  }