diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 59c2ee60b..adb2abb4d 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -17,15 +17,18 @@ import ( "k8s.io/client-go/discovery" clientrest "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" + "k8s.io/klog/v2" internal "github.com/clusterpedia-io/api/clusterpedia" "github.com/clusterpedia-io/api/clusterpedia/install" + "github.com/clusterpedia-io/clusterpedia/pkg/apiserver/features" "github.com/clusterpedia-io/clusterpedia/pkg/apiserver/registry/clusterpedia/collectionresources" "github.com/clusterpedia-io/clusterpedia/pkg/apiserver/registry/clusterpedia/resources" "github.com/clusterpedia-io/clusterpedia/pkg/generated/clientset/versioned" informers "github.com/clusterpedia-io/clusterpedia/pkg/generated/informers/externalversions" "github.com/clusterpedia-io/clusterpedia/pkg/kubeapiserver" "github.com/clusterpedia-io/clusterpedia/pkg/storage" + clusterpediafeature "github.com/clusterpedia-io/clusterpedia/pkg/utils/feature" "github.com/clusterpedia-io/clusterpedia/pkg/utils/filters" ) @@ -139,6 +142,10 @@ func (config completedConfig) New() (*ClusterPediaServer, error) { handler := handlerChainFunc(apiHandler, c) handler = filters.WithRequestQuery(handler) handler = filters.WithAcceptHeader(handler) + if clusterpediafeature.FeatureGate.Enabled(features.ResourcePathWithoutClusterpediaPrefix) { + klog.InfoS("Enable rewrite apiserver url") + handler = filters.WithRewriteFilter(handler) + } return handler } diff --git a/pkg/apiserver/features/features.go b/pkg/apiserver/features/features.go new file mode 100644 index 000000000..c9f7d37f4 --- /dev/null +++ b/pkg/apiserver/features/features.go @@ -0,0 +1,26 @@ +package features + +import ( + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/component-base/featuregate" + + clusterpediafeature "github.com/clusterpedia-io/clusterpedia/pkg/utils/feature" +) + +const ( + + // ResourcePathWithoutClusterpediaPrefix is a feature gate for rewrite apiserver request's URL + // owner: @huiwq1990 + // alpha: v0.8.0 + ResourcePathWithoutClusterpediaPrefix featuregate.Feature = "ResourcePathWithoutClusterpediaPrefix" +) + +func init() { + runtime.Must(clusterpediafeature.MutableFeatureGate.Add(defaultResourcePathWithoutClusterpediaPrefixFeatureGates)) +} + +// defaultResourcePathWithoutClusterpediaPrefixFeatureGates consists of all known apiserver feature keys. +// To add a new feature, define a key for it above and add it here. +var defaultResourcePathWithoutClusterpediaPrefixFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ + ResourcePathWithoutClusterpediaPrefix: {Default: false, PreRelease: featuregate.Alpha}, +} diff --git a/pkg/utils/filters/rewrite.go b/pkg/utils/filters/rewrite.go new file mode 100644 index 000000000..0bb5eef54 --- /dev/null +++ b/pkg/utils/filters/rewrite.go @@ -0,0 +1,37 @@ +package filters + +import ( + "net/http" + "net/url" + "strings" + + "k8s.io/klog/v2" +) + +const OriginPathHeaderKey = "X-Rewrite-Original-Path" +const OldResourceApiServerPrefixWithoutSlash = "/apis/clusterpedia.io/v1beta1/resources" +const OldResourceApiServerPrefix = OldResourceApiServerPrefixWithoutSlash + "/" + +func WithRewriteFilter(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + oldPath := req.URL.EscapedPath() + if rewritePath, ok := urlPrefixRewrite(oldPath); ok { + req.URL.Path = rewritePath + req.Header.Set(OriginPathHeaderKey, oldPath) + klog.V(5).InfoS("request need rewrite", "oldPath", oldPath, "newPath", req.URL.EscapedPath()) + } + handler.ServeHTTP(w, req) + }) +} + +func urlPrefixRewrite(oldPath string) (string, bool) { + if strings.HasPrefix(oldPath, OldResourceApiServerPrefix) || oldPath == OldResourceApiServerPrefixWithoutSlash || oldPath == "" { + return "", false + } + + rewritePath, err := url.JoinPath(OldResourceApiServerPrefix, oldPath) + if err != nil { + return "", false + } + return rewritePath, true +} diff --git a/pkg/utils/filters/rewrite_test.go b/pkg/utils/filters/rewrite_test.go new file mode 100644 index 000000000..8e8b3652e --- /dev/null +++ b/pkg/utils/filters/rewrite_test.go @@ -0,0 +1,136 @@ +package filters + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +type testCase struct { + name string + urls []kubeRequest +} + +type kubeRequest struct { + from string + to string + rewrite bool +} + +var tests = []testCase{ + { + name: "do rewrite", + urls: []kubeRequest{ + { + from: "/apis/clusterpedia.io/v1beta1/resourcesany", + to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/resourcesany", + rewrite: true, + }, + {from: "/api/v1/namespaces/default/pods?limit=100", + to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods?limit=100", + rewrite: true}, + + {from: "/apis/clusterpedia.io/v1beta1/clusters", + to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters", + rewrite: true, + }, + }, + }, + { + name: "not need rewrite", + urls: []kubeRequest{ + { + from: "/apis/clusterpedia.io/v1beta1/resources", + to: "/apis/clusterpedia.io/v1beta1/resources", + rewrite: false, + }, + {from: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods", + to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods", + rewrite: false, + }, + + {from: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters", + to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters", + rewrite: false, + }, + }, + }, + { + name: "special cases", + urls: []kubeRequest{ + {from: "/api/v1/namespaces/default/pods?name=abc#xx", + to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods?name=abc#xx", + rewrite: true, + }, + }, + }, +} + +func TestUrlPrefixRewrite(t *testing.T) { + for _, test := range tests { + t.Logf("Test - name: %s", test.name) + + for _, tmp := range test.urls { + fromPath, err := url.Parse(tmp.from) + if err != nil { + t.Error(err) + } + + rewritePath, doRewrite := urlPrefixRewrite(fromPath.EscapedPath()) + if doRewrite != tmp.rewrite { + t.Errorf("Test failed \n from : %s \n to : %s \n needRewrite: %v \n doRewrite: %v", + tmp.from, tmp.to, tmp.rewrite, doRewrite) + } + + if doRewrite { + oldURL, err := url.Parse(tmp.to) + if err != nil { + t.Error(err) + } + + if oldURL.EscapedPath() != rewritePath { + t.Errorf("Test failed \n from : %s \n to : %s \n oldPath: %s \n rewritePath: %s", + tmp.from, tmp.to, oldURL.EscapedPath(), rewritePath) + } + } + } + } +} + +func TestRewrite(t *testing.T) { + for _, test := range tests { + t.Logf("Test - name: %s", test.name) + + for _, tmp := range test.urls { + req, err := http.NewRequest("GET", tmp.from, nil) + if err != nil { + t.Fatalf("create HTTP request error: %v", err) + } + + oldPath := req.URL.EscapedPath() + + h := WithRewriteFilter( + http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + }), + ) + + t.Logf("From: %s", req.URL.String()) + + res := httptest.NewRecorder() + h.ServeHTTP(res, req) + + t.Logf("Rewrited: %s", req.URL.String()) + if req.URL.String() != tmp.to { + t.Errorf("Test failed \n from : %s \n to : %s \n result: %s", + tmp.from, tmp.to, req.URL.RequestURI()) + } + + if oldHeaderPath := req.Header.Get(OriginPathHeaderKey); oldHeaderPath != "" { + if oldPath != oldHeaderPath { + t.Error("incorrect flag") + } + } + } + } +}