Skip to content

Add regex matching for headers and query params for HTTPRoute and GRPCRoute #3093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 10, 2025
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
18 changes: 18 additions & 0 deletions examples/advanced-routing/cafe-routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ spec:
backendRefs:
- name: coffee-v2-svc
port: 80
- matches:
- path:
type: PathPrefix
value: /coffee
headers:
- name: headerRegex
type: RegularExpression
value: "header-[a-z]{1}"
- path:
type: PathPrefix
value: /coffee
queryParams:
- name: queryRegex
type: RegularExpression
value: "query-[a-z]{1}"
backendRefs:
- name: coffee-v3-svc
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
Expand Down
33 changes: 33 additions & 0 deletions examples/advanced-routing/coffee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,36 @@ spec:
name: http
selector:
app: coffee-v2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee-v3
spec:
replicas: 1
selector:
matchLabels:
app: coffee-v3
template:
metadata:
labels:
app: coffee-v3
spec:
containers:
- name: coffee-v3
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee-v3-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee-v3
14 changes: 14 additions & 0 deletions examples/grpc-routing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ There are 3 options to configure gRPC routing. To access the application and tes
2024/04/29 09:32:46 Received: version two
```

We'll send a request with the header `headerRegex: grpc-header-a`

```shell
grpcurl -plaintext -proto grpc.proto -authority bar.com -d '{"name": "version two regex"}' -H 'headerRegex: grpc-header-a' ${GW_IP}:${GW_PORT} helloworld.Greeter/SayHello
```

```text
{
"message": "Hello version two regex"
}
```

Verify logs of `${POD_V2}` to ensure response is from the correct service.

Finally, we'll send a request with the headers `version: two` and `color: orange`

```shell
Expand Down
9 changes: 9 additions & 0 deletions examples/grpc-routing/headers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ spec:
backendRefs:
- name: grpc-infra-backend-v2
port: 8080
# Matches "headerRegex: grpc-header-[a-z]{1}"
- matches:
- headers:
- name: headerRegex
value: "grpc-header-[a-z]{1}"
type: RegularExpression
backendRefs:
- name: grpc-infra-backend-v2
port: 8080
# Matches "version: two" AND "color: orange"
- matches:
- headers:
Expand Down
12 changes: 8 additions & 4 deletions internal/mode/static/nginx/config/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,19 +716,23 @@ func createRouteMatch(match dataplane.Match, redirectPath string) routeMatch {
return hm
}

// The name and values are delimited by "=". A name and value can always be recovered using strings.SplitN(arg,"=", 2).
// The name, match type and values are delimited by "=".
// A name, match type and value can always be recovered using strings.SplitN(arg,"=", 3).
// Query Parameters are case-sensitive so case is preserved.
// The match type is optional and defaults to "Exact".
func createQueryParamKeyValString(p dataplane.HTTPQueryParamMatch) string {
return p.Name + "=" + p.Value
return p.Name + "=" + string(p.Type) + "=" + p.Value
}

// The name and values are delimited by ":". A name and value can always be recovered using strings.Split(arg, ":").
// The name, match type and values are delimited by ":".
// A name, match type and value can always be recovered using strings.Split(arg, ":").
// Header names are case-insensitive and header values are case-sensitive.
// The match type is optional and defaults to "Exact".
// Ex. foo:bar == FOO:bar, but foo:bar != foo:BAR,
// We preserve the case of the name here because NGINX allows us to look up the header names in a case-insensitive
// manner.
func createHeaderKeyValString(h dataplane.HTTPHeaderMatch) string {
return h.Name + HeaderMatchSeparator + h.Value
return h.Name + HeaderMatchSeparator + string(h.Type) + HeaderMatchSeparator + h.Value
}

func isPathOnlyMatch(match dataplane.Match) bool {
Expand Down
101 changes: 74 additions & 27 deletions internal/mode/static/nginx/config/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,25 +710,30 @@ func TestCreateServers(t *testing.T) {
{
Name: "Version",
Value: "V1",
Type: dataplane.MatchTypeExact,
},
{
Name: "test",
Value: "foo",
Type: dataplane.MatchTypeExact,
},
{
Name: "my-header",
Value: "my-value",
Type: dataplane.MatchTypeExact,
},
},
QueryParams: []dataplane.HTTPQueryParamMatch{
{
// query names and values should not be normalized to lowercase
Name: "GrEat",
Value: "EXAMPLE",
Type: dataplane.MatchTypeExact,
},
{
Name: "test",
Value: "foo=bar",
Type: dataplane.MatchTypeExact,
},
},
},
Expand Down Expand Up @@ -797,6 +802,7 @@ func TestCreateServers(t *testing.T) {
{
Name: "redirect",
Value: "this",
Type: dataplane.MatchTypeExact,
},
},
},
Expand Down Expand Up @@ -839,6 +845,7 @@ func TestCreateServers(t *testing.T) {
{
Name: "rewrite",
Value: "this",
Type: dataplane.MatchTypeExact,
},
},
},
Expand Down Expand Up @@ -878,6 +885,7 @@ func TestCreateServers(t *testing.T) {
{
Name: "filter",
Value: "this",
Type: dataplane.MatchTypeExact,
},
},
},
Expand Down Expand Up @@ -1071,23 +1079,23 @@ func TestCreateServers(t *testing.T) {
"1_1": {
{
Method: "GET",
Headers: []string{"Version:V1", "test:foo", "my-header:my-value"},
QueryParams: []string{"GrEat=EXAMPLE", "test=foo=bar"},
Headers: []string{"Version:Exact:V1", "test:Exact:foo", "my-header:Exact:my-value"},
QueryParams: []string{"GrEat=Exact=EXAMPLE", "test=Exact=foo=bar"},
RedirectPath: "/_ngf-internal-rule1-route0",
},
},
"1_6": {
{RedirectPath: "/_ngf-internal-rule6-route0", Headers: []string{"redirect:this"}},
{RedirectPath: "/_ngf-internal-rule6-route0", Headers: []string{"redirect:Exact:this"}},
},
"1_8": {
{
Headers: []string{"rewrite:this"},
Headers: []string{"rewrite:Exact:this"},
RedirectPath: "/_ngf-internal-rule8-route0",
},
},
"1_10": {
{
Headers: []string{"filter:this"},
Headers: []string{"filter:Exact:this"},
RedirectPath: "/_ngf-internal-rule10-route0",
},
},
Expand Down Expand Up @@ -2702,14 +2710,17 @@ func TestCreateRouteMatch(t *testing.T) {
{
Name: "header-1",
Value: "val-1",
Type: dataplane.MatchTypeExact,
},
{
Name: "header-2",
Value: "val-2",
Type: dataplane.MatchTypeExact,
},
{
Name: "header-3",
Value: "val-3",
Type: dataplane.MatchTypeExact,
},
}

Expand All @@ -2725,19 +2736,22 @@ func TestCreateRouteMatch(t *testing.T) {
{
Name: "arg1",
Value: "val1",
Type: dataplane.MatchTypeExact,
},
{
Name: "arg2",
Value: "val2=another-val",
Type: dataplane.MatchTypeExact,
},
{
Name: "arg3",
Value: "==val3",
Type: dataplane.MatchTypeExact,
},
}

expectedHeaders := []string{"header-1:val-1", "header-2:val-2", "header-3:val-3"}
expectedArgs := []string{"arg1=val1", "arg2=val2=another-val", "arg3===val3"}
expectedHeaders := []string{"header-1:Exact:val-1", "header-2:Exact:val-2", "header-3:Exact:val-3"}
expectedArgs := []string{"arg1=Exact=val1", "arg2=Exact=val2=another-val", "arg3=Exact===val3"}

tests := []struct {
match dataplane.Match
Expand Down Expand Up @@ -2856,41 +2870,74 @@ func TestCreateRouteMatch(t *testing.T) {

func TestCreateQueryParamKeyValString(t *testing.T) {
t.Parallel()
g := NewWithT(t)

expected := "key=value"

result := createQueryParamKeyValString(
dataplane.HTTPQueryParamMatch{
Name: "key",
Value: "value",
tests := []struct {
msg string
input dataplane.HTTPQueryParamMatch
expected string
}{
{
msg: "Exact match",
input: dataplane.HTTPQueryParamMatch{
Name: "key",
Value: "value",
Type: dataplane.MatchTypeExact,
},
expected: "key=Exact=value",
},
)

g.Expect(result).To(Equal(expected))

expected = "KeY=vaLUe=="

result = createQueryParamKeyValString(
dataplane.HTTPQueryParamMatch{
Name: "KeY",
Value: "vaLUe==",
{
msg: "RegularExpression match",
input: dataplane.HTTPQueryParamMatch{
Name: "KeY",
Value: "vaLUe-[a-z]==",
Type: dataplane.MatchTypeRegularExpression,
},
expected: "KeY=RegularExpression=vaLUe-[a-z]==",
},
)
{
msg: "empty match type",
input: dataplane.HTTPQueryParamMatch{
Name: "keY",
Value: "vaLUe==",
Type: dataplane.MatchTypeExact,
},
expected: "keY=Exact=vaLUe==",
},
}

g.Expect(result).To(Equal(expected))
for _, tc := range tests {
t.Run(tc.msg, func(t *testing.T) {
t.Parallel()
g := NewWithT(t)
result := createQueryParamKeyValString(tc.input)
g.Expect(result).To(Equal(tc.expected))
})
}
}

func TestCreateHeaderKeyValString(t *testing.T) {
t.Parallel()
g := NewWithT(t)

expected := "kEy:vALUe"
expected := "kEy:Exact:vALUe"

result := createHeaderKeyValString(
dataplane.HTTPHeaderMatch{
Name: "kEy",
Value: "vALUe",
Type: dataplane.MatchTypeExact,
},
)

g.Expect(result).To(Equal(expected))

expected = "kEy:RegularExpression:vALUe-[0-9]"

result = createHeaderKeyValString(
dataplane.HTTPHeaderMatch{
Name: "kEy",
Value: "vALUe-[0-9]",
Type: dataplane.MatchTypeRegularExpression,
},
)

Expand Down
Loading
Loading