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 Dec 7, 2023
1 parent 630cf19 commit 99f0dce
Show file tree
Hide file tree
Showing 4 changed files with 234 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.ResourcePathWithoutClusterpediaPrefix) {
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 (

// 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},
}
41 changes: 41 additions & 0 deletions pkg/utils/filters/rewrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package filters

import (
"net/http"
"net/url"
"strings"

"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"
)

const OriginPathHeaderKey = "X-Rewrite-Original-Path"
const ApiServicePrefix = "/apis/clusterpedia.io"
const OldResourceApiServerPrefixWithoutSlash = "/apis/clusterpedia.io/v1beta1/resources"
const OldResourceApiServerPrefix = OldResourceApiServerPrefixWithoutSlash + "/"

var ExcludePrefixPaths = sets.NewString("", "/livez", "/readyz", 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, ApiServicePrefix) || ExcludePrefixPaths.Has(oldPath) {
return "", false
}

rewritePath, err := url.JoinPath(OldResourceApiServerPrefix, oldPath)
if err != nil {
return "", false
}
return rewritePath, true
}
160 changes: 160 additions & 0 deletions pkg/utils/filters/rewrite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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/cluster.clusterpedia.io/v1beta1/resourcesany",
to: "/apis/clusterpedia.io/v1beta1/resources/apis/cluster.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,
},
},
},
{
name: "none resources paths",
urls: []kubeRequest{
{
from: "/livez",
to: "/livez",
rewrite: false,
},
{
from: "/readyz",
to: "/readyz",
rewrite: false,
},
},
},
{
name: "not need rewrite",
urls: []kubeRequest{
{
from: "/apis/clusterpedia.io",
to: "/apis/clusterpedia.io",
rewrite: false,
},
{
from: "/apis/clusterpedia.io/",
to: "/apis/clusterpedia.io/",
rewrite: false,
},
{
from: "/apis/clusterpedia.io/any",
to: "/apis/clusterpedia.io/any",
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")
}
}
}
}
}

0 comments on commit 99f0dce

Please sign in to comment.