diff --git a/apis/projectcontour/v1alpha1/compression.go b/apis/projectcontour/v1alpha1/compression.go new file mode 100644 index 00000000000..f3f00b3f7c3 --- /dev/null +++ b/apis/projectcontour/v1alpha1/compression.go @@ -0,0 +1,53 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import "fmt" + +// CompressionAlgorithm defines the type of compression algorithm applied in default HTTP listener filter chain. +// Allowable values are defined as names of well known compression algorithms (plus "disabled"). +type CompressionAlgorithm string + +// EnvoyCompression defines configuration related to compression in the default HTTP Listener filter chain. +type EnvoyCompression struct { + // Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + // Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + // Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + // +kubebuilder:validation:Enum="gzip";"brotli";"zstd";"disabled" + // +optional + Algorithm CompressionAlgorithm `json:"algorithm,omitempty"` +} + +func (a CompressionAlgorithm) Validate() error { + switch a { + case BrotliCompression, DisabledCompression, GzipCompression, ZstdCompression, "": + return nil + default: + return fmt.Errorf("invalid compression type: %q", a) + } +} + +const ( + // BrotliCompression specifies brotli as the default HTTP filter chain compression mechanism + BrotliCompression CompressionAlgorithm = "brotli" + + // DisabledCompression specifies that there will be no compression in the default HTTP filter chain + DisabledCompression CompressionAlgorithm = "disabled" + + // GzipCompression specifies gzip as the default HTTP filter chain compression mechanism + GzipCompression CompressionAlgorithm = "gzip" + + // ZstdCompression specifies zstd as the default HTTP filter chain compression mechanism + ZstdCompression CompressionAlgorithm = "zstd" +) diff --git a/apis/projectcontour/v1alpha1/compression_test.go b/apis/projectcontour/v1alpha1/compression_test.go new file mode 100644 index 00000000000..a2df6266790 --- /dev/null +++ b/apis/projectcontour/v1alpha1/compression_test.go @@ -0,0 +1,32 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + contour_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" +) + +func TestValidateEnvoyCompressionAlgorithmType(t *testing.T) { + require.Error(t, contour_v1alpha1.CompressionAlgorithm("foo").Validate()) + + require.NoError(t, contour_v1alpha1.CompressionAlgorithm("").Validate()) + require.NoError(t, contour_v1alpha1.BrotliCompression.Validate()) + require.NoError(t, contour_v1alpha1.DisabledCompression.Validate()) + require.NoError(t, contour_v1alpha1.GzipCompression.Validate()) + require.NoError(t, contour_v1alpha1.ZstdCompression.Validate()) +} diff --git a/apis/projectcontour/v1alpha1/contourconfig.go b/apis/projectcontour/v1alpha1/contourconfig.go index 0af7a50e08a..fcc6b38638c 100644 --- a/apis/projectcontour/v1alpha1/contourconfig.go +++ b/apis/projectcontour/v1alpha1/contourconfig.go @@ -348,6 +348,10 @@ type EnvoyListenerConfig struct { // +optional UseProxyProto *bool `json:"useProxyProtocol,omitempty"` + // Compression defines configuration related to compression in the default HTTP Listener filters. + // +optional + Compression *EnvoyCompression `json:"compression,omitempty"` + // DisableAllowChunkedLength disables the RFC-compliant Envoy behavior to // strip the "Content-Length" header if "Transfer-Encoding: chunked" is // also set. This is an emergency off-switch to revert back to Envoy's diff --git a/changelogs/unreleased/6546-chaosbox-small.md b/changelogs/unreleased/6546-chaosbox-small.md new file mode 100644 index 00000000000..9cde1d1c957 --- /dev/null +++ b/changelogs/unreleased/6546-chaosbox-small.md @@ -0,0 +1 @@ +The HTTP compression algorithm can now be configured using the `compression.algorithm` field in the configuration file or the `spec.envoy.listener.compression.algorithm` field in the `ContourConfiguration` CRD. The available values are `gzip` (default), `brotli`, `zstd`, and `disabled`. diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index 1c48f9fce88..e24d7a403a3 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -123,6 +123,7 @@ func registerServe(app *kingpin.Application) (*kingpin.CmdClause, *serveContext) return nil } + serve.Flag("accesslog-format", "Format for Envoy access logs.").PlaceHolder("").StringVar((*string)(&ctx.Config.AccessLogFormat)) serve.Flag("config-path", "Path to base configuration.").Short('c').PlaceHolder("/path/to/file").Action(parseConfig).ExistingFileVar(&configFile) @@ -447,6 +448,7 @@ func (s *Server) doServe() error { } listenerConfig := xdscache_v3.ListenerConfig{ + Compression: contourConfiguration.Envoy.Listener.Compression, UseProxyProto: *contourConfiguration.Envoy.Listener.UseProxyProto, HTTPAccessLog: contourConfiguration.Envoy.HTTPListener.AccessLog, HTTPSAccessLog: contourConfiguration.Envoy.HTTPSListener.AccessLog, diff --git a/cmd/contour/servecontext.go b/cmd/contour/servecontext.go index 3a1057b6479..6fd2a3643e0 100644 --- a/cmd/contour/servecontext.go +++ b/cmd/contour/servecontext.go @@ -333,6 +333,24 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_v1alpha1.Co accessLogLevel = contour_v1alpha1.LogLevelDisabled } + var compression *contour_v1alpha1.EnvoyCompression + if ctx.Config.Compression.Algorithm != "" { + var algorithm contour_v1alpha1.CompressionAlgorithm + switch ctx.Config.Compression.Algorithm { + case config.CompressionBrotli: + algorithm = contour_v1alpha1.BrotliCompression + case config.CompressionDisabled: + algorithm = contour_v1alpha1.DisabledCompression + case config.CompressionGzip: + algorithm = contour_v1alpha1.GzipCompression + case config.CompressionZstd: + algorithm = contour_v1alpha1.ZstdCompression + } + compression = &contour_v1alpha1.EnvoyCompression{ + Algorithm: algorithm, + } + } + var defaultHTTPVersions []contour_v1alpha1.HTTPVersionType for _, version := range ctx.Config.DefaultHTTPVersions { switch version { @@ -519,6 +537,7 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_v1alpha1.Co Envoy: &contour_v1alpha1.EnvoyConfig{ Listener: &contour_v1alpha1.EnvoyListenerConfig{ UseProxyProto: &ctx.useProxyProto, + Compression: compression, DisableAllowChunkedLength: &ctx.Config.DisableAllowChunkedLength, DisableMergeSlashes: &ctx.Config.DisableMergeSlashes, ServerHeaderTransformation: serverHeaderTransformation, diff --git a/cmd/contour/servecontext_test.go b/cmd/contour/servecontext_test.go index cf205d81a46..ce7665ef4d2 100644 --- a/cmd/contour/servecontext_test.go +++ b/cmd/contour/servecontext_test.go @@ -363,150 +363,150 @@ func TestParseHTTPVersions(t *testing.T) { } } -func TestConvertServeContext(t *testing.T) { - defaultContext := func() *serveContext { - ctx := newServeContext() - ctx.ServerConfig = ServerConfig{ - xdsAddr: "127.0.0.1", - xdsPort: 8001, - caFile: "/certs/ca.crt", - contourCert: "/certs/cert.crt", - contourKey: "/certs/cert.key", - } - return ctx +func defaultContext() *serveContext { + ctx := newServeContext() + ctx.ServerConfig = ServerConfig{ + xdsAddr: "127.0.0.1", + xdsPort: 8001, + caFile: "/certs/ca.crt", + contourCert: "/certs/cert.crt", + contourKey: "/certs/cert.key", } + return ctx +} - defaultContourConfiguration := func() contour_v1alpha1.ContourConfigurationSpec { - return contour_v1alpha1.ContourConfigurationSpec{ - XDSServer: &contour_v1alpha1.XDSServerConfig{ - Type: contour_v1alpha1.EnvoyServerType, - Address: "127.0.0.1", - Port: 8001, - TLS: &contour_v1alpha1.TLS{ - CAFile: "/certs/ca.crt", - CertFile: "/certs/cert.crt", - KeyFile: "/certs/cert.key", - Insecure: ptr.To(false), - }, - }, - Ingress: &contour_v1alpha1.IngressConfig{ - ClassNames: nil, - StatusAddress: "", - }, - Debug: &contour_v1alpha1.DebugConfig{ - Address: "127.0.0.1", - Port: 6060, +func defaultContourConfiguration() contour_v1alpha1.ContourConfigurationSpec { + return contour_v1alpha1.ContourConfigurationSpec{ + XDSServer: &contour_v1alpha1.XDSServerConfig{ + Type: contour_v1alpha1.EnvoyServerType, + Address: "127.0.0.1", + Port: 8001, + TLS: &contour_v1alpha1.TLS{ + CAFile: "/certs/ca.crt", + CertFile: "/certs/cert.crt", + KeyFile: "/certs/cert.key", + Insecure: ptr.To(false), }, - Health: &contour_v1alpha1.HealthConfig{ - Address: "0.0.0.0", - Port: 8000, + }, + Ingress: &contour_v1alpha1.IngressConfig{ + ClassNames: nil, + StatusAddress: "", + }, + Debug: &contour_v1alpha1.DebugConfig{ + Address: "127.0.0.1", + Port: 6060, + }, + Health: &contour_v1alpha1.HealthConfig{ + Address: "0.0.0.0", + Port: 8000, + }, + Envoy: &contour_v1alpha1.EnvoyConfig{ + Service: &contour_v1alpha1.NamespacedName{ + Name: "envoy", + Namespace: "projectcontour", }, - Envoy: &contour_v1alpha1.EnvoyConfig{ - Service: &contour_v1alpha1.NamespacedName{ - Name: "envoy", - Namespace: "projectcontour", - }, - Listener: &contour_v1alpha1.EnvoyListenerConfig{ - UseProxyProto: ptr.To(false), - DisableAllowChunkedLength: ptr.To(false), - DisableMergeSlashes: ptr.To(false), - ServerHeaderTransformation: contour_v1alpha1.OverwriteServerHeader, - TLS: &contour_v1alpha1.EnvoyTLS{ - MinimumProtocolVersion: "", - MaximumProtocolVersion: "", - }, - SocketOptions: &contour_v1alpha1.SocketOptions{ - TOS: 0, - TrafficClass: 0, - }, - }, - HTTPListener: &contour_v1alpha1.EnvoyListener{ - Address: "0.0.0.0", - Port: 8080, - AccessLog: "/dev/stdout", - }, - HTTPSListener: &contour_v1alpha1.EnvoyListener{ - Address: "0.0.0.0", - Port: 8443, - AccessLog: "/dev/stdout", - }, - Health: &contour_v1alpha1.HealthConfig{ - Address: "0.0.0.0", - Port: 8002, - }, - Metrics: &contour_v1alpha1.MetricsConfig{ - Address: "0.0.0.0", - Port: 8002, - }, - ClientCertificate: nil, - Logging: &contour_v1alpha1.EnvoyLogging{ - AccessLogFormat: contour_v1alpha1.EnvoyAccessLog, - AccessLogFormatString: "", - AccessLogLevel: contour_v1alpha1.LogLevelInfo, - AccessLogJSONFields: contour_v1alpha1.AccessLogJSONFields([]string{ - "@timestamp", - "authority", - "bytes_received", - "bytes_sent", - "downstream_local_address", - "downstream_remote_address", - "duration", - "method", - "path", - "protocol", - "request_id", - "requested_server_name", - "response_code", - "response_flags", - "uber_trace_id", - "upstream_cluster", - "upstream_host", - "upstream_local_address", - "upstream_service_time", - "user_agent", - "x_forwarded_for", - "grpc_status", - "grpc_status_number", - }), - }, - DefaultHTTPVersions: nil, - Timeouts: &contour_v1alpha1.TimeoutParameters{ - ConnectionIdleTimeout: ptr.To("60s"), - ConnectTimeout: ptr.To("2s"), - }, - Cluster: &contour_v1alpha1.ClusterParameters{ - DNSLookupFamily: contour_v1alpha1.AutoClusterDNSFamily, - GlobalCircuitBreakerDefaults: nil, - UpstreamTLS: &contour_v1alpha1.EnvoyTLS{ - MinimumProtocolVersion: "", - MaximumProtocolVersion: "", - }, + Listener: &contour_v1alpha1.EnvoyListenerConfig{ + UseProxyProto: ptr.To(false), + DisableAllowChunkedLength: ptr.To(false), + DisableMergeSlashes: ptr.To(false), + ServerHeaderTransformation: contour_v1alpha1.OverwriteServerHeader, + TLS: &contour_v1alpha1.EnvoyTLS{ + MinimumProtocolVersion: "", + MaximumProtocolVersion: "", }, - Network: &contour_v1alpha1.NetworkParameters{ - EnvoyAdminPort: ptr.To(9001), - XffNumTrustedHops: ptr.To(uint32(0)), + SocketOptions: &contour_v1alpha1.SocketOptions{ + TOS: 0, + TrafficClass: 0, }, }, - Gateway: nil, - HTTPProxy: &contour_v1alpha1.HTTPProxyConfig{ - DisablePermitInsecure: ptr.To(false), - FallbackCertificate: nil, + HTTPListener: &contour_v1alpha1.EnvoyListener{ + Address: "0.0.0.0", + Port: 8080, + AccessLog: "/dev/stdout", + }, + HTTPSListener: &contour_v1alpha1.EnvoyListener{ + Address: "0.0.0.0", + Port: 8443, + AccessLog: "/dev/stdout", }, - EnableExternalNameService: ptr.To(false), - RateLimitService: nil, - GlobalExternalAuthorization: nil, - Policy: &contour_v1alpha1.PolicyConfig{ - RequestHeadersPolicy: &contour_v1alpha1.HeadersPolicy{}, - ResponseHeadersPolicy: &contour_v1alpha1.HeadersPolicy{}, - ApplyToIngress: ptr.To(false), + Health: &contour_v1alpha1.HealthConfig{ + Address: "0.0.0.0", + Port: 8002, }, Metrics: &contour_v1alpha1.MetricsConfig{ Address: "0.0.0.0", - Port: 8000, + Port: 8002, + }, + ClientCertificate: nil, + Logging: &contour_v1alpha1.EnvoyLogging{ + AccessLogFormat: contour_v1alpha1.EnvoyAccessLog, + AccessLogFormatString: "", + AccessLogLevel: contour_v1alpha1.LogLevelInfo, + AccessLogJSONFields: contour_v1alpha1.AccessLogJSONFields([]string{ + "@timestamp", + "authority", + "bytes_received", + "bytes_sent", + "downstream_local_address", + "downstream_remote_address", + "duration", + "method", + "path", + "protocol", + "request_id", + "requested_server_name", + "response_code", + "response_flags", + "uber_trace_id", + "upstream_cluster", + "upstream_host", + "upstream_local_address", + "upstream_service_time", + "user_agent", + "x_forwarded_for", + "grpc_status", + "grpc_status_number", + }), + }, + DefaultHTTPVersions: nil, + Timeouts: &contour_v1alpha1.TimeoutParameters{ + ConnectionIdleTimeout: ptr.To("60s"), + ConnectTimeout: ptr.To("2s"), + }, + Cluster: &contour_v1alpha1.ClusterParameters{ + DNSLookupFamily: contour_v1alpha1.AutoClusterDNSFamily, + GlobalCircuitBreakerDefaults: nil, + UpstreamTLS: &contour_v1alpha1.EnvoyTLS{ + MinimumProtocolVersion: "", + MaximumProtocolVersion: "", + }, }, - } + Network: &contour_v1alpha1.NetworkParameters{ + EnvoyAdminPort: ptr.To(9001), + XffNumTrustedHops: ptr.To(uint32(0)), + }, + }, + Gateway: nil, + HTTPProxy: &contour_v1alpha1.HTTPProxyConfig{ + DisablePermitInsecure: ptr.To(false), + FallbackCertificate: nil, + }, + EnableExternalNameService: ptr.To(false), + RateLimitService: nil, + GlobalExternalAuthorization: nil, + Policy: &contour_v1alpha1.PolicyConfig{ + RequestHeadersPolicy: &contour_v1alpha1.HeadersPolicy{}, + ResponseHeadersPolicy: &contour_v1alpha1.HeadersPolicy{}, + ApplyToIngress: ptr.To(false), + }, + Metrics: &contour_v1alpha1.MetricsConfig{ + Address: "0.0.0.0", + Port: 8000, + }, } +} +func TestConvertServeContext(t *testing.T) { cases := map[string]struct { getServeContext func(ctx *serveContext) *serveContext getContourConfiguration func(cfg contour_v1alpha1.ContourConfigurationSpec) contour_v1alpha1.ContourConfigurationSpec @@ -912,3 +912,29 @@ func TestConvertServeContext(t *testing.T) { }) } } + +func TestServeContextCompressionOptions(t *testing.T) { + cases := map[string]struct { + serveCompression config.CompressionAlgorithm + configCompression contour_v1alpha1.CompressionAlgorithm + }{ + "Brotli": {config.CompressionBrotli, contour_v1alpha1.BrotliCompression}, + "Disabled": {config.CompressionDisabled, contour_v1alpha1.DisabledCompression}, + "Gzip": {config.CompressionGzip, contour_v1alpha1.GzipCompression}, + "Zstd": {config.CompressionZstd, contour_v1alpha1.ZstdCompression}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + testServeContext := defaultContext() + testServeContext.Config.Compression.Algorithm = tc.serveCompression + + want := defaultContourConfiguration() + want.Envoy.Listener.Compression = &contour_v1alpha1.EnvoyCompression{ + Algorithm: tc.configCompression, + } + + assert.Equal(t, want, testServeContext.convertToContourConfigurationSpec()) + }) + } +} diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index 0beece5bc51..48601e397a0 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -281,6 +281,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related to + compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer @@ -4063,6 +4079,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related + to compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index 1e085adae27..51aeaa1a389 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -501,6 +501,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related to + compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer @@ -4283,6 +4299,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related + to compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index b3633a2e0cd..7ea0dc3c500 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -292,6 +292,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related to + compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer @@ -4074,6 +4090,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related + to compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index 0db31e989d5..e7981bf7f37 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -317,6 +317,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related to + compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer @@ -4099,6 +4115,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related + to compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index d78bac68c7b..c1b7b931224 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -501,6 +501,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related to + compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer @@ -4283,6 +4299,22 @@ spec: description: Listener hold various configurable Envoy listener values. properties: + compression: + description: Compression defines configuration related + to compression in the default HTTP Listener filters. + properties: + algorithm: + description: |- + Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. + Values: `gzip` (default), `brotli`, `zstd`, `disabled`. + Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response + enum: + - gzip + - brotli + - zstd + - disabled + type: string + type: object connectionBalancer: description: |- ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer diff --git a/internal/contourconfig/contourconfiguration_test.go b/internal/contourconfig/contourconfiguration_test.go index bb13b8229da..294e6a4609b 100644 --- a/internal/contourconfig/contourconfiguration_test.go +++ b/internal/contourconfig/contourconfiguration_test.go @@ -54,11 +54,14 @@ func TestOverlayOnDefaults(t *testing.T) { }, Envoy: &contour_v1alpha1.EnvoyConfig{ Listener: &contour_v1alpha1.EnvoyListenerConfig{ - UseProxyProto: ptr.To(true), - DisableAllowChunkedLength: ptr.To(true), - DisableMergeSlashes: ptr.To(true), + UseProxyProto: ptr.To(true), + Compression: &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.BrotliCompression, + }, MaxRequestsPerConnection: ptr.To(uint32(1)), HTTP2MaxConcurrentStreams: ptr.To(uint32(10)), + DisableAllowChunkedLength: ptr.To(true), + DisableMergeSlashes: ptr.To(true), ServerHeaderTransformation: contour_v1alpha1.PassThroughServerHeader, ConnectionBalancer: "yesplease", TLS: &contour_v1alpha1.EnvoyTLS{ diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index 5d1f2c233ce..da152e4f225 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -23,7 +23,9 @@ import ( envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_compression_brotli_compressor_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/compressor/v3" envoy_compression_gzip_compressor_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/compressor/v3" + envoy_compression_zstd_compressor_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/zstd/compressor/v3" envoy_filter_http_compressor_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/compressor/v3" envoy_filter_http_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" envoy_filter_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" @@ -41,6 +43,7 @@ import ( envoy_transport_socket_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" @@ -188,6 +191,7 @@ type httpConnectionManagerBuilder struct { maxRequestsPerConnection *uint32 http2MaxConcurrentStreams *uint32 enableWebsockets bool + compression *contour_v1alpha1.EnvoyCompression } func (b *httpConnectionManagerBuilder) EnableWebsockets(enable bool) *httpConnectionManagerBuilder { @@ -271,6 +275,13 @@ func (b *httpConnectionManagerBuilder) MergeSlashes(enabled bool) *httpConnectio return b } +// SetDefaultFilterCompression configures the builder to set the compression method applied by DefaultFilters() to the +// given value `compressor`. When chaining builder method calls, this method should be called before DefaultFilters(). +func (b *httpConnectionManagerBuilder) SetDefaultFilterCompression(compressor *contour_v1alpha1.EnvoyCompression) *httpConnectionManagerBuilder { + b.compression = compressor + return b +} + func (b *httpConnectionManagerBuilder) ServerHeaderTransformation(value contour_v1alpha1.ServerHeaderTransformationType) *httpConnectionManagerBuilder { switch value { case contour_v1alpha1.OverwriteServerHeader: @@ -308,34 +319,56 @@ func (b *httpConnectionManagerBuilder) DefaultFilters() *httpConnectionManagerBu // Add a default set of ordered http filters. // The names are not required to match anything and are // identified by the TypeURL of each filter. - b.filters = append(b.filters, - &envoy_filter_network_http_connection_manager_v3.HttpFilter{ - Name: CompressorFilterName, - ConfigType: &envoy_filter_network_http_connection_manager_v3.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_filter_http_compressor_v3.Compressor{ - CompressorLibrary: &envoy_config_core_v3.TypedExtensionConfig{ - Name: "gzip", - TypedConfig: protobuf.MustMarshalAny( - &envoy_compression_gzip_compressor_v3.Gzip{}, - ), - }, - ResponseDirectionConfig: &envoy_filter_http_compressor_v3.Compressor_ResponseDirectionConfig{ - CommonConfig: &envoy_filter_http_compressor_v3.Compressor_CommonDirectionConfig{ - ContentType: []string{ - // Default content-types https://github.com/envoyproxy/envoy/blob/e74999dbdb12aa4d6b7a5d62d51731ea86bf72be/source/extensions/filters/http/compressor/compressor_filter.cc#L35-L38 - "text/html", "text/plain", "text/css", "application/javascript", "application/x-javascript", - "text/javascript", "text/x-javascript", "text/ecmascript", "text/js", "text/jscript", - "text/x-js", "application/ecmascript", "application/x-json", "application/xml", - "application/json", "image/svg+xml", "text/xml", "application/xhtml+xml", - // Additional content-types for grpc-web https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2 - "application/grpc-web", "application/grpc-web+proto", "application/grpc-web+json", "application/grpc-web+thrift", - "application/grpc-web-text", "application/grpc-web-text+proto", "application/grpc-web-text+thrift", + var compressor proto.Message = &envoy_compression_gzip_compressor_v3.Gzip{} + compressorName := string(contour_v1alpha1.GzipCompression) + if b.compression != nil { + switch b.compression.Algorithm { + case contour_v1alpha1.BrotliCompression: + compressorName = "brotli" + compressor = &envoy_compression_brotli_compressor_v3.Brotli{} + case contour_v1alpha1.DisabledCompression: + compressor = nil + case contour_v1alpha1.ZstdCompression: + compressorName = "zstd" + compressor = &envoy_compression_zstd_compressor_v3.Zstd{} + default: + compressorName = "gzip" + compressor = &envoy_compression_gzip_compressor_v3.Gzip{} + } + } + + if compressor != nil { + // If compression is enabled add compressor filter + b.filters = append(b.filters, + &envoy_filter_network_http_connection_manager_v3.HttpFilter{ + Name: CompressorFilterName, + ConfigType: &envoy_filter_network_http_connection_manager_v3.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_filter_http_compressor_v3.Compressor{ + CompressorLibrary: &envoy_config_core_v3.TypedExtensionConfig{ + Name: compressorName, + TypedConfig: protobuf.MustMarshalAny( + compressor, + ), + }, + ResponseDirectionConfig: &envoy_filter_http_compressor_v3.Compressor_ResponseDirectionConfig{ + CommonConfig: &envoy_filter_http_compressor_v3.Compressor_CommonDirectionConfig{ + ContentType: []string{ + // Default content-types https://github.com/envoyproxy/envoy/blob/e74999dbdb12aa4d6b7a5d62d51731ea86bf72be/source/extensions/filters/http/compressor/compressor_filter.cc#L35-L38 + "text/html", "text/plain", "text/css", "application/javascript", "application/x-javascript", + "text/javascript", "text/x-javascript", "text/ecmascript", "text/js", "text/jscript", + "text/x-js", "application/ecmascript", "application/x-json", "application/xml", + "application/json", "image/svg+xml", "text/xml", "application/xhtml+xml", + // Additional content-types for grpc-web https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2 + "application/grpc-web", "application/grpc-web+proto", "application/grpc-web+json", "application/grpc-web+thrift", + "application/grpc-web-text", "application/grpc-web-text+proto", "application/grpc-web-text+thrift", + }, }, }, - }, - }), - }, - }, + }), + }, + }) + } + b.filters = append(b.filters, &envoy_filter_network_http_connection_manager_v3.HttpFilter{ Name: GRPCWebFilterName, ConfigType: &envoy_filter_network_http_connection_manager_v3.HttpFilter_TypedConfig{ diff --git a/internal/featuretests/v3/compression_test.go b/internal/featuretests/v3/compression_test.go new file mode 100644 index 00000000000..9b1cf7cf53e --- /dev/null +++ b/internal/featuretests/v3/compression_test.go @@ -0,0 +1,95 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v3 + +import ( + "testing" + + envoy_service_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" + core_v1 "k8s.io/api/core/v1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + contour_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + contour_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" + envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" + "github.com/projectcontour/contour/internal/fixture" + xdscache_v3 "github.com/projectcontour/contour/internal/xdscache/v3" +) + +func TestCompression(t *testing.T) { + tests := map[string]struct { + algorithm contour_v1alpha1.CompressionAlgorithm + want contour_v1alpha1.CompressionAlgorithm + }{ + "default": {algorithm: "", want: contour_v1alpha1.GzipCompression}, + "disabled": {algorithm: contour_v1alpha1.DisabledCompression, want: contour_v1alpha1.DisabledCompression}, + "brotli": {algorithm: contour_v1alpha1.BrotliCompression, want: contour_v1alpha1.BrotliCompression}, + "zstd": {algorithm: contour_v1alpha1.ZstdCompression, want: contour_v1alpha1.ZstdCompression}, + "gzip": {algorithm: contour_v1alpha1.GzipCompression, want: contour_v1alpha1.GzipCompression}, + } + + s1 := fixture.NewService("backend"). + WithPorts(core_v1.ServicePort{Name: "http", Port: 80}) + + hp1 := &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "simple", + Namespace: s1.Namespace, + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "example.com", + }, + Routes: []contour_v1.Route{{ + Conditions: matchconditions(prefixMatchCondition("/")), + Services: []contour_v1.Service{{ + Name: s1.Name, + Port: 80, + }}, + }}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + rh, c, done := setup(t, func(conf *xdscache_v3.ListenerConfig) { + if tc.algorithm != "" { + conf.Compression = &contour_v1alpha1.EnvoyCompression{ + Algorithm: tc.algorithm, + } + } + }) + defer done() + + rh.OnAdd(s1) + rh.OnAdd(hp1) + httpListener := defaultHTTPListener() + httpListener.FilterChains = envoy_v3.FilterChains(envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(&contour_v1alpha1.EnvoyCompression{ + Algorithm: tc.want, + }). + RouteConfigName(xdscache_v3.ENVOY_HTTP_LISTENER). + MetricsPrefix(xdscache_v3.ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(xdscache_v3.DEFAULT_HTTP_ACCESS_LOG, "", nil, contour_v1alpha1.LogLevelInfo)). + DefaultFilters(). + Get(), + ) + + c.Request(listenerType, xdscache_v3.ENVOY_HTTP_LISTENER).Equals(&envoy_service_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, httpListener), + }) + }) + } +} diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index 6176fdcaad0..9b19e3d5e92 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -66,6 +66,9 @@ type ListenerConfig struct { // If not set, defaults to false. UseProxyProto bool + // Compression defines configuration related to compression in the default HTTP Listener filters. + Compression *contour_v1alpha1.EnvoyCompression + // MinimumTLSVersion defines the minimum TLS protocol version the proxy should accept. MinimumTLSVersion string @@ -393,6 +396,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { // order for the HTTPS virtualhosts. if len(listener.VirtualHosts) > 0 { cm := envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(cfg.Compression). Codec(envoy_v3.CodecForVersions(cfg.DefaultHTTPVersions...)). DefaultFilters(). RouteConfigName(httpRouteConfigName(listener)). @@ -465,6 +469,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { // Contour versions since the metrics prefix will be // coded into monitoring dashboards. cm := envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(cfg.Compression). Codec(envoy_v3.CodecForVersions(cfg.DefaultHTTPVersions...)). AddFilter(envoy_v3.FilterMisdirectedRequests(vh.VirtualHost.Name)). DefaultFilters(). @@ -549,6 +554,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { } cm := envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(cfg.Compression). DefaultFilters(). AddFilter(authzFilter). RouteConfigName(fallbackCertRouteConfigName(listener)). diff --git a/internal/xdscache/v3/listener_test.go b/internal/xdscache/v3/listener_test.go index 095f248210a..63c80d54cdb 100644 --- a/internal/xdscache/v3/listener_test.go +++ b/internal/xdscache/v3/listener_test.go @@ -3074,6 +3074,236 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), }), }, + "httpproxy with disabled compression set in listener config": { + ListenerConfig: ListenerConfig{ + Compression: &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.DisabledCompression, + }, + }, + objs: []any{ + &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_v1.Route{{ + Conditions: []contour_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + service, + }, + want: listenermap(&envoy_config_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(&contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.DisabledCompression, + }). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, contour_v1alpha1.LogLevelInfo)). + DefaultFilters(). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, + "httpproxy with gzip compression set in listener config": { + ListenerConfig: ListenerConfig{ + Compression: &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.GzipCompression, + }, + }, + objs: []any{ + &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_v1.Route{{ + Conditions: []contour_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + service, + }, + want: listenermap(&envoy_config_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(&contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.GzipCompression, + }). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, contour_v1alpha1.LogLevelInfo)). + DefaultFilters(). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, + "httpproxy with brotli compression set in listener config": { + ListenerConfig: ListenerConfig{ + Compression: &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.BrotliCompression, + }, + }, + objs: []any{ + &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_v1.Route{{ + Conditions: []contour_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + service, + }, + want: listenermap(&envoy_config_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(&contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.BrotliCompression, + }). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, contour_v1alpha1.LogLevelInfo)). + DefaultFilters(). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, + "httpproxy with zstd compression set in listener config": { + ListenerConfig: ListenerConfig{ + Compression: &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.ZstdCompression, + }, + }, + objs: []any{ + &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_v1.Route{{ + Conditions: []contour_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + service, + }, + want: listenermap(&envoy_config_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(&contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.ZstdCompression, + }). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, contour_v1alpha1.LogLevelInfo)). + DefaultFilters(). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, + "httpproxy with invalid compression set in listener config": { + ListenerConfig: ListenerConfig{ + Compression: &contour_v1alpha1.EnvoyCompression{ + Algorithm: "invalid value", + }, + }, + objs: []any{ + &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_v1.Route{{ + Conditions: []contour_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + service, + }, + want: listenermap(&envoy_config_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + SetDefaultFilterCompression(&contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.GzipCompression, + }). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, contour_v1alpha1.LogLevelInfo)). + DefaultFilters(). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, "httpsproxy with PerConnectionBufferLimitBytes set in listener config": { ListenerConfig: ListenerConfig{ PerConnectionBufferLimitBytes: ptr.To(uint32(32768)), diff --git a/pkg/config/parameters.go b/pkg/config/parameters.go index 04931c8d349..7c9e0360364 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -651,6 +651,10 @@ type Parameters struct { // which strips duplicate slashes from request URL paths. DisableMergeSlashes bool `yaml:"disableMergeSlashes,omitempty"` + // Compression defines configuration relating to compression in the default HTTP filter chain. + // +optional + Compression CompressionParameters `yaml:"compression,omitempty"` + // Defines the action to be applied to the Server header on the response path. // When configured as overwrite, overwrites any Server header with "envoy". // When configured as append_if_absent, if a Server header is present, pass it through, otherwise set it to "envoy". @@ -966,6 +970,33 @@ const ( LogLevelDisabled AccessLogLevel = "disabled" ) +// CompressionParameters is a type defining configurable compression related values. +// At present this is just the compression algorithm but this could be extended later with algorithm specific config. +type CompressionParameters struct { + // Algorithm configures which compression algorithm, if any, to use in the default HTTP listener filter chain. + // Valid options are 'gzip' (default), 'brotli', 'zstd' and 'disabled'. + // +optional + Algorithm CompressionAlgorithm `yaml:"algorithm,omitempty"` +} + +func (c CompressionParameters) Validate() error { + return c.Algorithm.Validate() +} + +type CompressionAlgorithm string + +func (c CompressionAlgorithm) Validate() error { + return contour_v1alpha1.CompressionAlgorithm(c).Validate() +} + +const ( + CompressionGzip CompressionAlgorithm = "gzip" + CompressionBrotli CompressionAlgorithm = "brotli" + CompressionDisabled CompressionAlgorithm = "disabled" + CompressionZstd CompressionAlgorithm = "zstd" + CompressionDefault = CompressionGzip +) + // Validate verifies that the parameter values do not have any syntax errors. func (p *Parameters) Validate() error { if err := p.Cluster.DNSLookupFamily.Validate(); err != nil { @@ -996,6 +1027,10 @@ func (p *Parameters) Validate() error { return err } + if err := p.Compression.Validate(); err != nil { + return err + } + if err := p.TLS.Validate(); err != nil { return err } @@ -1029,6 +1064,9 @@ func (p *Parameters) Validate() error { return p.Listener.Validate() } +// DefaultCompressionAlgorithm is the compression mechanism in the default HTTP filter chain +const DefaultCompressionAlgorithm = CompressionGzip + // Defaults returns the default set of parameters. func Defaults() Parameters { contourNamespace := GetenvOr("CONTOUR_NAMESPACE", "projectcontour") diff --git a/pkg/config/parameters_test.go b/pkg/config/parameters_test.go index 7f6ad7a7dcd..5b4bc717417 100644 --- a/pkg/config/parameters_test.go +++ b/pkg/config/parameters_test.go @@ -325,6 +325,15 @@ func TestTLSParametersValidation(t *testing.T) { }.Validate()) } +func TestCompressionValidation(t *testing.T) { + require.NoError(t, CompressionParameters{""}.Validate()) + require.NoError(t, CompressionParameters{CompressionBrotli}.Validate()) + require.NoError(t, CompressionParameters{CompressionDisabled}.Validate()) + require.NoError(t, CompressionParameters{CompressionGzip}.Validate()) + require.NoError(t, CompressionParameters{CompressionZstd}.Validate()) + require.True(t, strings.Contains(CompressionParameters{"bogus"}.Validate().Error(), "invalid compression type")) +} + func TestConfigFileValidation(t *testing.T) { check := func(yamlIn string) { t.Helper() diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index 57bc87795fd..19c8e70e9bd 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -5851,6 +5851,37 @@

ClusterParameters +

CompressionAlgorithm +(string alias)

+

+(Appears on: +EnvoyCompression) +

+

+

CompressionAlgorithm defines the type of compression algorithm applied in default HTTP listener filter chain. +Allowable values are defined as names of well known compression algorithms (plus “disabled”).

+

+ + + + + + + + + + + + + + + + +
ValueDescription

"brotli"

BrotliCompression specifies brotli as the default HTTP filter chain compression mechanism

+

"disabled"

DisabledCompression specifies that there will be no compression in the default HTTP filter chain

+

"gzip"

GzipCompression specifies gzip as the default HTTP filter chain compression mechanism

+

"zstd"

ZstdCompression specifies zstd as the default HTTP filter chain compression mechanism

+

ContourConfigurationSpec

@@ -6595,6 +6626,42 @@

DeploymentSettings +

EnvoyCompression +

+

+(Appears on: +EnvoyListenerConfig) +

+

+

EnvoyCompression defines configuration related to compression in the default HTTP Listener filter chain.

+

+ + + + + + + + + + + + + +
FieldDescription
+algorithm +
+ + +CompressionAlgorithm + + +
+(Optional) +

Algorithm selects the compression type applied in the compression HTTP filter of the default Listener filters. +Values: gzip (default), brotli, zstd, disabled. +Setting this to disabled will make Envoy skip “Accept-Encoding: gzip,deflate” request header and always return uncompressed response

+

EnvoyConfig

@@ -6900,6 +6967,21 @@

EnvoyListenerConfig +compression +
+ + +EnvoyCompression + + + + +(Optional) +

Compression defines configuration related to compression in the default HTTP Listener filters.

+ + + + disableAllowChunkedLength
diff --git a/site/content/docs/main/configuration.md b/site/content/docs/main/configuration.md index 5277ab79e38..a914ffeba65 100644 --- a/site/content/docs/main/configuration.md +++ b/site/content/docs/main/configuration.md @@ -72,37 +72,38 @@ The Contour configuration file is optional. In its absence, Contour will operate with reasonable defaults. Where Contour settings can also be specified with command-line flags, the command-line value takes precedence over the configuration file. -| Field Name | Type | Default | Description | -|---------------------------| ---------------------- |------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| accesslog-format | string | `envoy` | This key sets the global [access log format][2] for Envoy. Valid options are `envoy` or `json`. | -| accesslog-format-string | string | None | If present, this specifies custom access log format for Envoy. See [Envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage) for more information about the syntax. This field only has effect if `accesslog-format` is `envoy` | -| accesslog-level | string | `info` | This field specifies the verbosity level of the access log. Valid options are `info` (default, all requests are logged), `error` (all non-success, i.e. 300+ response code, requests are logged), `critical` (all server error, i.e. 500+ response code, requests are logged) and `disabled`. | -| debug | boolean | `false` | Enables debug logging. | -| default-http-versions | string array | HTTP/1.1
HTTP/2 | This array specifies the HTTP versions that Contour should program Envoy to serve. HTTP versions are specified as strings of the form "HTTP/x", where "x" represents the version number. | -| disableAllowChunkedLength | boolean | `false` | If this field is true, Contour will disable the RFC-compliant Envoy behavior to strip the `Content-Length` header if `Transfer-Encoding: chunked` is also set. This is an emergency off-switch to revert back to Envoy's default behavior in case of failures. -| disableMergeSlashes | boolean | `false` | This field disables Envoy's non-standard merge_slashes path transformation behavior that strips duplicate slashes from request URL paths. -| serverHeaderTransformation | string | `overwrite` | This field defines the action to be applied to the Server header on the response path. Values: `overwrite` (default), `append_if_absent`, `pass_through` -| disablePermitInsecure | boolean | `false` | If this field is true, Contour will ignore `PermitInsecure` field in HTTPProxy documents. | -| envoy-service-name | string | `envoy` | This sets the service name that will be inspected for address details to be applied to Ingress objects. | -| envoy-service-namespace | string | `projectcontour` | This sets the namespace of the service that will be inspected for address details to be applied to Ingress objects. If the `CONTOUR_NAMESPACE` environment variable is present, Contour will populate this field with its value. | -| ingress-status-address | string | None | If present, this specifies the address that will be copied into the Ingress status for each Ingress that Contour manages. It is exclusive with `envoy-service-name` and `envoy-service-namespace`. | -| incluster | boolean | `false` | This field specifies that Contour is running in a Kubernetes cluster and should use the in-cluster client access configuration. | -| json-fields | string array | [fields][5] | This is the list the field names to include in the JSON [access log format][2]. This field only has effect if `accesslog-format` is `json`. | -| kubeconfig | string | `$HOME/.kube/config` | Path to a Kubernetes [kubeconfig file][3] for when Contour is executed outside a cluster. | -| kubernetesClientQPS | float32 | | QPS allowed for the Kubernetes client. | -| kubernetesClientBurst | int | | Burst allowed for the Kubernetes client. | -| policy | PolicyConfig | | The default [policy configuration](#policy-configuration). | -| tls | TLS | | The default [TLS configuration](#tls-configuration). | -| timeouts | TimeoutConfig | | The [timeout configuration](#timeout-configuration). | -| cluster | ClusterConfig | | The [cluster configuration](#cluster-configuration). | -| network | NetworkConfig | | The [network configuration](#network-configuration). | -| listener | ListenerConfig | | The [listener configuration](#listener-configuration). | -| server | ServerConfig | | The [server configuration](#server-configuration) for `contour serve` command. | -| gateway | GatewayConfig | | The [gateway-api Gateway configuration](#gateway-configuration). | -| rateLimitService | RateLimitServiceConfig | | The [rate limit service configuration](#rate-limit-service-configuration). | -| enableExternalNameService | boolean | `false` | Enable ExternalName Service processing. Enabling this has security implications. Please see the [advisory](https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc) for more details. | -| metrics | MetricsParameters | | The [metrics configuration](#metrics-configuration) | -| featureFlags | string array | `[]` | Defines the toggle to enable new contour features. Available toggles are:
1. `useEndpointSlices` - configures contour to fetch endpoint data from k8s endpoint slices. | +| Field Name | Type | Default | Description | +|----------------------------|------------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| accesslog-format | string | `envoy` | This key sets the global [access log format][2] for Envoy. Valid options are `envoy` or `json`. | +| accesslog-format-string | string | None | If present, this specifies custom access log format for Envoy. See [Envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage) for more information about the syntax. This field only has effect if `accesslog-format` is `envoy` | +| accesslog-level | string | `info` | This field specifies the verbosity level of the access log. Valid options are `info` (default, all requests are logged), `error` (all non-success, i.e. 300+ response code, requests are logged), `critical` (all server error, i.e. 500+ response code, requests are logged) and `disabled`. | +| debug | boolean | `false` | Enables debug logging. | +| default-http-versions | string array | HTTP/1.1
HTTP/2 | This array specifies the HTTP versions that Contour should program Envoy to serve. HTTP versions are specified as strings of the form "HTTP/x", where "x" represents the version number. | +| disableAllowChunkedLength | boolean | `false` | If this field is true, Contour will disable the RFC-compliant Envoy behavior to strip the `Content-Length` header if `Transfer-Encoding: chunked` is also set. This is an emergency off-switch to revert back to Envoy's default behavior in case of failures. | +| compression | CompressionParameters | | Sets the compression configuration applied in the compression HTTP filter of the default Listener filters. | +| disableMergeSlashes | boolean | `false` | This field disables Envoy's non-standard merge_slashes path transformation behavior that strips duplicate slashes from request URL paths. | +| serverHeaderTransformation | string | `overwrite` | This field defines the action to be applied to the Server header on the response path. Values: `overwrite` (default), `append_if_absent`, `pass_through` | +| disablePermitInsecure | boolean | `false` | If this field is true, Contour will ignore `PermitInsecure` field in HTTPProxy documents. | +| envoy-service-name | string | `envoy` | This sets the service name that will be inspected for address details to be applied to Ingress objects. | +| envoy-service-namespace | string | `projectcontour` | This sets the namespace of the service that will be inspected for address details to be applied to Ingress objects. If the `CONTOUR_NAMESPACE` environment variable is present, Contour will populate this field with its value. | +| ingress-status-address | string | None | If present, this specifies the address that will be copied into the Ingress status for each Ingress that Contour manages. It is exclusive with `envoy-service-name` and `envoy-service-namespace`. | +| incluster | boolean | `false` | This field specifies that Contour is running in a Kubernetes cluster and should use the in-cluster client access configuration. | +| json-fields | string array | [fields][5] | This is the list the field names to include in the JSON [access log format][2]. This field only has effect if `accesslog-format` is `json`. | +| kubeconfig | string | `$HOME/.kube/config` | Path to a Kubernetes [kubeconfig file][3] for when Contour is executed outside a cluster. | +| kubernetesClientQPS | float32 | | QPS allowed for the Kubernetes client. | +| kubernetesClientBurst | int | | Burst allowed for the Kubernetes client. | +| policy | PolicyConfig | | The default [policy configuration](#policy-configuration). | +| tls | TLS | | The default [TLS configuration](#tls-configuration). | +| timeouts | TimeoutConfig | | The [timeout configuration](#timeout-configuration). | +| cluster | ClusterConfig | | The [cluster configuration](#cluster-configuration). | +| network | NetworkConfig | | The [network configuration](#network-configuration). | +| listener | ListenerConfig | | The [listener configuration](#listener-configuration). | +| server | ServerConfig | | The [server configuration](#server-configuration) for `contour serve` command. | +| gateway | GatewayConfig | | The [gateway-api Gateway configuration](#gateway-configuration). | +| rateLimitService | RateLimitServiceConfig | | The [rate limit service configuration](#rate-limit-service-configuration). | +| enableExternalNameService | boolean | `false` | Enable ExternalName Service processing. Enabling this has security implications. Please see the [advisory](https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc) for more details. | +| metrics | MetricsParameters | | The [metrics configuration](#metrics-configuration) | +| featureFlags | string array | `[]` | Defines the toggle to enable new contour features. Available toggles are:
1. `useEndpointSlices` - configures contour to fetch endpoint data from k8s endpoint slices. | ### TLS Configuration @@ -301,6 +302,13 @@ Metrics and health endpoints cannot have the same port number when metrics are s | max-requests | int | 0 | The maximum parallel requests a single Envoy instance allows to the Kubernetes Service; defaults to 1024 | | max-retries | int | 0 | The maximum number of parallel retries a single Envoy instance allows to the Kubernetes Service; defaults to 3. This setting only makes sense if the cluster is configured to do retries.| +### Compression Parameters + +| Field Name | Type | Default | Description | +|------------|--------|--------|-------------------------| +| algorithm | string | "gzip" | Compression algorithm. Setting this to `disabled` will make Envoy skip "Accept-Encoding: gzip,deflate" request header and always return uncompressed response. Values:`gzip` (default), `brotli`, `zstd`, `disabled`. | + + ### Configuration Example The following is an example ConfigMap with configuration file included: diff --git a/test/e2e/deployment.go b/test/e2e/deployment.go index 6fc95eec9a5..4128f0d524b 100644 --- a/test/e2e/deployment.go +++ b/test/e2e/deployment.go @@ -490,7 +490,7 @@ func (d *Deployment) EnsureResourcesForLocalContour() error { return err } - session.Wait("2s") + session.Wait("3s") bootstrapContents, err := io.ReadAll(bFile) if err != nil { return err diff --git a/test/e2e/httpproxy/envoy_compression_test.go b/test/e2e/httpproxy/envoy_compression_test.go new file mode 100644 index 00000000000..6f277815fdd --- /dev/null +++ b/test/e2e/httpproxy/envoy_compression_test.go @@ -0,0 +1,84 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package httpproxy + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + contour_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + "github.com/projectcontour/contour/test/e2e" +) + +func testEnvoyDisableCompression(namespace, acceptEncoding, contentEncoding string, disabled bool) { + testSpec := fmt.Sprintf("responses compressed with accept-encoding %s expecting content-encoding %s", acceptEncoding, contentEncoding) + if disabled { + testSpec = "responses are plaintext when compression disabled" + } + + Specify(testSpec, func() { + resp := "minimum_text_to_enable_gzipminimum_text_to_enable_gzipminimum_text_to_enable_gzipminimum_text_to_enable_gzipminimum_text_to_enable_gzipminimum_text_to_enable_gzip" + p := &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "direct-response", + Namespace: namespace, + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: fmt.Sprintf("%s-fqdn.projectcontour.io", namespace), + }, Routes: []contour_v1.Route{ + { + Conditions: []contour_v1.MatchCondition{{ + Prefix: "/directresponse", + }}, + DirectResponsePolicy: &contour_v1.HTTPDirectResponsePolicy{ + StatusCode: 200, + Body: resp, + }, + }, + }, + }, + } + require.True(f.T(), f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid)) + + require.EventuallyWithT(f.T(), func(c *assert.CollectT) { + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Path: "/directresponse", + Host: p.Spec.VirtualHost.Fqdn, + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{ + "Accept-Encoding": fmt.Sprintf("%s, deflate", acceptEncoding), + }), + }, + Condition: e2e.HasStatusCode(200), + }) + assert.NotNil(c, res, "request never succeeded") + assert.Truef(c, ok, "expected 200 response code, got %d", res.StatusCode) + contentEncodingHeaderValue := res.Headers.Get("Content-Encoding") + if disabled { + assert.NotEqual(c, contentEncodingHeaderValue, contentEncoding, "expected plain text") + return + } + assert.Equal(c, contentEncoding, contentEncodingHeaderValue, "expected plain text") + }, 20*time.Second, f.RetryInterval) + }) +} diff --git a/test/e2e/httpproxy/httpproxy_test.go b/test/e2e/httpproxy/httpproxy_test.go index ff01a0af897..0a838a2294a 100644 --- a/test/e2e/httpproxy/httpproxy_test.go +++ b/test/e2e/httpproxy/httpproxy_test.go @@ -388,6 +388,52 @@ var _ = Describe("HTTPProxy", func() { }) }) + f.NamespacedTest("httpproxy-default-compression", func(namespace string) { + testEnvoyDisableCompression(namespace, "gzip", "gzip", false) + }) + + f.NamespacedTest("httpproxy-disable-compression", func(namespace string) { + Context("with compression disabled", func() { + BeforeEach(func() { + contourConfig.Compression = config.CompressionParameters{ + Algorithm: config.CompressionDisabled, + } + contourConfiguration.Spec.Envoy.Listener.Compression = &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.DisabledCompression, + } + }) + testEnvoyDisableCompression(namespace, "gzip", "gzip", true) + }) + }) + + f.NamespacedTest("httpproxy-brotli-compression", func(namespace string) { + Context("with brotli compression", func() { + BeforeEach(func() { + contourConfig.Compression = config.CompressionParameters{ + Algorithm: config.CompressionBrotli, + } + contourConfiguration.Spec.Envoy.Listener.Compression = &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.BrotliCompression, + } + }) + testEnvoyDisableCompression(namespace, "br", "br", false) + }) + }) + + f.NamespacedTest("httpproxy-zstd-compression", func(namespace string) { + Context("with zstd compression", func() { + BeforeEach(func() { + contourConfig.Compression = config.CompressionParameters{ + Algorithm: config.CompressionZstd, + } + contourConfiguration.Spec.Envoy.Listener.Compression = &contour_v1alpha1.EnvoyCompression{ + Algorithm: contour_v1alpha1.ZstdCompression, + } + }) + testEnvoyDisableCompression(namespace, "zstd", "zstd", false) + }) + }) + f.NamespacedTest("httpproxy-external-auth", testExternalAuth) f.NamespacedTest("httpproxy-http-health-checks", testHTTPHealthChecks)