Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion apis/projectcontour/v1alpha1/compression.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,23 @@ type CompressionAlgorithm string

// EnvoyCompression defines configuration related to compression in the default HTTP Listener filter chain.
type EnvoyCompression struct {
Comment on lines 22 to 24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could give early feedback by adding this CEL expression here

Suggested change
// EnvoyCompression defines configuration related to compression in the default HTTP Listener filter chain.
type EnvoyCompression struct {
// EnvoyCompression defines configuration related to compression in the default HTTP Listener filter chain.
// +kubebuilder:validation:XValidation:rule="!(has(self.algorithm) && has(self.algorithms))",message="compression algorithm and algorithms are mutually exclusive"
type EnvoyCompression struct {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. EnvoyCompression now has this CEL rule so the API server rejects algorithm and algorithms together.

// Algorithm selects the response compression type applied in the compression HTTP filter of the default Listener filters.
// Algorithm selects a single response 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.
// Mutually exclusive with Algorithms. Prefer Algorithms to enable more than one encoding.
// +kubebuilder:validation:Enum="gzip";"brotli";"zstd";"disabled"
// +optional
Algorithm CompressionAlgorithm `json:"algorithm,omitempty"`

// Algorithms selects one or more response compression types.
// Envoy installs one compressor filter per entry and negotiates the encoding from the request Accept-Encoding header.
// List order is the preference when q-values are equal (the first entry is preferred).
// Values: `gzip`, `brotli`, `zstd`. Cannot include `disabled`; set algorithm: disabled instead.
// Mutually exclusive with Algorithm.
// +kubebuilder:validation:MaxItems=3
// +kubebuilder:validation:items:Enum=gzip;brotli;zstd
// +optional
Algorithms []CompressionAlgorithm `json:"algorithms,omitempty"`
}

func (a CompressionAlgorithm) Validate() error {
Expand All @@ -38,6 +49,59 @@ func (a CompressionAlgorithm) Validate() error {
}
}

// Validate reports whether the compression configuration is internally consistent.
func (c *EnvoyCompression) Validate() error {
if c == nil {
return nil
}
if err := c.Algorithm.Validate(); err != nil {
return err
}
if c.Algorithm != "" && len(c.Algorithms) > 0 {
return fmt.Errorf("compression algorithm and algorithms are mutually exclusive")
}

seen := make(map[CompressionAlgorithm]struct{}, len(c.Algorithms))
for _, algorithm := range c.Algorithms {
if algorithm == DisabledCompression {
return fmt.Errorf("compression algorithms cannot include %q; set algorithm: %q instead", DisabledCompression, DisabledCompression)
}
if err := algorithm.Validate(); err != nil {
return err
}
if algorithm == "" {
return fmt.Errorf("compression algorithms cannot include an empty value")
}
if _, ok := seen[algorithm]; ok {
return fmt.Errorf("duplicate compression algorithm %q", algorithm)
}
seen[algorithm] = struct{}{}
}
return nil
}

// EffectiveAlgorithms returns the ordered list of compressor libraries to program
// on the default HTTP listener. A nil result means compression is disabled.
// When neither field is set, gzip is used.
func (c *EnvoyCompression) EffectiveAlgorithms() []CompressionAlgorithm {
if c == nil {
return []CompressionAlgorithm{GzipCompression}
}
if c.Algorithm == DisabledCompression {
return nil
}
if len(c.Algorithms) > 0 {
return append([]CompressionAlgorithm(nil), c.Algorithms...)
}
switch c.Algorithm {
case BrotliCompression, ZstdCompression, GzipCompression:
return []CompressionAlgorithm{c.Algorithm}
default:
// Unset or unknown values fall back to gzip, matching historical behavior.
return []CompressionAlgorithm{GzipCompression}
}
}

const (
// BrotliCompression specifies brotli as the default HTTP filter chain compression mechanism
BrotliCompression CompressionAlgorithm = "brotli"
Expand Down
51 changes: 51 additions & 0 deletions apis/projectcontour/v1alpha1/compression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,54 @@ func TestValidateEnvoyCompressionAlgorithmType(t *testing.T) {
require.NoError(t, contour_v1alpha1.GzipCompression.Validate())
require.NoError(t, contour_v1alpha1.ZstdCompression.Validate())
}

func TestValidateEnvoyCompression(t *testing.T) {
require.NoError(t, (*contour_v1alpha1.EnvoyCompression)(nil).Validate())
require.NoError(t, (&contour_v1alpha1.EnvoyCompression{}).Validate())
require.NoError(t, (&contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.GzipCompression}).Validate())
require.NoError(t, (&contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{
contour_v1alpha1.BrotliCompression,
contour_v1alpha1.GzipCompression,
},
}).Validate())

require.ErrorContains(t, (&contour_v1alpha1.EnvoyCompression{Algorithm: "bogus"}).Validate(), "invalid compression type")
require.ErrorContains(t, (&contour_v1alpha1.EnvoyCompression{
Algorithm: contour_v1alpha1.GzipCompression,
Algorithms: []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.BrotliCompression},
}).Validate(), "mutually exclusive")
require.ErrorContains(t, (&contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.DisabledCompression},
}).Validate(), "cannot include")
require.ErrorContains(t, (&contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.GzipCompression, contour_v1alpha1.GzipCompression},
}).Validate(), "duplicate")
require.ErrorContains(t, (&contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{""},
}).Validate(), "empty value")
require.ErrorContains(t, (&contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{"bogus"},
}).Validate(), "invalid compression type")
}

func TestEffectiveAlgorithms(t *testing.T) {
require.Equal(t, []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.GzipCompression},
(*contour_v1alpha1.EnvoyCompression)(nil).EffectiveAlgorithms())
require.Equal(t, []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.GzipCompression},
(&contour_v1alpha1.EnvoyCompression{}).EffectiveAlgorithms())
require.Nil(t, (&contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.DisabledCompression}).EffectiveAlgorithms())
require.Equal(t, []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.BrotliCompression},
(&contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.BrotliCompression}).EffectiveAlgorithms())
require.Equal(t, []contour_v1alpha1.CompressionAlgorithm{
contour_v1alpha1.BrotliCompression,
contour_v1alpha1.GzipCompression,
}, (&contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{
contour_v1alpha1.BrotliCompression,
contour_v1alpha1.GzipCompression,
},
}).EffectiveAlgorithms())
require.Equal(t, []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.GzipCompression},
(&contour_v1alpha1.EnvoyCompression{Algorithm: "bogus"}).EffectiveAlgorithms())
}
10 changes: 7 additions & 3 deletions apis/projectcontour/v1alpha1/contourconfig_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,13 @@ func (e *EnvoyConfig) Validate() error {
}
}

// Envoy TLS configuration
if e.Listener != nil && e.Listener.TLS != nil {
return e.Listener.TLS.Validate()
if e.Listener != nil {
if err := e.Listener.Compression.Validate(); err != nil {
return err
}
if e.Listener.TLS != nil {
return e.Listener.TLS.Validate()
}
}

return nil
Expand Down
20 changes: 20 additions & 0 deletions apis/projectcontour/v1alpha1/contourconfig_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ func TestContourConfigurationSpecValidate(t *testing.T) {
c.Envoy.Cluster.DNSLookupFamily = "foo"
require.Error(t, c.Validate())

c = contour_v1alpha1.ContourConfigurationSpec{
Envoy: &contour_v1alpha1.EnvoyConfig{
Listener: &contour_v1alpha1.EnvoyListenerConfig{
Compression: &contour_v1alpha1.EnvoyCompression{
Algorithm: contour_v1alpha1.GzipCompression,
Algorithms: []contour_v1alpha1.CompressionAlgorithm{contour_v1alpha1.BrotliCompression},
},
},
},
}
require.Error(t, c.Validate())

c.Envoy.Listener.Compression = &contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{
contour_v1alpha1.BrotliCompression,
contour_v1alpha1.GzipCompression,
},
}
require.NoError(t, c.Validate())

c = contour_v1alpha1.ContourConfigurationSpec{
Envoy: &contour_v1alpha1.EnvoyConfig{
Listener: &contour_v1alpha1.EnvoyListenerConfig{
Expand Down
7 changes: 6 additions & 1 deletion apis/projectcontour/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions changelogs/unreleased/7699-anonrig-small.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
HTTP compression can now be configured with a list of algorithms via `compression.algorithms` in the configuration file or `spec.envoy.listener.compression.algorithms` in the `ContourConfiguration` CRD. Envoy negotiates the encoding from the request `Accept-Encoding` header. The existing `algorithm` field remains supported for a single encoding.
17 changes: 2 additions & 15 deletions cmd/contour/servecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,21 +341,8 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_v1alpha1.Co
}

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,
}
if ctx.Config.Compression.Algorithm != "" || len(ctx.Config.Compression.Algorithms) > 0 {
compression = ctx.Config.Compression.ToEnvoyCompression()
}

var defaultHTTPVersions []contour_v1alpha1.HTTPVersionType
Expand Down
41 changes: 31 additions & 10 deletions cmd/contour/servecontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -962,24 +962,45 @@ func TestConvertServeContext(t *testing.T) {

func TestServeContextCompressionOptions(t *testing.T) {
cases := map[string]struct {
serveCompression config.CompressionAlgorithm
configCompression contour_v1alpha1.CompressionAlgorithm
serveCompression config.CompressionParameters
configCompression *contour_v1alpha1.EnvoyCompression
}{
"Brotli": {config.CompressionBrotli, contour_v1alpha1.BrotliCompression},
"Disabled": {config.CompressionDisabled, contour_v1alpha1.DisabledCompression},
"Gzip": {config.CompressionGzip, contour_v1alpha1.GzipCompression},
"Zstd": {config.CompressionZstd, contour_v1alpha1.ZstdCompression},
"Brotli": {
serveCompression: config.CompressionParameters{Algorithm: config.CompressionBrotli},
configCompression: &contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.BrotliCompression},
},
"Disabled": {
serveCompression: config.CompressionParameters{Algorithm: config.CompressionDisabled},
configCompression: &contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.DisabledCompression},
},
"Gzip": {
serveCompression: config.CompressionParameters{Algorithm: config.CompressionGzip},
configCompression: &contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.GzipCompression},
},
"Zstd": {
serveCompression: config.CompressionParameters{Algorithm: config.CompressionZstd},
configCompression: &contour_v1alpha1.EnvoyCompression{Algorithm: contour_v1alpha1.ZstdCompression},
},
"Algorithms": {
serveCompression: config.CompressionParameters{
Algorithms: []config.CompressionAlgorithm{config.CompressionBrotli, config.CompressionGzip},
},
configCompression: &contour_v1alpha1.EnvoyCompression{
Algorithms: []contour_v1alpha1.CompressionAlgorithm{
contour_v1alpha1.BrotliCompression,
contour_v1alpha1.GzipCompression,
},
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
testServeContext := defaultContext()
testServeContext.Config.Compression.Algorithm = tc.serveCompression
testServeContext.Config.Compression = tc.serveCompression

want := defaultContourConfiguration()
want.Envoy.Listener.Compression = &contour_v1alpha1.EnvoyCompression{
Algorithm: tc.configCompression,
}
want.Envoy.Listener.Compression = tc.configCompression

assert.Equal(t, want, testServeContext.convertToContourConfigurationSpec())
})
Expand Down
42 changes: 40 additions & 2 deletions examples/contour/01-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,34 @@ spec:
properties:
algorithm:
description: |-
Algorithm selects the response compression type applied in the compression HTTP filter of the default Listener filters.
Algorithm selects a single response 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.
Mutually exclusive with Algorithms. Prefer Algorithms to enable more than one encoding.
enum:
- gzip
- brotli
- zstd
- disabled
type: string
algorithms:
description: |-
Algorithms selects one or more response compression types.
Envoy installs one compressor filter per entry and negotiates the encoding from the request Accept-Encoding header.
List order is the preference when q-values are equal (the first entry is preferred).
Values: `gzip`, `brotli`, `zstd`. Cannot include `disabled`; set algorithm: disabled instead.
Mutually exclusive with Algorithm.
items:
description: |-
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").
enum:
- gzip
- brotli
- zstd
type: string
maxItems: 3
type: array
type: object
connectionBalancer:
description: |-
Expand Down Expand Up @@ -4457,15 +4476,34 @@ spec:
properties:
algorithm:
description: |-
Algorithm selects the response compression type applied in the compression HTTP filter of the default Listener filters.
Algorithm selects a single response 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.
Mutually exclusive with Algorithms. Prefer Algorithms to enable more than one encoding.
enum:
- gzip
- brotli
- zstd
- disabled
type: string
algorithms:
description: |-
Algorithms selects one or more response compression types.
Envoy installs one compressor filter per entry and negotiates the encoding from the request Accept-Encoding header.
List order is the preference when q-values are equal (the first entry is preferred).
Values: `gzip`, `brotli`, `zstd`. Cannot include `disabled`; set algorithm: disabled instead.
Mutually exclusive with Algorithm.
items:
description: |-
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").
enum:
- gzip
- brotli
- zstd
type: string
maxItems: 3
type: array
type: object
connectionBalancer:
description: |-
Expand Down
Loading