From c09bdce4d7f58197ec5603e5c0667840148e1692 Mon Sep 17 00:00:00 2001 From: huiwq1990 Date: Fri, 15 Sep 2023 14:01:29 +0800 Subject: [PATCH] support apiserver url rewrite Signed-off-by: huiwq1990 --- pkg/apiserver/apiserver.go | 7 ++ pkg/apiserver/features/features.go | 26 +++++ pkg/utils/filters/rewrite.go | 157 +++++++++++++++++++++++++++++ pkg/utils/filters/rewrite_test.go | 69 +++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 pkg/apiserver/features/features.go create mode 100644 pkg/utils/filters/rewrite.go create mode 100644 pkg/utils/filters/rewrite_test.go 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..e8ccff2ca --- /dev/null +++ b/pkg/utils/filters/rewrite.go @@ -0,0 +1,157 @@ +package filters + +import ( + "fmt" + "net/http" + "net/url" + "path" + "regexp" + "strings" + + "k8s.io/klog/v2" +) + +const OriginURIHeaderKey = "X-Rewrite-Original-URI" + +type Rule struct { + Pattern string + To string + *regexp.Regexp +} + +var regfmt = regexp.MustCompile(`:[^/#?()\.\\]+`) + +func NewRule(pattern, to string) (*Rule, error) { + pattern = regfmt.ReplaceAllStringFunc(pattern, func(m string) string { + return fmt.Sprintf(`(?P<%s>[^/#?]+)`, m[1:]) + }) + + reg, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + + return &Rule{ + pattern, + to, + reg, + }, nil +} + +func (r *Rule) Rewrite(req *http.Request) bool { + oriPath := req.URL.Path + + if strings.HasPrefix(oriPath, OldResourceApiServerPrefix) { + return false + } + + if !r.MatchString(oriPath) { + return false + } + + klog.V(4).InfoS("Rewrite url from", "URL", req.URL.EscapedPath()) + + to := path.Clean(r.Replace(req.URL)) + + u, e := url.Parse(to) + if e != nil { + return false + } + + req.Header.Set(OriginURIHeaderKey, req.URL.RequestURI()) + + req.URL.Path = u.Path + req.URL.RawPath = u.RawPath + if u.RawQuery != "" { + req.URL.RawQuery = u.RawQuery + } + + klog.V(4).InfoS("Rewrite url to", "URL", req.URL.EscapedPath()) + + return true +} + +func (r *Rule) Replace(u *url.URL) string { + if !hit("\\$|\\:", r.To) { + return r.To + } + + uri := u.RequestURI() + + regFrom := regexp.MustCompile(r.Pattern) + match := regFrom.FindStringSubmatchIndex(uri) + + result := regFrom.ExpandString([]byte(""), r.To, uri, match) + + str := string(result[:]) + + if hit("\\:", str) { + return r.replaceNamedParams(uri, str) + } + + return str +} + +func (r *Rule) replaceNamedParams(from, to string) string { + fromMatches := r.FindStringSubmatch(from) + + if len(fromMatches) > 0 { + for i, name := range r.SubexpNames() { + if len(name) > 0 { + to = strings.Replace(to, ":"+name, fromMatches[i], -1) + } + } + } + + return to +} + +func NewHandler(rules map[string]string) RewriteHandler { + var h RewriteHandler + + for key, val := range rules { + r, e := NewRule(key, val) + if e != nil { + panic(e) + } + + h.rules = append(h.rules, r) + } + + return h +} + +const OldResourceApiServerPrefix = "/apis/clusterpedia.io/v1beta1/resources" + +func WithRewriteFilter(handler http.Handler) http.Handler { + rh := NewHandler(map[string]string{ + "/(.*)": fmt.Sprintf("%s/$1", OldResourceApiServerPrefix), + }) + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + rh.ServeHTTP(w, req) + handler.ServeHTTP(w, req) + }) +} + +type RewriteHandler struct { + rules []*Rule +} + +func (h *RewriteHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { + for _, r := range h.rules { + ok := r.Rewrite(req) + if ok { + break + } + } +} + +func hit(pattern, str string) bool { + r, e := regexp.MatchString(pattern, str) + if e != nil { + return false + } + + return r +} diff --git a/pkg/utils/filters/rewrite_test.go b/pkg/utils/filters/rewrite_test.go new file mode 100644 index 000000000..4e0872fad --- /dev/null +++ b/pkg/utils/filters/rewrite_test.go @@ -0,0 +1,69 @@ +package filters + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type testFixture struct { + from string + to string +} + +type testCase struct { + pattern string + to string + fixtures []testFixture +} + +func TestRewrite(t *testing.T) { + tests := []testCase{ + { + pattern: "/(.*)", + to: "/apis/clusterpedia.io/v1beta1/resources/$1", + fixtures: []testFixture{ + {from: "/api/v1/namespaces/default/pods", to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods"}, + {from: "/apis/clusterpedia.io/v1beta1/clusters", to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters"}, + {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"}, + }, + }, + } + + for _, test := range tests { + t.Logf("Test - pattern: %s, to: %s", test.pattern, test.to) + + for _, fixture := range test.fixtures { + req, err := http.NewRequest("GET", fixture.from, nil) + if err != nil { + t.Fatalf("Fixture %v - create HTTP request error: %v", fixture, err) + } + + h := NewHandler(map[string]string{ + test.pattern: test.to, + }) + + t.Logf("From: %s", req.URL.EscapedPath()) + if req.URL.EscapedPath() != fixture.from { + t.Errorf("Invalid test fixture: %s", fixture.from) + } + + res := httptest.NewRecorder() + h.ServeHTTP(res, req) + + t.Logf("Rewrited: %s", req.URL.EscapedPath()) + if req.URL.EscapedPath() != fixture.to { + t.Errorf("Test failed \n pattern: %s, to: %s, \n fixture: %s to %s, \n result: %s", + test.pattern, test.to, fixture.from, fixture.to, req.URL.EscapedPath()) + } + + if req.Header.Get(OriginURIHeaderKey) != "" { + // matched + if req.Header.Get(OriginURIHeaderKey) != fixture.from { + t.Error("incorrect flag") + } + } + } + } +}