diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 4183c12830f9..79b0e5a540a7 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -67,7 +67,6 @@ type BackendTrafficPolicySpec struct { // The compression config for the http streams. // // +optional - // +notImplementedHide Compression []*Compression `json:"compression,omitempty"` // ResponseOverride defines the configuration to override specific responses with a custom one. diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index cbf2c9226d0b..e0c431858d49 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -233,6 +233,9 @@ const ( // EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter. EnvoyFilterCustomResponse EnvoyFilter = "envoy.filters.http.custom_response" + // EnvoyFilterCompressor defines the Envoy HTTP compressor filter. + EnvoyFilterCompressor EnvoyFilter = "envoy.filters.http.compressor" + // EnvoyFilterRouter defines the Envoy HTTP router filter. EnvoyFilterRouter EnvoyFilter = "envoy.filters.http.router" ) diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 0934629428bd..55455154c571 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -453,6 +453,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway( ds *ir.DNS h2 *ir.HTTP2Settings ro *ir.ResponseOverride + cp *ir.Compression err, errs error ) @@ -495,6 +496,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway( err = perr.WithMessage(err, "ResponseOverride") errs = errors.Join(errs, err) } + cp = buildCompression(policy.Spec.Compression) ds = translateDNS(policy.Spec.ClusterSettings) @@ -579,6 +581,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway( HTTP2: h2, DNS: ds, ResponseOverride: ro, + Compression: cp, } // Update the Host field in HealthCheck, now that we have access to the Route Hostname. @@ -930,3 +933,13 @@ func defaultResponseOverrideRuleName(policy *egv1a1.BackendTrafficPolicy, index irConfigName(policy), strconv.Itoa(index)) } + +func buildCompression(compression []*egv1a1.Compression) *ir.Compression { + if len(compression) == 0 { + return nil + } + + // Only Gzip is supported for now, so we don't need to do anything special here + return &ir.Compression{ + } +} diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 4db7b8445aaa..c73a851a437a 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -731,6 +731,12 @@ type HeaderBasedSessionPersistence struct { Name string `json:"name"` } +// Compression holds the configuration for HTTP compression. +// Currently, only the default compressor(gzip) is supported. +// +k8s:deepcopy-gen=true +type Compression struct { +} + // TrafficFeatures holds the information associated with the Backend Traffic Policy. // +k8s:deepcopy-gen=true type TrafficFeatures struct { @@ -762,6 +768,8 @@ type TrafficFeatures struct { DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"` // ResponseOverride defines the schema for overriding the response. ResponseOverride *ResponseOverride `json:"responseOverride,omitempty" yaml:"responseOverride,omitempty"` + // Compression settings for HTTP Response + Compression *Compression } func (b *TrafficFeatures) Validate() error { diff --git a/internal/xds/translator/compressor.go b/internal/xds/translator/compressor.go new file mode 100644 index 000000000000..5eaf12ced09f --- /dev/null +++ b/internal/xds/translator/compressor.go @@ -0,0 +1,164 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "errors" + "fmt" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + gzipv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/compressor/v3" + compressorv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/compressor/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "google.golang.org/protobuf/types/known/anypb" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/protocov" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +func init() { + registerHTTPFilter(&compressor{}) +} + +type compressor struct{} + +var _ httpFilter = &compressor{} + +// patchHCM builds and appends the compressor Filter to the HTTP Connection Manager +// if applicable, and it does not already exist. +func (*compressor) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + if mgr == nil { + return errors.New("hcm is nil") + } + if irListener == nil { + return errors.New("ir listener is nil") + } + if hcmContainsFilter(mgr, egv1a1.EnvoyFilterCompressor.String()) { + return nil + } + + var ( + irCompression *ir.Compression + filter *hcmv3.HttpFilter + err error + ) + + for _, route := range irListener.Routes { + if route.Traffic != nil && route.Traffic.Compression != nil { + irCompression = route.Traffic.Compression + } + } + if irCompression == nil { + return nil + } + + // The HCM-level filter config doesn't matter since it is overridden at the route level. + if filter, err = buildHCMCompressorFilter(); err != nil { + return err + } + mgr.HttpFilters = append(mgr.HttpFilters, filter) + return err +} + +// buildHCMCompressorFilter returns a Compressor HTTP filter from the provided IR HTTPRoute. +func buildHCMCompressorFilter() (*hcmv3.HttpFilter, error) { + var ( + compressorProto *compressorv3.Compressor + gzipAny *anypb.Any + compressorAny *anypb.Any + err error + ) + + if gzipAny, err = protocov.ToAnyWithValidation(&gzipv3.Gzip{}); err != nil { + return nil, err + } + + compressorProto = &compressorv3.Compressor{ + CompressorLibrary: &corev3.TypedExtensionConfig{ + Name: "envoy.compressor.gzip", + TypedConfig:gzipAny, + }, + } + + if compressorAny, err = protocov.ToAnyWithValidation(compressorProto); err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: egv1a1.EnvoyFilterBasicAuth.String(), + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: compressorAny, + }, + Disabled: true, + }, nil +} + +func (*compressor) patchResources(*types.ResourceVersionTable, []*ir.HTTPRoute) error { + return nil +} + +// patchRoute patches the provided route with the compressor config if applicable. +// Note: this method overwrites the HCM level filter config with the per route filter config. +func (*compressor) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { + if route == nil { + return errors.New("xds route is nil") + } + if irRoute == nil { + return errors.New("ir route is nil") + } + if irRoute.Security == nil || irRoute.Security.BasicAuth == nil { + return nil + } + + var ( + perFilterCfg map[string]*anypb.Any + compressorAny *anypb.Any + err error + ) + + perFilterCfg = route.GetTypedPerFilterConfig() + if _, ok := perFilterCfg[egv1a1.EnvoyFilterBasicAuth.String()]; ok { + // This should not happen since this is the only place where the filter + // config is added in a route. + return fmt.Errorf("route already contains filter config: %s, %+v", + egv1a1.EnvoyFilterBasicAuth.String(), route) + } + + // Overwrite the HCM level filter config with the per route filter config. + compressorProto := compressorPerRouteConfig(irRoute) + + if compressorProto == nil { + return nil + } + + if compressorAny, err = protocov.ToAnyWithValidation(compressorProto); err != nil { + return err + } + + if perFilterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + route.TypedPerFilterConfig[egv1a1.EnvoyFilterBasicAuth.String()] = compressorAny + + return nil +} + +func compressorPerRouteConfig(irRoute *ir.HTTPRoute) *compressorv3.CompressorPerRoute { + // Disable compression on this route if no compression is configured. + if irRoute.Traffic==nil || irRoute.Traffic.Compression == nil { + return &compressorv3.CompressorPerRoute{ + Override: &compressorv3.CompressorPerRoute_Disabled{ + Disabled: true, + }, + } + + } + // Use the default compressor at the HCM level + return nil +}