diff --git a/envoy/configs/test-proxy.yaml b/envoy/configs/test-proxy.yaml index a5e196f..aa4ee3a 100644 --- a/envoy/configs/test-proxy.yaml +++ b/envoy/configs/test-proxy.yaml @@ -1,6 +1,6 @@ name: chained_envoy_hosts domains: - - "envoy.local" + - "envoy.local:8080" routes: - match: prefix: "/exemplar" @@ -13,6 +13,7 @@ routes: - socketAddress: address: openapi-exemplar.default.svc port: 80 + protocol: HTTP mutations: prefixRewrite: "/" @@ -27,5 +28,6 @@ routes: - socketAddress: address: www.envoyproxy.io port: 443 + protocol: HTTPS mutations: prefixRewrite: "/" \ No newline at end of file diff --git a/envoy/src/main/kotlin/ayansen/playground/envoy/SpringConfiguration.kt b/envoy/src/main/kotlin/ayansen/playground/envoy/SpringConfiguration.kt index 176593c..4edfcc5 100644 --- a/envoy/src/main/kotlin/ayansen/playground/envoy/SpringConfiguration.kt +++ b/envoy/src/main/kotlin/ayansen/playground/envoy/SpringConfiguration.kt @@ -2,6 +2,7 @@ package ayansen.playground.envoy import DiscoveryServer import ayansen.playground.envoy.entity.ListenersConfiguration +import ayansen.playground.envoy.entity.ProxyProviderConfigurations import ayansen.playground.envoy.provider.ProxyProvider import ayansen.playground.envoy.provider.FileProxyProvider import io.envoyproxy.controlplane.cache.v3.SimpleCache diff --git a/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ListenersConfiguration.kt b/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ListenersConfiguration.kt index a39b44c..80f87b0 100644 --- a/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ListenersConfiguration.kt +++ b/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ListenersConfiguration.kt @@ -1,6 +1,10 @@ package ayansen.playground.envoy.entity +import io.envoyproxy.envoy.config.accesslog.v3.AccessLog +import io.envoyproxy.envoy.config.core.v3.ApiConfigSource import io.envoyproxy.envoy.config.listener.v3.FilterChain +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds import org.springframework.boot.context.properties.ConfigurationProperties @@ -8,9 +12,18 @@ import org.springframework.boot.context.properties.ConfigurationProperties data class ListenersConfiguration( var listeners: List = emptyList() ) { + data class Listener( + var name: String = "", + var socketAddress: SocketAddress = SocketAddress() + ) + + data class SocketAddress( + var address: String = "", + var port: Int = 0 + ) fun toProtoListeners(): List { - return listeners.map { + val listeners = listeners.map { io.envoyproxy.envoy.config.listener.v3.Listener.newBuilder() .setName(it.name) .setAddress( @@ -29,6 +42,40 @@ data class ListenersConfiguration( io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.newBuilder() .setStatPrefix(it.name) .setCodecType(io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.CodecType.AUTO) + .addHttpFilters( + HttpFilter.newBuilder().setName("envoy.filters.http.router").build() + ) + .addAccessLog( + AccessLog.newBuilder().setName("envoy.access_loggers.file").setTypedConfig( + com.google.protobuf.Any.pack( + io.envoyproxy.envoy.extensions.access_loggers.file.v3.FileAccessLog.newBuilder() + .setPath("/dev/stdout") + .build() + ) + ).build() + ) + .setRds( + Rds.newBuilder() + .setRouteConfigName("chained_envoy_hosts") + .setConfigSource( + io.envoyproxy.envoy.config.core.v3.ConfigSource.newBuilder() + .setResourceApiVersion(io.envoyproxy.envoy.config.core.v3.ApiVersion.V3) + .setApiConfigSource( + ApiConfigSource.newBuilder() + .setApiType(ApiConfigSource.ApiType.GRPC) + .setTransportApiVersion(io.envoyproxy.envoy.config.core.v3.ApiVersion.V3) + .addGrpcServices( + io.envoyproxy.envoy.config.core.v3.GrpcService.newBuilder() + .setEnvoyGrpc( + io.envoyproxy.envoy.config.core.v3.GrpcService.EnvoyGrpc.newBuilder() + .setClusterName("envoy_control_plane") + ) + ) + ) + .build() + ) + .build() + ) .build() ) ) @@ -37,15 +84,6 @@ data class ListenersConfiguration( ) .build() } + return listeners } - data class Listener( - val name: String, - val socketAddress: SocketAddress - ) - - - data class SocketAddress( - val address: String, - val port: Int - ) } \ No newline at end of file diff --git a/envoy/src/main/kotlin/ayansen/playground/envoy/entity/Proxy.kt b/envoy/src/main/kotlin/ayansen/playground/envoy/entity/Proxy.kt index 5aea2ab..6cf18a3 100644 --- a/envoy/src/main/kotlin/ayansen/playground/envoy/entity/Proxy.kt +++ b/envoy/src/main/kotlin/ayansen/playground/envoy/entity/Proxy.kt @@ -13,11 +13,15 @@ data class Proxy( val routes: List ) { + enum class PROTOCOL(val value: String) { + HTTP("HTTP1"), + HTTPS("HTTPS") + } + data class Route( val match: Match, val cluster: Cluster, val mutations: Mutations - ) data class Match( @@ -42,7 +46,8 @@ data class Proxy( data class SocketAddress( val address: String, - val port: Int + val port: Int, + val protocol: PROTOCOL = PROTOCOL.HTTP ) fun toProtoRoute(): io.envoyproxy.envoy.config.route.v3.RouteConfiguration { @@ -63,6 +68,7 @@ data class Proxy( .setRoute( io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() .setCluster(route.cluster.name) + .setPrefixRewrite(route.mutations.prefixRewrite) ) .build() } @@ -72,6 +78,7 @@ data class Proxy( ) .build() } + fun toProtoEndpoints(): List { return routes.map { ClusterLoadAssignment.newBuilder() @@ -98,17 +105,66 @@ data class Proxy( .build() } } + fun toProtoClusters(): List { return routes.map { - io.envoyproxy.envoy.config.cluster.v3.Cluster.newBuilder() + val cluster = io.envoyproxy.envoy.config.cluster.v3.Cluster.newBuilder() .setName(it.cluster.name) .setConnectTimeout( com.google.protobuf.Duration.newBuilder() .setSeconds(it.cluster.connectTimeout.toLong()) ) + .setDnsLookupFamily(io.envoyproxy.envoy.config.cluster.v3.Cluster.DnsLookupFamily.V4_ONLY) .setType(io.envoyproxy.envoy.config.cluster.v3.Cluster.DiscoveryType.valueOf(it.cluster.type)) .setLbPolicy(io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy.valueOf(it.cluster.lbPolicy)) - .build() + .setLoadAssignment( + ClusterLoadAssignment.newBuilder() + .setClusterName(it.cluster.name) + .addAllEndpoints( + (it.cluster.hosts.map { host -> + LocalityLbEndpoints.newBuilder() + .addLbEndpoints( + LbEndpoint.newBuilder() + .setEndpoint( + Endpoint.newBuilder() + .setAddress( + Address.newBuilder() + .setSocketAddress( + io.envoyproxy.envoy.config.core.v3.SocketAddress.newBuilder() + .setAddress(host.socketAddress.address) + .setPortValue(host.socketAddress.port) + ) + ) + ) + ).build() + }) + ) + .build() + ) + if (it.cluster.hosts.first().socketAddress.protocol == PROTOCOL.HTTPS) { + cluster.setTransportSocket( + io.envoyproxy.envoy.config.core.v3.TransportSocket.newBuilder() + .setName("envoy.transport_sockets.tls") + .setTypedConfig( + com.google.protobuf.Any.pack( + io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext.newBuilder() + .setCommonTlsContext( + io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext.newBuilder() + .setTlsParams( + io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.TlsParameters.newBuilder() + .setTlsMinimumProtocolVersion(io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.TlsParameters.TlsProtocol.TLS_AUTO) + .build() + + ) + .build() + ) + .setSni(it.cluster.hosts.first().socketAddress.address) + .build() + ) + ) + ) + } + cluster.build() } } } diff --git a/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ProxyProviderConfigurations.kt b/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ProxyProviderConfigurations.kt index 36378c3..c25edcc 100644 --- a/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ProxyProviderConfigurations.kt +++ b/envoy/src/main/kotlin/ayansen/playground/envoy/entity/ProxyProviderConfigurations.kt @@ -1,4 +1,4 @@ -package ayansen.playground.envoy +package ayansen.playground.envoy.entity import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/envoy/src/main/kotlin/ayansen/playground/envoy/provider/FileProxyProvider.kt b/envoy/src/main/kotlin/ayansen/playground/envoy/provider/FileProxyProvider.kt index d3088c7..dd52873 100644 --- a/envoy/src/main/kotlin/ayansen/playground/envoy/provider/FileProxyProvider.kt +++ b/envoy/src/main/kotlin/ayansen/playground/envoy/provider/FileProxyProvider.kt @@ -9,6 +9,7 @@ import io.envoyproxy.controlplane.cache.v3.SimpleCache import kotlinx.coroutines.* import org.slf4j.LoggerFactory +import java.io.File import java.nio.file.FileSystems import java.nio.file.Path import java.nio.file.StandardWatchEventKinds @@ -39,7 +40,7 @@ class FileProxyProvider( override fun getProxies(): List { return Path.of(proxyFolderPath).toFile().listFiles()?.map { file -> - parseYamlFile(file.toPath()) + parseYamlFile(file) } ?: emptyList() } @@ -51,8 +52,8 @@ class FileProxyProvider( throw NotImplementedError("proxy deletion can be done by deleting the file from the folder") } - private inline fun parseYamlFile(path: Path): T { - return mapper.readValue(path.toFile(), T::class.java) + private inline fun parseYamlFile(file: File): T { + return mapper.readValue(file, T::class.java) } private fun watchForChanges(path: Path) { diff --git a/envoy/src/main/resources/application.yml b/envoy/src/main/resources/application.yml index dfd88d2..65cd48d 100644 --- a/envoy/src/main/resources/application.yml +++ b/envoy/src/main/resources/application.yml @@ -44,8 +44,9 @@ config-provider: file: path: "./configs" -listeners: - - name: http - socketAddress: - address: 0.0.0.0 - port: 10000 \ No newline at end of file +envoy-listeners: + listeners: + - name: http + socketAddress: + address: 0.0.0.0 + port: 10000 diff --git a/envoy/src/test/kotlin/ayansen/playground/envoy/Fixtures.kt b/envoy/src/test/kotlin/ayansen/playground/envoy/Fixtures.kt new file mode 100644 index 0000000..4820911 --- /dev/null +++ b/envoy/src/test/kotlin/ayansen/playground/envoy/Fixtures.kt @@ -0,0 +1,17 @@ +package ayansen.playground.envoy + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.KotlinModule +import java.io.File + +object Fixtures { + + val mapper: ObjectMapper = ObjectMapper(YAMLFactory()).apply { + registerModule(KotlinModule.Builder().build()) + } + + inline fun parseYamlFile(file: File): T { + return mapper.readValue(file, T::class.java) + } +} \ No newline at end of file diff --git a/envoy/src/test/kotlin/ayansen/playground/envoy/entity/ListenersConfigurationTest.kt b/envoy/src/test/kotlin/ayansen/playground/envoy/entity/ListenersConfigurationTest.kt new file mode 100644 index 0000000..f418a82 --- /dev/null +++ b/envoy/src/test/kotlin/ayansen/playground/envoy/entity/ListenersConfigurationTest.kt @@ -0,0 +1,35 @@ +package ayansen.playground.envoy.entity + +import ayansen.playground.envoy.Fixtures.parseYamlFile +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertEquals + +class ListenersConfigurationTest { + + @Test + fun `toProtoListeners method returns a list of Listener protobuf objects when given a list of ListenerConfiguration objects`() { + // Given + val listeners: ListenersConfiguration = + parseYamlFile(File("src/test/resources/listeners/multiple_http_listeners.yaml")) + + + // When + val result = listeners.toProtoListeners() + + // Then + assertEquals(2, result.size) + listeners.listeners.forEachIndexed { index, listener -> + assertEquals(listener.name, result[index].name) + assertEquals( + listener.socketAddress.address, + result[index].address.socketAddress.address + ) + assertEquals(result[index].filterChainsCount, 1) + assertEquals( + listener.socketAddress.port, + result[index].address.socketAddress.portValue + ) + } + } +} \ No newline at end of file diff --git a/envoy/src/test/kotlin/ayansen/playground/envoy/entity/ProxyTest.kt b/envoy/src/test/kotlin/ayansen/playground/envoy/entity/ProxyTest.kt new file mode 100644 index 0000000..7626527 --- /dev/null +++ b/envoy/src/test/kotlin/ayansen/playground/envoy/entity/ProxyTest.kt @@ -0,0 +1,41 @@ +package ayansen.playground.envoy.entity + + +import ayansen.playground.envoy.Fixtures.parseYamlFile +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertNotNull + +class ProxyTest { + + @Test + fun `generates a unique cluster for each route in the proxy`() { + val proxy:Proxy = parseYamlFile(File("src/test/resources/proxies/proxy_with_multiple_routes.yaml")) + val clusters = proxy.toProtoClusters() + + assert(clusters.size == 2) + assert(clusters[0].name != clusters[1].name) + } + + @Test + fun `generates multiple endpoints with the right cluster name for each route in the proxy`() { + val proxy:Proxy = parseYamlFile(File("src/test/resources/proxies/proxy_with_multiple_routes.yaml")) + val clusters = proxy.toProtoClusters() + val endpoints = proxy.toProtoEndpoints() + + assert(clusters.size == 2) + assert(endpoints.size == 2) + assert(clusters[0].name != clusters[1].name) + assert(endpoints[0].clusterName == clusters[0].name) + assert(endpoints[1].clusterName == clusters[1].name) + } + + @Test + fun `generates a single route configuration for a unique domain name`() { + val proxy:Proxy = parseYamlFile(File("src/test/resources/proxies/proxy_with_multiple_routes.yaml")) + + val routeConfiguration = proxy.toProtoRoute() + assertNotNull(routeConfiguration) + } + +} \ No newline at end of file diff --git a/envoy/src/test/resources/listeners/multiple_http_listeners.yaml b/envoy/src/test/resources/listeners/multiple_http_listeners.yaml new file mode 100644 index 0000000..2600fc8 --- /dev/null +++ b/envoy/src/test/resources/listeners/multiple_http_listeners.yaml @@ -0,0 +1,10 @@ + +listeners: + - name: http + socketAddress: + address: 0.0.0.0 + port: 10000 + - name: http + socketAddress: + address: 0.0.0.0 + port: 10001 \ No newline at end of file diff --git a/envoy/src/test/resources/proxies/proxy_with_multiple_routes.yaml b/envoy/src/test/resources/proxies/proxy_with_multiple_routes.yaml new file mode 100644 index 0000000..a5e196f --- /dev/null +++ b/envoy/src/test/resources/proxies/proxy_with_multiple_routes.yaml @@ -0,0 +1,31 @@ +name: chained_envoy_hosts +domains: + - "envoy.local" +routes: + - match: + prefix: "/exemplar" + cluster: + name: openapi-exemplar + connectTimeout: 30 + type: LOGICAL_DNS + lbPolicy: ROUND_ROBIN + hosts: + - socketAddress: + address: openapi-exemplar.default.svc + port: 80 + + mutations: + prefixRewrite: "/" + - match: + prefix: "/" + cluster: + name: service_envoyproxy_io + connectTimeout: 30 + type: LOGICAL_DNS + lbPolicy: ROUND_ROBIN + hosts: + - socketAddress: + address: www.envoyproxy.io + port: 443 + mutations: + prefixRewrite: "/" \ No newline at end of file