diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 59c2ee60b..d53ac4b1b 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.ApiServerURLRewrite) { + 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..a075cc394 --- /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 ( + + // ApiServerURLRewrite is a feature gate for rewrite apiserver request's URL + // owner: @huiwq1990 + // alpha: v0.7.0 + ApiServerURLRewrite featuregate.Feature = "ApiServerURLRewrite" +) + +func init() { + runtime.Must(clusterpediafeature.MutableFeatureGate.Add(defaultApiServerFeatureGates)) +} + +// defaultApiServerFeatureGates consists of all known apiserver feature keys. +// To add a new feature, define a key for it above and add it here. +var defaultApiServerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ + ApiServerURLRewrite: {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..47c477d19 --- /dev/null +++ b/pkg/utils/filters/rewrite.go @@ -0,0 +1,40 @@ +package filters + +import ( + "net/http" + "net/url" + "strings" + + "k8s.io/klog/v2" +) + +const OriginPathHeaderKey = "X-Rewrite-Original-Path" +const OldResourceApiServerPrefix = "/apis/clusterpedia.io/v1beta1/resources" + +func WithRewriteFilter(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if !doUrlRewrite(req) { + klog.V(5).InfoS("request not need rewrite", "path", req.URL.EscapedPath()) + } + + handler.ServeHTTP(w, req) + }) +} + +func doUrlRewrite(req *http.Request) bool { + oldPath := req.URL.EscapedPath() + if strings.HasPrefix(oldPath, OldResourceApiServerPrefix) { + return false + } + + rewritePath, err := url.JoinPath(OldResourceApiServerPrefix, req.URL.Path) + if err != nil { + return false + } + req.URL.Path = rewritePath + req.Header.Set(OriginPathHeaderKey, oldPath) + + klog.V(5).InfoS("Rewrite url", "oldPath", oldPath, "newPath", req.URL.EscapedPath()) + + return true +} diff --git a/pkg/utils/filters/rewrite_test.go b/pkg/utils/filters/rewrite_test.go new file mode 100644 index 000000000..54a852071 --- /dev/null +++ b/pkg/utils/filters/rewrite_test.go @@ -0,0 +1,77 @@ +package filters + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type testCase struct { + name string + urls []kubeRequest +} + +type kubeRequest struct { + from string + to string +} + +func TestRewrite2(t *testing.T) { + tests := []testCase{ + //{ + // name: "do rewrite", + // urls: []kubeRequest{ + // {from: "/api/v1/namespaces/default/pods?limit=100", to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods?limit=100"}, + // {from: "/apis/clusterpedia.io/v1beta1/clusters", to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters"}, + // }, + //}, + //{ + // name: "not need rewrite", + // urls: []kubeRequest{ + // {from: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods", to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods"}, + // {from: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters", to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters"}, + // }, + //}, + { + 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"}, + }, + }, + } + + 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") + } + } + } + } +}