Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/19647.txt
Original file line number Diff line number Diff line change
@@ -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.
```
8 changes: 5 additions & 3 deletions agent/structs/config_entry_discoverychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,10 @@ func (m *ServiceRouteMatch) IsEmpty() bool {

// ServiceRouteHTTPMatch is a set of http-specific match criteria.
type ServiceRouteHTTPMatch struct {
PathExact string `json:",omitempty" alias:"path_exact"`
PathPrefix string `json:",omitempty" alias:"path_prefix"`
PathRegex string `json:",omitempty" alias:"path_regex"`
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"`
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions agent/structs/config_entry_discoverychain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
304 changes: 304 additions & 0 deletions agent/structs/config_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
Expand Down
5 changes: 5 additions & 0 deletions agent/xds/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions api/config_entry_discoverychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ type ServiceRouteMatch struct {
}

type ServiceRouteHTTPMatch struct {
PathExact string `json:",omitempty" alias:"path_exact"`
PathPrefix string `json:",omitempty" alias:"path_prefix"`
PathRegex string `json:",omitempty" alias:"path_regex"`
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"`
Expand Down
Loading