Skip to content

Commit c09bdce

Browse files
committed
support apiserver url rewrite
Signed-off-by: huiwq1990 <[email protected]>
1 parent 41975c5 commit c09bdce

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed

pkg/apiserver/apiserver.go

+7
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ import (
1717
"k8s.io/client-go/discovery"
1818
clientrest "k8s.io/client-go/rest"
1919
"k8s.io/client-go/restmapper"
20+
"k8s.io/klog/v2"
2021

2122
internal "github.com/clusterpedia-io/api/clusterpedia"
2223
"github.com/clusterpedia-io/api/clusterpedia/install"
24+
"github.com/clusterpedia-io/clusterpedia/pkg/apiserver/features"
2325
"github.com/clusterpedia-io/clusterpedia/pkg/apiserver/registry/clusterpedia/collectionresources"
2426
"github.com/clusterpedia-io/clusterpedia/pkg/apiserver/registry/clusterpedia/resources"
2527
"github.com/clusterpedia-io/clusterpedia/pkg/generated/clientset/versioned"
2628
informers "github.com/clusterpedia-io/clusterpedia/pkg/generated/informers/externalversions"
2729
"github.com/clusterpedia-io/clusterpedia/pkg/kubeapiserver"
2830
"github.com/clusterpedia-io/clusterpedia/pkg/storage"
31+
clusterpediafeature "github.com/clusterpedia-io/clusterpedia/pkg/utils/feature"
2932
"github.com/clusterpedia-io/clusterpedia/pkg/utils/filters"
3033
)
3134

@@ -139,6 +142,10 @@ func (config completedConfig) New() (*ClusterPediaServer, error) {
139142
handler := handlerChainFunc(apiHandler, c)
140143
handler = filters.WithRequestQuery(handler)
141144
handler = filters.WithAcceptHeader(handler)
145+
if clusterpediafeature.FeatureGate.Enabled(features.ApiServerURLRewrite) {
146+
klog.InfoS("Enable rewrite apiserver url")
147+
handler = filters.WithRewriteFilter(handler)
148+
}
142149
return handler
143150
}
144151

pkg/apiserver/features/features.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package features
2+
3+
import (
4+
"k8s.io/apimachinery/pkg/util/runtime"
5+
"k8s.io/component-base/featuregate"
6+
7+
clusterpediafeature "github.com/clusterpedia-io/clusterpedia/pkg/utils/feature"
8+
)
9+
10+
const (
11+
12+
// ApiServerURLRewrite is a feature gate for rewrite apiserver request's URL
13+
// owner: @huiwq1990
14+
// alpha: v0.7.0
15+
ApiServerURLRewrite featuregate.Feature = "ApiServerURLRewrite"
16+
)
17+
18+
func init() {
19+
runtime.Must(clusterpediafeature.MutableFeatureGate.Add(defaultApiServerFeatureGates))
20+
}
21+
22+
// defaultApiServerFeatureGates consists of all known apiserver feature keys.
23+
// To add a new feature, define a key for it above and add it here.
24+
var defaultApiServerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
25+
ApiServerURLRewrite: {Default: false, PreRelease: featuregate.Alpha},
26+
}

pkg/utils/filters/rewrite.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package filters
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
"path"
8+
"regexp"
9+
"strings"
10+
11+
"k8s.io/klog/v2"
12+
)
13+
14+
const OriginURIHeaderKey = "X-Rewrite-Original-URI"
15+
16+
type Rule struct {
17+
Pattern string
18+
To string
19+
*regexp.Regexp
20+
}
21+
22+
var regfmt = regexp.MustCompile(`:[^/#?()\.\\]+`)
23+
24+
func NewRule(pattern, to string) (*Rule, error) {
25+
pattern = regfmt.ReplaceAllStringFunc(pattern, func(m string) string {
26+
return fmt.Sprintf(`(?P<%s>[^/#?]+)`, m[1:])
27+
})
28+
29+
reg, err := regexp.Compile(pattern)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
return &Rule{
35+
pattern,
36+
to,
37+
reg,
38+
}, nil
39+
}
40+
41+
func (r *Rule) Rewrite(req *http.Request) bool {
42+
oriPath := req.URL.Path
43+
44+
if strings.HasPrefix(oriPath, OldResourceApiServerPrefix) {
45+
return false
46+
}
47+
48+
if !r.MatchString(oriPath) {
49+
return false
50+
}
51+
52+
klog.V(4).InfoS("Rewrite url from", "URL", req.URL.EscapedPath())
53+
54+
to := path.Clean(r.Replace(req.URL))
55+
56+
u, e := url.Parse(to)
57+
if e != nil {
58+
return false
59+
}
60+
61+
req.Header.Set(OriginURIHeaderKey, req.URL.RequestURI())
62+
63+
req.URL.Path = u.Path
64+
req.URL.RawPath = u.RawPath
65+
if u.RawQuery != "" {
66+
req.URL.RawQuery = u.RawQuery
67+
}
68+
69+
klog.V(4).InfoS("Rewrite url to", "URL", req.URL.EscapedPath())
70+
71+
return true
72+
}
73+
74+
func (r *Rule) Replace(u *url.URL) string {
75+
if !hit("\\$|\\:", r.To) {
76+
return r.To
77+
}
78+
79+
uri := u.RequestURI()
80+
81+
regFrom := regexp.MustCompile(r.Pattern)
82+
match := regFrom.FindStringSubmatchIndex(uri)
83+
84+
result := regFrom.ExpandString([]byte(""), r.To, uri, match)
85+
86+
str := string(result[:])
87+
88+
if hit("\\:", str) {
89+
return r.replaceNamedParams(uri, str)
90+
}
91+
92+
return str
93+
}
94+
95+
func (r *Rule) replaceNamedParams(from, to string) string {
96+
fromMatches := r.FindStringSubmatch(from)
97+
98+
if len(fromMatches) > 0 {
99+
for i, name := range r.SubexpNames() {
100+
if len(name) > 0 {
101+
to = strings.Replace(to, ":"+name, fromMatches[i], -1)
102+
}
103+
}
104+
}
105+
106+
return to
107+
}
108+
109+
func NewHandler(rules map[string]string) RewriteHandler {
110+
var h RewriteHandler
111+
112+
for key, val := range rules {
113+
r, e := NewRule(key, val)
114+
if e != nil {
115+
panic(e)
116+
}
117+
118+
h.rules = append(h.rules, r)
119+
}
120+
121+
return h
122+
}
123+
124+
const OldResourceApiServerPrefix = "/apis/clusterpedia.io/v1beta1/resources"
125+
126+
func WithRewriteFilter(handler http.Handler) http.Handler {
127+
rh := NewHandler(map[string]string{
128+
"/(.*)": fmt.Sprintf("%s/$1", OldResourceApiServerPrefix),
129+
})
130+
131+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
132+
rh.ServeHTTP(w, req)
133+
handler.ServeHTTP(w, req)
134+
})
135+
}
136+
137+
type RewriteHandler struct {
138+
rules []*Rule
139+
}
140+
141+
func (h *RewriteHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
142+
for _, r := range h.rules {
143+
ok := r.Rewrite(req)
144+
if ok {
145+
break
146+
}
147+
}
148+
}
149+
150+
func hit(pattern, str string) bool {
151+
r, e := regexp.MatchString(pattern, str)
152+
if e != nil {
153+
return false
154+
}
155+
156+
return r
157+
}

pkg/utils/filters/rewrite_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package filters
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
)
8+
9+
type testFixture struct {
10+
from string
11+
to string
12+
}
13+
14+
type testCase struct {
15+
pattern string
16+
to string
17+
fixtures []testFixture
18+
}
19+
20+
func TestRewrite(t *testing.T) {
21+
tests := []testCase{
22+
{
23+
pattern: "/(.*)",
24+
to: "/apis/clusterpedia.io/v1beta1/resources/$1",
25+
fixtures: []testFixture{
26+
{from: "/api/v1/namespaces/default/pods", to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods"},
27+
{from: "/apis/clusterpedia.io/v1beta1/clusters", to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters"},
28+
{from: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods", to: "/apis/clusterpedia.io/v1beta1/resources/api/v1/namespaces/default/pods"},
29+
{from: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters", to: "/apis/clusterpedia.io/v1beta1/resources/apis/clusterpedia.io/v1beta1/clusters"},
30+
},
31+
},
32+
}
33+
34+
for _, test := range tests {
35+
t.Logf("Test - pattern: %s, to: %s", test.pattern, test.to)
36+
37+
for _, fixture := range test.fixtures {
38+
req, err := http.NewRequest("GET", fixture.from, nil)
39+
if err != nil {
40+
t.Fatalf("Fixture %v - create HTTP request error: %v", fixture, err)
41+
}
42+
43+
h := NewHandler(map[string]string{
44+
test.pattern: test.to,
45+
})
46+
47+
t.Logf("From: %s", req.URL.EscapedPath())
48+
if req.URL.EscapedPath() != fixture.from {
49+
t.Errorf("Invalid test fixture: %s", fixture.from)
50+
}
51+
52+
res := httptest.NewRecorder()
53+
h.ServeHTTP(res, req)
54+
55+
t.Logf("Rewrited: %s", req.URL.EscapedPath())
56+
if req.URL.EscapedPath() != fixture.to {
57+
t.Errorf("Test failed \n pattern: %s, to: %s, \n fixture: %s to %s, \n result: %s",
58+
test.pattern, test.to, fixture.from, fixture.to, req.URL.EscapedPath())
59+
}
60+
61+
if req.Header.Get(OriginURIHeaderKey) != "" {
62+
// matched
63+
if req.Header.Get(OriginURIHeaderKey) != fixture.from {
64+
t.Error("incorrect flag")
65+
}
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)