Skip to content

Commit 1f0a0fb

Browse files
authored
Add regex matching for headers and query params for HTTPRoute and GRPCRoute (#3093)
Add regex matching for headers and query params Problem: Users want to be able to specify RegularExpression as headers and query params type. Solution: Adds functionality to add RegularExpression type for headers in HTTPRoutes and GRPCRoutes and query params in HTTPRoutes along with functional tests
1 parent 45e4054 commit 1f0a0fb

27 files changed

+1031
-136
lines changed

examples/advanced-routing/cafe-routes.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ spec:
3131
backendRefs:
3232
- name: coffee-v2-svc
3333
port: 80
34+
- matches:
35+
- path:
36+
type: PathPrefix
37+
value: /coffee
38+
headers:
39+
- name: headerRegex
40+
type: RegularExpression
41+
value: "header-[a-z]{1}"
42+
- path:
43+
type: PathPrefix
44+
value: /coffee
45+
queryParams:
46+
- name: queryRegex
47+
type: RegularExpression
48+
value: "query-[a-z]{1}"
49+
backendRefs:
50+
- name: coffee-v3-svc
51+
port: 80
3452
---
3553
apiVersion: gateway.networking.k8s.io/v1
3654
kind: HTTPRoute

examples/advanced-routing/coffee.yaml

+33
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,36 @@ spec:
6363
name: http
6464
selector:
6565
app: coffee-v2
66+
---
67+
apiVersion: apps/v1
68+
kind: Deployment
69+
metadata:
70+
name: coffee-v3
71+
spec:
72+
replicas: 1
73+
selector:
74+
matchLabels:
75+
app: coffee-v3
76+
template:
77+
metadata:
78+
labels:
79+
app: coffee-v3
80+
spec:
81+
containers:
82+
- name: coffee-v3
83+
image: nginxdemos/nginx-hello:plain-text
84+
ports:
85+
- containerPort: 8080
86+
---
87+
apiVersion: v1
88+
kind: Service
89+
metadata:
90+
name: coffee-v3-svc
91+
spec:
92+
ports:
93+
- port: 80
94+
targetPort: 8080
95+
protocol: TCP
96+
name: http
97+
selector:
98+
app: coffee-v3

examples/grpc-routing/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ There are 3 options to configure gRPC routing. To access the application and tes
192192
2024/04/29 09:32:46 Received: version two
193193
```
194194
195+
We'll send a request with the header `headerRegex: grpc-header-a`
196+
197+
```shell
198+
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
199+
```
200+
201+
```text
202+
{
203+
"message": "Hello version two regex"
204+
}
205+
```
206+
207+
Verify logs of `${POD_V2}` to ensure response is from the correct service.
208+
195209
Finally, we'll send a request with the headers `version: two` and `color: orange`
196210
197211
```shell

examples/grpc-routing/headers.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ spec:
3636
backendRefs:
3737
- name: grpc-infra-backend-v2
3838
port: 8080
39+
# Matches "headerRegex: grpc-header-[a-z]{1}"
40+
- matches:
41+
- headers:
42+
- name: headerRegex
43+
value: "grpc-header-[a-z]{1}"
44+
type: RegularExpression
45+
backendRefs:
46+
- name: grpc-infra-backend-v2
47+
port: 8080
3948
# Matches "version: two" AND "color: orange"
4049
- matches:
4150
- headers:

internal/mode/static/nginx/config/servers.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -716,19 +716,23 @@ func createRouteMatch(match dataplane.Match, redirectPath string) routeMatch {
716716
return hm
717717
}
718718

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

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

734738
func isPathOnlyMatch(match dataplane.Match) bool {

internal/mode/static/nginx/config/servers_test.go

+74-27
Original file line numberDiff line numberDiff line change
@@ -710,25 +710,30 @@ func TestCreateServers(t *testing.T) {
710710
{
711711
Name: "Version",
712712
Value: "V1",
713+
Type: dataplane.MatchTypeExact,
713714
},
714715
{
715716
Name: "test",
716717
Value: "foo",
718+
Type: dataplane.MatchTypeExact,
717719
},
718720
{
719721
Name: "my-header",
720722
Value: "my-value",
723+
Type: dataplane.MatchTypeExact,
721724
},
722725
},
723726
QueryParams: []dataplane.HTTPQueryParamMatch{
724727
{
725728
// query names and values should not be normalized to lowercase
726729
Name: "GrEat",
727730
Value: "EXAMPLE",
731+
Type: dataplane.MatchTypeExact,
728732
},
729733
{
730734
Name: "test",
731735
Value: "foo=bar",
736+
Type: dataplane.MatchTypeExact,
732737
},
733738
},
734739
},
@@ -797,6 +802,7 @@ func TestCreateServers(t *testing.T) {
797802
{
798803
Name: "redirect",
799804
Value: "this",
805+
Type: dataplane.MatchTypeExact,
800806
},
801807
},
802808
},
@@ -839,6 +845,7 @@ func TestCreateServers(t *testing.T) {
839845
{
840846
Name: "rewrite",
841847
Value: "this",
848+
Type: dataplane.MatchTypeExact,
842849
},
843850
},
844851
},
@@ -878,6 +885,7 @@ func TestCreateServers(t *testing.T) {
878885
{
879886
Name: "filter",
880887
Value: "this",
888+
Type: dataplane.MatchTypeExact,
881889
},
882890
},
883891
},
@@ -1071,23 +1079,23 @@ func TestCreateServers(t *testing.T) {
10711079
"1_1": {
10721080
{
10731081
Method: "GET",
1074-
Headers: []string{"Version:V1", "test:foo", "my-header:my-value"},
1075-
QueryParams: []string{"GrEat=EXAMPLE", "test=foo=bar"},
1082+
Headers: []string{"Version:Exact:V1", "test:Exact:foo", "my-header:Exact:my-value"},
1083+
QueryParams: []string{"GrEat=Exact=EXAMPLE", "test=Exact=foo=bar"},
10761084
RedirectPath: "/_ngf-internal-rule1-route0",
10771085
},
10781086
},
10791087
"1_6": {
1080-
{RedirectPath: "/_ngf-internal-rule6-route0", Headers: []string{"redirect:this"}},
1088+
{RedirectPath: "/_ngf-internal-rule6-route0", Headers: []string{"redirect:Exact:this"}},
10811089
},
10821090
"1_8": {
10831091
{
1084-
Headers: []string{"rewrite:this"},
1092+
Headers: []string{"rewrite:Exact:this"},
10851093
RedirectPath: "/_ngf-internal-rule8-route0",
10861094
},
10871095
},
10881096
"1_10": {
10891097
{
1090-
Headers: []string{"filter:this"},
1098+
Headers: []string{"filter:Exact:this"},
10911099
RedirectPath: "/_ngf-internal-rule10-route0",
10921100
},
10931101
},
@@ -2702,14 +2710,17 @@ func TestCreateRouteMatch(t *testing.T) {
27022710
{
27032711
Name: "header-1",
27042712
Value: "val-1",
2713+
Type: dataplane.MatchTypeExact,
27052714
},
27062715
{
27072716
Name: "header-2",
27082717
Value: "val-2",
2718+
Type: dataplane.MatchTypeExact,
27092719
},
27102720
{
27112721
Name: "header-3",
27122722
Value: "val-3",
2723+
Type: dataplane.MatchTypeExact,
27132724
},
27142725
}
27152726

@@ -2725,19 +2736,22 @@ func TestCreateRouteMatch(t *testing.T) {
27252736
{
27262737
Name: "arg1",
27272738
Value: "val1",
2739+
Type: dataplane.MatchTypeExact,
27282740
},
27292741
{
27302742
Name: "arg2",
27312743
Value: "val2=another-val",
2744+
Type: dataplane.MatchTypeExact,
27322745
},
27332746
{
27342747
Name: "arg3",
27352748
Value: "==val3",
2749+
Type: dataplane.MatchTypeExact,
27362750
},
27372751
}
27382752

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

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

28572871
func TestCreateQueryParamKeyValString(t *testing.T) {
28582872
t.Parallel()
2859-
g := NewWithT(t)
2860-
2861-
expected := "key=value"
28622873

2863-
result := createQueryParamKeyValString(
2864-
dataplane.HTTPQueryParamMatch{
2865-
Name: "key",
2866-
Value: "value",
2874+
tests := []struct {
2875+
msg string
2876+
input dataplane.HTTPQueryParamMatch
2877+
expected string
2878+
}{
2879+
{
2880+
msg: "Exact match",
2881+
input: dataplane.HTTPQueryParamMatch{
2882+
Name: "key",
2883+
Value: "value",
2884+
Type: dataplane.MatchTypeExact,
2885+
},
2886+
expected: "key=Exact=value",
28672887
},
2868-
)
2869-
2870-
g.Expect(result).To(Equal(expected))
2871-
2872-
expected = "KeY=vaLUe=="
2873-
2874-
result = createQueryParamKeyValString(
2875-
dataplane.HTTPQueryParamMatch{
2876-
Name: "KeY",
2877-
Value: "vaLUe==",
2888+
{
2889+
msg: "RegularExpression match",
2890+
input: dataplane.HTTPQueryParamMatch{
2891+
Name: "KeY",
2892+
Value: "vaLUe-[a-z]==",
2893+
Type: dataplane.MatchTypeRegularExpression,
2894+
},
2895+
expected: "KeY=RegularExpression=vaLUe-[a-z]==",
28782896
},
2879-
)
2897+
{
2898+
msg: "empty match type",
2899+
input: dataplane.HTTPQueryParamMatch{
2900+
Name: "keY",
2901+
Value: "vaLUe==",
2902+
Type: dataplane.MatchTypeExact,
2903+
},
2904+
expected: "keY=Exact=vaLUe==",
2905+
},
2906+
}
28802907

2881-
g.Expect(result).To(Equal(expected))
2908+
for _, tc := range tests {
2909+
t.Run(tc.msg, func(t *testing.T) {
2910+
t.Parallel()
2911+
g := NewWithT(t)
2912+
result := createQueryParamKeyValString(tc.input)
2913+
g.Expect(result).To(Equal(tc.expected))
2914+
})
2915+
}
28822916
}
28832917

28842918
func TestCreateHeaderKeyValString(t *testing.T) {
28852919
t.Parallel()
28862920
g := NewWithT(t)
28872921

2888-
expected := "kEy:vALUe"
2922+
expected := "kEy:Exact:vALUe"
28892923

28902924
result := createHeaderKeyValString(
28912925
dataplane.HTTPHeaderMatch{
28922926
Name: "kEy",
28932927
Value: "vALUe",
2928+
Type: dataplane.MatchTypeExact,
2929+
},
2930+
)
2931+
2932+
g.Expect(result).To(Equal(expected))
2933+
2934+
expected = "kEy:RegularExpression:vALUe-[0-9]"
2935+
2936+
result = createHeaderKeyValString(
2937+
dataplane.HTTPHeaderMatch{
2938+
Name: "kEy",
2939+
Value: "vALUe-[0-9]",
2940+
Type: dataplane.MatchTypeRegularExpression,
28942941
},
28952942
)
28962943

0 commit comments

Comments
 (0)