rds: allow case_insensitive path matching (#3997)
- in xds_client, accept (not NACK) RDS resp with case_insensitive=true
- pass case_insensitive to xds resolver and routing balancer
- Note that after the config selector change, the routing balancer will be removed, and
this will be handled in the resolver config selector
diff --git a/xds/internal/balancer/xdsrouting/matcher_path.go b/xds/internal/balancer/xdsrouting/matcher_path.go
index 9c783ac..f6f4b7d 100644
--- a/xds/internal/balancer/xdsrouting/matcher_path.go
+++ b/xds/internal/balancer/xdsrouting/matcher_path.go
@@ -30,14 +30,26 @@
}
type pathExactMatcher struct {
- fullPath string
+ // fullPath is all upper case if caseInsensitive is true.
+ fullPath string
+ caseInsensitive bool
}
-func newPathExactMatcher(p string) *pathExactMatcher {
- return &pathExactMatcher{fullPath: p}
+func newPathExactMatcher(p string, caseInsensitive bool) *pathExactMatcher {
+ ret := &pathExactMatcher{
+ fullPath: p,
+ caseInsensitive: caseInsensitive,
+ }
+ if caseInsensitive {
+ ret.fullPath = strings.ToUpper(p)
+ }
+ return ret
}
func (pem *pathExactMatcher) match(path string) bool {
+ if pem.caseInsensitive {
+ return pem.fullPath == strings.ToUpper(path)
+ }
return pem.fullPath == path
}
@@ -46,7 +58,7 @@
if !ok {
return false
}
- return pem.fullPath == mm.fullPath
+ return pem.fullPath == mm.fullPath && pem.caseInsensitive == mm.caseInsensitive
}
func (pem *pathExactMatcher) String() string {
@@ -54,14 +66,26 @@
}
type pathPrefixMatcher struct {
- prefix string
+ // prefix is all upper case if caseInsensitive is true.
+ prefix string
+ caseInsensitive bool
}
-func newPathPrefixMatcher(p string) *pathPrefixMatcher {
- return &pathPrefixMatcher{prefix: p}
+func newPathPrefixMatcher(p string, caseInsensitive bool) *pathPrefixMatcher {
+ ret := &pathPrefixMatcher{
+ prefix: p,
+ caseInsensitive: caseInsensitive,
+ }
+ if caseInsensitive {
+ ret.prefix = strings.ToUpper(p)
+ }
+ return ret
}
func (ppm *pathPrefixMatcher) match(path string) bool {
+ if ppm.caseInsensitive {
+ return strings.HasPrefix(strings.ToUpper(path), ppm.prefix)
+ }
return strings.HasPrefix(path, ppm.prefix)
}
@@ -70,7 +94,7 @@
if !ok {
return false
}
- return ppm.prefix == mm.prefix
+ return ppm.prefix == mm.prefix && ppm.caseInsensitive == mm.caseInsensitive
}
func (ppm *pathPrefixMatcher) String() string {
diff --git a/xds/internal/balancer/xdsrouting/matcher_path_test.go b/xds/internal/balancer/xdsrouting/matcher_path_test.go
index ad7db74..b50d5d8 100644
--- a/xds/internal/balancer/xdsrouting/matcher_path_test.go
+++ b/xds/internal/balancer/xdsrouting/matcher_path_test.go
@@ -25,17 +25,21 @@
func TestPathFullMatcherMatch(t *testing.T) {
tests := []struct {
- name string
- fullPath string
- path string
- want bool
+ name string
+ fullPath string
+ caseInsensitive bool
+ path string
+ want bool
}{
{name: "match", fullPath: "/s/m", path: "/s/m", want: true},
+ {name: "case insensitive match", fullPath: "/s/m", caseInsensitive: true, path: "/S/m", want: true},
+ {name: "case insensitive match 2", fullPath: "/s/M", caseInsensitive: true, path: "/S/m", want: true},
{name: "not match", fullPath: "/s/m", path: "/a/b", want: false},
+ {name: "case insensitive not match", fullPath: "/s/m", caseInsensitive: true, path: "/a/b", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- fpm := newPathExactMatcher(tt.fullPath)
+ fpm := newPathExactMatcher(tt.fullPath, tt.caseInsensitive)
if got := fpm.match(tt.path); got != tt.want {
t.Errorf("{%q}.match(%q) = %v, want %v", tt.fullPath, tt.path, got, tt.want)
}
@@ -45,17 +49,21 @@
func TestPathPrefixMatcherMatch(t *testing.T) {
tests := []struct {
- name string
- prefix string
- path string
- want bool
+ name string
+ prefix string
+ caseInsensitive bool
+ path string
+ want bool
}{
{name: "match", prefix: "/s/", path: "/s/m", want: true},
+ {name: "case insensitive match", prefix: "/s/", caseInsensitive: true, path: "/S/m", want: true},
+ {name: "case insensitive match 2", prefix: "/S/", caseInsensitive: true, path: "/s/m", want: true},
{name: "not match", prefix: "/s/", path: "/a/b", want: false},
+ {name: "case insensitive not match", prefix: "/s/", caseInsensitive: true, path: "/a/b", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- fpm := newPathPrefixMatcher(tt.prefix)
+ fpm := newPathPrefixMatcher(tt.prefix, tt.caseInsensitive)
if got := fpm.match(tt.path); got != tt.want {
t.Errorf("{%q}.match(%q) = %v, want %v", tt.prefix, tt.path, got, tt.want)
}
diff --git a/xds/internal/balancer/xdsrouting/matcher_test.go b/xds/internal/balancer/xdsrouting/matcher_test.go
index e7d76e2..74c570a 100644
--- a/xds/internal/balancer/xdsrouting/matcher_test.go
+++ b/xds/internal/balancer/xdsrouting/matcher_test.go
@@ -38,7 +38,17 @@
}{
{
name: "both match",
- pm: newPathExactMatcher("/a/b"),
+ pm: newPathExactMatcher("/a/b", false),
+ hm: newHeaderExactMatcher("th", "tv"),
+ info: balancer.PickInfo{
+ FullMethodName: "/a/b",
+ Ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("th", "tv")),
+ },
+ want: true,
+ },
+ {
+ name: "both match with path case insensitive",
+ pm: newPathExactMatcher("/A/B", true),
hm: newHeaderExactMatcher("th", "tv"),
info: balancer.PickInfo{
FullMethodName: "/a/b",
@@ -48,7 +58,7 @@
},
{
name: "only one match",
- pm: newPathExactMatcher("/a/b"),
+ pm: newPathExactMatcher("/a/b", false),
hm: newHeaderExactMatcher("th", "tv"),
info: balancer.PickInfo{
FullMethodName: "/z/y",
@@ -58,7 +68,7 @@
},
{
name: "both not match",
- pm: newPathExactMatcher("/z/y"),
+ pm: newPathExactMatcher("/z/y", false),
hm: newHeaderExactMatcher("th", "abc"),
info: balancer.PickInfo{
FullMethodName: "/a/b",
@@ -68,7 +78,7 @@
},
{
name: "fake header",
- pm: newPathPrefixMatcher("/"),
+ pm: newPathPrefixMatcher("/", false),
hm: newHeaderExactMatcher("content-type", "fake"),
info: balancer.PickInfo{
FullMethodName: "/a/b",
@@ -80,7 +90,7 @@
},
{
name: "binary header",
- pm: newPathPrefixMatcher("/"),
+ pm: newPathPrefixMatcher("/", false),
hm: newHeaderPresentMatcher("t-bin", true),
info: balancer.PickInfo{
FullMethodName: "/a/b",
@@ -146,8 +156,8 @@
}{
{
name: "equal",
- pm: newPathExactMatcher("/a/b"),
- mm: newCompositeMatcher(newPathExactMatcher("/a/b"), nil, nil),
+ pm: newPathExactMatcher("/a/b", false),
+ mm: newCompositeMatcher(newPathExactMatcher("/a/b", false), nil, nil),
want: true,
},
{
@@ -158,9 +168,9 @@
},
{
name: "not equal",
- pm: newPathExactMatcher("/a/b"),
+ pm: newPathExactMatcher("/a/b", false),
fm: newFractionMatcher(123),
- mm: newCompositeMatcher(newPathExactMatcher("/a/b"), nil, nil),
+ mm: newCompositeMatcher(newPathExactMatcher("/a/b", false), nil, nil),
want: false,
},
}
diff --git a/xds/internal/balancer/xdsrouting/routing.go b/xds/internal/balancer/xdsrouting/routing.go
index ad5373a..fd92fd9 100644
--- a/xds/internal/balancer/xdsrouting/routing.go
+++ b/xds/internal/balancer/xdsrouting/routing.go
@@ -136,9 +136,9 @@
}
pathMatcher = newPathRegexMatcher(re)
case r.path != "":
- pathMatcher = newPathExactMatcher(r.path)
+ pathMatcher = newPathExactMatcher(r.path, r.caseInsensitive)
default:
- pathMatcher = newPathPrefixMatcher(r.prefix)
+ pathMatcher = newPathPrefixMatcher(r.prefix, r.caseInsensitive)
}
var headerMatchers []headerMatcherInterface
diff --git a/xds/internal/balancer/xdsrouting/routing_config.go b/xds/internal/balancer/xdsrouting/routing_config.go
index 78716a0..526fe27 100644
--- a/xds/internal/balancer/xdsrouting/routing_config.go
+++ b/xds/internal/balancer/xdsrouting/routing_config.go
@@ -52,6 +52,9 @@
// Path, Prefix and Regex can have at most one set. This is guaranteed by
// config parsing.
path, prefix, regex string
+ // Indicates if prefix/path matching should be case insensitive. The default
+ // is false (case sensitive).
+ caseInsensitive bool
headers []headerMatcher
fraction *uint32
@@ -74,6 +77,7 @@
type routeJSON struct {
// Path, Prefix and Regex can have at most one non-nil.
Path, Prefix, Regex *string
+ CaseInsensitive bool
// Zero or more header matchers.
Headers []*xdsclient.HeaderMatcher
MatchFraction *wrapperspb.UInt32Value
@@ -99,6 +103,7 @@
case r.Regex != nil:
tempR.regex = *r.Regex
}
+ tempR.caseInsensitive = r.CaseInsensitive
for _, h := range r.Headers {
var tempHeader headerMatcher
switch {
diff --git a/xds/internal/balancer/xdsrouting/routing_picker_test.go b/xds/internal/balancer/xdsrouting/routing_picker_test.go
index 83e494a..fd03c3b 100644
--- a/xds/internal/balancer/xdsrouting/routing_picker_test.go
+++ b/xds/internal/balancer/xdsrouting/routing_picker_test.go
@@ -52,7 +52,7 @@
{
name: "one route no match",
routes: []route{
- {m: newCompositeMatcher(newPathPrefixMatcher("/a/"), nil, nil), action: "action-0"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/a/", false), nil, nil), action: "action-0"},
},
pickers: map[string]*subBalancerState{
"action-0": {state: balancer.State{
@@ -66,7 +66,7 @@
{
name: "one route one match",
routes: []route{
- {m: newCompositeMatcher(newPathPrefixMatcher("/a/"), nil, nil), action: "action-0"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/a/", false), nil, nil), action: "action-0"},
},
pickers: map[string]*subBalancerState{
"action-0": {state: balancer.State{
@@ -80,8 +80,8 @@
{
name: "two routes first match",
routes: []route{
- {m: newCompositeMatcher(newPathPrefixMatcher("/a/"), nil, nil), action: "action-0"},
- {m: newCompositeMatcher(newPathPrefixMatcher("/z/"), nil, nil), action: "action-1"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/a/", false), nil, nil), action: "action-0"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/z/", false), nil, nil), action: "action-1"},
},
pickers: map[string]*subBalancerState{
"action-0": {state: balancer.State{
@@ -99,8 +99,8 @@
{
name: "two routes second match",
routes: []route{
- {m: newCompositeMatcher(newPathPrefixMatcher("/a/"), nil, nil), action: "action-0"},
- {m: newCompositeMatcher(newPathPrefixMatcher("/z/"), nil, nil), action: "action-1"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/a/", false), nil, nil), action: "action-0"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/z/", false), nil, nil), action: "action-1"},
},
pickers: map[string]*subBalancerState{
"action-0": {state: balancer.State{
@@ -118,8 +118,8 @@
{
name: "two routes both match former more specific",
routes: []route{
- {m: newCompositeMatcher(newPathExactMatcher("/a/b"), nil, nil), action: "action-0"},
- {m: newCompositeMatcher(newPathPrefixMatcher("/a/"), nil, nil), action: "action-1"},
+ {m: newCompositeMatcher(newPathExactMatcher("/a/b", false), nil, nil), action: "action-0"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/a/", false), nil, nil), action: "action-1"},
},
pickers: map[string]*subBalancerState{
"action-0": {state: balancer.State{
@@ -138,8 +138,8 @@
{
name: "tow routes both match latter more specific",
routes: []route{
- {m: newCompositeMatcher(newPathPrefixMatcher("/a/"), nil, nil), action: "action-0"},
- {m: newCompositeMatcher(newPathExactMatcher("/a/b"), nil, nil), action: "action-1"},
+ {m: newCompositeMatcher(newPathPrefixMatcher("/a/", false), nil, nil), action: "action-0"},
+ {m: newCompositeMatcher(newPathExactMatcher("/a/b", false), nil, nil), action: "action-1"},
},
pickers: map[string]*subBalancerState{
"action-0": {state: balancer.State{
diff --git a/xds/internal/client/client.go b/xds/internal/client/client.go
index c2bbeaf..583f09b 100644
--- a/xds/internal/client/client.go
+++ b/xds/internal/client/client.go
@@ -167,9 +167,12 @@
// indication of the action to take upon match.
type Route struct {
Path, Prefix, Regex *string
- Headers []*HeaderMatcher
- Fraction *uint32
- Action map[string]uint32 // action is weighted clusters.
+ // Indicates if prefix/path matching should be case insensitive. The default
+ // is false (case sensitive).
+ CaseInsensitive bool
+ Headers []*HeaderMatcher
+ Fraction *uint32
+ Action map[string]uint32 // action is weighted clusters.
}
// HeaderMatcher represents header matchers.
diff --git a/xds/internal/client/client_rds_test.go b/xds/internal/client/client_rds_test.go
index ee09211..7b39158 100644
--- a/xds/internal/client/client_rds_test.go
+++ b/xds/internal/client/client_rds_test.go
@@ -280,7 +280,14 @@
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
}}}}}}},
- wantError: true,
+ wantUpdate: RouteConfigUpdate{
+ VirtualHosts: []*VirtualHost{
+ {
+ Domains: []string{ldsTarget},
+ Routes: []*Route{{Prefix: newStringP("/"), CaseInsensitive: true, Action: map[string]uint32{clusterName: 1}}},
+ },
+ },
+ },
},
{
name: "good-route-config-with-empty-string-route",
@@ -434,7 +441,7 @@
t.Run(test.name, func(t *testing.T) {
gotUpdate, gotError := generateRDSUpdateFromRouteConfiguration(test.rc, nil)
if (gotError != nil) != test.wantError || !cmp.Equal(gotUpdate, test.wantUpdate, cmpopts.EquateEmpty()) {
- t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) = %v, want %v", test.rc, ldsTarget, gotUpdate, test.wantUpdate)
+ t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) returned unexpected, diff (-want +got):\\n%s", test.rc, ldsTarget, cmp.Diff(test.wantUpdate, gotUpdate, cmpopts.EquateEmpty()))
}
})
}
@@ -654,8 +661,22 @@
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
CaseSensitive: &wrapperspb.BoolValue{Value: false},
},
+ Action: &v3routepb.Route_Route{
+ Route: &v3routepb.RouteAction{
+ ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
+ WeightedClusters: &v3routepb.WeightedCluster{
+ Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
+ {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
+ {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
+ },
+ TotalWeight: &wrapperspb.UInt32Value{Value: 100},
+ }}}},
}},
- wantErr: true,
+ wantRoutes: []*Route{{
+ Prefix: newStringP("/"),
+ CaseInsensitive: true,
+ Action: map[string]uint32{"A": 40, "B": 60},
+ }},
},
{
name: "good",
diff --git a/xds/internal/client/client_xds.go b/xds/internal/client/client_xds.go
index b8598c0..af25ce2 100644
--- a/xds/internal/client/client_xds.go
+++ b/xds/internal/client/client_xds.go
@@ -171,10 +171,6 @@
continue
}
- if caseSensitive := match.GetCaseSensitive(); caseSensitive != nil && !caseSensitive.Value {
- return nil, fmt.Errorf("route %+v has case-sensitive false", r)
- }
-
pathSp := match.GetPathSpecifier()
if pathSp == nil {
return nil, fmt.Errorf("route %+v doesn't have a path specifier", r)
@@ -193,6 +189,10 @@
continue
}
+ if caseSensitive := match.GetCaseSensitive(); caseSensitive != nil {
+ route.CaseInsensitive = !caseSensitive.Value
+ }
+
for _, h := range match.GetHeaders() {
var header HeaderMatcher
switch ht := h.GetHeaderMatchSpecifier().(type) {
diff --git a/xds/internal/resolver/serviceconfig.go b/xds/internal/resolver/serviceconfig.go
index 965817c..f585e43 100644
--- a/xds/internal/resolver/serviceconfig.go
+++ b/xds/internal/resolver/serviceconfig.go
@@ -56,12 +56,13 @@
}
type route struct {
- Path *string `json:"path,omitempty"`
- Prefix *string `json:"prefix,omitempty"`
- Regex *string `json:"regex,omitempty"`
- Headers []*xdsclient.HeaderMatcher `json:"headers,omitempty"`
- Fraction *wrapperspb.UInt32Value `json:"matchFraction,omitempty"`
- Action string `json:"action"`
+ Path *string `json:"path,omitempty"`
+ Prefix *string `json:"prefix,omitempty"`
+ Regex *string `json:"regex,omitempty"`
+ CaseInsensitive bool `json:"caseInsensitive"`
+ Headers []*xdsclient.HeaderMatcher `json:"headers,omitempty"`
+ Fraction *wrapperspb.UInt32Value `json:"matchFraction,omitempty"`
+ Action string `json:"action"`
}
type xdsActionConfig struct {
@@ -80,10 +81,11 @@
var rts []*route
for _, rt := range routes {
t := &route{
- Path: rt.Path,
- Prefix: rt.Prefix,
- Regex: rt.Regex,
- Headers: rt.Headers,
+ Path: rt.Path,
+ Prefix: rt.Prefix,
+ Regex: rt.Regex,
+ Headers: rt.Headers,
+ CaseInsensitive: rt.CaseInsensitive,
}
if f := rt.Fraction; f != nil {
diff --git a/xds/internal/resolver/serviceconfig_test.go b/xds/internal/resolver/serviceconfig_test.go
index 5969bd8..cce30c1 100644
--- a/xds/internal/resolver/serviceconfig_test.go
+++ b/xds/internal/resolver/serviceconfig_test.go
@@ -144,6 +144,7 @@
},
{
"prefix":"/service_2/method_1",
+ "caseInsensitive":true,
"action":"cluster_1_0"
},
{
@@ -214,8 +215,9 @@
Action: map[string]uint32{"cluster_1": 1},
},
{
- Prefix: newStringP("/service_2/method_1"),
- Action: map[string]uint32{"cluster_1": 1},
+ Prefix: newStringP("/service_2/method_1"),
+ CaseInsensitive: true,
+ Action: map[string]uint32{"cluster_1": 1},
},
{
Regex: newStringP("^/service_2/method_3$"),