Skip to content

Commit 63572c7

Browse files
committed
Add support for RequestMirror filter
Problem: As a user, I want to be able to mirror requests to another backend(s) using the RequestMirror filter on an HTTPRoute or GRPCRoute. Solution: Add support for the RequestMirror filter, allowing users to mirror requests with HTTPRoutes or GRPCRoutes.
1 parent 7477df8 commit 63572c7

28 files changed

+1322
-89
lines changed

Diff for: internal/mode/static/nginx/config/http/config.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import (
55
)
66

77
const (
8-
InternalRoutePathPrefix = "/_ngf-internal"
9-
HTTPSScheme = "https"
8+
InternalRoutePathPrefix = "/_ngf-internal"
9+
InternalMirrorRoutePathPrefix = InternalRoutePathPrefix + "-mirror"
10+
HTTPSScheme = "https"
1011
)
1112

1213
// Server holds all configuration for an HTTP server.
@@ -41,6 +42,7 @@ type Location struct {
4142
Return *Return
4243
ResponseHeaders ResponseHeaders
4344
Rewrites []string
45+
MirrorPaths []string
4446
Includes []shared.Include
4547
GRPC bool
4648
}

Diff for: internal/mode/static/nginx/config/servers.go

+26-13
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func createLocations(
237237
matchPairs := make(httpMatchPairs)
238238

239239
var rootPathExists bool
240-
var grpc bool
240+
var grpcServer bool
241241

242242
for pathRuleIdx, rule := range server.PathRules {
243243
matches := make([]routeMatch, 0, len(rule.MatchRules))
@@ -247,7 +247,7 @@ func createLocations(
247247
}
248248

249249
if rule.GRPC {
250-
grpc = true
250+
grpcServer = true
251251
}
252252

253253
extLocations := initializeExternalLocations(rule, pathsAndTypes)
@@ -277,7 +277,7 @@ func createLocations(
277277
internalLocations := make([]http.Location, 0, len(rule.MatchRules))
278278

279279
for matchRuleIdx, r := range rule.MatchRules {
280-
intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match, grpc)
280+
intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match, rule.GRPC)
281281
intLocation.Includes = createIncludesFromPolicyGenerateResult(
282282
generator.GenerateForInternalLocation(rule.Policies),
283283
)
@@ -313,7 +313,7 @@ func createLocations(
313313
locs = append(locs, createDefaultRootLocation())
314314
}
315315

316-
return locs, matchPairs, grpc
316+
return locs, matchPairs, grpcServer
317317
}
318318

319319
func needsInternalLocations(rule dataplane.PathRule) bool {
@@ -433,6 +433,13 @@ func updateLocation(
433433
return location
434434
}
435435

436+
if strings.HasPrefix(path, http.InternalMirrorRoutePathPrefix) {
437+
location.Type = http.InternalLocationType
438+
if grpc {
439+
location.Rewrites = []string{"^ $request_uri break"}
440+
}
441+
}
442+
436443
location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
437444

438445
if filters.RequestRedirect != nil {
@@ -445,6 +452,20 @@ func updateLocation(
445452
}
446453

447454
rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
455+
if rewrites != nil {
456+
if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
457+
location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
458+
}
459+
if rewrites.MainRewrite != "" {
460+
location.Rewrites = append(location.Rewrites, rewrites.MainRewrite)
461+
}
462+
}
463+
464+
for _, filter := range filters.RequestMirrors {
465+
if filter.Target != nil {
466+
location.MirrorPaths = append(location.MirrorPaths, *filter.Target)
467+
}
468+
}
448469

449470
extraHeaders := make([]http.Header, 0, 3)
450471
if grpc {
@@ -457,15 +478,6 @@ func updateLocation(
457478
proxySetHeaders := generateProxySetHeaders(&matchRule.Filters, createBaseProxySetHeaders(extraHeaders...))
458479
responseHeaders := generateResponseHeaders(&matchRule.Filters)
459480

460-
if rewrites != nil {
461-
if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
462-
location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
463-
}
464-
if rewrites.MainRewrite != "" {
465-
location.Rewrites = append(location.Rewrites, rewrites.MainRewrite)
466-
}
467-
}
468-
469481
location.ProxySetHeaders = proxySetHeaders
470482
location.ProxySSLVerify = createProxyTLSFromBackends(matchRule.BackendGroup.Backends)
471483
proxyPass := createProxyPass(
@@ -740,6 +752,7 @@ func isPathOnlyMatch(match dataplane.Match) bool {
740752
return match.Method == nil && len(match.Headers) == 0 && len(match.QueryParams) == 0
741753
}
742754

755+
// TODO(sberman): if this is an internal mirror location, don't set request_uri (test this).
743756
func createProxyPass(
744757
backendGroup dataplane.BackendGroup,
745758
filter *dataplane.HTTPURLRewriteFilter,

Diff for: internal/mode/static/nginx/config/servers_template.go

+4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ server {
101101
rewrite {{ $r }};
102102
{{- end }}
103103
104+
{{- range $m := $l.MirrorPaths }}
105+
mirror {{ $m }};
106+
{{- end }}
107+
104108
{{- if $l.Return }}
105109
return {{ $l.Return.Code }} "{{ $l.Return.Body }}";
106110
{{- end }}

Diff for: internal/mode/static/nginx/config/servers_test.go

+116-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
. "github.com/onsi/gomega"
10+
"github.com/onsi/gomega/format"
1011
"k8s.io/apimachinery/pkg/types"
1112

1213
"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
@@ -862,6 +863,66 @@ func TestCreateServers(t *testing.T) {
862863
},
863864
},
864865
},
866+
{
867+
Path: "/mirror",
868+
PathType: dataplane.PathTypePrefix,
869+
MatchRules: []dataplane.MatchRule{
870+
{
871+
Match: dataplane.Match{},
872+
Filters: dataplane.HTTPFilters{
873+
RequestMirrors: []*dataplane.HTTPRequestMirrorFilter{
874+
{
875+
Name: helpers.GetPointer("mirror-filter"),
876+
Namespace: helpers.GetPointer("test-ns"),
877+
Target: helpers.GetPointer(http.InternalMirrorRoutePathPrefix + "-my-backend"),
878+
},
879+
},
880+
},
881+
BackendGroup: fooGroup,
882+
},
883+
},
884+
},
885+
{
886+
Path: http.InternalMirrorRoutePathPrefix + "-my-backend",
887+
PathType: dataplane.PathTypeExact,
888+
MatchRules: []dataplane.MatchRule{
889+
{
890+
Match: dataplane.Match{},
891+
BackendGroup: fooGroup,
892+
},
893+
},
894+
},
895+
{
896+
Path: "/grpc/mirror",
897+
PathType: dataplane.PathTypeExact,
898+
MatchRules: []dataplane.MatchRule{
899+
{
900+
Match: dataplane.Match{},
901+
Filters: dataplane.HTTPFilters{
902+
RequestMirrors: []*dataplane.HTTPRequestMirrorFilter{
903+
{
904+
Name: helpers.GetPointer("grpc-mirror-filter"),
905+
Namespace: helpers.GetPointer("test-ns"),
906+
Target: helpers.GetPointer(http.InternalMirrorRoutePathPrefix + "-my-grpc-backend"),
907+
},
908+
},
909+
},
910+
BackendGroup: fooGroup,
911+
},
912+
},
913+
GRPC: true,
914+
},
915+
{
916+
Path: http.InternalMirrorRoutePathPrefix + "-my-grpc-backend",
917+
PathType: dataplane.PathTypeExact,
918+
MatchRules: []dataplane.MatchRule{
919+
{
920+
Match: dataplane.Match{},
921+
BackendGroup: fooGroup,
922+
},
923+
},
924+
GRPC: true,
925+
},
865926
{
866927
Path: "/invalid-filter",
867928
PathType: dataplane.PathTypePrefix,
@@ -1093,25 +1154,25 @@ func TestCreateServers(t *testing.T) {
10931154
RedirectPath: "/_ngf-internal-rule8-route0",
10941155
},
10951156
},
1096-
"1_10": {
1157+
"1_14": {
10971158
{
10981159
Headers: []string{"filter:Exact:this"},
1099-
RedirectPath: "/_ngf-internal-rule10-route0",
1160+
RedirectPath: "/_ngf-internal-rule14-route0",
11001161
},
11011162
},
1102-
"1_12": {
1163+
"1_16": {
11031164
{
11041165
Method: "GET",
1105-
RedirectPath: "/_ngf-internal-rule12-route0",
1166+
RedirectPath: "/_ngf-internal-rule16-route0",
11061167
Headers: nil,
11071168
QueryParams: nil,
11081169
Any: false,
11091170
},
11101171
},
1111-
"1_17": {
1172+
"1_21": {
11121173
{
11131174
Method: "GET",
1114-
RedirectPath: "/_ngf-internal-rule17-route0",
1175+
RedirectPath: "/_ngf-internal-rule21-route0",
11151176
},
11161177
},
11171178
}
@@ -1342,6 +1403,47 @@ func TestCreateServers(t *testing.T) {
13421403
Type: http.InternalLocationType,
13431404
Includes: internalIncludes,
13441405
},
1406+
{
1407+
Path: "/mirror/",
1408+
ProxyPass: "http://test_foo_80$request_uri",
1409+
ProxySetHeaders: httpBaseHeaders,
1410+
MirrorPaths: []string{"/_ngf-internal-mirror-my-backend"},
1411+
Type: http.ExternalLocationType,
1412+
Includes: externalIncludes,
1413+
},
1414+
{
1415+
Path: "= /mirror",
1416+
ProxyPass: "http://test_foo_80$request_uri",
1417+
ProxySetHeaders: httpBaseHeaders,
1418+
MirrorPaths: []string{"/_ngf-internal-mirror-my-backend"},
1419+
Type: http.ExternalLocationType,
1420+
Includes: externalIncludes,
1421+
},
1422+
{
1423+
Path: "= /_ngf-internal-mirror-my-backend",
1424+
ProxyPass: "http://test_foo_80$request_uri",
1425+
ProxySetHeaders: httpBaseHeaders,
1426+
Type: http.InternalLocationType,
1427+
Includes: externalIncludes,
1428+
},
1429+
{
1430+
Path: "= /grpc/mirror",
1431+
GRPC: true,
1432+
ProxyPass: "grpc://test_foo_80",
1433+
ProxySetHeaders: grpcBaseHeaders,
1434+
MirrorPaths: []string{"/_ngf-internal-mirror-my-grpc-backend"},
1435+
Type: http.ExternalLocationType,
1436+
Includes: externalIncludes,
1437+
},
1438+
{
1439+
Path: "= /_ngf-internal-mirror-my-grpc-backend",
1440+
GRPC: true,
1441+
ProxyPass: "grpc://test_foo_80",
1442+
Rewrites: []string{"^ $request_uri break"},
1443+
ProxySetHeaders: grpcBaseHeaders,
1444+
Type: http.InternalLocationType,
1445+
Includes: externalIncludes,
1446+
},
13451447
{
13461448
Path: "/invalid-filter/",
13471449
Return: &http.Return{
@@ -1360,18 +1462,18 @@ func TestCreateServers(t *testing.T) {
13601462
},
13611463
{
13621464
Path: "/invalid-filter-with-headers/",
1363-
HTTPMatchKey: ssl + "1_10",
1465+
HTTPMatchKey: ssl + "1_14",
13641466
Type: http.RedirectLocationType,
13651467
Includes: externalIncludes,
13661468
},
13671469
{
13681470
Path: "= /invalid-filter-with-headers",
1369-
HTTPMatchKey: ssl + "1_10",
1471+
HTTPMatchKey: ssl + "1_14",
13701472
Type: http.RedirectLocationType,
13711473
Includes: externalIncludes,
13721474
},
13731475
{
1374-
Path: "/_ngf-internal-rule10-route0",
1476+
Path: "/_ngf-internal-rule14-route0",
13751477
Return: &http.Return{
13761478
Code: http.StatusInternalServerError,
13771479
},
@@ -1387,12 +1489,12 @@ func TestCreateServers(t *testing.T) {
13871489
},
13881490
{
13891491
Path: "= /test",
1390-
HTTPMatchKey: ssl + "1_12",
1492+
HTTPMatchKey: ssl + "1_16",
13911493
Type: http.RedirectLocationType,
13921494
Includes: externalIncludes,
13931495
},
13941496
{
1395-
Path: "/_ngf-internal-rule12-route0",
1497+
Path: "/_ngf-internal-rule16-route0",
13961498
ProxyPass: "http://test_foo_80$request_uri",
13971499
ProxySetHeaders: httpBaseHeaders,
13981500
Type: http.InternalLocationType,
@@ -1471,15 +1573,14 @@ func TestCreateServers(t *testing.T) {
14711573
},
14721574
{
14731575
Path: "= /include-header-match",
1474-
HTTPMatchKey: ssl + "1_17",
1576+
HTTPMatchKey: ssl + "1_21",
14751577
Type: http.RedirectLocationType,
14761578
Includes: externalIncludes,
14771579
},
14781580
{
1479-
Path: "/_ngf-internal-rule17-route0",
1581+
Path: "/_ngf-internal-rule21-route0",
14801582
ProxyPass: "http://test_foo_80$request_uri",
14811583
ProxySetHeaders: httpBaseHeaders,
1482-
Rewrites: []string{"^ $request_uri break"},
14831584
Type: http.InternalLocationType,
14841585
Includes: internalIncludes,
14851586
},
@@ -1572,6 +1673,7 @@ func TestCreateServers(t *testing.T) {
15721673

15731674
result, httpMatchPair := createServers(conf, fakeGenerator, keepAliveCheck)
15741675

1676+
format.MaxLength = 10000
15751677
g.Expect(httpMatchPair).To(Equal(allExpMatchPair))
15761678
g.Expect(helpers.Diff(expectedServers, result)).To(BeEmpty())
15771679
}

Diff for: internal/mode/static/nginx/config/validation/http_validator.go

+2
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ type HTTPValidator struct {
1515
HTTPPathValidator
1616
}
1717

18+
func (HTTPValidator) SkipValidation() bool { return false }
19+
1820
var _ validation.HTTPFieldsValidator = HTTPValidator{}

0 commit comments

Comments
 (0)