Repository: envoyproxy/gateway
Pull request #5221 (2025-02-06 06:09:58)

chore: add test for patch ratelimit env
fixes: https://github.com/envoyproxy/gateway/issues/5219

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5221?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 66.89%. Comparing base [(`48459d4`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/48459d48e47d04b96aba0ab78abe9222492fb2b9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`bc65630`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/bc6563002425d4456eb560ac39799d83fd8fe662?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5221      +/-   ##
==========================================
+ Coverage   66.85%   66.89%   +0.03%     
==========================================
  Files         210      210              
  Lines       32998    32998              
==========================================
+ Hits        22061    22073      +12     
+ Misses       9595     9587       -8     
+ Partials     1342     1338       -4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5221?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).


Patch:
diff --git a/examples/kubernetes/mergepatch.yaml b/examples/kubernetes/mergepatch.yaml
index 20cf575c7..45bb94d22 100644
--- a/examples/kubernetes/mergepatch.yaml
+++ b/examples/kubernetes/mergepatch.yaml
@@ -8,9 +8,9 @@ spec:
     type: Kubernetes
     kubernetes:
       envoyDeployment:
-        merge:
+        patch:
           type: StrategicMerge
-          object:
+          value:
             spec:
               template:
                 spec:
diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go
index 06e1ce3cd..160ec6810 100644
--- a/internal/gatewayapi/filters.go
+++ b/internal/gatewayapi/filters.go
@@ -57,7 +57,7 @@ type HTTPFilterIR struct {
 	AddResponseHeaders    []ir.AddHeader
 	RemoveResponseHeaders []string
 
-	Mirrors []*ir.RouteDestination
+	Mirrors []*ir.MirrorPolicy
 
 	ExtensionRefs []*ir.UnstructuredRef
 }
@@ -1003,11 +1003,19 @@ func (t *Translator) processRequestMirrorFilter(
 		return err
 	}
 
-	newMirror := &ir.RouteDestination{
+	routeDst := &ir.RouteDestination{
 		Name:     fmt.Sprintf("%s-mirror-%d", irRouteDestinationName(filterContext.Route, filterContext.RuleIdx), filterIdx),
 		Settings: []*ir.DestinationSetting{ds},
 	}
-	filterContext.Mirrors = append(filterContext.Mirrors, newMirror)
+
+	var percent *float32
+	if f := mirrorFilter.Fraction; f != nil {
+		percent = ptr.To(100 * float32(f.Numerator) / float32(ptr.Deref(f.Denominator, int32(100))))
+	} else if p := mirrorFilter.Percent; p != nil {
+		percent = ptr.To(float32(*p))
+	}
+
+	filterContext.Mirrors = append(filterContext.Mirrors, &ir.MirrorPolicy{Destination: routeDst, Percentage: percent})
 	return nil
 }
 
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 85633a686..aeb2f165b 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -701,7 +701,7 @@ type HTTPRoute struct {
 	// Redirections to be returned for this route. Takes precedence over Destinations.
 	Redirect *Redirect `json:"redirect,omitempty" yaml:"redirect,omitempty"`
 	// Destination that requests to this HTTPRoute will be mirrored to
-	Mirrors []*RouteDestination `json:"mirrors,omitempty" yaml:"mirrors,omitempty"`
+	Mirrors []*MirrorPolicy `json:"mirrors,omitempty" yaml:"mirrors,omitempty"`
 	// Destination associated with this matched route.
 	Destination *RouteDestination `json:"destination,omitempty" yaml:"destination,omitempty"`
 	// Rewrite to be changed for this route.
@@ -1266,6 +1266,19 @@ type FaultInjectionAbort struct {
 	Percentage *float32 `json:"percentage,omitempty" yaml:"percentage,omitempty"`
 }
 
+// MirrorPolicy specifies a destination to mirror traffic in addition
+// to the original destination
+//
+// +kubebuilder:object:generate=true
+type MirrorPolicy struct {
+	// Destination defines the target where the request will be mirrored.
+	Destination *RouteDestination `json:"destination" yaml:"destination"`
+	// Percentage of the traffic to be mirrored by the `destination` field.
+	// When absent, all the traffic (100%) will be mirrored.
+	// Values are in the range of [0.0, 100.0].
+	Percentage *float32 `json:"percentage,omitempty" yaml:"percentage,omitempty"`
+}
+
 // Validate the fields within the HTTPRoute structure
 func (h *HTTPRoute) Validate() error {
 	var errs error
@@ -1312,7 +1325,7 @@ func (h *HTTPRoute) Validate() error {
 	}
 	if h.Mirrors != nil {
 		for _, mirror := range h.Mirrors {
-			if err := mirror.Validate(); err != nil {
+			if err := mirror.Destination.Validate(); err != nil {
 				errs = errors.Join(errs, err)
 			}
 		}
@@ -1440,7 +1453,7 @@ type DestinationSetting struct {
 	Filters  *DestinationFilters `json:"filters,omitempty" yaml:"filters,omitempty"`
 }
 
-// Validate the fields within the RouteDestination structure
+// Validate the fields within the DestinationSetting structure
 func (d *DestinationSetting) Validate() error {
 	var errs error
 	for _, ep := range d.Endpoints {
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 1856e1da8..7ab45299c 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -1606,11 +1606,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
 	}
 	if in.Mirrors != nil {
 		in, out := &in.Mirrors, &out.Mirrors
-		*out = make([]*RouteDestination, len(*in))
+		*out = make([]*MirrorPolicy, len(*in))
 		for i := range *in {
 			if (*in)[i] != nil {
 				in, out := &(*in)[i], &(*out)[i]
-				*out = new(RouteDestination)
+				*out = new(MirrorPolicy)
 				(*in).DeepCopyInto(*out)
 			}
 		}
@@ -2152,6 +2152,31 @@ func (in *Metrics) DeepCopy() *Metrics {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MirrorPolicy) DeepCopyInto(out *MirrorPolicy) {
+	*out = *in
+	if in.Destination != nil {
+		in, out := &in.Destination, &out.Destination
+		*out = new(RouteDestination)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Percentage != nil {
+		in, out := &in.Percentage, &out.Percentage
+		*out = new(float32)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MirrorPolicy.
+func (in *MirrorPolicy) DeepCopy() *MirrorPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(MirrorPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OIDC) DeepCopyInto(out *OIDC) {
 	*out = *in
diff --git a/internal/xds/translator/fault.go b/internal/xds/translator/fault.go
index 192ce5bf8..d48e3f4df 100644
--- a/internal/xds/translator/fault.go
+++ b/internal/xds/translator/fault.go
@@ -13,7 +13,6 @@ import (
 	xdsfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3"
 	xdshttpfaultv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
 	hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
-	xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3"
 	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
 	"google.golang.org/protobuf/types/known/anypb"
 	"google.golang.org/protobuf/types/known/durationpb"
@@ -179,12 +178,3 @@ func (*fault) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error {
 
 	return nil
 }
-
-// translatePercentToFractionalPercent translates a v1alpha3 Percent instance
-// to an envoy.type.FractionalPercent instance.
-func translatePercentToFractionalPercent(p *float32) *xdstype.FractionalPercent {
-	return &xdstype.FractionalPercent{
-		Numerator:   uint32(*p * 10000),
-		Denominator: xdstype.FractionalPercent_MILLION,
-	}
-}
diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go
index 369b5011f..ec7bbc729 100644
--- a/internal/xds/translator/route.go
+++ b/internal/xds/translator/route.go
@@ -462,16 +462,38 @@ func buildXdsDirectResponseAction(res *ir.CustomResponse) *routev3.DirectRespons
 	return routeAction
 }
 
-func buildXdsRequestMirrorPolicies(mirrorDestinations []*ir.RouteDestination) []*routev3.RouteAction_RequestMirrorPolicy {
-	var mirrorPolicies []*routev3.RouteAction_RequestMirrorPolicy
-
-	for _, mirrorDest := range mirrorDestinations {
-		mirrorPolicies = append(mirrorPolicies, &routev3.RouteAction_RequestMirrorPolicy{
-			Cluster: mirrorDest.Name,
-		})
+func buildXdsRequestMirrorPolicies(mirrorPolicies []*ir.MirrorPolicy) []*routev3.RouteAction_RequestMirrorPolicy {
+	var xdsMirrorPolicies []*routev3.RouteAction_RequestMirrorPolicy
+
+	for _, policy := range mirrorPolicies {
+		if mp := mirrorPercentByPolicy(policy); mp != nil && policy.Destination != nil {
+			xdsMirrorPolicies = append(xdsMirrorPolicies, &routev3.RouteAction_RequestMirrorPolicy{
+				Cluster:         policy.Destination.Name,
+				RuntimeFraction: mp,
+			})
+		}
 	}
 
-	return mirrorPolicies
+	return xdsMirrorPolicies
+}
+
+// mirrorPercentByPolicy computes the mirror percent to be used based on ir.MirrorPolicy.
+func mirrorPercentByPolicy(mirror *ir.MirrorPolicy) *corev3.RuntimeFractionalPercent {
+	switch {
+	case mirror.Percentage != nil:
+		if *mirror.Percentage > 0 {
+			return &corev3.RuntimeFractionalPercent{
+				DefaultValue: translatePercentToFractionalPercent(mirror.Percentage),
+			}
+		}
+		// If zero percent is provided explicitly, we should not mirror.
+		return nil
+	default:
+		// Default to 100 percent if percent is not given.
+		return &corev3.RuntimeFractionalPercent{
+			DefaultValue: translateIntegerToFractionalPercent(100),
+		}
+	}
 }
 
 func buildXdsAddedHeaders(headersToAdd []ir.AddHeader) []*corev3.HeaderValueOption {
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index 937f00238..071f929c6 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -565,15 +565,17 @@ func (t *Translator) addRouteToRouteConfig(
 		}
 
 		if httpRoute.Mirrors != nil {
-			for _, mirrorDest := range httpRoute.Mirrors {
-				if err = addXdsCluster(tCtx, &xdsClusterArgs{
-					name:         mirrorDest.Name,
-					settings:     mirrorDest.Settings,
-					tSocket:      nil,
-					endpointType: EndpointTypeStatic,
-					metrics:      metrics,
-				}); err != nil {
-					errs = errors.Join(errs, err)
+			for _, mrr := range httpRoute.Mirrors {
+				if mrr.Destination != nil {
+					if err = addXdsCluster(tCtx, &xdsClusterArgs{
+						name:         mrr.Destination.Name,
+						settings:     mrr.Destination.Settings,
+						tSocket:      nil,
+						endpointType: EndpointTypeStatic,
+						metrics:      metrics,
+					}); err != nil {
+						errs = errors.Join(errs, err)
+					}
 				}
 			}
 		}
diff --git a/internal/xds/translator/utils.go b/internal/xds/translator/utils.go
index a4a38be8e..b24f32d3d 100644
--- a/internal/xds/translator/utils.go
+++ b/internal/xds/translator/utils.go
@@ -16,6 +16,7 @@ import (
 	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3"
 	"google.golang.org/protobuf/types/known/anypb"
 	"k8s.io/utils/ptr"
 
@@ -236,3 +237,20 @@ func determineIPFamily(settings []*ir.DestinationSetting) *egv1a1.IPFamily {
 		return nil
 	}
 }
+
+// translatePercentToFractionalPercent translates a float to an envoy.type.FractionalPercent instance.
+func translatePercentToFractionalPercent(p *float32) *xdstype.FractionalPercent {
+	return &xdstype.FractionalPercent{
+		Numerator:   uint32(*p * 10000),
+		Denominator: xdstype.FractionalPercent_MILLION,
+	}
+}
+
+// translateIntegerToFractionalPercent translates an int32 instance to an
+// envoy.type.FractionalPercent instance.
+func translateIntegerToFractionalPercent(p int32) *xdstype.FractionalPercent {
+	return &xdstype.FractionalPercent{
+		Numerator:   uint32(p),
+		Denominator: xdstype.FractionalPercent_HUNDRED,
+	}
+}
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 288804f88..153ceea96 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -8,6 +8,7 @@ security updates: |
 
 # New features or capabilities added in this release.
 new features: |
+  Added support for percentage-based request mirroring
 
 bug fixes: |
 


Test patch:
diff --git a/internal/gatewayapi/testdata/httproute-with-mirror-filter-duplicates.out.yaml b/internal/gatewayapi/testdata/httproute-with-mirror-filter-duplicates.out.yaml
index 29d9dd320..80fa07db8 100644
--- a/internal/gatewayapi/testdata/httproute-with-mirror-filter-duplicates.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-mirror-filter-duplicates.out.yaml
@@ -145,22 +145,24 @@ xdsIR:
           name: httproute-1
           namespace: default
         mirrors:
-        - name: httproute/default/httproute-1/rule/0-mirror-0
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.7.7.7
-              port: 8080
-            protocol: HTTP
-            weight: 1
-        - name: httproute/default/httproute-1/rule/0-mirror-1
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.7.7.7
-              port: 8080
-            protocol: HTTP
-            weight: 1
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-0
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.7.7.7
+                port: 8080
+              protocol: HTTP
+              weight: 1
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-1
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.7.7.7
+                port: 8080
+              protocol: HTTP
+              weight: 1
         name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
         pathMatch:
           distinct: false
diff --git a/internal/gatewayapi/testdata/httproute-with-mirror-filter-multiple.out.yaml b/internal/gatewayapi/testdata/httproute-with-mirror-filter-multiple.out.yaml
index c6e534c9c..1609b805a 100644
--- a/internal/gatewayapi/testdata/httproute-with-mirror-filter-multiple.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-mirror-filter-multiple.out.yaml
@@ -170,22 +170,24 @@ xdsIR:
           name: httproute-1
           namespace: default
         mirrors:
-        - name: httproute/default/httproute-1/rule/0-mirror-1
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.7.7.7
-              port: 8080
-            protocol: HTTP
-            weight: 1
-        - name: httproute/default/httproute-1/rule/0-mirror-2
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.6.5.4
-              port: 8080
-            protocol: HTTP
-            weight: 1
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-1
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.7.7.7
+                port: 8080
+              protocol: HTTP
+              weight: 1
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-2
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.6.5.4
+                port: 8080
+              protocol: HTTP
+              weight: 1
         name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
         pathMatch:
           distinct: false
diff --git a/internal/gatewayapi/testdata/httproute-with-mirror-filter-percentage-mirroring.in.yaml b/internal/gatewayapi/testdata/httproute-with-mirror-filter-percentage-mirroring.in.yaml
new file mode 100644
index 000000000..8808857fb
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-mirror-filter-percentage-mirroring.in.yaml
@@ -0,0 +1,53 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: "*.envoyproxy.io"
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/"
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: RequestMirror
+        requestMirror:
+          backendRef:
+            kind: Service
+            name: service-1
+            port: 8080
+          percent: 20
+      - type: RequestMirror
+        requestMirror:
+          backendRef:
+            kind: Service
+            name: mirror-service
+            port: 8080
+          fraction:
+            denominator: 50
+            numerator: 25
diff --git a/internal/gatewayapi/testdata/httproute-with-mirror-filter-percentage-mirroring.out.yaml b/internal/gatewayapi/testdata/httproute-with-mirror-filter-percentage-mirroring.out.yaml
new file mode 100644
index 000000000..164873c64
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-mirror-filter-percentage-mirroring.out.yaml
@@ -0,0 +1,176 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - requestMirror:
+          backendRef:
+            kind: Service
+            name: service-1
+            port: 8080
+          percent: 20
+        type: RequestMirror
+      - requestMirror:
+          backendRef:
+            kind: Service
+            name: mirror-service
+            port: 8080
+          fraction:
+            denominator: 50
+            numerator: 25
+        type: RequestMirror
+      matches:
+      - path:
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        mirrors:
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-0
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.7.7.7
+                port: 8080
+              protocol: HTTP
+              weight: 1
+          percentage: 20
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-1
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.6.5.4
+                port: 8080
+              protocol: HTTP
+              weight: 1
+          percentage: 50
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
diff --git a/internal/gatewayapi/testdata/httproute-with-mirror-filter.out.yaml b/internal/gatewayapi/testdata/httproute-with-mirror-filter.out.yaml
index e9f94617b..58852fdcf 100644
--- a/internal/gatewayapi/testdata/httproute-with-mirror-filter.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-mirror-filter.out.yaml
@@ -139,14 +139,15 @@ xdsIR:
           name: httproute-1
           namespace: default
         mirrors:
-        - name: httproute/default/httproute-1/rule/0-mirror-0
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.7.7.7
-              port: 8080
-            protocol: HTTP
-            weight: 1
+        - destination:
+            name: httproute/default/httproute-1/rule/0-mirror-0
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.7.7.7
+                port: 8080
+              protocol: HTTP
+              weight: 1
         name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
         pathMatch:
           distinct: false
diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
index ed163fbe1..d3bd7243d 100644
--- a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
+++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
@@ -272,7 +272,38 @@ func TestDeployment(t *testing.T) {
 				Patch: &egv1a1.KubernetesPatchSpec{
 					Type: ptr.To(egv1a1.StrategicMerge),
 					Value: apiextensionsv1.JSON{
-						Raw: []byte("{\"spec\":{\"template\":{\"spec\":{\"hostNetwork\":true,\"dnsPolicy\":\"ClusterFirstWithHostNet\"}}}}"),
+						Raw: []byte(`{"spec":{"template":{"spec":{"hostNetwork":true,"dnsPolicy":"ClusterFirstWithHostNet"}}}}`),
+					},
+				},
+			},
+		},
+		{
+			caseName:  "patch-deployment-containers",
+			rateLimit: rateLimit,
+			deploy: &egv1a1.KubernetesDeploymentSpec{
+				Patch: &egv1a1.KubernetesPatchSpec{
+					Type: ptr.To(egv1a1.StrategicMerge),
+					Value: apiextensionsv1.JSON{
+						Raw: []byte(`{
+    "spec": {
+        "template": {
+            "spec": {
+                "containers": [
+                    {
+                        "name": "envoy-ratelimit",
+                        "imagePullPolicy": "Always",
+                        "env": [
+                            {
+                                "name": "REDIS_TYPE",
+                                "value": "sentinel"
+                            }
+                        ]
+                    }
+                ]
+            }
+        }
+    }
+}`),
 					},
 				},
 			},
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment-containers.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment-containers.yaml
new file mode 100644
index 000000000..309c2d20d
--- /dev/null
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment-containers.yaml
@@ -0,0 +1,156 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  creationTimestamp: null
+  labels:
+    app.kubernetes.io/component: ratelimit
+    app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
+  name: envoy-ratelimit
+  namespace: envoy-gateway-system
+  ownerReferences:
+  - apiVersion: apps/v1
+    kind: Deployment
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-deployment
+spec:
+  progressDeadlineSeconds: 600
+  revisionHistoryLimit: 10
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: ratelimit
+      app.kubernetes.io/managed-by: envoy-gateway
+      app.kubernetes.io/name: envoy-ratelimit
+  strategy:
+    type: RollingUpdate
+  template:
+    metadata:
+      annotations:
+        prometheus.io/path: /metrics
+        prometheus.io/port: "19001"
+        prometheus.io/scrape: "true"
+      creationTimestamp: null
+      labels:
+        app.kubernetes.io/component: ratelimit
+        app.kubernetes.io/managed-by: envoy-gateway
+        app.kubernetes.io/name: envoy-ratelimit
+    spec:
+      automountServiceAccountToken: false
+      containers:
+      - command:
+        - /bin/ratelimit
+        env:
+        - name: REDIS_TYPE
+          value: sentinel
+        - name: RUNTIME_ROOT
+          value: /data
+        - name: RUNTIME_SUBDIRECTORY
+          value: ratelimit
+        - name: RUNTIME_IGNOREDOTFILES
+          value: "true"
+        - name: RUNTIME_WATCH_ROOT
+          value: "false"
+        - name: LOG_LEVEL
+          value: info
+        - name: USE_STATSD
+          value: "false"
+        - name: CONFIG_TYPE
+          value: GRPC_XDS_SOTW
+        - name: CONFIG_GRPC_XDS_SERVER_URL
+          value: envoy-gateway:18001
+        - name: CONFIG_GRPC_XDS_NODE_ID
+          value: envoy-ratelimit
+        - name: GRPC_SERVER_USE_TLS
+          value: "true"
+        - name: GRPC_SERVER_TLS_CERT
+          value: /certs/tls.crt
+        - name: GRPC_SERVER_TLS_KEY
+          value: /certs/tls.key
+        - name: GRPC_SERVER_TLS_CA_CERT
+          value: /certs/ca.crt
+        - name: CONFIG_GRPC_XDS_SERVER_USE_TLS
+          value: "true"
+        - name: CONFIG_GRPC_XDS_CLIENT_TLS_CERT
+          value: /certs/tls.crt
+        - name: CONFIG_GRPC_XDS_CLIENT_TLS_KEY
+          value: /certs/tls.key
+        - name: CONFIG_GRPC_XDS_SERVER_TLS_CACERT
+          value: /certs/ca.crt
+        - name: FORCE_START_WITHOUT_INITIAL_CONFIG
+          value: "true"
+        - name: REDIS_SOCKET_TYPE
+          value: tcp
+        - name: REDIS_URL
+          value: redis.redis.svc:6379
+        - name: USE_PROMETHEUS
+          value: "true"
+        - name: PROMETHEUS_ADDR
+          value: :19001
+        - name: PROMETHEUS_MAPPER_YAML
+          value: /etc/statsd-exporter/conf.yaml
+        image: docker.io/envoyproxy/ratelimit:master
+        imagePullPolicy: Always
+        name: envoy-ratelimit
+        ports:
+        - containerPort: 8081
+          name: grpc
+          protocol: TCP
+        readinessProbe:
+          failureThreshold: 1
+          httpGet:
+            path: /healthcheck
+            port: 8080
+            scheme: HTTP
+          periodSeconds: 5
+          successThreshold: 1
+          timeoutSeconds: 1
+        resources:
+          requests:
+            cpu: 100m
+            memory: 512Mi
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop:
+            - ALL
+          privileged: false
+          readOnlyRootFilesystem: true
+          runAsGroup: 65534
+          runAsNonRoot: true
+          runAsUser: 65534
+          seccompProfile:
+            type: RuntimeDefault
+        startupProbe:
+          failureThreshold: 30
+          httpGet:
+            path: /healthcheck
+            port: 8080
+            scheme: HTTP
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        terminationMessagePath: /dev/termination-log
+        terminationMessagePolicy: File
+        volumeMounts:
+        - mountPath: /certs
+          name: certs
+          readOnly: true
+        - mountPath: /etc/statsd-exporter
+          name: statsd-exporter-config
+          readOnly: true
+      dnsPolicy: ClusterFirst
+      restartPolicy: Always
+      schedulerName: default-scheduler
+      serviceAccountName: envoy-ratelimit
+      terminationGracePeriodSeconds: 300
+      volumes:
+      - name: certs
+        secret:
+          defaultMode: 420
+          secretName: envoy-rate-limit
+      - configMap:
+          defaultMode: 420
+          name: statsd-exporter-config
+          optional: true
+        name: statsd-exporter-config
+status: {}
diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go
index d25996bbc..cfbd84c91 100644
--- a/internal/ir/xds_test.go
+++ b/internal/ir/xds_test.go
@@ -518,7 +518,11 @@ var (
 		PathMatch: &StringMatch{
 			Exact: ptr.To("mirrorfilter"),
 		},
-		Mirrors: []*RouteDestination{&happyRouteDestination},
+		Mirrors: []*MirrorPolicy{
+			{
+				Destination: &happyRouteDestination,
+			},
+		},
 	}
 
 	// RouteDestination
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-mirrors-percentage.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-mirrors-percentage.yaml
new file mode 100644
index 000000000..8243e3273
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-mirrors-percentage.yaml
@@ -0,0 +1,26 @@
+name: "http-route"
+http:
+- name: "first-listener"
+  address: "::"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "mirror-route"
+    hostname: "*"
+    destination:
+      name: "route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
+    mirrors:
+    - destination:
+        name: "mirror-route-dest"
+        settings:
+        - endpoints:
+          - host: "2.3.4.5"
+      percentage: 29
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors-percentage.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors-percentage.yaml
new file mode 100644
index 000000000..4f5036820
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors-percentage.yaml
@@ -0,0 +1,32 @@
+name: "http-route"
+http:
+- name: "first-listener"
+  address: "::"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "mirror-route"
+    hostname: "*"
+    destination:
+      name: "route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
+    mirrors:
+    - destination:
+        name: "mirror-route-dest"
+        settings:
+        - endpoints:
+          - host: "2.3.4.5"
+      percentage: 25
+    - destination:
+        name: "mirror-route-dest1"
+        settings:
+        - endpoints:
+          - host: "3.4.5.6"
+      percentage: 44
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors.yaml
index 02724f765..968ab4312 100644
--- a/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors.yaml
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-multiple-mirrors.yaml
@@ -18,11 +18,13 @@ http:
         - host: "1.2.3.4"
           port: 50000
     mirrors:
-    - name: "mirror-route-dest"
-      settings:
-      - endpoints:
-        - host: "2.3.4.5"
-    - name: "mirror-route-dest1"
-      settings:
-      - endpoints:
-        - host: "3.4.5.6"
+    - destination:
+        name: "mirror-route-dest"
+        settings:
+        - endpoints:
+          - host: "2.3.4.5"
+    - destination:
+        name: "mirror-route-dest1"
+        settings:
+        - endpoints:
+          - host: "3.4.5.6"
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.clusters.yaml
new file mode 100644
index 000000000..5f0b3deaa
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.clusters.yaml
@@ -0,0 +1,34 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: mirror-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: mirror-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.endpoints.yaml
new file mode 100644
index 000000000..b16924a0e
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.endpoints.yaml
@@ -0,0 +1,24 @@
+- clusterName: route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: route-dest/backend/0
+- clusterName: mirror-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 2.3.4.5
+            portValue: 0
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: mirror-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.listeners.yaml
new file mode 100644
index 000000000..80ae84fd1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.listeners.yaml
@@ -0,0 +1,34 @@
+- address:
+    socketAddress:
+      address: '::'
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.routes.yaml
new file mode 100644
index 000000000..9f74797a1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-mirrors-percentage.routes.yaml
@@ -0,0 +1,20 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        prefix: /
+      name: mirror-route
+      route:
+        cluster: route-dest
+        requestMirrorPolicies:
+        - cluster: mirror-route-dest
+          runtimeFraction:
+            defaultValue:
+              denominator: MILLION
+              numerator: 290000
+        upgradeConfigs:
+        - upgradeType: websocket
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.clusters.yaml
new file mode 100644
index 000000000..6ca132c71
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.clusters.yaml
@@ -0,0 +1,51 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: mirror-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: mirror-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: mirror-route-dest1
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: mirror-route-dest1
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.endpoints.yaml
new file mode 100644
index 000000000..f741af39c
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.endpoints.yaml
@@ -0,0 +1,36 @@
+- clusterName: route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: route-dest/backend/0
+- clusterName: mirror-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 2.3.4.5
+            portValue: 0
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: mirror-route-dest/backend/0
+- clusterName: mirror-route-dest1
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 3.4.5.6
+            portValue: 0
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: mirror-route-dest1/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.listeners.yaml
new file mode 100644
index 000000000..80ae84fd1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.listeners.yaml
@@ -0,0 +1,34 @@
+- address:
+    socketAddress:
+      address: '::'
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.routes.yaml
new file mode 100644
index 000000000..c84213bd2
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors-percentage.routes.yaml
@@ -0,0 +1,25 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        prefix: /
+      name: mirror-route
+      route:
+        cluster: route-dest
+        requestMirrorPolicies:
+        - cluster: mirror-route-dest
+          runtimeFraction:
+            defaultValue:
+              denominator: MILLION
+              numerator: 250000
+        - cluster: mirror-route-dest1
+          runtimeFraction:
+            defaultValue:
+              denominator: MILLION
+              numerator: 440000
+        upgradeConfigs:
+        - upgradeType: websocket
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.routes.yaml
index f46cc50eb..37b12b36d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.routes.yaml
@@ -12,6 +12,12 @@
         cluster: route-dest
         requestMirrorPolicies:
         - cluster: mirror-route-dest
+          runtimeFraction:
+            defaultValue:
+              numerator: 100
         - cluster: mirror-route-dest1
+          runtimeFraction:
+            defaultValue:
+              numerator: 100
         upgradeConfigs:
         - upgradeType: websocket