From 6ce3e7e91550fa73edbe0cf5c4534b0cf0a1a42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Thu, 20 Apr 2023 09:53:28 +0300 Subject: [PATCH] Use absolute path instead requestURI in openapiv3 discovery Currently, openapiv3 discovery uses requestURI to discover resources. However, that does not work when the rest endpoint contains prefixes (e.g. `http://localhost/test-endpoint/`). Because requestURI overwrites prefixes also in rest endpoint (e.g. `http://localhost/openapiv3/apis/apps/v1`). Since `absPath` keeps the prefixes in the rest endpoint, this PR changes to absPath instead requestURI. Kubernetes-commit: ee1d7eb5d82f3b2a76afc57bc33bc7e08c34bf27 --- openapi/client.go | 7 ++- openapi/groupversion.go | 42 +++++++++++--- openapi/groupversion_test.go | 106 +++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 openapi/groupversion_test.go diff --git a/openapi/client.go b/openapi/client.go index 7b58762acf..6a43057187 100644 --- a/openapi/client.go +++ b/openapi/client.go @@ -19,6 +19,7 @@ package openapi import ( "context" "encoding/json" + "strings" "k8s.io/client-go/rest" "k8s.io/kube-openapi/pkg/handler3" @@ -58,7 +59,11 @@ func (c *client) Paths() (map[string]GroupVersion, error) { // Create GroupVersions for each element of the result result := map[string]GroupVersion{} for k, v := range discoMap.Paths { - result[k] = newGroupVersion(c, v) + // If the server returned a URL rooted at /openapi/v3, preserve any additional client-side prefix. + // If the server returned a URL not rooted at /openapi/v3, treat it as an actual server-relative URL. + // See https://github.com/kubernetes/kubernetes/issues/117463 for details + useClientPrefix := strings.HasPrefix(v.ServerRelativeURL, "/openapi/v3") + result[k] = newGroupVersion(c, v, useClientPrefix) } return result, nil } diff --git a/openapi/groupversion.go b/openapi/groupversion.go index 32133a29b8..601dcbe3cc 100644 --- a/openapi/groupversion.go +++ b/openapi/groupversion.go @@ -18,6 +18,7 @@ package openapi import ( "context" + "net/url" "k8s.io/kube-openapi/pkg/handler3" ) @@ -29,18 +30,41 @@ type GroupVersion interface { } type groupversion struct { - client *client - item handler3.OpenAPIV3DiscoveryGroupVersion + client *client + item handler3.OpenAPIV3DiscoveryGroupVersion + useClientPrefix bool } -func newGroupVersion(client *client, item handler3.OpenAPIV3DiscoveryGroupVersion) *groupversion { - return &groupversion{client: client, item: item} +func newGroupVersion(client *client, item handler3.OpenAPIV3DiscoveryGroupVersion, useClientPrefix bool) *groupversion { + return &groupversion{client: client, item: item, useClientPrefix: useClientPrefix} } func (g *groupversion) Schema(contentType string) ([]byte, error) { - return g.client.restClient.Get(). - RequestURI(g.item.ServerRelativeURL). - SetHeader("Accept", contentType). - Do(context.TODO()). - Raw() + if !g.useClientPrefix { + return g.client.restClient.Get(). + RequestURI(g.item.ServerRelativeURL). + SetHeader("Accept", contentType). + Do(context.TODO()). + Raw() + } + + locator, err := url.Parse(g.item.ServerRelativeURL) + if err != nil { + return nil, err + } + + path := g.client.restClient.Get(). + AbsPath(locator.Path). + SetHeader("Accept", contentType) + + // Other than root endpoints(openapiv3/apis), resources have hash query parameter to support etags. + // However, absPath does not support handling query parameters internally, + // so that hash query parameter is added manually + for k, value := range locator.Query() { + for _, v := range value { + path.Param(k, v) + } + } + + return path.Do(context.TODO()).Raw() } diff --git a/openapi/groupversion_test.go b/openapi/groupversion_test.go new file mode 100644 index 0000000000..4ea237e43d --- /dev/null +++ b/openapi/groupversion_test.go @@ -0,0 +1,106 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" +) + +func TestGroupVersion(t *testing.T) { + tests := []struct { + name string + prefix string + serverReturnsPrefix bool + }{ + { + name: "no prefix", + prefix: "", + serverReturnsPrefix: false, + }, + { + name: "prefix not in discovery", + prefix: "/test-endpoint", + serverReturnsPrefix: false, + }, + { + name: "prefix in discovery", + prefix: "/test-endpoint", + serverReturnsPrefix: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.URL.Path == test.prefix+"/openapi/v3/apis/apps/v1" && r.URL.RawQuery == "hash=014fbff9a07c": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"}}`)) + case r.URL.Path == test.prefix+"/openapi/v3": + // return root content + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if test.serverReturnsPrefix { + w.Write([]byte(fmt.Sprintf(`{"paths":{"apis/apps/v1":{"serverRelativeURL":"%s/openapi/v3/apis/apps/v1?hash=014fbff9a07c"}}}`, test.prefix))) + } else { + w.Write([]byte(`{"paths":{"apis/apps/v1":{"serverRelativeURL":"/openapi/v3/apis/apps/v1?hash=014fbff9a07c"}}}`)) + } + default: + t.Errorf("unexpected request: %s", r.URL.String()) + w.WriteHeader(http.StatusNotFound) + return + } + })) + defer server.Close() + + c, err := rest.RESTClientFor(&rest.Config{ + Host: server.URL + test.prefix, + ContentConfig: rest.ContentConfig{ + NegotiatedSerializer: scheme.Codecs, + GroupVersion: &appsv1.SchemeGroupVersion, + }, + }) + + if err != nil { + t.Fatalf("unexpected error occurred: %v", err) + } + + openapiClient := NewClient(c) + paths, err := openapiClient.Paths() + if err != nil { + t.Fatalf("unexpected error occurred: %v", err) + } + schema, err := paths["apis/apps/v1"].Schema(runtime.ContentTypeJSON) + if err != nil { + t.Fatalf("unexpected error occurred: %v", err) + } + expectedResult := `{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"}}` + if string(schema) != expectedResult { + t.Fatalf("unexpected result actual: %s expected: %s", string(schema), expectedResult) + } + }) + } +}