Skip to content

Commit

Permalink
Added compression filter configuration (#423)
Browse files Browse the repository at this point in the history
* allegro-internal/flex-roadmap#687 Added http compression filter configuration
  • Loading branch information
nastassia-dailidava authored Jul 30, 2024
1 parent 1a64a5f commit 91c51ae
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 80 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Lists all changes with user impact.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## [0.20.18]
### Changed
- Added http compression filter configuration

## [0.20.17]
### Fixed
- Fix JWT provider configuration to not impact lds cache
Expand Down
152 changes: 82 additions & 70 deletions docs/configuration.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ class SnapshotProperties {
var retryPolicy = RetryPolicyProperties()
var tcpDumpsEnabled: Boolean = true
var shouldAuditGlobalSnapshot: Boolean = true
var compression: CompressionProperties = CompressionProperties()
}

class PathNormalizationProperties {
var enabled = true
var mergeSlashes = true
var pathWithEscapedSlashesAction = "KEEP_UNCHANGED"
}

class MetricsProperties {
var cacheSetSnapshot = false
}
Expand All @@ -64,7 +66,7 @@ class AccessLogProperties {
var enabled = false
var timeFormat = "%START_TIME(%FT%T.%3fZ)%"
var messageFormat = "%PROTOCOL% %REQ(:METHOD)% %REQ(:authority)% %REQ(:PATH)% " +
"%DOWNSTREAM_REMOTE_ADDRESS% -> %UPSTREAM_HOST%"
"%DOWNSTREAM_REMOTE_ADDRESS% -> %UPSTREAM_HOST%"
var level = "TRACE"
var logger = "envoy.AccessLog"
var customFields = mapOf<String, String>()
Expand Down Expand Up @@ -130,6 +132,7 @@ class ClientsListsProperties {
var defaultClientsList: List<String> = emptyList()
var customClientsLists: Map<String, List<String>> = mapOf()
}

class TlsProtocolProperties {
var cipherSuites: List<String> = listOf("ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256")
var minimumVersion = TlsParameters.TlsProtocol.TLSv1_2
Expand Down Expand Up @@ -377,6 +380,27 @@ class DynamicForwardProxyProperties {
var connectionTimeout = Duration.ofSeconds(1)
}

class CompressionProperties {
var gzip = GzipProperties()
var brotli = BrotliProperties()
var minContentLength = 100
var disableOnEtagHeader = true
var requestCompressionEnabled = false
var responseCompressionEnabled = false
var enableForServices: List<String> = emptyList()
}

class BrotliProperties {
var enabled = false
var quality = 11
var chooseFirst = true
}

class GzipProperties {
var enabled = false
var chooseFirst = false
}

data class OAuthProvider(
var jwksUri: URI = URI.create("http://localhost"),
var createCluster: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters

import com.google.protobuf.BoolValue
import com.google.protobuf.UInt32Value
import io.envoyproxy.envoy.config.core.v3.RuntimeFeatureFlag
import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig
import io.envoyproxy.envoy.config.filter.http.gzip.v2.Gzip.CompressionLevel.Enum.BEST_VALUE
import io.envoyproxy.envoy.extensions.compression.brotli.compressor.v3.Brotli
import io.envoyproxy.envoy.extensions.compression.gzip.compressor.v3.Gzip
import io.envoyproxy.envoy.extensions.filters.http.compressor.v3.Compressor
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter
import pl.allegro.tech.servicemesh.envoycontrol.groups.Group
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties

class CompressionFilterFactory(val properties: SnapshotProperties) {

private val brotliCompressionFilter: HttpFilter = compressionFilter(
TypedExtensionConfig.newBuilder()
.setName("envoy.compression.brotli.compressor")
.setTypedConfig(
com.google.protobuf.Any.pack(
Brotli.newBuilder()
.setQuality(UInt32Value.of(properties.compression.brotli.quality))
.build()
)
),
properties.compression.brotli.chooseFirst
)

private val gzipCompressionFilter: HttpFilter = compressionFilter(
TypedExtensionConfig.newBuilder()
.setName("envoy.compression.gzip.compressor")
.setTypedConfig(
com.google.protobuf.Any.pack(
Gzip.newBuilder()
.setCompressionStrategy(Gzip.CompressionStrategy.DEFAULT_STRATEGY)
.setCompressionLevel(Gzip.CompressionLevel.forNumber(BEST_VALUE))
.build()
)
),
properties.compression.gzip.chooseFirst
)

fun gzipCompressionFilter(group: Group): HttpFilter? {
return if (properties.compression.gzip.enabled && group.hasCompressionEnabled()) {
gzipCompressionFilter
} else null
}

fun brotliCompressionFilter(group: Group): HttpFilter? {
return if (properties.compression.brotli.enabled && group.hasCompressionEnabled()) {
brotliCompressionFilter
} else null
}

private fun compressionFilter(library: TypedExtensionConfig.Builder, chooseFirst: Boolean) =
HttpFilter.newBuilder()
.setName("envoy.filters.http.compressor")
.setTypedConfig(
com.google.protobuf.Any.pack(
Compressor.newBuilder()
.setChooseFirst(chooseFirst)
.setRequestDirectionConfig(
Compressor.RequestDirectionConfig.newBuilder()
.setCommonConfig(
commonDirectionConfig(
"request_compressor_enabled",
properties.compression.requestCompressionEnabled
)
)
).setResponseDirectionConfig(
Compressor.ResponseDirectionConfig.newBuilder()
.setCommonConfig(
commonDirectionConfig(
"response_compressor_enabled",
properties.compression.responseCompressionEnabled
)
)
.setDisableOnEtagHeader(properties.compression.disableOnEtagHeader)
)
.setCompressorLibrary(library)
.build()
)
).build()

private fun commonDirectionConfig(runtimeKey: String, defaultValue: Boolean) =
Compressor.CommonDirectionConfig.newBuilder()
.setEnabled(
RuntimeFeatureFlag.newBuilder().setRuntimeKey(runtimeKey)
.setDefaultValue(BoolValue.of(defaultValue))
)
.setMinContentLength(UInt32Value.of(properties.compression.minContentLength))

private fun Group.hasCompressionEnabled() = properties.compression.enableForServices.contains(this.serviceName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class EnvoyDefaultFilters(
properties = snapshotProperties.routing.serviceTags
)

private val compressionFilterFactory = CompressionFilterFactory(snapshotProperties)

private val defaultServiceTagHeaderToMetadataFilterRules = serviceTagFilterFactory.headerToMetadataFilterRules()
private val defaultHeaderToMetadataConfig = headerToMetadataConfig(defaultServiceTagHeaderToMetadataFilterRules)
private val headerToMetadataHttpFilter = headerToMetadataHttpFilter(defaultHeaderToMetadataConfig)
Expand Down Expand Up @@ -62,8 +64,21 @@ class EnvoyDefaultFilters(
val defaultAuthorizationHeaderFilter = { _: Group, _: GlobalSnapshot ->
authorizationHeaderToMetadataFilter()
}

val defaultGzipCompressionFilter = { group: Group, _: GlobalSnapshot ->
compressionFilterFactory.gzipCompressionFilter(group)
}

val defaultBrotliCompressionFilter = { group: Group, _: GlobalSnapshot ->
compressionFilterFactory.brotliCompressionFilter(group)
}

val defaultEgressFilters = listOf(
defaultHeaderToMetadataFilter, defaultServiceTagFilter, defaultEnvoyRouterHttpFilter
defaultHeaderToMetadataFilter,
defaultServiceTagFilter,
defaultGzipCompressionFilter,
defaultBrotliCompressionFilter,
defaultEnvoyRouterHttpFilter,
)

val defaultCurrentZoneHeaderFilter = { _: Group, _: GlobalSnapshot ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package pl.allegro.tech.servicemesh.envoycontrol

import okhttp3.Response
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.ObjectAssert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted
import pl.allegro.tech.servicemesh.envoycontrol.config.AdsAllDependencies
import pl.allegro.tech.servicemesh.envoycontrol.config.Xds
import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoContainer
import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.service.GenericServiceExtension

class CompressionFilterTest {

companion object {
private const val SERVICE_NAME = "service-1"
private const val DOWNSTREAM_SERVICE_NAME = "echo2"
private const val LONG_STRING = "Workshallmeantheworkofauthorship,whetherinSourceorObjectform," +
"madeavailableundertheLicensesindicatedbyacopyrightnoticethatisincludedinorattachedto" +
"thework(anexampleisprovidedintheAppendixbelow)."
private val serviceConfig = """
node:
metadata:
proxy_settings:
incoming:
unlistedEndpointsPolicy: log
endpoints: []
""".trimIndent()
private val config = Xds.copy(configOverride = serviceConfig, serviceName = SERVICE_NAME)
private val unListedServiceConfig = AdsAllDependencies
private val longText = LONG_STRING.repeat(100)

@JvmField
@RegisterExtension
val consul = ConsulExtension()

@JvmField
@RegisterExtension
val envoyControl = EnvoyControlExtension(
consul,
mapOf(
"envoy-control.envoy.snapshot.compression.gzip.enabled" to true,
"envoy-control.envoy.snapshot.compression.brotli.enabled" to true,
"envoy-control.envoy.snapshot.compression.minContentLength" to 100,
"envoy-control.envoy.snapshot.compression.responseCompressionEnabled" to true,
"envoy-control.envoy.snapshot.compression.enableForServices" to listOf(DOWNSTREAM_SERVICE_NAME),
"envoy-control.envoy.snapshot.outgoing-permissions.servicesAllowedToUseWildcard" to "test-service"

)
)

@JvmField
@RegisterExtension
val service =
GenericServiceExtension(EchoContainer(longText))

@JvmField
@RegisterExtension
val noCompressionService = GenericServiceExtension(EchoContainer(longText))

@JvmField
@RegisterExtension
val downstreamService = EchoServiceExtension()

@JvmField
@RegisterExtension
val downstreamEnvoy = EnvoyExtension(envoyControl, downstreamService, Xds)

@JvmField
@RegisterExtension
val noCompressionEnvoy = EnvoyExtension(envoyControl, noCompressionService, unListedServiceConfig)

@JvmField
@RegisterExtension
val serviceEnvoy = EnvoyExtension(envoyControl, config = config, localService = service)
}

@Test
fun `should compress response with brotli`() {
consul.server.operations.registerServiceWithEnvoyOnIngress(serviceEnvoy, name = SERVICE_NAME)
downstreamEnvoy.waitForReadyServices(SERVICE_NAME)
untilAsserted {
val response =
downstreamEnvoy.egressOperations.callService(SERVICE_NAME, headers = mapOf("accept-encoding" to "br"))
assertThat(response).isCompressedWith("br")
}
}

@Test
fun `should compress response with gzip`() {
consul.server.operations.registerServiceWithEnvoyOnIngress(serviceEnvoy, name = SERVICE_NAME)
downstreamEnvoy.waitForReadyServices(SERVICE_NAME)
untilAsserted {
val response =
downstreamEnvoy.egressOperations.callService(SERVICE_NAME, headers = mapOf("accept-encoding" to "gzip"))
assertThat(response).isCompressedWith("gzip")
}
}

@Test
fun `should not compress response when accept encoding header is missing`() {
consul.server.operations.registerServiceWithEnvoyOnIngress(serviceEnvoy, name = SERVICE_NAME)
downstreamEnvoy.waitForReadyServices(SERVICE_NAME)
untilAsserted {
val response =
downstreamEnvoy.egressOperations.callService(SERVICE_NAME)
assertThat(response).isNotCompressed()
}
}

@Test
fun `should not enable compression on unlisted service`() {
consul.server.operations.registerServiceWithEnvoyOnIngress(serviceEnvoy, name = SERVICE_NAME)
noCompressionEnvoy.waitForReadyServices(SERVICE_NAME)
untilAsserted {
val response =
noCompressionEnvoy.egressOperations.callService(SERVICE_NAME, headers = mapOf("accept-encoding" to "gzip"))
println(response.headers.toString())
assertThat(response).isNotCompressed()
}
}

private fun ObjectAssert<Response>.isCompressedWith(encoding: String): ObjectAssert<Response> {
matches { it.isSuccessful && it.headers.any { x -> x.first == "content-encoding" && x.second == encoding } }
return this
}

private fun ObjectAssert<Response>.isNotCompressed(): ObjectAssert<Response> {
matches { it.isSuccessful && it.headers.none { x -> x.first == "content-encoding" } }
return this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.testcontainers.containers.Network
import pl.allegro.tech.servicemesh.envoycontrol.logger

class ConsulExtension : BeforeAllCallback, AfterAllCallback, AfterEachCallback {

Expand All @@ -16,15 +17,17 @@ class ConsulExtension : BeforeAllCallback, AfterAllCallback, AfterEachCallback {
}

val server = SHARED_CONSUL
private val logger by logger()
private var started = false

override fun beforeAll(context: ExtensionContext) {
if (started) {
return
}

logger.info("Consul extension is starting.")
server.container.start()
started = true
logger.info("Consul extension started.")
}

override fun afterEach(context: ExtensionContext) {
Expand Down
Loading

0 comments on commit 91c51ae

Please sign in to comment.