Skip to content

Commit b26d9b7

Browse files
committed
Add support for percentage based request mirroring
1 parent f8ee6d1 commit b26d9b7

21 files changed

+1324
-213
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: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,32 @@ 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] = 100.0
237+
continue
238+
}
239+
240+
percentage := *mirrorFilter.Percent
241+
242+
if _, exists := mirrorTargets[*mirrorFilter.Target]; !exists || percentage > mirrorTargets[*mirrorFilter.Target] {
243+
mirrorTargets[*mirrorFilter.Target] = percentage // set a higher percentage if it exists
244+
}
245+
}
246+
}
247+
}
248+
}
249+
250+
return mirrorTargets
251+
}
252+
227253
type httpMatchPairs map[string][]routeMatch
228254

229255
func createLocations(
@@ -239,6 +265,8 @@ func createLocations(
239265
var rootPathExists bool
240266
var grpcServer bool
241267

268+
mirrorPathToPercentage := extractMirrorTargetsWithPercentages(server.PathRules)
269+
242270
for pathRuleIdx, rule := range server.PathRules {
243271
matches := make([]routeMatch, 0, len(rule.MatchRules))
244272

@@ -250,6 +278,11 @@ func createLocations(
250278
grpcServer = true
251279
}
252280

281+
mirrorPercentage, exists := mirrorPathToPercentage[rule.Path]
282+
if !exists {
283+
mirrorPercentage = -1
284+
}
285+
253286
extLocations := initializeExternalLocations(rule, pathsAndTypes)
254287
for i := range extLocations {
255288
extLocations[i].Includes = createIncludesFromPolicyGenerateResult(
@@ -267,6 +300,7 @@ func createLocations(
267300
rule.Path,
268301
rule.GRPC,
269302
keepAliveCheck,
303+
mirrorPercentage,
270304
)
271305
}
272306

@@ -290,6 +324,7 @@ func createLocations(
290324
rule.Path,
291325
rule.GRPC,
292326
keepAliveCheck,
327+
mirrorPercentage,
293328
)
294329

295330
internalLocations = append(internalLocations, intLocation)
@@ -427,31 +462,59 @@ func updateLocation(
427462
path string,
428463
grpc bool,
429464
keepAliveCheck keepAliveChecker,
465+
mirrorPercentage float64,
430466
) http.Location {
431467
if filters.InvalidFilter != nil {
432468
location.Return = &http.Return{Code: http.StatusInternalServerError}
433469
return location
434470
}
435471

472+
location = updateLocationMirrorRoute(location, path, grpc)
473+
location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
474+
475+
if filters.RequestRedirect != nil {
476+
return updateLocationRedirectFilter(location, filters.RequestRedirect, listenerPort, path)
477+
}
478+
479+
location = updateLocationRewriteFilter(location, filters.RequestURLRewrite, path)
480+
location = updateLocationMirrorFilters(location, filters.RequestMirrors, path, mirrorPercentage)
481+
location = updateLocationProxySettings(location, matchRule, grpc, keepAliveCheck)
482+
483+
return location
484+
}
485+
486+
func updateLocationMirrorRoute(location http.Location, path string, grpc bool) http.Location {
436487
if strings.HasPrefix(path, http.InternalMirrorRoutePathPrefix) {
437488
location.Type = http.InternalLocationType
438489
if grpc {
439490
location.Rewrites = []string{"^ $request_uri break"}
440491
}
441492
}
442493

443-
location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
494+
return location
495+
}
444496

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
497+
func updateLocationRedirectFilter(
498+
location http.Location,
499+
redirectFilter *dataplane.HTTPRequestRedirectFilter,
500+
listenerPort int32,
501+
path string,
502+
) http.Location {
503+
ret, rewrite := createReturnAndRewriteConfigForRedirectFilter(redirectFilter, listenerPort, path)
504+
if rewrite.MainRewrite != "" {
505+
location.Rewrites = append(location.Rewrites, rewrite.MainRewrite)
452506
}
507+
location.Return = ret
453508

454-
rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
509+
return location
510+
}
511+
512+
func updateLocationRewriteFilter(
513+
location http.Location,
514+
rewriteFilter *dataplane.HTTPURLRewriteFilter,
515+
path string,
516+
) http.Location {
517+
rewrites := createRewritesValForRewriteFilter(rewriteFilter, path)
455518
if rewrites != nil {
456519
if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
457520
location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
@@ -461,12 +524,42 @@ func updateLocation(
461524
}
462525
}
463526

464-
for _, filter := range filters.RequestMirrors {
527+
return location
528+
}
529+
530+
func updateLocationMirrorFilters(
531+
location http.Location,
532+
mirrorFilters []*dataplane.HTTPRequestMirrorFilter,
533+
path string,
534+
mirrorPercentage float64,
535+
) http.Location {
536+
for _, filter := range mirrorFilters {
465537
if filter.Target != nil {
466538
location.MirrorPaths = append(location.MirrorPaths, *filter.Target)
467539
}
468540
}
469541

542+
if location.MirrorPaths != nil {
543+
location.MirrorPaths = deduplicateStrings(location.MirrorPaths)
544+
}
545+
546+
// if the mirrorPercentage is 100.0 or negative (we set it to negative when there is no mirror filter because 0.0
547+
// is valid), the split clients variable is not generated, and we want to let all the traffic get mirrored.
548+
if mirrorPercentage != 100.0 && mirrorPercentage >= 0.0 {
549+
location.MirrorSplitClientsVariableName = convertSplitClientVariableName(
550+
fmt.Sprintf("%s_%.2f", path, mirrorPercentage),
551+
)
552+
}
553+
554+
return location
555+
}
556+
557+
func updateLocationProxySettings(
558+
location http.Location,
559+
matchRule dataplane.MatchRule,
560+
grpc bool,
561+
keepAliveCheck keepAliveChecker,
562+
) http.Location {
470563
extraHeaders := make([]http.Header, 0, 3)
471564
if grpc {
472565
extraHeaders = append(extraHeaders, grpcAuthorityHeader)
@@ -504,11 +597,21 @@ func updateLocations(
504597
path string,
505598
grpc bool,
506599
keepAliveCheck keepAliveChecker,
600+
mirrorPercentage float64,
507601
) []http.Location {
508602
updatedLocations := make([]http.Location, len(buildLocations))
509603

510604
for i, loc := range buildLocations {
511-
updatedLocations[i] = updateLocation(filters, loc, matchRule, listenerPort, path, grpc, keepAliveCheck)
605+
updatedLocations[i] = updateLocation(
606+
filters,
607+
loc,
608+
matchRule,
609+
listenerPort,
610+
path,
611+
grpc,
612+
keepAliveCheck,
613+
mirrorPercentage,
614+
)
512615
}
513616

514617
return updatedLocations
@@ -962,3 +1065,18 @@ func getConnectionHeader(keepAliveCheck keepAliveChecker, backends []dataplane.B
9621065

9631066
return httpConnectionHeader
9641067
}
1068+
1069+
// deduplicateStrings removes duplicate strings from a slice while preserving order.
1070+
func deduplicateStrings(content []string) []string {
1071+
seen := make(map[string]bool)
1072+
result := make([]string, 0, len(content))
1073+
1074+
for _, str := range content {
1075+
if !seen[str] {
1076+
seen[str] = true
1077+
result = append(result, str)
1078+
}
1079+
}
1080+
1081+
return result
1082+
}

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)