...

Source file src/k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator/aggregator_test.go

Documentation: k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package aggregator
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"net/http"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/emicklei/go-restful/v3"
    27  	"github.com/stretchr/testify/assert"
    28  
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apiserver/pkg/endpoints/metrics"
    31  	openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
    32  	genericapiserver "k8s.io/apiserver/pkg/server"
    33  	"k8s.io/apiserver/pkg/server/mux"
    34  	"k8s.io/component-base/metrics/legacyregistry"
    35  	"k8s.io/component-base/metrics/testutil"
    36  	v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
    37  	openapicommon "k8s.io/kube-openapi/pkg/common"
    38  	"k8s.io/kube-openapi/pkg/handler3"
    39  	kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
    40  )
    41  
    42  type testV3APIService struct {
    43  	etag string
    44  	data []byte
    45  }
    46  
    47  var _ http.Handler = testV3APIService{}
    48  
    49  func (h testV3APIService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    50  	// Create an APIService with a handler for one group/version
    51  	if r.URL.Path == "/openapi/v3" {
    52  		group := &handler3.OpenAPIV3Discovery{
    53  			Paths: map[string]handler3.OpenAPIV3DiscoveryGroupVersion{
    54  				"apis/group.example.com/v1": {
    55  					ServerRelativeURL: "/openapi/v3/apis/group.example.com/v1?hash=" + h.etag,
    56  				},
    57  			},
    58  		}
    59  
    60  		j, _ := json.Marshal(group)
    61  		w.Write(j)
    62  		return
    63  	}
    64  
    65  	if r.URL.Path == "/openapi/v3/apis/group.example.com/v1" {
    66  		if len(h.etag) > 0 {
    67  			w.Header().Add("Etag", h.etag)
    68  		}
    69  		ifNoneMatches := r.Header["If-None-Match"]
    70  		for _, match := range ifNoneMatches {
    71  			if match == h.etag {
    72  				w.WriteHeader(http.StatusNotModified)
    73  				return
    74  			}
    75  		}
    76  		w.Write(h.data)
    77  	}
    78  }
    79  
    80  type testV2APIService struct{}
    81  
    82  var _ http.Handler = testV2APIService{}
    83  
    84  func (h testV2APIService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    85  	// Create an APIService with a handler for one group/version
    86  	if r.URL.Path == "/openapi/v2" {
    87  		w.Write([]byte(`{"swagger":"2.0","info":{"title":"Kubernetes","version":"unversioned"}}`))
    88  		return
    89  	}
    90  	w.WriteHeader(404)
    91  }
    92  
    93  func TestV2APIService(t *testing.T) {
    94  	downloader := Downloader{}
    95  	pathHandler := mux.NewPathRecorderMux("aggregator_test")
    96  	var serveHandler http.Handler = pathHandler
    97  	specProxier, err := BuildAndRegisterAggregator(downloader, genericapiserver.NewEmptyDelegate(), nil, nil, pathHandler)
    98  	if err != nil {
    99  		t.Error(err)
   100  	}
   101  	handler := testV2APIService{}
   102  	apiService := &v1.APIService{
   103  		Spec: v1.APIServiceSpec{
   104  			Group:   "group.example.com",
   105  			Version: "v1",
   106  		},
   107  	}
   108  	apiService.Name = "v1.group.example.com"
   109  	specProxier.AddUpdateAPIService(handler, apiService)
   110  	specProxier.UpdateAPIServiceSpec("v1.group.example.com")
   111  
   112  	data := sendReq(t, serveHandler, "/openapi/v3")
   113  	groupVersionList := handler3.OpenAPIV3Discovery{}
   114  	if err := json.Unmarshal(data, &groupVersionList); err != nil {
   115  		t.Fatal(err)
   116  	}
   117  
   118  	// A legacy APIService will not publish OpenAPI V3
   119  	// Ensure that we can still aggregate its V2 spec and convert it to V3.
   120  	path, ok := groupVersionList.Paths["apis/group.example.com/v1"]
   121  	if !ok {
   122  		t.Error("Expected group.example.com/v1 to be in group version list")
   123  	}
   124  	gotSpecJSON := sendReq(t, serveHandler, path.ServerRelativeURL)
   125  
   126  	expectedV3Bytes := []byte(`{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"},"components":{}}`)
   127  
   128  	if bytes.Compare(gotSpecJSON, expectedV3Bytes) != 0 {
   129  		t.Errorf("Spec mismatch, expected %s, got %s", expectedV3Bytes, gotSpecJSON)
   130  	}
   131  
   132  	apiServiceNames := specProxier.GetAPIServiceNames()
   133  	assert.ElementsMatch(t, []string{openAPIV2Converter, apiService.Name}, apiServiceNames)
   134  
   135  	// Ensure that OpenAPI v3 for legacy APIService is removed.
   136  	specProxier.RemoveAPIServiceSpec(apiService.Name)
   137  	data = sendReq(t, serveHandler, "/openapi/v3")
   138  	groupVersionList = handler3.OpenAPIV3Discovery{}
   139  	if err := json.Unmarshal(data, &groupVersionList); err != nil {
   140  		t.Fatal(err)
   141  	}
   142  
   143  	path, ok = groupVersionList.Paths["apis/group.example.com/v1"]
   144  	if ok {
   145  		t.Error("Expected group.example.com/v1 not to be in group version list")
   146  	}
   147  }
   148  
   149  func TestV3APIService(t *testing.T) {
   150  	downloader := Downloader{}
   151  
   152  	pathHandler := mux.NewPathRecorderMux("aggregator_test")
   153  	var serveHandler http.Handler = pathHandler
   154  	specProxier, err := BuildAndRegisterAggregator(downloader, genericapiserver.NewEmptyDelegate(), nil, nil, pathHandler)
   155  	if err != nil {
   156  		t.Error(err)
   157  	}
   158  	specJSON := []byte(`{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"}}`)
   159  	handler := testV3APIService{
   160  		etag: "6E8F849B434D4B98A569B9D7718876E9-356ECAB19D7FBE1336BABB1E70F8F3025050DE218BE78256BE81620681CFC9A268508E542B8B55974E17B2184BBFC8FFFAA577E51BE195D32B3CA2547818ABE4",
   161  		data: specJSON,
   162  	}
   163  	apiService := &v1.APIService{
   164  		Spec: v1.APIServiceSpec{
   165  			Group:   "group.example.com",
   166  			Version: "v1",
   167  		},
   168  	}
   169  	apiService.Name = "v1.group.example.com"
   170  	specProxier.AddUpdateAPIService(handler, apiService)
   171  	specProxier.UpdateAPIServiceSpec("v1.group.example.com")
   172  
   173  	data := sendReq(t, serveHandler, "/openapi/v3")
   174  	groupVersionList := handler3.OpenAPIV3Discovery{}
   175  	if err := json.Unmarshal(data, &groupVersionList); err != nil {
   176  		t.Fatal(err)
   177  	}
   178  	path, ok := groupVersionList.Paths["apis/group.example.com/v1"]
   179  	if !ok {
   180  		t.Error("Expected group.example.com/v1 to be in group version list")
   181  	}
   182  	gotSpecJSON := sendReq(t, serveHandler, path.ServerRelativeURL)
   183  	if bytes.Compare(gotSpecJSON, specJSON) != 0 {
   184  		t.Errorf("Spec mismatch, expected %s, got %s", specJSON, gotSpecJSON)
   185  	}
   186  
   187  	apiServiceNames := specProxier.GetAPIServiceNames()
   188  	assert.ElementsMatch(t, []string{openAPIV2Converter, apiService.Name}, apiServiceNames)
   189  }
   190  
   191  func TestV3RootAPIService(t *testing.T) {
   192  	ws := new(restful.WebService)
   193  	{
   194  		ws.Path("/apis/apiregistration.k8s.io/v1")
   195  		ws.Doc("API at/apis/apiregistration.k8s.io/v1 ")
   196  		ws.Consumes("*/*")
   197  		ws.Produces("application/json")
   198  		ws.ApiVersion("apiregistration.k8s.io/v1")
   199  		routeBuilder := ws.GET("apiservices").
   200  			To(func(request *restful.Request, response *restful.Response) {}).
   201  			Doc("list or watch objects of kind APIService").
   202  			Operation("listAPIService").
   203  			Produces("application/json").
   204  			Returns(http.StatusOK, "OK", v1.APIService{}).
   205  			Writes(v1.APIService{})
   206  		ws.Route(routeBuilder)
   207  	}
   208  	openapiConfig := genericapiserver.DefaultOpenAPIV3Config(getTestAPIServiceOpenAPIDefinitions, openapinamer.NewDefinitionNamer(runtime.NewScheme()))
   209  
   210  	downloader := Downloader{}
   211  	goRestfulContainer := restful.NewContainer()
   212  	goRestfulContainer.Add(ws)
   213  	pathHandler := mux.NewPathRecorderMux("aggregator_test")
   214  	var serveHandler http.Handler = pathHandler
   215  	specProxier, err := BuildAndRegisterAggregator(downloader, genericapiserver.NewEmptyDelegate(), goRestfulContainer, openapiConfig, pathHandler)
   216  	if err != nil {
   217  		t.Error(err)
   218  	}
   219  	expectedSpecJSON := []byte(`{"openapi":"3.0.0","info":{"title":"Generic API Server"},"paths":{"/apis/apiregistration.k8s.io/v1/apiservices":{"get":{"tags":["apiregistration_v1"],"description":"list or watch objects of kind APIService","operationId":"listApiregistrationV1APIService","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1.APIService"}}}}}}}},"components":{"schemas":{"io.k8s.kube-aggregator.pkg.apis.apiregistration.v1.APIService":{"description":"APIService represents a server for a particular GroupVersion. Name must be \"version.group\".","type":"object"}}}}`)
   220  
   221  	data := sendReq(t, serveHandler, "/openapi/v3")
   222  	groupVersionList := handler3.OpenAPIV3Discovery{}
   223  	if err := json.Unmarshal(data, &groupVersionList); err != nil {
   224  		t.Fatal(err)
   225  	}
   226  	path, ok := groupVersionList.Paths["apis/apiregistration.k8s.io/v1"]
   227  	if !ok {
   228  		t.Error("Expected apiregistration.k8s.io/v1 to be in group version list")
   229  	}
   230  	gotSpecJSON := sendReq(t, serveHandler, path.ServerRelativeURL)
   231  	if bytes.Compare(gotSpecJSON, expectedSpecJSON) != 0 {
   232  		t.Errorf("Spec mismatch, expected %s, got %s", expectedSpecJSON, gotSpecJSON)
   233  	}
   234  
   235  	apiServiceNames := specProxier.GetAPIServiceNames()
   236  	assert.ElementsMatch(t, []string{"k8s_internal_local_kube_aggregator_types", openAPIV2Converter}, apiServiceNames)
   237  }
   238  
   239  func TestOpenAPIRequestMetrics(t *testing.T) {
   240  	metrics.Register()
   241  	metrics.Reset()
   242  
   243  	downloader := Downloader{}
   244  
   245  	pathHandler := mux.NewPathRecorderMux("aggregator_metrics_test")
   246  	var serveHandler http.Handler = pathHandler
   247  	specProxier, err := BuildAndRegisterAggregator(downloader, genericapiserver.NewEmptyDelegate(), nil, nil, pathHandler)
   248  	if err != nil {
   249  		t.Error(err)
   250  	}
   251  	specJSON := []byte(`{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"}}`)
   252  	handler := testV3APIService{
   253  		etag: "6E8F849B434D4B98A569B9D7718876E9-356ECAB19D7FBE1336BABB1E70F8F3025050DE218BE78256BE81620681CFC9A268508E542B8B55974E17B2184BBFC8FFFAA577E51BE195D32B3CA2547818ABE4",
   254  		data: specJSON,
   255  	}
   256  	apiService := &v1.APIService{
   257  		Spec: v1.APIServiceSpec{
   258  			Group:   "group.example.com",
   259  			Version: "v1",
   260  		},
   261  	}
   262  	apiService.Name = "v1.group.example.com"
   263  	specProxier.AddUpdateAPIService(handler, apiService)
   264  	specProxier.UpdateAPIServiceSpec("v1.group.example.com")
   265  
   266  	data := sendReq(t, serveHandler, "/openapi/v3")
   267  	groupVersionList := handler3.OpenAPIV3Discovery{}
   268  	if err := json.Unmarshal(data, &groupVersionList); err != nil {
   269  		t.Fatal(err)
   270  	}
   271  	_, ok := groupVersionList.Paths["apis/group.example.com/v1"]
   272  	if !ok {
   273  		t.Error("Expected group.example.com/v1 to be in group version list")
   274  	}
   275  
   276  	// Metrics should be updated after requesting the root document.
   277  	if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(`
   278  # HELP apiserver_request_total [STABLE] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response code.
   279  # TYPE apiserver_request_total counter
   280  apiserver_request_total{code="200",component="",dry_run="",group="",resource="",scope="",subresource="openapi/v3",verb="GET",version=""} 1
   281  `), "apiserver_request_total"); err != nil {
   282  		t.Fatal(err)
   283  	}
   284  
   285  	_ = sendReq(t, serveHandler, "/openapi/v3/apis/group.example.com/v1")
   286  
   287  	// Metrics should be updated after requesting OpenAPI for a group version.
   288  	if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(`
   289  # HELP apiserver_request_total [STABLE] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response code.
   290  # TYPE apiserver_request_total counter
   291  apiserver_request_total{code="200",component="",dry_run="",group="",resource="",scope="",subresource="openapi/v3",verb="GET",version=""} 1
   292  apiserver_request_total{code="200",component="",dry_run="",group="",resource="",scope="",subresource="openapi/v3/",verb="GET",version=""} 1
   293  `), "apiserver_request_total"); err != nil {
   294  		t.Fatal(err)
   295  	}
   296  
   297  }
   298  
   299  func sendReq(t *testing.T, handler http.Handler, path string) []byte {
   300  	req, err := http.NewRequest("GET", path, nil)
   301  	if err != nil {
   302  		t.Fatal(err)
   303  	}
   304  	writer := newInMemoryResponseWriter()
   305  	handler.ServeHTTP(writer, req)
   306  	return writer.data
   307  }
   308  
   309  func getTestAPIServiceOpenAPIDefinitions(_ openapicommon.ReferenceCallback) map[string]openapicommon.OpenAPIDefinition {
   310  	return map[string]openapicommon.OpenAPIDefinition{
   311  		"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1.APIService": buildTestAPIServiceOpenAPIDefinition(),
   312  	}
   313  }
   314  
   315  func buildTestAPIServiceOpenAPIDefinition() openapicommon.OpenAPIDefinition {
   316  	return openapicommon.OpenAPIDefinition{
   317  		Schema: kubeopenapispec.Schema{
   318  			SchemaProps: kubeopenapispec.SchemaProps{
   319  				Description: "APIService represents a server for a particular GroupVersion. Name must be \"version.group\".",
   320  				Type:        []string{"object"},
   321  			},
   322  		},
   323  	}
   324  }
   325  

View as plain text