Skip to content

Commit c56b771

Browse files
authored
Add support for percentage based request mirroring (#3627)
Add support for percentage based request mirroring by supporting the percent and fraction fields in the HTTPRequestMirrorFilter API. Problem: Users would like to enable percentage based request mirroring. Solution: Add support for the percent and fraction fields in the HTTPRequestMirrorFilter API. Testing: Added unit tests, updated conformance tests run, manually ran through a number of test cases.
1 parent fc0a5a7 commit c56b771

22 files changed

+1355
-231
lines changed

internal/controller/nginx/config/http/config.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,19 @@ const (
3333

3434
// Location holds all configuration for an HTTP location.
3535
type Location struct {
36-
Path string
37-
ProxyPass string
38-
HTTPMatchKey string
39-
Type LocationType
40-
ProxySetHeaders []Header
41-
ProxySSLVerify *ProxySSLVerify
42-
Return *Return
43-
ResponseHeaders ResponseHeaders
44-
Rewrites []string
45-
MirrorPaths []string
46-
Includes []shared.Include
47-
GRPC bool
36+
Path string
37+
ProxyPass string
38+
HTTPMatchKey string
39+
MirrorSplitClientsVariableName string
40+
Type LocationType
41+
ProxySetHeaders []Header
42+
ProxySSLVerify *ProxySSLVerify
43+
Return *Return
44+
ResponseHeaders ResponseHeaders
45+
Rewrites []string
46+
MirrorPaths []string
47+
Includes []shared.Include
48+
GRPC bool
4849
}
4950

5051
// Header defines an HTTP header to be passed to the proxied server.

internal/controller/nginx/config/servers.go

Lines changed: 137 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,33 @@ type rewriteConfig struct {
224224
MainRewrite string
225225
}
226226

227+
// extractMirrorTargetsWithPercentages extracts mirror targets and their percentages from path rules.
228+
func extractMirrorTargetsWithPercentages(pathRules []dataplane.PathRule) map[string]*float64 {
229+
mirrorTargets := make(map[string]*float64)
230+
231+
for _, rule := range pathRules {
232+
for _, matchRule := range rule.MatchRules {
233+
for _, mirrorFilter := range matchRule.Filters.RequestMirrors {
234+
if mirrorFilter.Target != nil {
235+
if mirrorFilter.Percent == nil {
236+
mirrorTargets[*mirrorFilter.Target] = helpers.GetPointer(100.0)
237+
continue
238+
}
239+
240+
percentage := mirrorFilter.Percent
241+
242+
if _, exists := mirrorTargets[*mirrorFilter.Target]; !exists ||
243+
*percentage > *mirrorTargets[*mirrorFilter.Target] {
244+
mirrorTargets[*mirrorFilter.Target] = percentage // set a higher percentage if it exists
245+
}
246+
}
247+
}
248+
}
249+
}
250+
251+
return mirrorTargets
252+
}
253+
227254
type httpMatchPairs map[string][]routeMatch
228255

229256
func createLocations(
@@ -239,6 +266,8 @@ func createLocations(
239266
var rootPathExists bool
240267
var grpcServer bool
241268

269+
mirrorPathToPercentage := extractMirrorTargetsWithPercentages(server.PathRules)
270+
242271
for pathRuleIdx, rule := range server.PathRules {
243272
matches := make([]routeMatch, 0, len(rule.MatchRules))
244273

@@ -250,6 +279,8 @@ func createLocations(
250279
grpcServer = true
251280
}
252281

282+
mirrorPercentage := mirrorPathToPercentage[rule.Path]
283+
253284
extLocations := initializeExternalLocations(rule, pathsAndTypes)
254285
for i := range extLocations {
255286
extLocations[i].Includes = createIncludesFromPolicyGenerateResult(
@@ -260,13 +291,12 @@ func createLocations(
260291
if !needsInternalLocations(rule) {
261292
for _, r := range rule.MatchRules {
262293
extLocations = updateLocations(
263-
r.Filters,
264-
extLocations,
265294
r,
295+
rule,
296+
extLocations,
266297
server.Port,
267-
rule.Path,
268-
rule.GRPC,
269298
keepAliveCheck,
299+
mirrorPercentage,
270300
)
271301
}
272302

@@ -283,13 +313,12 @@ func createLocations(
283313
)
284314

285315
intLocation = updateLocation(
286-
r.Filters,
287-
intLocation,
288316
r,
317+
rule,
318+
intLocation,
289319
server.Port,
290-
rule.Path,
291-
rule.GRPC,
292320
keepAliveCheck,
321+
mirrorPercentage,
293322
)
294323

295324
internalLocations = append(internalLocations, intLocation)
@@ -420,38 +449,68 @@ func initializeInternalLocation(
420449

421450
// updateLocation updates a location with any relevant configurations, like proxy_pass, filters, tls settings, etc.
422451
func updateLocation(
423-
filters dataplane.HTTPFilters,
424-
location http.Location,
425452
matchRule dataplane.MatchRule,
453+
pathRule dataplane.PathRule,
454+
location http.Location,
426455
listenerPort int32,
427-
path string,
428-
grpc bool,
429456
keepAliveCheck keepAliveChecker,
457+
mirrorPercentage *float64,
430458
) http.Location {
459+
filters := matchRule.Filters
460+
path := pathRule.Path
461+
grpc := pathRule.GRPC
462+
431463
if filters.InvalidFilter != nil {
432464
location.Return = &http.Return{Code: http.StatusInternalServerError}
433465
return location
434466
}
435467

468+
location = updateLocationMirrorRoute(location, path, grpc)
469+
location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
470+
471+
if filters.RequestRedirect != nil {
472+
return updateLocationRedirectFilter(location, filters.RequestRedirect, listenerPort, path)
473+
}
474+
475+
location = updateLocationRewriteFilter(location, filters.RequestURLRewrite, path)
476+
location = updateLocationMirrorFilters(location, filters.RequestMirrors, path, mirrorPercentage)
477+
location = updateLocationProxySettings(location, matchRule, grpc, keepAliveCheck)
478+
479+
return location
480+
}
481+
482+
func updateLocationMirrorRoute(location http.Location, path string, grpc bool) http.Location {
436483
if strings.HasPrefix(path, http.InternalMirrorRoutePathPrefix) {
437484
location.Type = http.InternalLocationType
438485
if grpc {
439486
location.Rewrites = []string{"^ $request_uri break"}
440487
}
441488
}
442489

443-
location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
490+
return location
491+
}
444492

445-
if filters.RequestRedirect != nil {
446-
ret, rewrite := createReturnAndRewriteConfigForRedirectFilter(filters.RequestRedirect, listenerPort, path)
447-
if rewrite.MainRewrite != "" {
448-
location.Rewrites = append(location.Rewrites, rewrite.MainRewrite)
449-
}
450-
location.Return = ret
451-
return location
493+
func updateLocationRedirectFilter(
494+
location http.Location,
495+
redirectFilter *dataplane.HTTPRequestRedirectFilter,
496+
listenerPort int32,
497+
path string,
498+
) http.Location {
499+
ret, rewrite := createReturnAndRewriteConfigForRedirectFilter(redirectFilter, listenerPort, path)
500+
if rewrite.MainRewrite != "" {
501+
location.Rewrites = append(location.Rewrites, rewrite.MainRewrite)
452502
}
503+
location.Return = ret
504+
505+
return location
506+
}
453507

454-
rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
508+
func updateLocationRewriteFilter(
509+
location http.Location,
510+
rewriteFilter *dataplane.HTTPURLRewriteFilter,
511+
path string,
512+
) http.Location {
513+
rewrites := createRewritesValForRewriteFilter(rewriteFilter, path)
455514
if rewrites != nil {
456515
if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
457516
location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
@@ -461,12 +520,42 @@ func updateLocation(
461520
}
462521
}
463522

464-
for _, filter := range filters.RequestMirrors {
523+
return location
524+
}
525+
526+
func updateLocationMirrorFilters(
527+
location http.Location,
528+
mirrorFilters []*dataplane.HTTPRequestMirrorFilter,
529+
path string,
530+
mirrorPercentage *float64,
531+
) http.Location {
532+
for _, filter := range mirrorFilters {
465533
if filter.Target != nil {
466534
location.MirrorPaths = append(location.MirrorPaths, *filter.Target)
467535
}
468536
}
469537

538+
if location.MirrorPaths != nil {
539+
location.MirrorPaths = deduplicateStrings(location.MirrorPaths)
540+
}
541+
542+
// if mirrorPercentage is nil (no mirror filter configured) or 100.0, the split clients variable is not generated,
543+
// and we let all traffic get mirrored.
544+
if mirrorPercentage != nil && *mirrorPercentage != 100.0 {
545+
location.MirrorSplitClientsVariableName = convertSplitClientVariableName(
546+
fmt.Sprintf("%s_%.2f", path, *mirrorPercentage),
547+
)
548+
}
549+
550+
return location
551+
}
552+
553+
func updateLocationProxySettings(
554+
location http.Location,
555+
matchRule dataplane.MatchRule,
556+
grpc bool,
557+
keepAliveCheck keepAliveChecker,
558+
) http.Location {
470559
extraHeaders := make([]http.Header, 0, 3)
471560
if grpc {
472561
extraHeaders = append(extraHeaders, grpcAuthorityHeader)
@@ -497,18 +586,24 @@ func updateLocation(
497586
// updateLocations updates the existing locations with any relevant configurations, like proxy_pass,
498587
// filters, tls settings, etc.
499588
func updateLocations(
500-
filters dataplane.HTTPFilters,
501-
buildLocations []http.Location,
502589
matchRule dataplane.MatchRule,
590+
pathRule dataplane.PathRule,
591+
buildLocations []http.Location,
503592
listenerPort int32,
504-
path string,
505-
grpc bool,
506593
keepAliveCheck keepAliveChecker,
594+
mirrorPercentage *float64,
507595
) []http.Location {
508596
updatedLocations := make([]http.Location, len(buildLocations))
509597

510598
for i, loc := range buildLocations {
511-
updatedLocations[i] = updateLocation(filters, loc, matchRule, listenerPort, path, grpc, keepAliveCheck)
599+
updatedLocations[i] = updateLocation(
600+
matchRule,
601+
pathRule,
602+
loc,
603+
listenerPort,
604+
keepAliveCheck,
605+
mirrorPercentage,
606+
)
512607
}
513608

514609
return updatedLocations
@@ -962,3 +1057,18 @@ func getConnectionHeader(keepAliveCheck keepAliveChecker, backends []dataplane.B
9621057

9631058
return httpConnectionHeader
9641059
}
1060+
1061+
// deduplicateStrings removes duplicate strings from a slice while preserving order.
1062+
func deduplicateStrings(content []string) []string {
1063+
seen := make(map[string]struct{})
1064+
result := make([]string, 0, len(content))
1065+
1066+
for _, str := range content {
1067+
if _, exists := seen[str]; !exists {
1068+
seen[str] = struct{}{}
1069+
result = append(result, str)
1070+
}
1071+
}
1072+
1073+
return result
1074+
}

internal/controller/nginx/config/servers_template.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ server {
9393
internal;
9494
{{ end }}
9595
96+
{{ if ne $l.MirrorSplitClientsVariableName "" -}}
97+
if (${{ $l.MirrorSplitClientsVariableName }} = "") {
98+
return 204;
99+
}
100+
{{- end }}
101+
96102
{{- range $i := $l.Includes }}
97103
include {{ $i.Name }};
98104
{{- end -}}

0 commit comments

Comments
 (0)