From fd942f5cdaaa72145ff2a28ad1e3552d8cf9931a Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 3 Jan 2025 09:04:03 +0000 Subject: [PATCH 01/15] implement compression Signed-off-by: Huabing Zhao --- api/v1alpha1/backendtrafficpolicy_types.go | 1 - api/v1alpha1/envoyproxy_types.go | 3 + internal/gatewayapi/backendtrafficpolicy.go | 17 + .../backendtrafficpolicy-compression.in.yaml | 48 +++ .../backendtrafficpolicy-compression.out.yaml | 169 ++++++++++ internal/ir/xds.go | 9 + internal/ir/zz_generated.deepcopy.go | 20 ++ internal/xds/translator/compressor.go | 157 +++++++++ .../testdata/in/xds-ir/compression.yaml | 39 +++ .../out/xds-ir/compression.clusters.yaml | 17 + .../out/xds-ir/compression.endpoints.yaml | 12 + .../out/xds-ir/compression.listeners.yaml | 42 +++ .../out/xds-ir/compression.routes.yaml | 33 ++ site/content/en/latest/api/extension_types.md | 2 + site/content/zh/latest/api/extension_types.md | 2 + test/e2e/testdata/compression.yaml | 47 +++ test/e2e/tests/compression.go | 97 ++++++ test/e2e/tests/roundtripper.go | 309 ++++++++++++++++++ 18 files changed, 1023 insertions(+), 1 deletion(-) create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml create mode 100644 internal/xds/translator/compressor.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/compression.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/compression.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml create mode 100644 test/e2e/testdata/compression.yaml create mode 100644 test/e2e/tests/compression.go create mode 100644 test/e2e/tests/roundtripper.go diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 4183c12830f..79b0e5a540a 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 317d302a4c0..d555ca97cab 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -241,6 +241,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 0934629428b..560cb8bb7f5 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -305,6 +305,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute( ds *ir.DNS h2 *ir.HTTP2Settings ro *ir.ResponseOverride + cp *ir.Compression err, errs error ) @@ -354,6 +355,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute( err = perr.WithMessage(err, "ResponseOverride") errs = errors.Join(errs, err) } + cp = buildCompression(policy.Spec.Compression) ds = translateDNS(policy.Spec.ClusterSettings) @@ -417,6 +419,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute( DNS: ds, Timeout: to, ResponseOverride: ro, + Compression: cp, } // Update the Host field in HealthCheck, now that we have access to the Route Hostname. @@ -453,6 +456,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway( ds *ir.DNS h2 *ir.HTTP2Settings ro *ir.ResponseOverride + cp *ir.Compression err, errs error ) @@ -495,6 +499,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 +584,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 +936,14 @@ 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{ + Type: "GZip", + } +} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml new file mode 100644 index 00000000000..5dd8e1adf21 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml @@ -0,0 +1,48 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + compression: + - type: Gzip diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml new file mode 100644 index 00000000000..caeff8722a7 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml @@ -0,0 +1,169 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + compression: + - type: Gzip + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + compression: + type: GZip diff --git a/internal/ir/xds.go b/internal/ir/xds.go index c87a5f03722..08990eb5425 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -752,6 +752,13 @@ 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 { + Type string `json:"type" yaml:"type"` +} + // TrafficFeatures holds the information associated with the Backend Traffic Policy. // +k8s:deepcopy-gen=true type TrafficFeatures struct { @@ -783,6 +790,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 `json:"compression,omitempty" yaml:"compression,omitempty"` } func (b *TrafficFeatures) Validate() error { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 0c734dbec70..7a5abdca402 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -506,6 +506,21 @@ func (in *ClientTimeout) DeepCopy() *ClientTimeout { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Compression) DeepCopyInto(out *Compression) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Compression. +func (in *Compression) DeepCopy() *Compression { + if in == nil { + return nil + } + out := new(Compression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) { *out = *in @@ -3340,6 +3355,11 @@ func (in *TrafficFeatures) DeepCopyInto(out *TrafficFeatures) { *out = new(ResponseOverride) (*in).DeepCopyInto(*out) } + if in.Compression != nil { + in, out := &in.Compression, &out.Compression + *out = new(Compression) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficFeatures. diff --git a/internal/xds/translator/compressor.go b/internal/xds/translator/compressor.go new file mode 100644 index 00000000000..b9a500170df --- /dev/null +++ b/internal/xds/translator/compressor.go @@ -0,0 +1,157 @@ +// 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.EnvoyFilterCompressor.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.Traffic == nil || irRoute.Traffic.Compression == nil { + return nil + } + + var ( + perFilterCfg map[string]*anypb.Any + compressorAny *anypb.Any + err error + ) + + perFilterCfg = route.GetTypedPerFilterConfig() + if _, ok := perFilterCfg[egv1a1.EnvoyFilterCompressor.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.EnvoyFilterCompressor.String(), route) + } + + // Overwrite the HCM level filter config with the per route filter config. + compressorProto := compressorPerRouteConfig(irRoute.Traffic.Compression) + + if compressorAny, err = protocov.ToAnyWithValidation(compressorProto); err != nil { + return err + } + + if perFilterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + route.TypedPerFilterConfig[egv1a1.EnvoyFilterCompressor.String()] = compressorAny + + return nil +} + +func compressorPerRouteConfig(_ *ir.Compression) *compressorv3.CompressorPerRoute { + // Enable compression on this route if compression is configured. + return &compressorv3.CompressorPerRoute{ + Override: &compressorv3.CompressorPerRoute_Overrides{ + Overrides: &compressorv3.CompressorOverrides{ + ResponseDirectionConfig: &compressorv3.ResponseDirectionOverrides{}, + }, + }, + } +} diff --git a/internal/xds/translator/testdata/in/xds-ir/compression.yaml b/internal/xds/translator/testdata/in/xds-ir/compression.yaml new file mode 100644 index 00000000000..5d98feb3267 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/compression.yaml @@ -0,0 +1,39 @@ +http: +- address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + compression: + type: GZip diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml new file mode 100644 index 00000000000..c24d059eeaa --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml @@ -0,0 +1,17 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.endpoints.yaml new file mode 100644 index 00000000000..29bb6b4e444 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/compression.endpoints.yaml @@ -0,0 +1,12 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml new file mode 100644 index 00000000000..f0fb7e01890 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml @@ -0,0 +1,42 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - disabled: true + name: envoy.filters.http.compressor + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor + compressorLibrary: + name: envoy.compressor.gzip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: envoy-gateway/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: envoy-gateway/gateway-1/http + name: envoy-gateway/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml new file mode 100644 index 00000000000..b137baf43f8 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml @@ -0,0 +1,33 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-1/http + virtualHosts: + - domains: + - gateway.envoyproxy.io + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http/gateway_envoyproxy_io + routes: + - match: + prefix: / + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.compressor: + '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute + overrides: {} diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index b7398e09b65..26e16d97ca3 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -451,6 +451,7 @@ _Appears in:_ | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | | `useClientProtocol` | _boolean_ | false | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | +| `compression` | _[Compression](#compression) array_ | false | The compression config for the http streams. | | `responseOverride` | _[ResponseOverride](#responseoverride) array_ | false | ResponseOverride defines the configuration to override specific responses with a custom one.
If multiple configurations are specified, the first one to match wins. | @@ -1008,6 +1009,7 @@ _Appears in:_ | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| | `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
| +| `envoy.filters.http.compressor` | EnvoyFilterCompressor defines the Envoy HTTP compressor filter.
| | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index b7398e09b65..26e16d97ca3 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -451,6 +451,7 @@ _Appears in:_ | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | | `useClientProtocol` | _boolean_ | false | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | +| `compression` | _[Compression](#compression) array_ | false | The compression config for the http streams. | | `responseOverride` | _[ResponseOverride](#responseoverride) array_ | false | ResponseOverride defines the configuration to override specific responses with a custom one.
If multiple configurations are specified, the first one to match wins. | @@ -1008,6 +1009,7 @@ _Appears in:_ | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| | `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
| +| `envoy.filters.http.compressor` | EnvoyFilterCompressor defines the Envoy HTTP compressor filter.
| | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| diff --git a/test/e2e/testdata/compression.yaml b/test/e2e/testdata/compression.yaml new file mode 100644 index 00000000000..b5c077768eb --- /dev/null +++ b/test/e2e/testdata/compression.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: compression + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /compression + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: no-compression + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /no-compression + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: compression + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: compression + compression: + - type: Gzip diff --git a/test/e2e/tests/compression.go b/test/e2e/tests/compression.go new file mode 100644 index 00000000000..778e40a18db --- /dev/null +++ b/test/e2e/tests/compression.go @@ -0,0 +1,97 @@ +// 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. + +//go:build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, CompressionTest) +} + +var CompressionTest = suite.ConformanceTest{ + ShortName: "Compression", + Description: "Test response compression on HTTPRoute", + Manifests: []string{"testdata/compression.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("HTTPRoute with compression", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "compression", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "compression", Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/compression", + Headers: map[string]string{ + "Accept-encoding": "gzip", + }, + }, + Response: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "content-encoding": "gzip", + }, + }, + Namespace: ns, + } + roundTripper := &DefaultRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig} + http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + + t.Run("HTTPRoute without compression", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "no-compression", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "compression", Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/no-compression", + Headers: map[string]string{ + "Accept-encoding": "gzip", + }, + }, + Response: http.Response{ + StatusCode: 200, + AbsentHeaders: []string{"content-encoding"}, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +} diff --git a/test/e2e/tests/roundtripper.go b/test/e2e/tests/roundtripper.go new file mode 100644 index 00000000000..9890f5a2eb5 --- /dev/null +++ b/test/e2e/tests/roundtripper.go @@ -0,0 +1,309 @@ +// 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. + +/* +Copyright 2022 The Kubernetes 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. +*/ + +// This file is copied from gateway-api/conformance/utils/roundtripper/roundtripper.go and modified to add the compression support. +// TODO: remove this file when the compression support is added to the roundtripper in the gateway-api repo. + +package tests + +import ( + "compress/gzip" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "testing" + + "golang.org/x/net/http2" + "sigs.k8s.io/gateway-api/conformance/utils/config" + "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" +) + +const ( + H2CPriorKnowledgeProtocol = "H2C_PRIOR_KNOWLEDGE" +) + +// RoundTripper is an interface used to make requests within conformance tests. +// This can be overridden with custom implementations whenever necessary. +type RoundTripper interface { + CaptureRoundTrip(roundtripper.Request) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) +} + +// DefaultRoundTripper is the default implementation of a RoundTripper. It will +// be used if a custom implementation is not specified. +type DefaultRoundTripper struct { + Debug bool + TimeoutConfig config.TimeoutConfig + CustomDialContext func(context.Context, string, string) (net.Conn, error) +} + +func (d *DefaultRoundTripper) httpTransport(request roundtripper.Request) (http.RoundTripper, error) { + transport := &http.Transport{ + DialContext: d.CustomDialContext, + // We disable keep-alives so that we don't leak established TCP connections. + // Leaking TCP connections is bad because we could eventually hit the + // threshold of maximum number of open TCP connections to a specific + // destination. Keep-alives are not presently utilized so disabling this has + // no adverse affect. + // + // Ref. https://github.com/kubernetes-sigs/gateway-api/issues/2357 + DisableKeepAlives: true, + } + if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 { + tlsConfig, err := tlsClientConfig(request.Server, request.CertPem, request.KeyPem) + if err != nil { + return nil, err + } + transport.TLSClientConfig = tlsConfig + } + + return transport, nil +} + +func (d *DefaultRoundTripper) h2cPriorKnowledgeTransport(request roundtripper.Request) (http.RoundTripper, error) { + if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 { + return nil, errors.New("request has configured cert and key but h2 prior knowledge is not encrypted") + } + + transport := &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, addr) + }, + } + + return transport, nil +} + +// CaptureRoundTrip makes a request with the provided parameters and returns the +// captured request and response from echoserver. An error will be returned if +// there is an error running the function but not if an HTTP error status code +// is received. +func (d *DefaultRoundTripper) CaptureRoundTrip(request roundtripper.Request) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { + var transport http.RoundTripper + var err error + + switch request.Protocol { + case H2CPriorKnowledgeProtocol: + transport, err = d.h2cPriorKnowledgeTransport(request) + default: + transport, err = d.httpTransport(request) + } + + if err != nil { + return nil, nil, err + } + + return d.defaultRoundTrip(request, transport) +} + +func (d *DefaultRoundTripper) defaultRoundTrip(request roundtripper.Request, transport http.RoundTripper) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { + client := &http.Client{} + + if request.UnfollowRedirect { + client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + } + } + + client.Transport = transport + + method := "GET" + if request.Method != "" { + method = request.Method + } + ctx, cancel := context.WithTimeout(context.Background(), d.TimeoutConfig.RequestTimeout) + defer cancel() + ctx = withT(ctx, request.T) + req, err := http.NewRequestWithContext(ctx, method, request.URL.String(), nil) + if err != nil { + return nil, nil, err + } + + if request.Host != "" { + req.Host = request.Host + } + + if request.Headers != nil { + for name, value := range request.Headers { + req.Header.Set(name, value[0]) + } + } + + if d.Debug { + var dump []byte + dump, err = httputil.DumpRequestOut(req, true) + if err != nil { + return nil, nil, err + } + + tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump, "< ")) + } + + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + if d.Debug { + var dump []byte + dump, err = httputil.DumpResponse(resp, true) + if err != nil { + return nil, nil, err + } + + tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump, "< ")) + } + + cReq := &roundtripper.CapturedRequest{} + var body []byte + if resp.Header.Get("Content-Encoding") == "gzip" { + reader, err := gzip.NewReader(resp.Body) + if err != nil { + return nil, nil, err + } + defer reader.Close() + if body, err = io.ReadAll(reader); err != nil { + return nil, nil, err + } + } else { + if body, err = io.ReadAll(resp.Body); err != nil { + return nil, nil, err + } + } + + // we cannot assume the response is JSON + if resp.Header.Get("Content-type") == "application/json" { + err = json.Unmarshal(body, cReq) + if err != nil { + return nil, nil, fmt.Errorf("unexpected error reading response: %w", err) + } + } else { + cReq.Method = method // assume it made the right request if the service being called isn't echoing + } + + cRes := &roundtripper.CapturedResponse{ + StatusCode: resp.StatusCode, + ContentLength: resp.ContentLength, + Protocol: resp.Proto, + Headers: resp.Header, + } + + if resp.TLS != nil { + cRes.PeerCertificates = resp.TLS.PeerCertificates + } + + if IsRedirect(resp.StatusCode) { + redirectURL, err := resp.Location() + if err != nil { + return nil, nil, err + } + cRes.RedirectRequest = &roundtripper.RedirectRequest{ + Scheme: redirectURL.Scheme, + Host: redirectURL.Hostname(), + Port: redirectURL.Port(), + Path: redirectURL.Path, + } + } + + return cReq, cRes, nil +} + +func tlsClientConfig(server string, certPem []byte, keyPem []byte) (*tls.Config, error) { + // Create a certificate from the provided cert and key + cert, err := tls.X509KeyPair(certPem, keyPem) + if err != nil { + return nil, fmt.Errorf("unexpected error creating cert: %w", err) + } + + // Add the provided cert as a trusted CA + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certPem) { + return nil, fmt.Errorf("unexpected error adding trusted CA: %w", err) + } + + if server == "" { + return nil, fmt.Errorf("unexpected error, server name required for TLS") + } + + // Create the tls Config for this provided host, cert, and trusted CA + // Disable G402: TLS MinVersion too low. (gosec) + // #nosec G402 + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: server, + RootCAs: certPool, + }, nil +} + +// IsRedirect returns true if a given status code is a redirect code. +func IsRedirect(statusCode int) bool { + switch statusCode { + case http.StatusMultipleChoices, + http.StatusMovedPermanently, + http.StatusFound, + http.StatusSeeOther, + http.StatusNotModified, + http.StatusUseProxy, + http.StatusTemporaryRedirect, + http.StatusPermanentRedirect: + return true + } + return false +} + +// IsTimeoutError returns true if a given status code is a timeout error code. +func IsTimeoutError(statusCode int) bool { + switch statusCode { + case http.StatusRequestTimeout, + http.StatusGatewayTimeout: + return true + } + return false +} + +// testingTContextKey is the key for adding testing.T to the context.Context +type testingTContextKey struct{} + +// withT returns a context with the testing.T added as a value. +func withT(ctx context.Context, t *testing.T) context.Context { + return context.WithValue(ctx, testingTContextKey{}, t) +} + +// TFromContext returns the testing.T added to the context if available. +func TFromContext(ctx context.Context) (*testing.T, bool) { + v := ctx.Value(testingTContextKey{}) + if v != nil { + if t, ok := v.(*testing.T); ok { + return t, true + } + } + return nil, false +} From f7800e397aa524f485ef432cc1e9fea2e59de009 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 04:55:53 +0000 Subject: [PATCH 02/15] fix lint Signed-off-by: Huabing Zhao --- test/e2e/tests/preservecase.go | 8 ++++---- test/e2e/tests/roundtripper.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/e2e/tests/preservecase.go b/test/e2e/tests/preservecase.go index 6c81dfe5092..1754186f351 100644 --- a/test/e2e/tests/preservecase.go +++ b/test/e2e/tests/preservecase.go @@ -32,8 +32,8 @@ func init() { // Copied from the conformance suite because it's needed in casePreservingRoundTrip var startLineRegex = regexp.MustCompile(`(?m)^`) -func formatDump(data []byte, prefix string) string { - data = startLineRegex.ReplaceAllLiteral(data, []byte(prefix)) +func formatDump(data []byte) string { + data = startLineRegex.ReplaceAllLiteral(data, []byte("< ")) return string(data) } @@ -67,7 +67,7 @@ func casePreservingRoundTrip(request roundtripper.Request, transport nethttp.Rou return nil, err } - fmt.Printf("Sending Request:\n%s\n\n", formatDump(dump, "< ")) + fmt.Printf("Sending Request:\n%s\n\n", formatDump(dump)) } resp, err := client.Do(req) @@ -83,7 +83,7 @@ func casePreservingRoundTrip(request roundtripper.Request, transport nethttp.Rou return nil, err } - fmt.Printf("Received Response:\n%s\n\n", formatDump(dump, "< ")) + fmt.Printf("Received Response:\n%s\n\n", formatDump(dump)) } cReq := map[string]any{} diff --git a/test/e2e/tests/roundtripper.go b/test/e2e/tests/roundtripper.go index 9890f5a2eb5..085886f3d0d 100644 --- a/test/e2e/tests/roundtripper.go +++ b/test/e2e/tests/roundtripper.go @@ -65,10 +65,10 @@ type DefaultRoundTripper struct { func (d *DefaultRoundTripper) httpTransport(request roundtripper.Request) (http.RoundTripper, error) { transport := &http.Transport{ DialContext: d.CustomDialContext, - // We disable keep-alives so that we don't leak established TCP connections. + // We disable keepalives so that we don't leak established TCP connections. // Leaking TCP connections is bad because we could eventually hit the // threshold of maximum number of open TCP connections to a specific - // destination. Keep-alives are not presently utilized so disabling this has + // destination. Keepalives are not presently utilized so disabling this has // no adverse affect. // // Ref. https://github.com/kubernetes-sigs/gateway-api/issues/2357 @@ -163,7 +163,7 @@ func (d *DefaultRoundTripper) defaultRoundTrip(request roundtripper.Request, tra return nil, nil, err } - tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump, "< ")) + tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump)) } resp, err := client.Do(req) @@ -179,7 +179,7 @@ func (d *DefaultRoundTripper) defaultRoundTrip(request roundtripper.Request, tra return nil, nil, err } - tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump, "< ")) + tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump)) } cReq := &roundtripper.CapturedRequest{} From 02c8d6d60d45bf57aa2d6d9c693a584441259343 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 05:03:20 +0000 Subject: [PATCH 03/15] fix gen Signed-off-by: Huabing Zhao --- internal/gatewayapi/backendtrafficpolicy.go | 4 +--- .../testdata/backendtrafficpolicy-compression.out.yaml | 3 +-- internal/ir/xds.go | 1 - .../translator/testdata/out/xds-ir/compression.routes.yaml | 3 ++- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 560cb8bb7f5..f7b64d21411 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -943,7 +943,5 @@ func buildCompression(compression []*egv1a1.Compression) *ir.Compression { } // Only Gzip is supported for now, so we don't need to do anything special here - return &ir.Compression{ - Type: "GZip", - } + return &ir.Compression{} } diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml index caeff8722a7..7d2a444cb37 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml @@ -165,5 +165,4 @@ xdsIR: name: "" prefix: / traffic: - compression: - type: GZip + compression: {} diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 08990eb5425..72e22571f6e 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -756,7 +756,6 @@ type HeaderBasedSessionPersistence struct { // Currently, only the default compressor(gzip) is supported. // +k8s:deepcopy-gen=true type Compression struct { - Type string `json:"type" yaml:"type"` } // TrafficFeatures holds the information associated with the Backend Traffic Policy. diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml index b137baf43f8..f9f92a1624c 100644 --- a/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml @@ -30,4 +30,5 @@ typedPerFilterConfig: envoy.filters.http.compressor: '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute - overrides: {} + overrides: + responseDirectionConfig: {} From 672898bd0f85aa199ff3f352d1b356a38482539f Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 05:07:07 +0000 Subject: [PATCH 04/15] fix gen Signed-off-by: Huabing Zhao --- internal/ir/xds.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 72e22571f6e..de6242549da 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -755,8 +755,7 @@ type HeaderBasedSessionPersistence struct { // Compression holds the configuration for HTTP compression. // Currently, only the default compressor(gzip) is supported. // +k8s:deepcopy-gen=true -type Compression struct { -} +type Compression struct{} // TrafficFeatures holds the information associated with the Backend Traffic Policy. // +k8s:deepcopy-gen=true From 7330fe2ff4bdcaaf6762d2aedaf9198b5ecadad9 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 05:10:39 +0000 Subject: [PATCH 05/15] fix gen Signed-off-by: Huabing Zhao --- internal/xds/translator/testdata/in/xds-ir/compression.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/xds/translator/testdata/in/xds-ir/compression.yaml b/internal/xds/translator/testdata/in/xds-ir/compression.yaml index 5d98feb3267..4d2c31af10c 100644 --- a/internal/xds/translator/testdata/in/xds-ir/compression.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/compression.yaml @@ -35,5 +35,4 @@ http: name: "" prefix: / traffic: - compression: - type: GZip + compression: {} From fee1a18882e19e427317e38f402b2526a82c7162 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 05:55:21 +0000 Subject: [PATCH 06/15] fix lint Signed-off-by: Huabing Zhao --- test/e2e/tests/roundtripper.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/tests/roundtripper.go b/test/e2e/tests/roundtripper.go index 085886f3d0d..4495be563fb 100644 --- a/test/e2e/tests/roundtripper.go +++ b/test/e2e/tests/roundtripper.go @@ -140,7 +140,7 @@ func (d *DefaultRoundTripper) defaultRoundTrip(request roundtripper.Request, tra } ctx, cancel := context.WithTimeout(context.Background(), d.TimeoutConfig.RequestTimeout) defer cancel() - ctx = withT(ctx, request.T) + ctx = withT(ctx, request.T) // codespell:ignore req, err := http.NewRequestWithContext(ctx, method, request.URL.String(), nil) if err != nil { return nil, nil, err @@ -292,8 +292,8 @@ func IsTimeoutError(statusCode int) bool { // testingTContextKey is the key for adding testing.T to the context.Context type testingTContextKey struct{} -// withT returns a context with the testing.T added as a value. -func withT(ctx context.Context, t *testing.T) context.Context { +// withT returns a context with the testing.T added as a value. // codespell:ignore +func withT(ctx context.Context, t *testing.T) context.Context { // codespell:ignore return context.WithValue(ctx, testingTContextKey{}, t) } From 6c80e8896786650c8712c7eea010b35d5eeb3b12 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 05:57:13 +0000 Subject: [PATCH 07/15] add release note Signed-off-by: Huabing Zhao --- release-notes/current.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 5bd2fe46230..91b6ef8545d 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -19,6 +19,7 @@ new features: | Added support for patching EnvoyProxy.spec.provider.kubernetes.envoyHpa and EnvoyProxy.spec.provider.kubernetes.envoyPDB Added support for defining rateLimitHpa in EnvoyGateway API Added support for preserving the user defined HTTPRoute match order in EnvoyProxy API + Added support for response compression in the BackendTrafficPolicy API bug fixes: | Fixed a nil pointer error that occurs when a SecurityPolicy refers to a UDS backend From 268df6ea718a4df31bb5141e01f6840519553be0 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 6 Jan 2025 06:59:30 +0000 Subject: [PATCH 08/15] fix test Signed-off-by: Huabing Zhao --- test/e2e/tests/compression.go | 117 ++++++++++++- test/e2e/tests/roundtripper.go | 309 --------------------------------- 2 files changed, 116 insertions(+), 310 deletions(-) delete mode 100644 test/e2e/tests/roundtripper.go diff --git a/test/e2e/tests/compression.go b/test/e2e/tests/compression.go index 778e40a18db..784a8c7a8db 100644 --- a/test/e2e/tests/compression.go +++ b/test/e2e/tests/compression.go @@ -8,14 +8,25 @@ package tests import ( + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "net" + nethttp "net/http" + "net/http/httputil" "testing" "k8s.io/apimachinery/pkg/types" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/config" "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/gatewayapi/resource" @@ -59,7 +70,7 @@ var CompressionTest = suite.ConformanceTest{ }, Namespace: ns, } - roundTripper := &DefaultRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig} + roundTripper := &CompressionRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig} http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) @@ -95,3 +106,107 @@ var CompressionTest = suite.ConformanceTest{ }) }, } + +// CompressionRoundTripper implements roundtripper.RoundTripper and adds support for gzip encoding. +type CompressionRoundTripper struct { + Debug bool + TimeoutConfig config.TimeoutConfig + CustomDialContext func(context.Context, string, string) (net.Conn, error) +} + +func (d *CompressionRoundTripper) CaptureRoundTrip(request roundtripper.Request) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { + return d.defaultRoundTrip(request, &nethttp.Transport{}) +} + +func (d *CompressionRoundTripper) defaultRoundTrip(request roundtripper.Request, transport nethttp.RoundTripper) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { + client := &nethttp.Client{} + + client.Transport = transport + + method := "GET" + if request.Method != "" { + method = request.Method + } + ctx, cancel := context.WithTimeout(context.Background(), d.TimeoutConfig.RequestTimeout) + defer cancel() + req, err := nethttp.NewRequestWithContext(ctx, method, request.URL.String(), nil) + if err != nil { + return nil, nil, err + } + + if request.Host != "" { + req.Host = request.Host + } + + if request.Headers != nil { + for name, value := range request.Headers { + req.Header.Set(name, value[0]) + } + } + + if d.Debug { + var dump []byte + dump, err = httputil.DumpRequestOut(req, true) + if err != nil { + return nil, nil, err + } + + tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + if d.Debug { + var dump []byte + dump, err = httputil.DumpResponse(resp, true) + if err != nil { + return nil, nil, err + } + + tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump)) + } + + cReq := &roundtripper.CapturedRequest{} + var body []byte + if resp.Header.Get("Content-Encoding") == "gzip" { + reader, err := gzip.NewReader(resp.Body) + if err != nil { + return nil, nil, err + } + defer reader.Close() + if body, err = io.ReadAll(reader); err != nil { + return nil, nil, err + } + } else { + if body, err = io.ReadAll(resp.Body); err != nil { + return nil, nil, err + } + } + + // we cannot assume the response is JSON + if resp.Header.Get("Content-type") == "application/json" { + err = json.Unmarshal(body, cReq) + if err != nil { + return nil, nil, fmt.Errorf("unexpected error reading response: %w", err) + } + } else { + cReq.Method = method // assume it made the right request if the service being called isn't echoing + } + + cRes := &roundtripper.CapturedResponse{ + StatusCode: resp.StatusCode, + ContentLength: resp.ContentLength, + Protocol: resp.Proto, + Headers: resp.Header, + } + + if resp.TLS != nil { + cRes.PeerCertificates = resp.TLS.PeerCertificates + } + + return cReq, cRes, nil +} diff --git a/test/e2e/tests/roundtripper.go b/test/e2e/tests/roundtripper.go deleted file mode 100644 index 4495be563fb..00000000000 --- a/test/e2e/tests/roundtripper.go +++ /dev/null @@ -1,309 +0,0 @@ -// 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. - -/* -Copyright 2022 The Kubernetes 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. -*/ - -// This file is copied from gateway-api/conformance/utils/roundtripper/roundtripper.go and modified to add the compression support. -// TODO: remove this file when the compression support is added to the roundtripper in the gateway-api repo. - -package tests - -import ( - "compress/gzip" - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/http/httputil" - "testing" - - "golang.org/x/net/http2" - "sigs.k8s.io/gateway-api/conformance/utils/config" - "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" - "sigs.k8s.io/gateway-api/conformance/utils/tlog" -) - -const ( - H2CPriorKnowledgeProtocol = "H2C_PRIOR_KNOWLEDGE" -) - -// RoundTripper is an interface used to make requests within conformance tests. -// This can be overridden with custom implementations whenever necessary. -type RoundTripper interface { - CaptureRoundTrip(roundtripper.Request) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) -} - -// DefaultRoundTripper is the default implementation of a RoundTripper. It will -// be used if a custom implementation is not specified. -type DefaultRoundTripper struct { - Debug bool - TimeoutConfig config.TimeoutConfig - CustomDialContext func(context.Context, string, string) (net.Conn, error) -} - -func (d *DefaultRoundTripper) httpTransport(request roundtripper.Request) (http.RoundTripper, error) { - transport := &http.Transport{ - DialContext: d.CustomDialContext, - // We disable keepalives so that we don't leak established TCP connections. - // Leaking TCP connections is bad because we could eventually hit the - // threshold of maximum number of open TCP connections to a specific - // destination. Keepalives are not presently utilized so disabling this has - // no adverse affect. - // - // Ref. https://github.com/kubernetes-sigs/gateway-api/issues/2357 - DisableKeepAlives: true, - } - if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 { - tlsConfig, err := tlsClientConfig(request.Server, request.CertPem, request.KeyPem) - if err != nil { - return nil, err - } - transport.TLSClientConfig = tlsConfig - } - - return transport, nil -} - -func (d *DefaultRoundTripper) h2cPriorKnowledgeTransport(request roundtripper.Request) (http.RoundTripper, error) { - if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 { - return nil, errors.New("request has configured cert and key but h2 prior knowledge is not encrypted") - } - - transport := &http2.Transport{ - AllowHTTP: true, - DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, network, addr) - }, - } - - return transport, nil -} - -// CaptureRoundTrip makes a request with the provided parameters and returns the -// captured request and response from echoserver. An error will be returned if -// there is an error running the function but not if an HTTP error status code -// is received. -func (d *DefaultRoundTripper) CaptureRoundTrip(request roundtripper.Request) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { - var transport http.RoundTripper - var err error - - switch request.Protocol { - case H2CPriorKnowledgeProtocol: - transport, err = d.h2cPriorKnowledgeTransport(request) - default: - transport, err = d.httpTransport(request) - } - - if err != nil { - return nil, nil, err - } - - return d.defaultRoundTrip(request, transport) -} - -func (d *DefaultRoundTripper) defaultRoundTrip(request roundtripper.Request, transport http.RoundTripper) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { - client := &http.Client{} - - if request.UnfollowRedirect { - client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { - return http.ErrUseLastResponse - } - } - - client.Transport = transport - - method := "GET" - if request.Method != "" { - method = request.Method - } - ctx, cancel := context.WithTimeout(context.Background(), d.TimeoutConfig.RequestTimeout) - defer cancel() - ctx = withT(ctx, request.T) // codespell:ignore - req, err := http.NewRequestWithContext(ctx, method, request.URL.String(), nil) - if err != nil { - return nil, nil, err - } - - if request.Host != "" { - req.Host = request.Host - } - - if request.Headers != nil { - for name, value := range request.Headers { - req.Header.Set(name, value[0]) - } - } - - if d.Debug { - var dump []byte - dump, err = httputil.DumpRequestOut(req, true) - if err != nil { - return nil, nil, err - } - - tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump)) - } - - resp, err := client.Do(req) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - - if d.Debug { - var dump []byte - dump, err = httputil.DumpResponse(resp, true) - if err != nil { - return nil, nil, err - } - - tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump)) - } - - cReq := &roundtripper.CapturedRequest{} - var body []byte - if resp.Header.Get("Content-Encoding") == "gzip" { - reader, err := gzip.NewReader(resp.Body) - if err != nil { - return nil, nil, err - } - defer reader.Close() - if body, err = io.ReadAll(reader); err != nil { - return nil, nil, err - } - } else { - if body, err = io.ReadAll(resp.Body); err != nil { - return nil, nil, err - } - } - - // we cannot assume the response is JSON - if resp.Header.Get("Content-type") == "application/json" { - err = json.Unmarshal(body, cReq) - if err != nil { - return nil, nil, fmt.Errorf("unexpected error reading response: %w", err) - } - } else { - cReq.Method = method // assume it made the right request if the service being called isn't echoing - } - - cRes := &roundtripper.CapturedResponse{ - StatusCode: resp.StatusCode, - ContentLength: resp.ContentLength, - Protocol: resp.Proto, - Headers: resp.Header, - } - - if resp.TLS != nil { - cRes.PeerCertificates = resp.TLS.PeerCertificates - } - - if IsRedirect(resp.StatusCode) { - redirectURL, err := resp.Location() - if err != nil { - return nil, nil, err - } - cRes.RedirectRequest = &roundtripper.RedirectRequest{ - Scheme: redirectURL.Scheme, - Host: redirectURL.Hostname(), - Port: redirectURL.Port(), - Path: redirectURL.Path, - } - } - - return cReq, cRes, nil -} - -func tlsClientConfig(server string, certPem []byte, keyPem []byte) (*tls.Config, error) { - // Create a certificate from the provided cert and key - cert, err := tls.X509KeyPair(certPem, keyPem) - if err != nil { - return nil, fmt.Errorf("unexpected error creating cert: %w", err) - } - - // Add the provided cert as a trusted CA - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(certPem) { - return nil, fmt.Errorf("unexpected error adding trusted CA: %w", err) - } - - if server == "" { - return nil, fmt.Errorf("unexpected error, server name required for TLS") - } - - // Create the tls Config for this provided host, cert, and trusted CA - // Disable G402: TLS MinVersion too low. (gosec) - // #nosec G402 - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - ServerName: server, - RootCAs: certPool, - }, nil -} - -// IsRedirect returns true if a given status code is a redirect code. -func IsRedirect(statusCode int) bool { - switch statusCode { - case http.StatusMultipleChoices, - http.StatusMovedPermanently, - http.StatusFound, - http.StatusSeeOther, - http.StatusNotModified, - http.StatusUseProxy, - http.StatusTemporaryRedirect, - http.StatusPermanentRedirect: - return true - } - return false -} - -// IsTimeoutError returns true if a given status code is a timeout error code. -func IsTimeoutError(statusCode int) bool { - switch statusCode { - case http.StatusRequestTimeout, - http.StatusGatewayTimeout: - return true - } - return false -} - -// testingTContextKey is the key for adding testing.T to the context.Context -type testingTContextKey struct{} - -// withT returns a context with the testing.T added as a value. // codespell:ignore -func withT(ctx context.Context, t *testing.T) context.Context { // codespell:ignore - return context.WithValue(ctx, testingTContextKey{}, t) -} - -// TFromContext returns the testing.T added to the context if available. -func TFromContext(ctx context.Context) (*testing.T, bool) { - v := ctx.Value(testingTContextKey{}) - if v != nil { - if t, ok := v.(*testing.T); ok { - return t, true - } - } - return nil, false -} From 326d5a5957a24e60cb67b71be2d6b2bce1a66891 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Wed, 8 Jan 2025 07:32:55 +0000 Subject: [PATCH 09/15] support brotli Signed-off-by: Huabing Zhao --- api/v1alpha1/compression_types.go | 20 ++- api/v1alpha1/zz_generated.deepcopy.go | 20 +++ ....envoyproxy.io_backendtrafficpolicies.yaml | 10 ++ .../gateway.envoyproxy.io_envoyproxies.yaml | 12 ++ go.mod | 1 + go.sum | 2 + internal/gatewayapi/backendtrafficpolicy.go | 16 ++- internal/gatewayapi/clustersettings.go | 3 +- .../backendtrafficpolicy-compression.in.yaml | 3 + .../backendtrafficpolicy-compression.out.yaml | 9 +- internal/ir/xds.go | 8 +- internal/ir/zz_generated.deepcopy.go | 10 +- internal/xds/translator/compressor.go | 129 +++++++++++++----- .../testdata/in/xds-ir/compression.yaml | 5 +- .../out/xds-ir/compression.listeners.yaml | 12 +- .../out/xds-ir/compression.routes.yaml | 6 +- site/content/en/latest/api/extension_types.md | 18 +++ site/content/zh/latest/api/extension_types.md | 18 +++ .../backendtrafficpolicy_test.go | 56 ++++++++ test/e2e/testdata/compression.yaml | 3 + test/e2e/tests/compression.go | 101 ++++++++------ tools/make/kube.mk | 3 +- 22 files changed, 371 insertions(+), 94 deletions(-) diff --git a/api/v1alpha1/compression_types.go b/api/v1alpha1/compression_types.go index 73cb0109ae5..cb4f90f7f3e 100644 --- a/api/v1alpha1/compression_types.go +++ b/api/v1alpha1/compression_types.go @@ -7,22 +7,40 @@ package v1alpha1 // CompressorType defines the types of compressor library supported by Envoy Gateway. // -// +kubebuilder:validation:Enum=Gzip +// +kubebuilder:validation:Enum=Gzip;Brotli type CompressorType string +const ( + GzipCompressorType CompressorType = "Gzip" + + BrotliCompressorType CompressorType = "Brotli" +) + // GzipCompressor defines the config for the Gzip compressor. // The default values can be found here: // https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/gzip/compressor/v3/gzip.proto#extension-envoy-compression-gzip-compressor type GzipCompressor struct{} +// BrotliCompressor defines the config for the Brotli compressor. +// The default values can be found here: +// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/brotli/compressor/v3/brotli.proto#extension-envoy-compression-brotli-compressor +type BrotliCompressor struct{} + // Compression defines the config of enabling compression. // This can help reduce the bandwidth at the expense of higher CPU. +// +kubebuilder:validation:XValidation:rule="self.type == 'Brotli' ? has(self.brotli) : !has(self.brotli)",message="If compression type is Brotli, brotli field needs to be set." +// +kubebuilder:validation:XValidation:rule="self.type == 'Gzip' ? has(self.gzip) : !has(self.gzip)",message="If compression type is Gzip, gzip field needs to be set." type Compression struct { // CompressorType defines the compressor type to use for compression. // // +required Type CompressorType `json:"type"` + // The configuration for Brotli compressor. + // + // +optional + Brotli *BrotliCompressor `json:"brotli,omitempty"` + // The configuration for GZIP compressor. // // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a01ae7856b7..8487bb54154 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -610,6 +610,21 @@ func (in *BodyToExtAuth) DeepCopy() *BodyToExtAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BrotliCompressor) DeepCopyInto(out *BrotliCompressor) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BrotliCompressor. +func (in *BrotliCompressor) DeepCopy() *BrotliCompressor { + if in == nil { + return nil + } + out := new(BrotliCompressor) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CORS) DeepCopyInto(out *CORS) { *out = *in @@ -1041,6 +1056,11 @@ func (in *ClusterSettings) DeepCopy() *ClusterSettings { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Compression) DeepCopyInto(out *Compression) { *out = *in + if in.Brotli != nil { + in, out := &in.Brotli, &out.Brotli + *out = new(BrotliCompressor) + **out = **in + } if in.Gzip != nil { in, out := &in.Gzip, &out.Gzip *out = new(GzipCompressor) diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index f9fb0f329dd..099c08feb80 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -106,6 +106,9 @@ spec: Compression defines the config of enabling compression. This can help reduce the bandwidth at the expense of higher CPU. properties: + brotli: + description: The configuration for Brotli compressor. + type: object gzip: description: The configuration for GZIP compressor. type: object @@ -114,10 +117,17 @@ spec: for compression. enum: - Gzip + - Brotli type: string required: - type type: object + x-kubernetes-validations: + - message: If compression type is Brotli, brotli field needs to + be set. + rule: 'self.type == ''Brotli'' ? has(self.brotli) : !has(self.brotli)' + - message: If compression type is Gzip, gzip field needs to be set. + rule: 'self.type == ''Gzip'' ? has(self.gzip) : !has(self.gzip)' type: array connection: description: Connection includes backend connection settings. diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index aee0cc5d467..cb23c9dc286 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -12497,6 +12497,9 @@ spec: scarce and large payloads can be effectively compressed at the expense of higher CPU load. properties: + brotli: + description: The configuration for Brotli compressor. + type: object gzip: description: The configuration for GZIP compressor. type: object @@ -12505,10 +12508,19 @@ spec: type to use for compression. enum: - Gzip + - Brotli type: string required: - type type: object + x-kubernetes-validations: + - message: If compression type is Brotli, brotli field + needs to be set. + rule: 'self.type == ''Brotli'' ? has(self.brotli) : + !has(self.brotli)' + - message: If compression type is Gzip, gzip field needs + to be set. + rule: 'self.type == ''Gzip'' ? has(self.gzip) : !has(self.gzip)' disable: description: Disable the Prometheus endpoint. type: boolean diff --git a/go.mod b/go.mod index 5b42488b56a..5131aebb5a6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( fortio.org/fortio v1.68.0 fortio.org/log v1.17.1 github.com/Masterminds/semver/v3 v3.3.1 + github.com/andybalholm/brotli v1.0.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc diff --git a/go.sum b/go.sum index 8d59f08a794..8ff04089b9a 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index f7b64d21411..fefd66aa8bb 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -305,7 +305,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute( ds *ir.DNS h2 *ir.HTTP2Settings ro *ir.ResponseOverride - cp *ir.Compression + cp []*ir.Compression err, errs error ) @@ -456,7 +456,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway( ds *ir.DNS h2 *ir.HTTP2Settings ro *ir.ResponseOverride - cp *ir.Compression + cp []*ir.Compression err, errs error ) @@ -937,11 +937,13 @@ func defaultResponseOverrideRuleName(policy *egv1a1.BackendTrafficPolicy, index strconv.Itoa(index)) } -func buildCompression(compression []*egv1a1.Compression) *ir.Compression { - if len(compression) == 0 { - return nil +func buildCompression(compression []*egv1a1.Compression) []*ir.Compression { + irCompression := make([]*ir.Compression, 0, len(compression)) + for _, c := range compression { + irCompression = append(irCompression, &ir.Compression{ + Type: c.Type, + }) } - // Only Gzip is supported for now, so we don't need to do anything special here - return &ir.Compression{} + return irCompression } diff --git a/internal/gatewayapi/clustersettings.go b/internal/gatewayapi/clustersettings.go index 40266553b46..5706504f42b 100644 --- a/internal/gatewayapi/clustersettings.go +++ b/internal/gatewayapi/clustersettings.go @@ -11,6 +11,7 @@ import ( "math" "math/big" "net/http" + "reflect" "strings" "time" @@ -76,7 +77,7 @@ func translateTrafficFeatures(policy *egv1a1.ClusterSettings) (*ir.TrafficFeatur // If nothing was set in any of the above calls, return nil instead of an empty // container var empty ir.TrafficFeatures - if empty == *ret { + if reflect.DeepEqual(empty, *ret) { ret = nil } diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml index 5dd8e1adf21..624bf7ed1f8 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml @@ -45,4 +45,7 @@ backendTrafficPolicies: kind: HTTPRoute name: httproute-1 compression: + - type: Brotli + brotli: {} - type: Gzip + gzip: {} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml index 7d2a444cb37..c6b26110e7d 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml @@ -7,7 +7,10 @@ backendTrafficPolicies: namespace: default spec: compression: - - type: Gzip + - brotli: {} + type: Brotli + - gzip: {} + type: Gzip targetRef: group: gateway.networking.k8s.io kind: HTTPRoute @@ -165,4 +168,6 @@ xdsIR: name: "" prefix: / traffic: - compression: {} + compression: + - type: Brotli + - type: Gzip diff --git a/internal/ir/xds.go b/internal/ir/xds.go index de6242549da..784f77a32d7 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -753,9 +753,11 @@ type HeaderBasedSessionPersistence struct { } // Compression holds the configuration for HTTP compression. -// Currently, only the default compressor(gzip) is supported. // +k8s:deepcopy-gen=true -type Compression struct{} +type Compression struct { + // Type of compression to be used. + Type egv1a1.CompressorType `json:"type,omitempty" yaml:"type,omitempty"` +} // TrafficFeatures holds the information associated with the Backend Traffic Policy. // +k8s:deepcopy-gen=true @@ -789,7 +791,7 @@ type TrafficFeatures struct { // ResponseOverride defines the schema for overriding the response. ResponseOverride *ResponseOverride `json:"responseOverride,omitempty" yaml:"responseOverride,omitempty"` // Compression settings for HTTP Response - Compression *Compression `json:"compression,omitempty" yaml:"compression,omitempty"` + Compression []*Compression `json:"compression,omitempty" yaml:"compression,omitempty"` } func (b *TrafficFeatures) Validate() error { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 7a5abdca402..b89e212dba9 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -3357,8 +3357,14 @@ func (in *TrafficFeatures) DeepCopyInto(out *TrafficFeatures) { } if in.Compression != nil { in, out := &in.Compression, &out.Compression - *out = new(Compression) - **out = **in + *out = make([]*Compression, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Compression) + **out = **in + } + } } } diff --git a/internal/xds/translator/compressor.go b/internal/xds/translator/compressor.go index b9a500170df..a5af6363f17 100644 --- a/internal/xds/translator/compressor.go +++ b/internal/xds/translator/compressor.go @@ -8,12 +8,15 @@ package translator import ( "errors" "fmt" + "strings" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + brotliv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/compressor/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/proto" "google.golang.org/protobuf/types/known/anypb" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" @@ -39,50 +42,85 @@ func (*compressor) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTT 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 + brotli bool + gzip bool + filter *hcmv3.HttpFilter + err error ) for _, route := range irListener.Routes { if route.Traffic != nil && route.Traffic.Compression != nil { - irCompression = route.Traffic.Compression + for _, irComp := range route.Traffic.Compression { + if irComp.Type == egv1a1.BrotliCompressorType { + brotli = true + } + if irComp.Type == egv1a1.GzipCompressorType { + gzip = true + } + } } } - if irCompression == nil { - return nil + + // Add the compressor filters for all the compression types required by the routes. + // All the compressor filters are disabled at the HCM level. + // The per route filter config will enable the compressor filters for the routes that require them. + if brotli { + brotliFilterName := compressorFilterName(egv1a1.BrotliCompressorType) + if !hcmContainsFilter(mgr, brotliFilterName) { + if filter, err = buildCompressorFilter(egv1a1.BrotliCompressorType); err != nil { + return err + } + mgr.HttpFilters = append(mgr.HttpFilters, filter) + } } - // The HCM-level filter config doesn't matter since it is overridden at the route level. - if filter, err = buildHCMCompressorFilter(); err != nil { - return err + if gzip { + gzipFilterName := compressorFilterName(egv1a1.GzipCompressorType) + if !hcmContainsFilter(mgr, gzipFilterName) { + if filter, err = buildCompressorFilter(egv1a1.GzipCompressorType); err != nil { + return err + } + mgr.HttpFilters = append(mgr.HttpFilters, filter) + } } - mgr.HttpFilters = append(mgr.HttpFilters, filter) + return err } -// buildHCMCompressorFilter returns a Compressor HTTP filter from the provided IR HTTPRoute. -func buildHCMCompressorFilter() (*hcmv3.HttpFilter, error) { +func compressorFilterName(compressorType egv1a1.CompressorType) string { + return fmt.Sprintf("%s.%s", egv1a1.EnvoyFilterCompressor.String(), strings.ToLower(string(compressorType))) +} + +// buildCompressorFilter builds a compressor filter with the provided compressionType. +func buildCompressorFilter(compressionType egv1a1.CompressorType) (*hcmv3.HttpFilter, error) { var ( compressorProto *compressorv3.Compressor - gzipAny *anypb.Any + extensionName string + extensionMsg proto.Message + extensionAny *anypb.Any compressorAny *anypb.Any err error ) - if gzipAny, err = protocov.ToAnyWithValidation(&gzipv3.Gzip{}); err != nil { + switch compressionType { + case egv1a1.BrotliCompressorType: + extensionName = "envoy.compression.brotli.compressor" + extensionMsg = &brotliv3.Brotli{} + case egv1a1.GzipCompressorType: + extensionName = "envoy.compression.gzip.compressor" + extensionMsg = &gzipv3.Gzip{} + } + + if extensionAny, err = protocov.ToAnyWithValidation(extensionMsg); err != nil { return nil, err } compressorProto = &compressorv3.Compressor{ CompressorLibrary: &corev3.TypedExtensionConfig{ - Name: "envoy.compressor.gzip", - TypedConfig: gzipAny, + Name: extensionName, + TypedConfig: extensionAny, }, } @@ -91,7 +129,7 @@ func buildHCMCompressorFilter() (*hcmv3.HttpFilter, error) { } return &hcmv3.HttpFilter{ - Name: egv1a1.EnvoyFilterCompressor.String(), + Name: compressorFilterName(compressionType), ConfigType: &hcmv3.HttpFilter_TypedConfig{ TypedConfig: compressorAny, }, @@ -112,40 +150,67 @@ func (*compressor) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error if irRoute == nil { return errors.New("ir route is nil") } - if irRoute.Traffic == nil || irRoute.Traffic.Compression == nil { + if irRoute.Traffic == nil || len(irRoute.Traffic.Compression) == 0 { return nil } var ( + brotli bool + gzip bool perFilterCfg map[string]*anypb.Any compressorAny *anypb.Any err error ) - perFilterCfg = route.GetTypedPerFilterConfig() - if _, ok := perFilterCfg[egv1a1.EnvoyFilterCompressor.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.EnvoyFilterCompressor.String(), route) + for _, irComp := range irRoute.Traffic.Compression { + if irComp.Type == egv1a1.BrotliCompressorType { + brotli = true + } + if irComp.Type == egv1a1.GzipCompressorType { + gzip = true + } + } + + if !brotli && !gzip { + return nil } // Overwrite the HCM level filter config with the per route filter config. - compressorProto := compressorPerRouteConfig(irRoute.Traffic.Compression) + perFilterCfg = route.GetTypedPerFilterConfig() + if perFilterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + compressorProto := compressorPerRouteConfig() if compressorAny, err = protocov.ToAnyWithValidation(compressorProto); err != nil { return err } - if perFilterCfg == nil { - route.TypedPerFilterConfig = make(map[string]*anypb.Any) + if brotli { + brotliFilterName := compressorFilterName(egv1a1.BrotliCompressorType) + if _, ok := perFilterCfg[brotliFilterName]; 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", + brotliFilterName, route) + } + route.TypedPerFilterConfig[brotliFilterName] = compressorAny + } + if gzip { + gzipFilterName := compressorFilterName(egv1a1.GzipCompressorType) + if _, ok := perFilterCfg[gzipFilterName]; 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", + gzipFilterName, route) + } + route.TypedPerFilterConfig[gzipFilterName] = compressorAny } - route.TypedPerFilterConfig[egv1a1.EnvoyFilterCompressor.String()] = compressorAny return nil } -func compressorPerRouteConfig(_ *ir.Compression) *compressorv3.CompressorPerRoute { +func compressorPerRouteConfig() *compressorv3.CompressorPerRoute { // Enable compression on this route if compression is configured. return &compressorv3.CompressorPerRoute{ Override: &compressorv3.CompressorPerRoute_Overrides{ diff --git a/internal/xds/translator/testdata/in/xds-ir/compression.yaml b/internal/xds/translator/testdata/in/xds-ir/compression.yaml index 4d2c31af10c..f4e6c338376 100644 --- a/internal/xds/translator/testdata/in/xds-ir/compression.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/compression.yaml @@ -35,4 +35,7 @@ http: name: "" prefix: / traffic: - compression: {} + compression: + - type: Brotli + - type: Gzip + - type: Brotli diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml index f0fb7e01890..c042cdcfa62 100644 --- a/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml @@ -15,11 +15,19 @@ maxConcurrentStreams: 100 httpFilters: - disabled: true - name: envoy.filters.http.compressor + name: envoy.filters.http.compressor.brotli typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor compressorLibrary: - name: envoy.compressor.gzip + name: envoy.compression.brotli.compressor + typedConfig: + '@type': type.googleapis.com/envoy.extensions.compression.brotli.compressor.v3.Brotli + - disabled: true + name: envoy.filters.http.compressor.gzip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor + compressorLibrary: + name: envoy.compression.gzip.compressor typedConfig: '@type': type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip - name: envoy.filters.http.router diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml index f9f92a1624c..666c80b48f8 100644 --- a/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml @@ -28,7 +28,11 @@ upgradeConfigs: - upgradeType: websocket typedPerFilterConfig: - envoy.filters.http.compressor: + envoy.filters.http.compressor.brotli: + '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute + overrides: + responseDirectionConfig: {} + envoy.filters.http.compressor.gzip: '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute overrides: responseDirectionConfig: {} diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 26e16d97ca3..1c232894312 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -499,6 +499,19 @@ _Appears in:_ | `JSONPatch` | JSONPatch applies the provided JSONPatches to the default bootstrap.
| +#### BrotliCompressor + + + +BrotliCompressor defines the config for the Brotli compressor. +The default values can be found here: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/brotli/compressor/v3/brotli.proto#extension-envoy-compression-brotli-compressor + +_Appears in:_ +- [Compression](#compression) + + + #### CIDR _Underlying type:_ _string_ @@ -741,6 +754,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | _[CompressorType](#compressortype)_ | true | CompressorType defines the compressor type to use for compression. | +| `brotli` | _[BrotliCompressor](#brotlicompressor)_ | false | The configuration for Brotli compressor. | | `gzip` | _[GzipCompressor](#gzipcompressor)_ | false | The configuration for GZIP compressor. | @@ -753,6 +767,10 @@ CompressorType defines the types of compressor library supported by Envoy Gatewa _Appears in:_ - [Compression](#compression) +| Value | Description | +| ----- | ----------- | +| `Gzip` | | +| `Brotli` | | #### ConnectionLimit diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 26e16d97ca3..1c232894312 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -499,6 +499,19 @@ _Appears in:_ | `JSONPatch` | JSONPatch applies the provided JSONPatches to the default bootstrap.
| +#### BrotliCompressor + + + +BrotliCompressor defines the config for the Brotli compressor. +The default values can be found here: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/brotli/compressor/v3/brotli.proto#extension-envoy-compression-brotli-compressor + +_Appears in:_ +- [Compression](#compression) + + + #### CIDR _Underlying type:_ _string_ @@ -741,6 +754,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | _[CompressorType](#compressortype)_ | true | CompressorType defines the compressor type to use for compression. | +| `brotli` | _[BrotliCompressor](#brotlicompressor)_ | false | The configuration for Brotli compressor. | | `gzip` | _[GzipCompressor](#gzipcompressor)_ | false | The configuration for GZIP compressor. | @@ -753,6 +767,10 @@ CompressorType defines the types of compressor library supported by Envoy Gatewa _Appears in:_ - [Compression](#compression) +| Value | Description | +| ----- | ----------- | +| `Gzip` | | +| `Brotli` | | #### ConnectionLimit diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index d5e6a1b2d1f..6ce12b72aa3 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -1502,6 +1502,62 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { "only ConfigMap is supported for ValueRe", }, }, + { + desc: "valid compressor", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + Compression: []*egv1a1.Compression{ + { + Type: egv1a1.BrotliCompressorType, + Brotli: &egv1a1.BrotliCompressor{}, + }, + { + Type: egv1a1.GzipCompressorType, + Gzip: &egv1a1.GzipCompressor{}, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "invalid compressor", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + Compression: []*egv1a1.Compression{ + { + Type: egv1a1.BrotliCompressorType, + Gzip: &egv1a1.GzipCompressor{}, + }, + { + Type: egv1a1.GzipCompressorType, + }, + }, + } + }, + wantErrors: []string{ + "If compression type is Brotli, brotli field needs to be set.", + "If compression type is Gzip, gzip field needs to be set.", + }, + }, } for _, tc := range cases { diff --git a/test/e2e/testdata/compression.yaml b/test/e2e/testdata/compression.yaml index b5c077768eb..38df837886b 100644 --- a/test/e2e/testdata/compression.yaml +++ b/test/e2e/testdata/compression.yaml @@ -44,4 +44,7 @@ spec: kind: HTTPRoute name: compression compression: + - type: Brotli + brotli: {} - type: Gzip + gzip: {} diff --git a/test/e2e/tests/compression.go b/test/e2e/tests/compression.go index 784a8c7a8db..f9cdf813ab3 100644 --- a/test/e2e/tests/compression.go +++ b/test/e2e/tests/compression.go @@ -18,6 +18,7 @@ import ( "net/http/httputil" "testing" + "github.com/andybalholm/brotli" "k8s.io/apimachinery/pkg/types" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -28,6 +29,7 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/conformance/utils/tlog" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/gatewayapi/resource" ) @@ -41,37 +43,12 @@ var CompressionTest = suite.ConformanceTest{ Description: "Test response compression on HTTPRoute", Manifests: []string{"testdata/compression.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { - t.Run("HTTPRoute with compression", func(t *testing.T) { - ns := "gateway-conformance-infra" - routeNN := types.NamespacedName{Name: "compression", Namespace: ns} - gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} - gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) - - ancestorRef := gwapiv1a2.ParentReference{ - Group: gatewayapi.GroupPtr(gwapiv1.GroupName), - Kind: gatewayapi.KindPtr(resource.KindGateway), - Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), - Name: gwapiv1.ObjectName(gwNN.Name), - } - BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "compression", Namespace: ns}, suite.ControllerName, ancestorRef) + t.Run("HTTPRoute with brotli compression", func(t *testing.T) { + testCompression(t, suite, egv1a1.BrotliCompressorType) + }) - expectedResponse := http.ExpectedResponse{ - Request: http.Request{ - Path: "/compression", - Headers: map[string]string{ - "Accept-encoding": "gzip", - }, - }, - Response: http.Response{ - StatusCode: 200, - Headers: map[string]string{ - "content-encoding": "gzip", - }, - }, - Namespace: ns, - } - roundTripper := &CompressionRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig} - http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + t.Run("HTTPRoute with gzip compression", func(t *testing.T) { + testCompression(t, suite, egv1a1.GzipCompressorType) }) t.Run("HTTPRoute without compression", func(t *testing.T) { @@ -107,6 +84,47 @@ var CompressionTest = suite.ConformanceTest{ }, } +func testCompression(t *testing.T, suite *suite.ConformanceTestSuite, compressionType egv1a1.CompressorType) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "compression", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "compression", Namespace: ns}, suite.ControllerName, ancestorRef) + + var encoding string + switch compressionType { + case egv1a1.BrotliCompressorType: + encoding = "br" + case egv1a1.GzipCompressorType: + encoding = "gzip" + } + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/compression", + Headers: map[string]string{ + "Accept-encoding": encoding, + }, + }, + Response: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "content-encoding": encoding, + }, + }, + Namespace: ns, + } + roundTripper := &CompressionRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig} + http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) +} + // CompressionRoundTripper implements roundtripper.RoundTripper and adds support for gzip encoding. type CompressionRoundTripper struct { Debug bool @@ -172,19 +190,20 @@ func (d *CompressionRoundTripper) defaultRoundTrip(request roundtripper.Request, cReq := &roundtripper.CapturedRequest{} var body []byte - if resp.Header.Get("Content-Encoding") == "gzip" { - reader, err := gzip.NewReader(resp.Body) - if err != nil { - return nil, nil, err - } - defer reader.Close() - if body, err = io.ReadAll(reader); err != nil { - return nil, nil, err - } - } else { - if body, err = io.ReadAll(resp.Body); err != nil { + + var reader io.Reader + switch resp.Header.Get("Content-Encoding") { + case "gzip": + if reader, err = gzip.NewReader(resp.Body); err != nil { return nil, nil, err } + case "br": + reader = brotli.NewReader(resp.Body) + default: + reader = resp.Body + } + if body, err = io.ReadAll(reader); err != nil { + return nil, nil, err } // we cannot assume the response is JSON diff --git a/tools/make/kube.mk b/tools/make/kube.mk index faf6cc4e8c1..1752bb8dbf7 100644 --- a/tools/make/kube.mk +++ b/tools/make/kube.mk @@ -3,7 +3,8 @@ # - https://github.com/kubernetes-sigs/controller-tools/blob/main/envtest-releases.yaml ENVTEST_K8S_VERSION ?= 1.28.3 # Need run cel validation across multiple versions of k8s -ENVTEST_K8S_VERSIONS ?= 1.28.3 1.29.5 1.30.3 1.31.0 +ENVTEST_K8S_VERSIONS ?= v1.29.10 v1.30.6 v1.31.4 v1.32.0 + # GATEWAY_API_VERSION refers to the version of Gateway API CRDs. # For more details, see https://gateway-api.sigs.k8s.io/guides/getting-started/#installing-gateway-api GATEWAY_API_VERSION ?= $(shell go list -m -f '{{.Version}}' sigs.k8s.io/gateway-api) From 7f697d90c3f354844e60b65ca8b17f411408d3ab Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Wed, 8 Jan 2025 08:27:41 +0000 Subject: [PATCH 10/15] fix cel tests Signed-off-by: Huabing Zhao --- internal/ir/xds.go | 2 +- test/e2e/tests/compression.go | 2 +- tools/make/kube.mk | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 784f77a32d7..a1a0f2b1d95 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -756,7 +756,7 @@ type HeaderBasedSessionPersistence struct { // +k8s:deepcopy-gen=true type Compression struct { // Type of compression to be used. - Type egv1a1.CompressorType `json:"type,omitempty" yaml:"type,omitempty"` + Type egv1a1.CompressorType `json:"type" yaml:"type"` } // TrafficFeatures holds the information associated with the Backend Traffic Policy. diff --git a/test/e2e/tests/compression.go b/test/e2e/tests/compression.go index f9cdf813ab3..2d67a2a4793 100644 --- a/test/e2e/tests/compression.go +++ b/test/e2e/tests/compression.go @@ -125,7 +125,7 @@ func testCompression(t *testing.T, suite *suite.ConformanceTestSuite, compressio http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) } -// CompressionRoundTripper implements roundtripper.RoundTripper and adds support for gzip encoding. +// CompressionRoundTripper implements roundtripper.RoundTripper and adds support for compression. type CompressionRoundTripper struct { Debug bool TimeoutConfig config.TimeoutConfig diff --git a/tools/make/kube.mk b/tools/make/kube.mk index 1752bb8dbf7..7b400b651b0 100644 --- a/tools/make/kube.mk +++ b/tools/make/kube.mk @@ -1,9 +1,9 @@ # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. # To know the available versions check: # - https://github.com/kubernetes-sigs/controller-tools/blob/main/envtest-releases.yaml -ENVTEST_K8S_VERSION ?= 1.28.3 +ENVTEST_K8S_VERSION ?= 1.29.4 # Need run cel validation across multiple versions of k8s -ENVTEST_K8S_VERSIONS ?= v1.29.10 v1.30.6 v1.31.4 v1.32.0 +ENVTEST_K8S_VERSIONS ?= 1.29.4 1.30.3 1.31.0 1.32.0 # GATEWAY_API_VERSION refers to the version of Gateway API CRDs. # For more details, see https://gateway-api.sigs.k8s.io/guides/getting-started/#installing-gateway-api From e7cd664ed65697dfab5804073caa266a34c6d142 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 10 Jan 2025 03:39:52 +0000 Subject: [PATCH 11/15] address comment Signed-off-by: Huabing Zhao --- internal/gatewayapi/backendtrafficpolicy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index fefd66aa8bb..2d07982cfef 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -938,6 +938,9 @@ func defaultResponseOverrideRuleName(policy *egv1a1.BackendTrafficPolicy, index } func buildCompression(compression []*egv1a1.Compression) []*ir.Compression { + if compression == nil { + return nil + } irCompression := make([]*ir.Compression, 0, len(compression)) for _, c := range compression { irCompression = append(irCompression, &ir.Compression{ From cff5168c94167223c71c9d7cf883adcc1ce36218 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 10 Jan 2025 03:51:08 +0000 Subject: [PATCH 12/15] address comment Signed-off-by: Huabing Zhao --- api/v1alpha1/compression_types.go | 2 - ....envoyproxy.io_backendtrafficpolicies.yaml | 6 -- .../gateway.envoyproxy.io_envoyproxies.yaml | 8 --- .../backendtrafficpolicy_test.go | 56 ------------------- 4 files changed, 72 deletions(-) diff --git a/api/v1alpha1/compression_types.go b/api/v1alpha1/compression_types.go index cb4f90f7f3e..edd36fe6eb7 100644 --- a/api/v1alpha1/compression_types.go +++ b/api/v1alpha1/compression_types.go @@ -28,8 +28,6 @@ type BrotliCompressor struct{} // Compression defines the config of enabling compression. // This can help reduce the bandwidth at the expense of higher CPU. -// +kubebuilder:validation:XValidation:rule="self.type == 'Brotli' ? has(self.brotli) : !has(self.brotli)",message="If compression type is Brotli, brotli field needs to be set." -// +kubebuilder:validation:XValidation:rule="self.type == 'Gzip' ? has(self.gzip) : !has(self.gzip)",message="If compression type is Gzip, gzip field needs to be set." type Compression struct { // CompressorType defines the compressor type to use for compression. // diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 099c08feb80..e1899fb3713 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -122,12 +122,6 @@ spec: required: - type type: object - x-kubernetes-validations: - - message: If compression type is Brotli, brotli field needs to - be set. - rule: 'self.type == ''Brotli'' ? has(self.brotli) : !has(self.brotli)' - - message: If compression type is Gzip, gzip field needs to be set. - rule: 'self.type == ''Gzip'' ? has(self.gzip) : !has(self.gzip)' type: array connection: description: Connection includes backend connection settings. diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index cb23c9dc286..601d96f93b6 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -12513,14 +12513,6 @@ spec: required: - type type: object - x-kubernetes-validations: - - message: If compression type is Brotli, brotli field - needs to be set. - rule: 'self.type == ''Brotli'' ? has(self.brotli) : - !has(self.brotli)' - - message: If compression type is Gzip, gzip field needs - to be set. - rule: 'self.type == ''Gzip'' ? has(self.gzip) : !has(self.gzip)' disable: description: Disable the Prometheus endpoint. type: boolean diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index 6ce12b72aa3..d5e6a1b2d1f 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -1502,62 +1502,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { "only ConfigMap is supported for ValueRe", }, }, - { - desc: "valid compressor", - mutate: func(btp *egv1a1.BackendTrafficPolicy) { - btp.Spec = egv1a1.BackendTrafficPolicySpec{ - PolicyTargetReferences: egv1a1.PolicyTargetReferences{ - TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ - Group: gwapiv1a2.Group("gateway.networking.k8s.io"), - Kind: gwapiv1a2.Kind("Gateway"), - Name: gwapiv1a2.ObjectName("eg"), - }, - }, - }, - Compression: []*egv1a1.Compression{ - { - Type: egv1a1.BrotliCompressorType, - Brotli: &egv1a1.BrotliCompressor{}, - }, - { - Type: egv1a1.GzipCompressorType, - Gzip: &egv1a1.GzipCompressor{}, - }, - }, - } - }, - wantErrors: []string{}, - }, - { - desc: "invalid compressor", - mutate: func(btp *egv1a1.BackendTrafficPolicy) { - btp.Spec = egv1a1.BackendTrafficPolicySpec{ - PolicyTargetReferences: egv1a1.PolicyTargetReferences{ - TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ - Group: gwapiv1a2.Group("gateway.networking.k8s.io"), - Kind: gwapiv1a2.Kind("Gateway"), - Name: gwapiv1a2.ObjectName("eg"), - }, - }, - }, - Compression: []*egv1a1.Compression{ - { - Type: egv1a1.BrotliCompressorType, - Gzip: &egv1a1.GzipCompressor{}, - }, - { - Type: egv1a1.GzipCompressorType, - }, - }, - } - }, - wantErrors: []string{ - "If compression type is Brotli, brotli field needs to be set.", - "If compression type is Gzip, gzip field needs to be set.", - }, - }, } for _, tc := range cases { From 1090150354411ddb0bf8f042179d62b4745e5aa7 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 10 Jan 2025 03:56:34 +0000 Subject: [PATCH 13/15] address comment Signed-off-by: Huabing Zhao --- .../testdata/backendtrafficpolicy-compression.in.yaml | 2 -- .../testdata/backendtrafficpolicy-compression.out.yaml | 6 ++---- test/e2e/testdata/compression.yaml | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml index 624bf7ed1f8..3e644a055b1 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml @@ -46,6 +46,4 @@ backendTrafficPolicies: name: httproute-1 compression: - type: Brotli - brotli: {} - type: Gzip - gzip: {} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml index c6b26110e7d..8bd4eac943f 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml @@ -7,10 +7,8 @@ backendTrafficPolicies: namespace: default spec: compression: - - brotli: {} - type: Brotli - - gzip: {} - type: Gzip + - type: Brotli + - type: Gzip targetRef: group: gateway.networking.k8s.io kind: HTTPRoute diff --git a/test/e2e/testdata/compression.yaml b/test/e2e/testdata/compression.yaml index 38df837886b..d2e5b030b48 100644 --- a/test/e2e/testdata/compression.yaml +++ b/test/e2e/testdata/compression.yaml @@ -45,6 +45,4 @@ spec: name: compression compression: - type: Brotli - brotli: {} - type: Gzip - gzip: {} From 969d0db77ab60a0b043de58282e5ad624c3b0346 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 10 Jan 2025 04:17:36 +0000 Subject: [PATCH 14/15] foo Signed-off-by: Huabing Zhao --- internal/gatewayapi/clustersettings.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/gatewayapi/clustersettings.go b/internal/gatewayapi/clustersettings.go index 5706504f42b..40266553b46 100644 --- a/internal/gatewayapi/clustersettings.go +++ b/internal/gatewayapi/clustersettings.go @@ -11,7 +11,6 @@ import ( "math" "math/big" "net/http" - "reflect" "strings" "time" @@ -77,7 +76,7 @@ func translateTrafficFeatures(policy *egv1a1.ClusterSettings) (*ir.TrafficFeatur // If nothing was set in any of the above calls, return nil instead of an empty // container var empty ir.TrafficFeatures - if reflect.DeepEqual(empty, *ret) { + if empty == *ret { ret = nil } From 20bd82af4025ad7b237fd0454f9175799b12a8d0 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 10 Jan 2025 04:29:02 +0000 Subject: [PATCH 15/15] Revert "foo" This reverts commit 969d0db77ab60a0b043de58282e5ad624c3b0346. Signed-off-by: Huabing Zhao --- internal/gatewayapi/clustersettings.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/gatewayapi/clustersettings.go b/internal/gatewayapi/clustersettings.go index 40266553b46..5706504f42b 100644 --- a/internal/gatewayapi/clustersettings.go +++ b/internal/gatewayapi/clustersettings.go @@ -11,6 +11,7 @@ import ( "math" "math/big" "net/http" + "reflect" "strings" "time" @@ -76,7 +77,7 @@ func translateTrafficFeatures(policy *egv1a1.ClusterSettings) (*ir.TrafficFeatur // If nothing was set in any of the above calls, return nil instead of an empty // container var empty ir.TrafficFeatures - if empty == *ret { + if reflect.DeepEqual(empty, *ret) { ret = nil }