Skip to content

Commit 6a99849

Browse files
authored
feat: automatically set first compressor with choose_first (#7406)
* feat: automatically set first compressor with choose_first Signed-off-by: Steven Kreitzer <[email protected]>
1 parent 4af9a89 commit 6a99849

File tree

9 files changed

+71
-98
lines changed

9 files changed

+71
-98
lines changed

api/v1alpha1/backendtrafficpolicy_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type BackendTrafficPolicySpec struct {
8484

8585
// The compressor config for the http streams.
8686
// This provides more granular control over compression configuration.
87+
// Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal.
8788
//
8889
// +patchMergeKey=type
8990
// +patchStrategy=merge

charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ spec:
148148
description: |-
149149
The compressor config for the http streams.
150150
This provides more granular control over compression configuration.
151+
Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal.
151152
items:
152153
description: |-
153154
Compression defines the config of enabling compression.

charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ spec:
147147
description: |-
148148
The compressor config for the http streams.
149149
This provides more granular control over compression configuration.
150+
Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal.
150151
items:
151152
description: |-
152153
Compression defines the config of enabling compression.

internal/xds/translator/compressor.go

Lines changed: 26 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -45,59 +45,22 @@ func (*compressor) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTT
4545
}
4646

4747
var (
48-
brotli bool
49-
gzip bool
50-
zstd bool
5148
filter *hcmv3.HttpFilter
5249
err error
5350
)
5451

5552
for _, route := range irListener.Routes {
5653
if route.Traffic != nil && route.Traffic.Compression != nil {
57-
for _, irComp := range route.Traffic.Compression {
58-
if irComp.Type == egv1a1.BrotliCompressorType {
59-
brotli = true
54+
for i, irComp := range route.Traffic.Compression {
55+
filterName := compressorFilterName(irComp.Type)
56+
if !hcmContainsFilter(mgr, filterName) {
57+
chooseFirst := i == 0
58+
if filter, err = buildCompressorFilter(irComp, chooseFirst); err != nil {
59+
return err
60+
}
61+
mgr.HttpFilters = append(mgr.HttpFilters, filter)
6062
}
61-
if irComp.Type == egv1a1.GzipCompressorType {
62-
gzip = true
63-
}
64-
if irComp.Type == egv1a1.ZstdCompressorType {
65-
zstd = true
66-
}
67-
}
68-
}
69-
}
70-
71-
// Add the compressor filters for all the compression types required by the routes.
72-
// All the compressor filters are disabled at the HCM level.
73-
// The per route filter config will enable the compressor filters for the routes that require them.
74-
if brotli {
75-
brotliFilterName := compressorFilterName(egv1a1.BrotliCompressorType)
76-
if !hcmContainsFilter(mgr, brotliFilterName) {
77-
if filter, err = buildCompressorFilter(egv1a1.BrotliCompressorType); err != nil {
78-
return err
79-
}
80-
mgr.HttpFilters = append(mgr.HttpFilters, filter)
81-
}
82-
}
83-
84-
if gzip {
85-
gzipFilterName := compressorFilterName(egv1a1.GzipCompressorType)
86-
if !hcmContainsFilter(mgr, gzipFilterName) {
87-
if filter, err = buildCompressorFilter(egv1a1.GzipCompressorType); err != nil {
88-
return err
8963
}
90-
mgr.HttpFilters = append(mgr.HttpFilters, filter)
91-
}
92-
}
93-
94-
if zstd {
95-
zstdFilterName := compressorFilterName(egv1a1.ZstdCompressorType)
96-
if !hcmContainsFilter(mgr, zstdFilterName) {
97-
if filter, err = buildCompressorFilter(egv1a1.ZstdCompressorType); err != nil {
98-
return err
99-
}
100-
mgr.HttpFilters = append(mgr.HttpFilters, filter)
10164
}
10265
}
10366

@@ -109,7 +72,7 @@ func compressorFilterName(compressorType egv1a1.CompressorType) string {
10972
}
11073

11174
// buildCompressorFilter builds a compressor filter with the provided compressionType.
112-
func buildCompressorFilter(compressionType egv1a1.CompressorType) (*hcmv3.HttpFilter, error) {
75+
func buildCompressorFilter(compression *ir.Compression, chooseFirst bool) (*hcmv3.HttpFilter, error) {
11376
var (
11477
compressorProto *compressorv3.Compressor
11578
extensionName string
@@ -119,7 +82,7 @@ func buildCompressorFilter(compressionType egv1a1.CompressorType) (*hcmv3.HttpFi
11982
err error
12083
)
12184

122-
switch compressionType {
85+
switch compression.Type {
12386
case egv1a1.BrotliCompressorType:
12487
extensionName = "envoy.compression.brotli.compressor"
12588
extensionMsg = &brotliv3.Brotli{}
@@ -142,12 +105,16 @@ func buildCompressorFilter(compressionType egv1a1.CompressorType) (*hcmv3.HttpFi
142105
},
143106
}
144107

108+
if chooseFirst {
109+
compressorProto.ChooseFirst = true
110+
}
111+
145112
if compressorAny, err = proto.ToAnyWithValidation(compressorProto); err != nil {
146113
return nil, err
147114
}
148115

149116
return &hcmv3.HttpFilter{
150-
Name: compressorFilterName(compressionType),
117+
Name: compressorFilterName(compression.Type),
151118
ConfigType: &hcmv3.HttpFilter_TypedConfig{
152119
TypedConfig: compressorAny,
153120
},
@@ -173,77 +140,39 @@ func (*compressor) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute, _ *ir
173140
}
174141

175142
var (
176-
brotli bool
177-
gzip bool
178-
zstd bool
179143
perFilterCfg map[string]*anypb.Any
180144
compressorAny *anypb.Any
181145
err error
182146
)
183147

184-
for _, irComp := range irRoute.Traffic.Compression {
185-
if irComp.Type == egv1a1.BrotliCompressorType {
186-
brotli = true
187-
}
188-
if irComp.Type == egv1a1.GzipCompressorType {
189-
gzip = true
190-
}
191-
if irComp.Type == egv1a1.ZstdCompressorType {
192-
zstd = true
193-
}
194-
}
195-
196-
if !brotli && !gzip && !zstd {
197-
return nil
198-
}
199-
200148
// Overwrite the HCM level filter config with the per route filter config.
201149
perFilterCfg = route.GetTypedPerFilterConfig()
202150
if perFilterCfg == nil {
203151
route.TypedPerFilterConfig = make(map[string]*anypb.Any)
204152
}
205153

206-
compressorProto := compressorPerRouteConfig()
207-
if compressorAny, err = proto.ToAnyWithValidation(compressorProto); err != nil {
208-
return err
209-
}
210-
211-
if brotli {
212-
brotliFilterName := compressorFilterName(egv1a1.BrotliCompressorType)
213-
if _, ok := perFilterCfg[brotliFilterName]; ok {
214-
// This should not happen since this is the only place where the filter
215-
// config is added in a route.
216-
return fmt.Errorf("route already contains filter config: %s, %+v",
217-
brotliFilterName, route)
218-
}
219-
route.TypedPerFilterConfig[brotliFilterName] = compressorAny
220-
}
221-
if gzip {
222-
gzipFilterName := compressorFilterName(egv1a1.GzipCompressorType)
223-
if _, ok := perFilterCfg[gzipFilterName]; ok {
154+
for _, irComp := range irRoute.Traffic.Compression {
155+
filterName := compressorFilterName(irComp.Type)
156+
if _, ok := perFilterCfg[filterName]; ok {
224157
// This should not happen since this is the only place where the filter
225158
// config is added in a route.
226159
return fmt.Errorf("route already contains filter config: %s, %+v",
227-
gzipFilterName, route)
160+
filterName, route)
228161
}
229-
route.TypedPerFilterConfig[gzipFilterName] = compressorAny
230-
}
231-
if zstd {
232-
zstdFilterName := compressorFilterName(egv1a1.ZstdCompressorType)
233-
if _, ok := perFilterCfg[zstdFilterName]; ok {
234-
// This should not happen since this is the only place where the filter
235-
// config is added in a route.
236-
return fmt.Errorf("route already contains filter config: %s, %+v",
237-
zstdFilterName, route)
162+
163+
compressorProto := compressorPerRouteConfig()
164+
if compressorAny, err = proto.ToAnyWithValidation(compressorProto); err != nil {
165+
return err
238166
}
239-
route.TypedPerFilterConfig[zstdFilterName] = compressorAny
167+
168+
route.TypedPerFilterConfig[filterName] = compressorAny
240169
}
241170

242171
return nil
243172
}
244173

174+
// Enable compression on this route if compression is configured.
245175
func compressorPerRouteConfig() *compressorv3.CompressorPerRoute {
246-
// Enable compression on this route if compression is configured.
247176
return &compressorv3.CompressorPerRoute{
248177
Override: &compressorv3.CompressorPerRoute_Overrides{
249178
Overrides: &compressorv3.CompressorOverrides{

internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
name: envoy.filters.http.compressor.brotli
1919
typedConfig:
2020
'@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor
21+
chooseFirst: true
2122
compressorLibrary:
2223
name: envoy.compression.brotli.compressor
2324
typedConfig:

site/content/en/latest/api/extension_types.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ _Appears in:_
542542
| `faultInjection` | _[FaultInjection](#faultinjection)_ | false | | FaultInjection defines the fault injection policy to be applied. This configuration can be used to<br />inject delays and abort requests to mimic failure scenarios such as service failures and overloads |
543543
| `useClientProtocol` | _boolean_ | false | | UseClientProtocol configures Envoy to prefer sending requests to backends using<br />the same HTTP protocol that the incoming request used. Defaults to false, which means<br />that Envoy will use the protocol indicated by the attached BackendRef. |
544544
| `compression` | _[Compression](#compression) array_ | false | | The compression config for the http streams.<br />Deprecated: Use Compressor instead. |
545-
| `compressor` | _[Compression](#compression) array_ | false | | The compressor config for the http streams.<br />This provides more granular control over compression configuration. |
545+
| `compressor` | _[Compression](#compression) array_ | false | | The compressor config for the http streams.<br />This provides more granular control over compression configuration.<br />Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal. |
546546
| `responseOverride` | _[ResponseOverride](#responseoverride) array_ | false | | ResponseOverride defines the configuration to override specific responses with a custom one.<br />If multiple configurations are specified, the first one to match wins. |
547547
| `httpUpgrade` | _[ProtocolUpgradeConfig](#protocolupgradeconfig) array_ | false | | HTTPUpgrade defines the configuration for HTTP protocol upgrades.<br />If not specified, the default upgrade configuration(websocket) will be used. |
548548
| `requestBuffer` | _[RequestBuffer](#requestbuffer)_ | false | | RequestBuffer allows the gateway to buffer and fully receive each request from a client before continuing to send the request<br />upstream to the backends. This can be helpful to shield your backend servers from slow clients, and also to enforce a maximum size per request<br />as any requests larger than the buffer size will be rejected.<br />This can have a negative performance impact so should only be enabled when necessary.<br />When enabling this option, you should also configure your connection buffer size to account for these request buffers. There will also be an<br />increase in memory usage for Envoy that should be accounted for in your deployment settings. |

test/e2e/tests/compression.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ var CompressionTest = suite.ConformanceTest{
5555
testCompression(t, suite, egv1a1.ZstdCompressorType)
5656
})
5757

58+
t.Run("HTTPRoute with brotli compression chooseFirst", func(t *testing.T) {
59+
testCompressionChooseFirst(t, suite, egv1a1.BrotliCompressorType)
60+
})
61+
5862
t.Run("HTTPRoute without compression", func(t *testing.T) {
5963
ns := "gateway-conformance-infra"
6064
routeNN := types.NamespacedName{Name: "no-compression", Namespace: ns}
@@ -122,6 +126,40 @@ func testCompression(t *testing.T, suite *suite.ConformanceTestSuite, compressio
122126
http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
123127
}
124128

129+
func testCompressionChooseFirst(t *testing.T, suite *suite.ConformanceTestSuite, compressionType egv1a1.CompressorType) {
130+
ns := "gateway-conformance-infra"
131+
routeNN := types.NamespacedName{Name: "compression", Namespace: ns}
132+
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
133+
gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN)
134+
135+
ancestorRef := gwapiv1.ParentReference{
136+
Group: gatewayapi.GroupPtr(gwapiv1.GroupName),
137+
Kind: gatewayapi.KindPtr(resource.KindGateway),
138+
Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
139+
Name: gwapiv1.ObjectName(gwNN.Name),
140+
}
141+
BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "compression", Namespace: ns}, suite.ControllerName, ancestorRef)
142+
143+
encoding := ContentEncoding(compressionType)
144+
expectedResponse := http.ExpectedResponse{
145+
Request: http.Request{
146+
Path: "/compression",
147+
Headers: map[string]string{
148+
"Accept-encoding": "gzip, br, zstd",
149+
},
150+
},
151+
Response: http.Response{
152+
StatusCodes: []int{200},
153+
Headers: map[string]string{
154+
"content-encoding": encoding,
155+
},
156+
},
157+
Namespace: ns,
158+
}
159+
roundTripper := &CompressionRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig}
160+
http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
161+
}
162+
125163
// CompressionRoundTripper implements roundtripper.RoundTripper and adds support for compression.
126164
type CompressionRoundTripper struct {
127165
Debug bool

test/helm/gateway-crds-helm/all.out.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21281,6 +21281,7 @@ spec:
2128121281
description: |-
2128221282
The compressor config for the http streams.
2128321283
This provides more granular control over compression configuration.
21284+
Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal.
2128421285
items:
2128521286
description: |-
2128621287
Compression defines the config of enabling compression.

test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ spec:
625625
description: |-
626626
The compressor config for the http streams.
627627
This provides more granular control over compression configuration.
628+
Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal.
628629
items:
629630
description: |-
630631
Compression defines the config of enabling compression.

0 commit comments

Comments
 (0)