Skip to content

Commit

Permalink
allegro-internal/flex-roadmap#687 Added http compression filter confi…
Browse files Browse the repository at this point in the history
…guration
  • Loading branch information
nastassia-dailidava committed Jul 24, 2024
1 parent 875069b commit c0767fd
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 79 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.16]
### Changed
- Added http compression filter configuration

## [0.20.16]
### Changed
- Add JWT failure reason to metadata and use it in jwt-status field on denied requests
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,6 +38,7 @@ class SnapshotProperties {
var retryPolicy = RetryPolicyProperties()
var tcpDumpsEnabled: Boolean = true
var shouldAuditGlobalSnapshot: Boolean = true
var compression: CompressionProperties = CompressionProperties()
}

class PathNormalizationProperties {
Expand Down Expand Up @@ -377,6 +378,26 @@ 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
}

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,92 @@
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.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(): HttpFilter? {
return if (properties.compression.gzip.enabled) {
gzipCompressionFilter
} else null
}

fun brotliCompressionFilter(): HttpFilter? {
return if (properties.compression.brotli.enabled) {
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))
}
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, _: GlobalSnapshot ->
compressionFilterFactory.gzipCompressionFilter()
}

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

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,112 @@
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.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 {

@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,
)
)
private const val SERVICE_NAME = "service-1"
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 longText = LONG_STRING.repeat(100)

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

@JvmField
@RegisterExtension
val downstreamService = EchoServiceExtension()

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

@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()
}
}

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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pl.allegro.tech.servicemesh.envoycontrol.config.envoy

import org.assertj.core.api.Assertions.assertThat
import org.awaitility.Awaitility
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
Expand All @@ -15,11 +16,12 @@ import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControl
import pl.allegro.tech.servicemesh.envoycontrol.config.service.ServiceExtension
import pl.allegro.tech.servicemesh.envoycontrol.logger
import java.time.Duration
import java.util.concurrent.TimeUnit

class EnvoyExtension(
private val envoyControl: EnvoyControlExtensionBase,
private val localService: ServiceExtension<*>? = null,
private val config: EnvoyConfig = RandomConfigFile
config: EnvoyConfig = RandomConfigFile
) : BeforeAllCallback, AfterAllCallback, AfterEachCallback {

companion object {
Expand All @@ -38,15 +40,21 @@ class EnvoyExtension(
override fun beforeAll(context: ExtensionContext) {
localService?.beforeAll(context)
envoyControl.beforeAll(context)

try {
container.start()
waitUntilHealthy()
} catch (e: Exception) {
logger.error("Logs from failed container: ${container.logs}")
throw e
}
}

private fun waitUntilHealthy() {
Awaitility.await().atMost(1, TimeUnit.MINUTES).untilAsserted {
assertThat(container.admin().isIngressReady())
}
}

override fun afterAll(context: ExtensionContext) {
container.stop()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension
import pl.allegro.tech.servicemesh.envoycontrol.logger
import java.util.concurrent.TimeUnit

interface EnvoyControlExtensionBase : BeforeAllCallback, AfterAllCallback {
Expand All @@ -16,6 +17,7 @@ class EnvoyControlExtension(private val consul: ConsulExtension, override val ap
: EnvoyControlExtensionBase {

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

constructor(consul: ConsulExtension, properties: Map<String, Any> = mapOf())
: this(consul, EnvoyControlRunnerTestApp(
Expand All @@ -29,9 +31,11 @@ class EnvoyControlExtension(private val consul: ConsulExtension, override val ap
}

consul.beforeAll(context)
logger.info("Envoy control extension is starting.")
app.run()
waitUntilHealthy()
started = true
logger.info("Envoy control extension started.")
}

private fun waitUntilHealthy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ package pl.allegro.tech.servicemesh.envoycontrol.config.service
import org.testcontainers.containers.Network
import org.testcontainers.containers.wait.strategy.Wait
import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer
import java.util.UUID
import java.util.Locale
import java.util.UUID

class EchoContainer : GenericContainer<EchoContainer>("jxlwqq/http-echo"), ServiceContainer {

val response = UUID.randomUUID().toString()
class EchoContainer(val response: String = UUID.randomUUID().toString()) :
GenericContainer<EchoContainer>("jxlwqq/http-echo"), ServiceContainer {

override fun configure() {
super.configure()
Expand Down
Loading

0 comments on commit c0767fd

Please sign in to comment.