This is an automated email from the ASF dual-hosted git repository.
alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git
The following commit(s) were added to refs/heads/develop by this push:
new f498fe5f feat: add support for header based route (#565)
f498fe5f is described below
commit f498fe5f84eca8d20d34ebe187cd6c82e3405a4f
Author: sh2 <[email protected]>
AuthorDate: Sun Jul 2 09:30:33 2023 +0800
feat: add support for header based route (#565)
* add header route in router
* fix ci
* fix review
* mixup route action and route match into trie
* only implement header based route for now
---
pixiu/pkg/common/router/router.go | 50 ++++++++++-
pixiu/pkg/common/router/router_test.go | 150 +++++++++++++++++++++++++++++++++
pixiu/pkg/common/router/trie/trie.go | 2 +-
pixiu/pkg/model/router.go | 57 +++++++++++--
4 files changed, 252 insertions(+), 7 deletions(-)
diff --git a/pixiu/pkg/common/router/router.go
b/pixiu/pkg/common/router/router.go
index 7148d5a5..c807d3bd 100644
--- a/pixiu/pkg/common/router/router.go
+++ b/pixiu/pkg/common/router/router.go
@@ -18,15 +18,21 @@
package router
import (
+ stdHttp "net/http"
"strings"
"sync"
)
+import (
+ "github.com/pkg/errors"
+)
+
import (
"github.com/apache/dubbo-go-pixiu/pixiu/pkg/common/constant"
"github.com/apache/dubbo-go-pixiu/pixiu/pkg/common/router/trie"
"github.com/apache/dubbo-go-pixiu/pixiu/pkg/common/util/stringutil"
"github.com/apache/dubbo-go-pixiu/pixiu/pkg/context/http"
+ "github.com/apache/dubbo-go-pixiu/pixiu/pkg/logger"
"github.com/apache/dubbo-go-pixiu/pixiu/pkg/model"
"github.com/apache/dubbo-go-pixiu/pixiu/pkg/server"
)
@@ -46,6 +52,7 @@ func CreateRouterCoordinator(routeConfig
*model.RouteConfiguration) *RouterCoord
server.GetRouterManager().AddRouterListener(rc)
}
rc.initTrie()
+ rc.initRegex()
return rc
}
@@ -54,7 +61,7 @@ func (rm *RouterCoordinator) Route(hc *http.HttpContext)
(*model.RouteAction, er
rm.rw.RLock()
defer rm.rw.RUnlock()
- return rm.activeConfig.Route(hc.Request)
+ return rm.route(hc.Request)
}
func (rm *RouterCoordinator) RouteByPathAndName(path, method string)
(*model.RouteAction, error) {
@@ -64,6 +71,31 @@ func (rm *RouterCoordinator) RouteByPathAndName(path, method
string) (*model.Rou
return rm.activeConfig.RouteByPathAndMethod(path, method)
}
+func (rm *RouterCoordinator) route(req *stdHttp.Request) (*model.RouteAction,
error) {
+ // match those route that only contains headers first
+ var matched []*model.Router
+ for _, route := range rm.activeConfig.Routes {
+ if len(route.Match.Prefix) > 0 {
+ continue
+ }
+ if route.Match.MatchHeader(req) {
+ matched = append(matched, route)
+ }
+ }
+
+ // always return the first match of header if got any
+ if len(matched) > 0 {
+ if len(matched[0].Route.Cluster) == 0 {
+ return nil, errors.New("action is nil. please check
your configuration.")
+ }
+ return &matched[0].Route, nil
+ }
+
+ // match those route that only contains prefix
+ // TODO: may consider implementing both prefix and header in the future
+ return rm.activeConfig.Route(req)
+}
+
func getTrieKey(method string, path string, isPrefix bool) string {
if isPrefix {
if !strings.HasSuffix(path, constant.PathSlash) {
@@ -83,6 +115,22 @@ func (rm *RouterCoordinator) initTrie() {
}
}
+func (rm *RouterCoordinator) initRegex() {
+ for _, router := range rm.activeConfig.Routes {
+ headers := router.Match.Headers
+ for i := range headers {
+ if headers[i].Regex && len(headers[i].Values) > 0 {
+ // regexp always use first value of header
+ err :=
headers[i].SetValueRegex(headers[i].Values[0])
+ if err != nil {
+ logger.Errorf("invalid regexp in
headers[%d]: %v", i, err)
+ panic(err)
+ }
+ }
+ }
+ }
+}
+
// OnAddRouter add router
func (rm *RouterCoordinator) OnAddRouter(r *model.Router) {
//TODO: lock move to trie node
diff --git a/pixiu/pkg/common/router/router_test.go
b/pixiu/pkg/common/router/router_test.go
index 27c35853..b41a0b5b 100644
--- a/pixiu/pkg/common/router/router_test.go
+++ b/pixiu/pkg/common/router/router_test.go
@@ -74,3 +74,153 @@ func TestCreateRouterCoordinator(t *testing.T) {
r.OnAddRouter(router)
r.OnDeleteRouter(router)
}
+
+func TestRoute(t *testing.T) {
+ const (
+ Cluster1 = "test-cluster-1"
+ )
+
+ hcmc := model.HttpConnectionManagerConfig{
+ RouteConfig: model.RouteConfiguration{
+ Routes: []*model.Router{
+ {
+ ID: "1",
+ Match: model.RouterMatch{
+ Headers: []model.HeaderMatcher{
+ {
+ Name: "A",
+ Values:
[]string{"1", "2", "3"},
+ },
+ {
+ Name: "A",
+ Values:
[]string{"3", "4", "5"},
+ },
+ {
+ Name: "B",
+ Values:
[]string{"1"},
+ },
+ {
+ Name:
"normal-regex",
+ Values:
[]string{"(k){2}"},
+ Regex: true,
+ },
+ {
+ Name:
"broken-regex",
+ Values:
[]string{"(t){2]]"},
+ Regex: true,
+ },
+ },
+ Methods: []string{"GET",
"POST"},
+ },
+ Route: model.RouteAction{
+ Cluster:
Cluster1,
+ ClusterNotFoundResponseCode:
505,
+ },
+ },
+ },
+ Dynamic: false,
+ },
+ HTTPFilters: []*model.HTTPFilter{
+ {
+ Name: "test",
+ Config: nil,
+ },
+ },
+ ServerName: "test_http_dubbo",
+ GenerateRequestID: false,
+ IdleTimeoutStr: "100",
+ }
+
+ testCases := []struct {
+ Name string
+ URL string
+ Method string
+ Header map[string]string
+ Expect string
+ }{
+ {
+ Name: "one override header",
+ URL: "/user",
+ Header: map[string]string{
+ "A": "1",
+ },
+ Expect: "test-cluster-1",
+ },
+ {
+ Name: "one header matched",
+ URL: "/user",
+ Header: map[string]string{
+ "A": "3",
+ },
+ Expect: Cluster1,
+ },
+ {
+ Name: "more header with one regex matched",
+ URL: "/user",
+ Header: map[string]string{
+ "A": "5",
+ "normal-regex": "kkkk",
+ },
+ Expect: Cluster1,
+ },
+ {
+ Name: "one header but wrong method",
+ URL: "/user",
+ Method: "PUT",
+ Header: map[string]string{
+ "A": "3",
+ },
+ Expect: "route failed for PUT/user, no rules matched.",
+ },
+ {
+ Name: "one broken regex header",
+ URL: "/user",
+ Header: map[string]string{
+ "broken-regex": "tt",
+ },
+ Expect: "route failed for GET/user, no rules matched.",
+ },
+ {
+ Name: "one matched header 2",
+ Header: map[string]string{
+ "B": "1",
+ },
+ Expect: Cluster1,
+ },
+ {
+ Name: "only header but wrong method",
+ Method: "DELETE",
+ Header: map[string]string{
+ "B": "1",
+ },
+ Expect: "route failed for DELETE, no rules matched.",
+ },
+ }
+
+ r := CreateRouterCoordinator(&hcmc.RouteConfig)
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+ method := "GET"
+ if len(tc.Method) > 0 {
+ method = tc.Method
+ }
+ request, err := http.NewRequest(method, tc.URL, nil)
+ assert.NoError(t, err)
+
+ if tc.Header != nil {
+ for k, v := range tc.Header {
+ request.Header.Set(k, v)
+ }
+ }
+ c := mock.GetMockHTTPContext(request)
+
+ a, err := r.Route(c)
+ if err != nil {
+ assert.Equal(t, tc.Expect, err.Error())
+ } else {
+ assert.Equal(t, tc.Expect, a.Cluster)
+ }
+ })
+ }
+}
diff --git a/pixiu/pkg/common/router/trie/trie.go
b/pixiu/pkg/common/router/trie/trie.go
index b422e9b0..bb533800 100644
--- a/pixiu/pkg/common/router/trie/trie.go
+++ b/pixiu/pkg/common/router/trie/trie.go
@@ -49,7 +49,7 @@ func NewTrieWithDefault(path string, defVal interface{}) Trie
{
// Node
type Node struct {
- matchStr string //abc match abc, :a match all words
as a variable names a , * match all words ,** match all words and children.
+ matchStr string // abc match abc, :a match all words
as a variable names a , * match all words ,** match all words and children.
children map[string]*Node // in path /a/b/c , b is child of a
, c is child of b
PathVariablesSet map[string]*Node // in path /:a/b/c/:d , :a is a path
variable node of level1 , :d is path variable node of level4
PathVariableNode *Node // in path /:a/b/c/:d , /b/c/:d is a
child tree of pathVariable node :a ,and some special logic for match
pathVariable it better not store in children.
diff --git a/pixiu/pkg/model/router.go b/pixiu/pkg/model/router.go
index 7730a23b..ce41669d 100644
--- a/pixiu/pkg/model/router.go
+++ b/pixiu/pkg/model/router.go
@@ -44,8 +44,8 @@ type (
Prefix string `yaml:"prefix" json:"prefix"
mapstructure:"prefix"`
Path string `yaml:"path" json:"path" mapstructure:"path"`
// Regex string `yaml:"regex" json:"regex"
mapstructure:"regex"` TODO: next version
- Methods []string `yaml:"methods" json:"methods"
mapstructure:"methods"`
- // Headers []HeaderMatcher `yaml:"headers" json:"headers"
mapstructure:"headers"`
+ Methods []string `yaml:"methods" json:"methods"
mapstructure:"methods"`
+ Headers []HeaderMatcher `yaml:"headers,omitempty"
json:"headers,omitempty" mapstructure:"headers"`
// pathRE *regexp.Regexp
}
@@ -62,7 +62,7 @@ type (
Dynamic bool `yaml:"dynamic" json:"dynamic"
mapstructure:"dynamic"`
}
- // Name header key, Value header value, Regex header value is regex
+ // HeaderMatcher include Name header key, Values header value, Regex
regex value
HeaderMatcher struct {
Name string `yaml:"name" json:"name" mapstructure:"name"`
Values []string `yaml:"values" json:"values"
mapstructure:"values"`
@@ -82,15 +82,62 @@ func (rc *RouteConfiguration) RouteByPathAndMethod(path,
method string) (*RouteA
node, _, _ := rc.RouteTrie.Match(stringutil.GetTrieKey(method, path))
if node == nil {
- return nil, errors.Errorf("route failed for %s,no rules
matched.", stringutil.GetTrieKey(method, path))
+ return nil, errors.Errorf("route failed for %s, no rules
matched.", stringutil.GetTrieKey(method, path))
}
if node.GetBizInfo() == nil {
- return nil, errors.Errorf("info is nil.please check your
configuration.")
+ return nil, errors.Errorf("action is nil. please check your
configuration.")
}
ret := (node.GetBizInfo()).(RouteAction)
+
return &ret, nil
}
func (rc *RouteConfiguration) Route(req *stdHttp.Request) (*RouteAction,
error) {
return rc.RouteByPathAndMethod(req.URL.Path, req.Method)
}
+
+// MatchHeader used when there's only headers to match
+func (rm *RouterMatch) MatchHeader(req *stdHttp.Request) bool {
+ if len(rm.Methods) > 0 {
+ for _, method := range rm.Methods {
+ if method == req.Method {
+ goto HEADER
+ }
+ }
+ return false
+ }
+HEADER:
+ for _, header := range rm.Headers {
+ if val := req.Header.Get(header.Name); len(val) > 0 {
+ if header.MatchValues(val) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// MatchValues match values in header, including regex type
+func (hm *HeaderMatcher) MatchValues(dst string) bool {
+ if hm.Regex && hm.valueRE != nil {
+ return hm.valueRE.MatchString(dst)
+ }
+
+ for _, src := range hm.Values {
+ if src == dst {
+ return true
+ }
+ }
+ return false
+}
+
+// SetValueRegex compile the regex, disable regex if it failed
+func (hm *HeaderMatcher) SetValueRegex(regex string) error {
+ r, err := regexp.Compile(regex)
+ if err == nil {
+ hm.valueRE = r
+ return nil
+ }
+ hm.Regex = false
+ return err
+}