mirror of https://github.com/status-im/consul.git
Case sensitive route match (#19647)
Add case insensitive param on service route match This commit adds in a new feature that allows service routers to specify that paths and path prefixes should ignore upper / lower casing when matching URLs. Co-authored-by: Derek Menteer <105233703+hashi-derek@users.noreply.github.com>
This commit is contained in:
parent
34b343a980
commit
758ddf84e9
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
connect: Add `CaseInsensitive` flag to service-routers that allows paths and path prefixes to ignore URL upper and lower casing.
|
||||
```
|
|
@ -375,6 +375,7 @@ type ServiceRouteHTTPMatch struct {
|
|||
PathExact string `json:",omitempty" alias:"path_exact"`
|
||||
PathPrefix string `json:",omitempty" alias:"path_prefix"`
|
||||
PathRegex string `json:",omitempty" alias:"path_regex"`
|
||||
CaseInsensitive bool `json:",omitempty" alias:"case_insensitive"`
|
||||
|
||||
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
||||
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
|
||||
|
@ -385,6 +386,7 @@ func (m *ServiceRouteHTTPMatch) IsEmpty() bool {
|
|||
return m.PathExact == "" &&
|
||||
m.PathPrefix == "" &&
|
||||
m.PathRegex == "" &&
|
||||
!m.CaseInsensitive &&
|
||||
len(m.Header) == 0 &&
|
||||
len(m.QueryParam) == 0 &&
|
||||
len(m.Methods) == 0
|
||||
|
|
|
@ -2742,6 +2742,20 @@ func TestServiceRouterConfigEntry(t *testing.T) {
|
|||
}),
|
||||
validateErr: "contains an invalid retry condition: \"invalid-retry-condition\"",
|
||||
},
|
||||
////////////////
|
||||
{
|
||||
name: "default route with case insensitive match",
|
||||
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||
CaseInsensitive: true,
|
||||
}))),
|
||||
},
|
||||
{
|
||||
name: "route with path prefix and case insensitive match /apI",
|
||||
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||
PathPrefix: "/apI",
|
||||
CaseInsensitive: true,
|
||||
}))),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
|
|
@ -889,6 +889,310 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-router: kitchen sink case insensitive",
|
||||
snake: `
|
||||
kind = "service-router"
|
||||
name = "main"
|
||||
meta {
|
||||
"foo" = "bar"
|
||||
"gir" = "zim"
|
||||
}
|
||||
routes = [
|
||||
{
|
||||
match {
|
||||
http {
|
||||
path_exact = "/foo"
|
||||
case_insensitive = true
|
||||
header = [
|
||||
{
|
||||
name = "debug1"
|
||||
present = true
|
||||
},
|
||||
{
|
||||
name = "debug2"
|
||||
present = false
|
||||
invert = true
|
||||
},
|
||||
{
|
||||
name = "debug3"
|
||||
exact = "1"
|
||||
},
|
||||
{
|
||||
name = "debug4"
|
||||
prefix = "aaa"
|
||||
},
|
||||
{
|
||||
name = "debug5"
|
||||
suffix = "bbb"
|
||||
},
|
||||
{
|
||||
name = "debug6"
|
||||
regex = "a.*z"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
destination {
|
||||
service = "carrot"
|
||||
service_subset = "kale"
|
||||
namespace = "leek"
|
||||
prefix_rewrite = "/alternate"
|
||||
request_timeout = "99s"
|
||||
idle_timeout = "99s"
|
||||
num_retries = 12345
|
||||
retry_on_connect_failure = true
|
||||
retry_on_status_codes = [401, 209]
|
||||
request_headers {
|
||||
add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
match {
|
||||
http {
|
||||
path_prefix = "/foo"
|
||||
methods = [ "GET", "DELETE" ]
|
||||
query_param = [
|
||||
{
|
||||
name = "hack1"
|
||||
present = true
|
||||
},
|
||||
{
|
||||
name = "hack2"
|
||||
exact = "1"
|
||||
},
|
||||
{
|
||||
name = "hack3"
|
||||
regex = "a.*z"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
match {
|
||||
http {
|
||||
path_regex = "/foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
`,
|
||||
camel: `
|
||||
Kind = "service-router"
|
||||
Name = "main"
|
||||
Meta {
|
||||
"foo" = "bar"
|
||||
"gir" = "zim"
|
||||
}
|
||||
Routes = [
|
||||
{
|
||||
Match {
|
||||
HTTP {
|
||||
PathExact = "/foo"
|
||||
CaseInsensitive = true
|
||||
Header = [
|
||||
{
|
||||
Name = "debug1"
|
||||
Present = true
|
||||
},
|
||||
{
|
||||
Name = "debug2"
|
||||
Present = false
|
||||
Invert = true
|
||||
},
|
||||
{
|
||||
Name = "debug3"
|
||||
Exact = "1"
|
||||
},
|
||||
{
|
||||
Name = "debug4"
|
||||
Prefix = "aaa"
|
||||
},
|
||||
{
|
||||
Name = "debug5"
|
||||
Suffix = "bbb"
|
||||
},
|
||||
{
|
||||
Name = "debug6"
|
||||
Regex = "a.*z"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Destination {
|
||||
Service = "carrot"
|
||||
ServiceSubset = "kale"
|
||||
Namespace = "leek"
|
||||
PrefixRewrite = "/alternate"
|
||||
RequestTimeout = "99s"
|
||||
IdleTimeout = "99s"
|
||||
NumRetries = 12345
|
||||
RetryOnConnectFailure = true
|
||||
RetryOnStatusCodes = [401, 209]
|
||||
RequestHeaders {
|
||||
Add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
ResponseHeaders {
|
||||
Add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Match {
|
||||
HTTP {
|
||||
PathPrefix = "/foo"
|
||||
Methods = [ "GET", "DELETE" ]
|
||||
QueryParam = [
|
||||
{
|
||||
Name = "hack1"
|
||||
Present = true
|
||||
},
|
||||
{
|
||||
Name = "hack2"
|
||||
Exact = "1"
|
||||
},
|
||||
{
|
||||
Name = "hack3"
|
||||
Regex = "a.*z"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Match {
|
||||
HTTP {
|
||||
PathRegex = "/foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
`,
|
||||
expect: &ServiceRouterConfigEntry{
|
||||
Kind: "service-router",
|
||||
Name: "main",
|
||||
Meta: map[string]string{
|
||||
"foo": "bar",
|
||||
"gir": "zim",
|
||||
},
|
||||
Routes: []ServiceRoute{
|
||||
{
|
||||
Match: &ServiceRouteMatch{
|
||||
HTTP: &ServiceRouteHTTPMatch{
|
||||
PathExact: "/foo",
|
||||
CaseInsensitive: true,
|
||||
Header: []ServiceRouteHTTPMatchHeader{
|
||||
{
|
||||
Name: "debug1",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Name: "debug2",
|
||||
Present: false,
|
||||
Invert: true,
|
||||
},
|
||||
{
|
||||
Name: "debug3",
|
||||
Exact: "1",
|
||||
},
|
||||
{
|
||||
Name: "debug4",
|
||||
Prefix: "aaa",
|
||||
},
|
||||
{
|
||||
Name: "debug5",
|
||||
Suffix: "bbb",
|
||||
},
|
||||
{
|
||||
Name: "debug6",
|
||||
Regex: "a.*z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: &ServiceRouteDestination{
|
||||
Service: "carrot",
|
||||
ServiceSubset: "kale",
|
||||
Namespace: "leek",
|
||||
PrefixRewrite: "/alternate",
|
||||
RequestTimeout: 99 * time.Second,
|
||||
IdleTimeout: 99 * time.Second,
|
||||
NumRetries: 12345,
|
||||
RetryOnConnectFailure: true,
|
||||
RetryOnStatusCodes: []uint32{401, 209},
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"x-foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"x-foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Match: &ServiceRouteMatch{
|
||||
HTTP: &ServiceRouteHTTPMatch{
|
||||
PathPrefix: "/foo",
|
||||
Methods: []string{"GET", "DELETE"},
|
||||
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
||||
{
|
||||
Name: "hack1",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Name: "hack2",
|
||||
Exact: "1",
|
||||
},
|
||||
{
|
||||
Name: "hack3",
|
||||
Regex: "a.*z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Match: &ServiceRouteMatch{
|
||||
HTTP: &ServiceRouteHTTPMatch{
|
||||
PathRegex: "/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-splitter: kitchen sink",
|
||||
snake: `
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/xds/response"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
// routesFromSnapshot returns the xDS API representation of the "routes" in the
|
||||
|
@ -859,6 +860,10 @@ func makeRouteMatchForDiscoveryRoute(discoveryRoute *structs.DiscoveryRoute) *en
|
|||
}
|
||||
}
|
||||
|
||||
if match.HTTP.CaseInsensitive {
|
||||
em.CaseSensitive = wrapperspb.Bool(false)
|
||||
}
|
||||
|
||||
if len(match.HTTP.Header) > 0 {
|
||||
em.Headers = make([]*envoy_route_v3.HeaderMatcher, 0, len(match.HTTP.Header))
|
||||
for _, hdr := range match.HTTP.Header {
|
||||
|
|
|
@ -44,6 +44,7 @@ type ServiceRouteHTTPMatch struct {
|
|||
PathExact string `json:",omitempty" alias:"path_exact"`
|
||||
PathPrefix string `json:",omitempty" alias:"path_prefix"`
|
||||
PathRegex string `json:",omitempty" alias:"path_regex"`
|
||||
CaseInsensitive bool `json:",omitempty" alias:"case_insensitive"`
|
||||
|
||||
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
||||
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
|
||||
|
|
|
@ -121,6 +121,7 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
|||
"alternate",
|
||||
"test-split",
|
||||
"test-route",
|
||||
"test-route-case-insensitive",
|
||||
} {
|
||||
serviceDefaults := &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
|
@ -306,6 +307,67 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
|||
},
|
||||
verify: verifyRouter,
|
||||
},
|
||||
{
|
||||
name: "mega router case insensitive", // use one mega object to avoid multiple trips
|
||||
entry: &ServiceRouterConfigEntry{
|
||||
Kind: ServiceRouter,
|
||||
Name: "test-route-case-insensitive",
|
||||
Partition: defaultPartition,
|
||||
Namespace: defaultNamespace,
|
||||
Routes: []ServiceRoute{
|
||||
{
|
||||
Match: &ServiceRouteMatch{
|
||||
HTTP: &ServiceRouteHTTPMatch{
|
||||
PathPrefix: "/prEfix",
|
||||
CaseInsensitive: true,
|
||||
Header: []ServiceRouteHTTPMatchHeader{
|
||||
{Name: "x-debug", Exact: "1"},
|
||||
},
|
||||
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
||||
{Name: "debug", Exact: "1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: &ServiceRouteDestination{
|
||||
Service: "test-failover",
|
||||
ServiceSubset: "v2",
|
||||
Namespace: defaultNamespace,
|
||||
Partition: defaultPartition,
|
||||
PrefixRewrite: "/",
|
||||
RequestTimeout: 5 * time.Second,
|
||||
NumRetries: 5,
|
||||
RetryOnConnectFailure: true,
|
||||
RetryOnStatusCodes: []uint32{500, 503, 401},
|
||||
RetryOn: []string{
|
||||
"gateway-error",
|
||||
"reset",
|
||||
"envoy-ratelimited",
|
||||
"retriable-4xx",
|
||||
"refused-stream",
|
||||
"cancelled",
|
||||
"deadline-exceeded",
|
||||
"internal",
|
||||
"resource-exhausted",
|
||||
"unavailable",
|
||||
},
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"x-foo": "bar",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Remove: []string{"x-foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: map[string]string{
|
||||
"foo": "bar",
|
||||
"gir": "zim",
|
||||
},
|
||||
},
|
||||
verify: verifyRouter,
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
|
||||
|
|
|
@ -59,6 +59,17 @@ routes = [
|
|||
prefix_rewrite = "/"
|
||||
}
|
||||
},
|
||||
{
|
||||
match { http {
|
||||
path_prefix = "/prefix-case-insensitive/"
|
||||
case_insensitive = true
|
||||
}
|
||||
}
|
||||
destination {
|
||||
service_subset = "v1"
|
||||
prefix_rewrite = "/"
|
||||
}
|
||||
},
|
||||
{
|
||||
match { http {
|
||||
path_regex = "/deb[ug]{2}"
|
||||
|
|
|
@ -56,6 +56,11 @@ load helpers
|
|||
assert_expected_fortio_name s2-v1 localhost 5000 /prefix-alt
|
||||
}
|
||||
|
||||
@test "test prefix path case insensitive" {
|
||||
assert_expected_fortio_name s2-v1 localhost 5000 /prefix-case-Insensitive
|
||||
assert_expected_fortio_name s2-v1 localhost 5000 /prefix-case-INSENSITIVE
|
||||
}
|
||||
|
||||
@test "test regex path" {
|
||||
assert_expected_fortio_name s2-v2 localhost 5000 "" regex-path
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ The following list outlines field hierarchy, language-specific data types, and r
|
|||
- [`PathExact`](#routes-match-http-pathexact): string
|
||||
- [`PathPrefix`](#routes-match-http-pathprefix): string
|
||||
- [`PathRegex`](#routes-match-http-pathregex): string
|
||||
- [`CaseInsensitive`](#routes-match-http-caseinsensitive): boolean | `false`
|
||||
- [`Methods`](#routes-match-http-methods): list
|
||||
- [`Header`](#routes-match-http-header): list
|
||||
- [`Name`](#routes-match-http-header-name): string
|
||||
|
@ -456,6 +457,15 @@ Specifies the path prefix to match on the HTTP request path. When using this fie
|
|||
- Default: None
|
||||
- Data type: String
|
||||
|
||||
### `Routes[].Match{}.HTTP{}.CaseInsensitive`
|
||||
|
||||
Specifies the path prefix to match on the HTTP request path must be case insensitive or not.
|
||||
|
||||
#### Values
|
||||
|
||||
- Default: `false`
|
||||
- Data type: Boolean
|
||||
|
||||
### `Routes[].Match{}.HTTP{}.PathRegex`
|
||||
|
||||
Specifies a regular expression to match on the HTTP request path. When using this field, do not configure `PathExact` or `PathPrefix` in the same HTTP map. The syntax for the regular expression field is proxy-specific. When [using Envoy](/consul/docs/connect/proxies/envoy), refer to [the documentation for Envoy v1.11.2 or newer](https://github.com/google/re2/wiki/Syntax) or [the documentation for Envoy v1.11.1 or older](https://en.cppreference.com/w/cpp/regex/ecmascript), depending on the version of Envoy you use.
|
||||
|
@ -1372,6 +1382,78 @@ spec:
|
|||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Path prefix matching with case insensitive
|
||||
|
||||
The following example routes HTTP requests for the `web` service to a service named `admin` when they have `/admin` or `/Admin` at the start of their path.
|
||||
|
||||
<Tabs>
|
||||
<Tab heading="HCL" group="hcl">
|
||||
|
||||
```hcl
|
||||
Kind = "service-router"
|
||||
Name = "web"
|
||||
Routes = [
|
||||
{
|
||||
Match {
|
||||
HTTP {
|
||||
PathPrefix = "/Admin"
|
||||
CaseInsensitive = true
|
||||
}
|
||||
}
|
||||
|
||||
Destination {
|
||||
Service = "admin"
|
||||
}
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab heading="YAML" group="yaml">
|
||||
|
||||
```yaml
|
||||
apiVersion: consul.hashicorp.com/v1alpha1
|
||||
kind: ServiceRouter
|
||||
metadata:
|
||||
name: web
|
||||
spec:
|
||||
routes:
|
||||
- match:
|
||||
http:
|
||||
pathPrefix: /Admin
|
||||
caseInsensitive: true
|
||||
destination:
|
||||
service: admin
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab heading="JSON" group="json">
|
||||
|
||||
```json
|
||||
{
|
||||
"Kind": "service-router",
|
||||
"Name": "web",
|
||||
"Routes": [
|
||||
{
|
||||
"Match": {
|
||||
"HTTP": {
|
||||
"PathPrefix": "/Admin",
|
||||
"CaseInsensitive": true
|
||||
}
|
||||
},
|
||||
"Destination": {
|
||||
"Service": "admin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Match a header and query parameter
|
||||
|
||||
The following example routes HTTP traffic to the `web` service to a subset of `canary` instances when the requests have `x-debug` in either the header or the URL parameter.
|
||||
|
|
Loading…
Reference in New Issue