Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added compression filter configuration #423

Merged
merged 2 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
nastassia-dailidava marked this conversation as resolved.
Show resolved Hide resolved
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
kozjan marked this conversation as resolved.
Show resolved Hide resolved
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
Loading