Skip to content

Commit

Permalink
support apiserver url rewrite
Browse files Browse the repository at this point in the history
Signed-off-by: huiwq1990 <[email protected]>
  • Loading branch information
huiwq1990 committed Nov 16, 2023
1 parent 41975c5 commit c09bdce
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
7 changes: 7 additions & 0 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/apiserver/features/features.go
Original file line number Diff line number Diff line change
@@ -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},
}
157 changes: 157 additions & 0 deletions pkg/utils/filters/rewrite.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions pkg/utils/filters/rewrite_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
}

0 comments on commit c09bdce

Please sign in to comment.