├── tools ├── envoy │ ├── ingress-access.log │ └── Dockerfile ├── run-with-local-ec.sh ├── service │ ├── Dockerfile │ └── register_and_run.sh ├── envoy-control │ ├── run.sh │ └── Dockerfile └── docker-compose.yaml ├── requirements.txt ├── docs ├── changelog_symlink.md ├── assets │ ├── images │ │ ├── logo.png │ │ ├── high_level_architecture.png │ │ └── envoy-control-modules-drawing.png │ └── extra.js ├── features │ ├── service_transformers.md │ ├── timeouts.md │ └── access_log_filter.md ├── integrations │ └── consul.md ├── deployment │ └── deployment.md ├── development.md ├── quickstart.md ├── index.md ├── ec_vs_other_software.md └── performance.md ├── envoy-control-tests ├── src │ └── main │ │ ├── resources │ │ ├── ratelimit_config.yaml │ │ ├── testcontainers │ │ │ ├── ssl │ │ │ │ ├── root-ca.srl │ │ │ │ ├── root-ca2.srl │ │ │ │ ├── device-csr_echo2.pem │ │ │ │ ├── device-csr_echo3.pem │ │ │ │ ├── device-csr_root-ca2.pem │ │ │ │ ├── device-csr.pem │ │ │ │ ├── root-ca.crt │ │ │ │ ├── root-ca2.crt │ │ │ │ ├── cert_echo.pem │ │ │ │ ├── cert_echo2.pem │ │ │ │ ├── cert_echo3.pem │ │ │ │ ├── cert_echo_root-ca2.pem │ │ │ │ ├── fullchain_echo.pem │ │ │ │ ├── fullchain_echo2.pem │ │ │ │ ├── fullchain_echo3.pem │ │ │ │ ├── fullchain.pem │ │ │ │ ├── privkey_echo2.pem │ │ │ │ ├── privkey_echo4.pem │ │ │ │ ├── root-ca.key.pem │ │ │ │ ├── root-ca2.key.pem │ │ │ │ ├── privkey_echo3.pem │ │ │ │ ├── privkey_echo5.pem │ │ │ │ └── privkey.pem │ │ │ ├── consul-low-rpc-rate.json │ │ │ └── host_ip.sh │ │ ├── META-INF │ │ │ └── services │ │ │ │ └── org.junit.jupiter.api.extension.Extension │ │ ├── oauth │ │ │ ├── Dockerfile │ │ │ └── invalid_jwks_token │ │ └── envoy │ │ │ └── launch_envoy.sh │ │ └── kotlin │ │ └── pl │ │ └── allegro │ │ └── tech │ │ └── servicemesh │ │ └── envoycontrol │ │ ├── config │ │ ├── service │ │ │ ├── HttpContainer.kt │ │ │ ├── ServiceContainer.kt │ │ │ ├── UpstreamService.kt │ │ │ ├── ServiceExtension.kt │ │ │ ├── RedisContainer.kt │ │ │ ├── HttpsEchoExtension.kt │ │ │ ├── GenericServiceExtension.kt │ │ │ ├── RedirectServiceContainer.kt │ │ │ ├── EchoServiceExtension.kt │ │ │ ├── EchoContainer.kt │ │ │ ├── OAuthServerContainer.kt │ │ │ ├── OAuthServerExtension.kt │ │ │ ├── RedisBasedRateLimitContainer.kt │ │ │ └── HttpsEchoContainer.kt │ │ ├── envoy │ │ │ ├── HttpResponseCloserExtension.kt │ │ │ ├── ResponseWithBody.kt │ │ │ ├── HttpResponseCloser.kt │ │ │ └── CallStats.kt │ │ ├── containers │ │ │ ├── SSLGenericContainer.kt │ │ │ ├── ProxyOperations.kt │ │ │ ├── ToxiproxyExtension.kt │ │ │ └── ToxiproxyContainer.kt │ │ ├── testcontainers │ │ │ └── LogRecorder.kt │ │ ├── sharing │ │ │ ├── ContainerPool.kt │ │ │ ├── BeforeAndAfterAllOnce.kt │ │ │ └── ContainerExtension.kt │ │ ├── consul │ │ │ ├── ConsulExtension.kt │ │ │ ├── ConsulConfig.kt │ │ │ └── ConsulSetup.kt │ │ ├── envoycontrol │ │ │ └── EnvoyControlClusteredExtension.kt │ │ └── ClientsFactory.kt │ │ ├── assertions │ │ ├── AwaitAssertions.kt │ │ ├── HttpsEchoResponseAssertions.kt │ │ └── ResponseAssertions.kt │ │ ├── routing │ │ └── ServiceTagsAndCanaryTest.kt │ │ ├── LuaTest.kt │ │ ├── reliability │ │ ├── EnvoyControlDownTest.kt │ │ ├── LocalConsulAgentToMasterCutOff.kt │ │ └── NoConsulLeaderTest.kt │ │ ├── OriginalDestinationTest.kt │ │ ├── EnvoyControlV3SmokeTest.kt │ │ ├── HealthIndicatorTest.kt │ │ ├── StatusRouteTest.kt │ │ ├── LocalServiceCustomHealthCheckRouteTest.kt │ │ ├── LocalServiceTest.kt │ │ └── AddUpstreamHeaderTest.kt └── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── workflows │ ├── markdown-links-config.json │ ├── ci-min-envoy-version.yaml │ ├── markdown-links-check.yaml │ ├── changelog.yaml │ ├── publish.yaml │ ├── flaky.yaml │ ├── resilence.yaml │ └── ci.yaml └── dependabot.yaml ├── envoy-control-services ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── pl │ │ └── allegro │ │ └── tech │ │ └── servicemesh │ │ └── envoycontrol │ │ └── services │ │ ├── transformers │ │ ├── ServiceInstancesTransformer.kt │ │ ├── InvalidPortFilter.kt │ │ ├── EmptyAddressFilter.kt │ │ ├── RegexServiceInstancesFilter.kt │ │ ├── IpAddressFilter.kt │ │ └── InstanceMerger.kt │ │ ├── ClusterStateChanges.kt │ │ ├── ClusterState.kt │ │ ├── ServiceInstances.kt │ │ └── ServicesState.kt │ └── test │ └── kotlin │ └── pl │ └── allegro │ └── tech │ └── servicemesh │ └── envoycontrol │ └── services │ └── MultiClusterStateTest.kt ├── envoy-control-runner ├── src │ └── main │ │ ├── resources │ │ ├── application-docker.yaml │ │ ├── application-local.yaml │ │ ├── logback.xml │ │ └── application.yaml │ │ └── kotlin │ │ └── pl │ │ └── allegro │ │ └── tech │ │ └── servicemesh │ │ └── envoycontrol │ │ ├── infrastructure │ │ ├── consul │ │ │ └── JacksonConfig.kt │ │ └── health │ │ │ └── EnvoyControlHealthIndicator.kt │ │ ├── chaos │ │ ├── storage │ │ │ ├── ChaosDataStore.kt │ │ │ └── SimpleChaosDataStore.kt │ │ └── domain │ │ │ └── ChaosService.kt │ │ ├── EnvoyControl.kt │ │ └── synchronization │ │ ├── StateController.kt │ │ └── RestTemplateControlPlaneClient.kt └── build.gradle ├── envoy-control-core ├── src │ ├── test │ │ └── kotlin │ │ │ └── pl │ │ │ └── allegro │ │ │ └── tech │ │ │ └── servicemesh │ │ │ └── envoycontrol │ │ │ ├── utils │ │ │ ├── MockitoUtils.kt │ │ │ ├── EndpointsOperations.kt │ │ │ ├── TestData.kt │ │ │ └── ClusterOperations.kt │ │ │ ├── snapshot │ │ │ └── resource │ │ │ │ └── listeners │ │ │ │ └── filters │ │ │ │ ├── SanUriMatcherFactoryTest.kt │ │ │ │ └── TcpProxyFilterFactoryTest.kt │ │ │ ├── ControlPlaneTest.kt │ │ │ └── metrics │ │ │ └── ThreadPoolMetricTest.kt │ └── main │ │ ├── kotlin │ │ └── pl │ │ │ └── allegro │ │ │ └── tech │ │ │ └── servicemesh │ │ │ └── envoycontrol │ │ │ ├── protocol │ │ │ └── HttpMethod.kt │ │ │ ├── synchronization │ │ │ ├── ControlPlaneInstanceFetcher.kt │ │ │ ├── ControlPlaneClient.kt │ │ │ ├── SyncProperties.kt │ │ │ └── RemoteClusterStateChanges.kt │ │ │ ├── snapshot │ │ │ ├── resource │ │ │ │ ├── routes │ │ │ │ │ ├── AuthorizationRoute.kt │ │ │ │ │ ├── RoutesMatchers.kt │ │ │ │ │ └── CustomRoutesFactory.kt │ │ │ │ ├── SnapshotUtils.kt │ │ │ │ └── listeners │ │ │ │ │ └── filters │ │ │ │ │ ├── TcpProxyFilterFactory.kt │ │ │ │ │ ├── EnvoyHttpFilters.kt │ │ │ │ │ └── DynamicForwardProxyFilter.kt │ │ │ ├── SnapshotChangeAuditor.kt │ │ │ └── GlobalSnapshot.kt │ │ │ ├── server │ │ │ ├── ReadinessStateHandler.kt │ │ │ └── ServerProperties.kt │ │ │ ├── Logger.kt │ │ │ ├── utils │ │ │ └── Ports.kt │ │ │ ├── EnvoyControlProperties.kt │ │ │ └── EnvoyControlMetrics.kt │ │ ├── resources │ │ └── lua │ │ │ ├── ingress_current_zone_header.lua │ │ │ ├── egress_auto_service_tags.lua │ │ │ ├── ingress_service_tag_preference.lua │ │ │ ├── ingress_client_name_header.lua │ │ │ └── egress_service_tag_preference.lua │ │ └── java │ │ └── pl │ │ └── allegro │ │ └── tech │ │ └── servicemesh │ │ └── envoycontrol │ │ └── v3 │ │ ├── SimpleCache.java │ │ └── SimpleCacheNoInitialResourcesHandling.java └── build.gradle ├── settings.gradle ├── readme.md ├── .gitignore ├── envoy-control-source-consul ├── src │ ├── main │ │ └── kotlin │ │ │ └── pl │ │ │ └── allegro │ │ │ └── tech │ │ │ └── servicemesh │ │ │ └── envoycontrol │ │ │ └── consul │ │ │ ├── services │ │ │ ├── ServiceWatchPolicy.kt │ │ │ └── ConsulLocalClusterStateChanges.kt │ │ │ ├── ConsulProperties.kt │ │ │ └── synchronization │ │ │ └── SimpleConsulInstanceFetcher.kt │ └── test │ │ └── kotlin │ │ └── pl │ │ └── allegro │ │ └── tech │ │ └── servicemesh │ │ └── envoycontrol │ │ └── consul │ │ └── services │ │ ├── ServiceWatchPolicyTest.kt │ │ └── ConsulClusterStateChangesDisposeTest.kt └── build.gradle └── mkdocs.yml /tools/envoy/ingress-access.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material==4.4.0 2 | -------------------------------------------------------------------------------- /docs/changelog_symlink.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md 2 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/ratelimit_config.yaml: -------------------------------------------------------------------------------- 1 | domain: rl 2 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/root-ca.srl: -------------------------------------------------------------------------------- 1 | F53201FA4B19BB79 2 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/root-ca2.srl: -------------------------------------------------------------------------------- 1 | AF0C0F60E33E9054 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | githubUsername="" 3 | githubPassword="" 4 | -------------------------------------------------------------------------------- /docs/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/envoy-control/HEAD/docs/assets/images/logo.png -------------------------------------------------------------------------------- /tools/run-with-local-ec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose up --no-deps --build consul envoy http-echo 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/envoy-control/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/workflows/markdown-links-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^http://localhost" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /docs/assets/images/high_level_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/envoy-control/HEAD/docs/assets/images/high_level_architecture.png -------------------------------------------------------------------------------- /docs/assets/images/envoy-control-modules-drawing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/envoy-control/HEAD/docs/assets/images/envoy-control-modules-drawing.png -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/consul-low-rpc-rate.json: -------------------------------------------------------------------------------- 1 | { 2 | "limits": { 3 | "rpc_rate": 1, 4 | "rpc_max_burst": 1 5 | } 6 | } -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension: -------------------------------------------------------------------------------- 1 | pl.allegro.tech.servicemesh.envoycontrol.config.envoy.HttpResponseCloserExtension 2 | -------------------------------------------------------------------------------- /envoy-control-services/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib' 3 | api group: 'io.projectreactor', name: 'reactor-core' 4 | } 5 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/resources/application-docker.yaml: -------------------------------------------------------------------------------- 1 | envoy-control: 2 | source: 3 | consul: 4 | host: consul 5 | port: 8500 6 | 7 | chaos: 8 | username: "user" 9 | password: "pass" -------------------------------------------------------------------------------- /envoy-control-runner/src/main/resources/application-local.yaml: -------------------------------------------------------------------------------- 1 | envoy-control: 2 | source: 3 | consul: 4 | host: localhost 5 | port: 18500 6 | 7 | chaos: 8 | username: "user" 9 | password: "pass" 10 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/utils/MockitoUtils.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.utils 2 | 3 | import org.mockito.Mockito 4 | 5 | fun any(type: Class): T = Mockito.any(type) 6 | -------------------------------------------------------------------------------- /.github/workflows/ci-min-envoy-version.yaml: -------------------------------------------------------------------------------- 1 | name: CI - min envoy version 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | ci: 8 | uses: ./.github/workflows/ci.yaml 9 | with: 10 | envoyVersion: min 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/HttpContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | interface HttpContainer: ServiceContainer { 4 | fun httpPort(): Int 5 | } 6 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/protocol/HttpMethod.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.protocol 2 | 3 | enum class HttpMethod { 4 | GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, CONNECT, PATCH 5 | } 6 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/oauth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk add openjdk11-jre git 3 | RUN git clone --depth 1 --branch 0.0.1 https://github.com/allegro/oauth-mock.git 4 | WORKDIR ./oauth-mock 5 | RUN ./gradlew installDist 6 | CMD ["./build/install/oauth-mock/bin/oauth-mock"] 7 | -------------------------------------------------------------------------------- /tools/service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mendhak/http-https-echo 2 | USER root 3 | RUN apk add --update \ 4 | curl \ 5 | && rm -rf /var/cache/apk/* 6 | WORKDIR / 7 | COPY register_and_run.sh /register_and_run.sh 8 | RUN chmod a+x /register_and_run.sh 9 | ENTRYPOINT ["/register_and_run.sh"] 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/synchronization/ControlPlaneInstanceFetcher.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.synchronization 2 | 3 | import java.net.URI 4 | 5 | interface ControlPlaneInstanceFetcher { 6 | fun instances(cluster: String): List 7 | } 8 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/ServiceContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | interface ServiceContainer { 4 | 5 | fun ipAddress(): String 6 | 7 | fun port(): Int 8 | 9 | fun start() 10 | 11 | fun stop() 12 | } 13 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/AuthorizationRoute.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes 2 | 3 | import io.envoyproxy.envoy.config.route.v3.Route 4 | 5 | class AuthorizationRoute( 6 | val authorized: Route, 7 | val unauthorized: Route 8 | ) 9 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'envoy-control' 2 | 3 | include 'envoy-control-services' 4 | include 'envoy-control-core' 5 | include 'envoy-control-runner' 6 | include 'envoy-control-tests' 7 | include 'envoy-control-source-consul' 8 | 9 | dependencyResolutionManagement { 10 | repositories { 11 | mavenCentral() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/UpstreamService.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.ResponseWithBody 4 | 5 | interface UpstreamService { 6 | fun id(): String 7 | fun isSourceOf(response: ResponseWithBody): Boolean 8 | } 9 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/ServiceExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 4 | 5 | interface ServiceExtension : BeforeAndAfterAllOnce { 6 | 7 | fun container(): T 8 | } 9 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/transformers/ServiceInstancesTransformer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services.transformers 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 4 | 5 | interface ServiceInstancesTransformer { 6 | fun transform(services: Sequence): Sequence 7 | } 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Envoy Control 2 | 3 | Envoy Control is a production-ready Control Plane for Service Mesh based on [Envoy Proxy](https://www.envoyproxy.io/) 4 | Data Plane that is platform agnostic. 5 | 6 | # Docs 7 | 8 | Full docs are hosted at https://envoy-control.readthedocs.io/en/latest/ 9 | 10 | # Quick start 11 | 12 | Quick start guide is located at https://envoy-control.readthedocs.io/en/latest/quickstart 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij Idea project files 2 | .idea/* 3 | *.iml 4 | *.ipr 5 | *.iws 6 | 7 | # gradle config 8 | .gradle 9 | 10 | # project binaries 11 | build 12 | out 13 | classes 14 | 15 | # sonar 16 | sonar-project.properties 17 | .sonar 18 | 19 | # mac os x 20 | .DS_Store 21 | 22 | # netbeans 23 | .nb-gradle 24 | 25 | /config 26 | !/config/detekt/ 27 | !/config/detekt/* 28 | 29 | deployment.yml 30 | 31 | tools/tmp 32 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/synchronization/ControlPlaneClient.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.synchronization 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServicesState 4 | import java.net.URI 5 | import java.util.concurrent.CompletableFuture 6 | 7 | interface ControlPlaneClient { 8 | fun getState(uri: URI): CompletableFuture 9 | } 10 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/server/ReadinessStateHandler.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.server 2 | 3 | interface ReadinessStateHandler { 4 | fun ready() 5 | fun unready() 6 | } 7 | 8 | object NoopReadinessStateHandler: ReadinessStateHandler { 9 | override fun ready() { 10 | return 11 | } 12 | 13 | override fun unready() { 14 | return 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server.port: 8080 2 | 3 | application: 4 | name: envoy-control 5 | 6 | envoy-control: 7 | source: 8 | consul: 9 | host: localhost 10 | 11 | chaos: 12 | username: "user" 13 | password: "pass" 14 | 15 | management: 16 | endpoint: 17 | metrics.enabled: true 18 | prometheus.enabled: true 19 | endpoints.web.exposure.include: "*" 20 | prometheus.metrics.export.enabled: true 21 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/HttpResponseCloserExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.envoy 2 | 3 | import org.junit.jupiter.api.extension.AfterEachCallback 4 | import org.junit.jupiter.api.extension.ExtensionContext 5 | 6 | class HttpResponseCloserExtension : AfterEachCallback { 7 | override fun afterEach(context: ExtensionContext?) { 8 | HttpResponseCloser.closeResponses() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/infrastructure/consul/JacksonConfig.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.infrastructure.consul 2 | 3 | import com.fasterxml.jackson.module.kotlin.KotlinModule 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | 7 | @Configuration 8 | open class JacksonConfig { 9 | 10 | @Bean 11 | fun kotlinModule() = KotlinModule.Builder().build() 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/keeping-your-dependencies-updated-automatically 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gradle" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | open-pull-requests-limit: 5 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every week 13 | interval: "weekly" 14 | 15 | 16 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/resources/lua/ingress_current_zone_header.lua: -------------------------------------------------------------------------------- 1 | function envoy_on_request(handle) 2 | end 3 | 4 | function envoy_on_response(handle) 5 | local traffic_splitting_zone_header_name = handle:metadata():get("traffic_splitting_zone_header_name") or "" 6 | local current_zone = handle:metadata():get("current_zone") or "" 7 | if traffic_splitting_zone_header_name == "" then 8 | return 9 | end 10 | handle:headers():add(traffic_splitting_zone_header_name, current_zone) 11 | end 12 | -------------------------------------------------------------------------------- /tools/envoy-control/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | START_ARGUMENTS="" 4 | CONFIG_FILE=/etc/envoy-control/application.yaml 5 | if [ -f "$CONFIG_FILE" ]; then 6 | START_ARGUMENTS="--spring.config.location=file:$CONFIG_FILE " 7 | fi 8 | 9 | if [ ! -z "${ENVOY_CONTROL_PROPERTIES}" ]; then 10 | START_ARGUMENTS="$START_ARGUMENTS $ENVOY_CONTROL_PROPERTIES" 11 | fi 12 | 13 | echo "Launching Envoy-control with $START_ARGUMENTS" 14 | /bin/envoy-control/envoy-control-runner/bin/envoy-control-runner $START_ARGUMENTS 15 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/chaos/storage/ChaosDataStore.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.chaos.storage 2 | 3 | interface ChaosDataStore { 4 | fun save(item: NetworkDelay): NetworkDelay 5 | fun get(): List 6 | fun delete(id: String) 7 | } 8 | 9 | data class NetworkDelay( 10 | val id: String, 11 | val affectedService: String, 12 | val delay: String, 13 | val duration: String, 14 | val targetService: String 15 | ) 16 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/java/pl/allegro/tech/servicemesh/envoycontrol/v3/SimpleCache.java: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.v3; 2 | 3 | import io.envoyproxy.controlplane.cache.NodeGroup; 4 | import io.envoyproxy.controlplane.cache.v3.Snapshot; 5 | 6 | public class SimpleCache extends pl.allegro.tech.servicemesh.envoycontrol.SimpleCache { 7 | public SimpleCache(NodeGroup nodeGroup, Boolean shouldSendMissingEndpoints) { 8 | super(nodeGroup, shouldSendMissingEndpoints); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/oauth/invalid_jwks_token: -------------------------------------------------------------------------------- 1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjE2MTkxMDEzNTcsImp0aSI6IjgzNmE4NGY2LTcxMGQtNDkzNC1iYzRiLTE3OGY2MWYwYjQyMSIsImNsaWVudF9pZCI6ImNsaWVudCJ9.f6GF_7KCHxhyLwmieQkDIKI3an-pjkzkveBGhIpGUd_ZUfzTYsgXQY0U2WMdmpaEOcJk9qStxSDTBvGxlEvGeEjiUjZ5dB1ByXTp3xupOyc0sehjxf4VYh9d9LD2sAx-RbSogCM-grJMDquY7oDrJ2evbRLoBU46V8Roq-pFahN-Hh4sErxbBWh_HfSZXrb-K48VbjTQlY_xM82aA2_PBpchsURn0mYlxep4L8PVZcfprQDlFd2Dh8FyXbhEAhO1ciYgPpSE7neaKrnS3z9Zy1fIUGZMkU2RzI5rxyK-0E5wPiEouISuQ3lPAiu8cekJFrACUfSdDJTBRKZpzgeM7g -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/ClusterStateChanges.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services 2 | 3 | import reactor.core.publisher.Flux 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | interface ClusterStateChanges { 7 | fun stream(): Flux 8 | } 9 | 10 | interface LocalClusterStateChanges : ClusterStateChanges { 11 | val latestServiceState: AtomicReference 12 | fun isInitialStateLoaded(): Boolean 13 | } 14 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/transformers/InvalidPortFilter.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services.transformers 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 4 | 5 | class InvalidPortFilter : ServiceInstancesTransformer { 6 | 7 | override fun transform(services: Sequence): Sequence = 8 | services.map { serviceInstances -> serviceInstances.withoutInvalidPortInstances() } 9 | } 10 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/ResponseWithBody.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.envoy 2 | 3 | import okhttp3.Response 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.UpstreamService 5 | 6 | data class ResponseWithBody(val response: Response) { 7 | val body = response.use { it.body?.string() } ?: "" 8 | fun isFrom(upstreamService: UpstreamService) = upstreamService.isSourceOf(this) 9 | fun isOk() = response.isSuccessful 10 | } 11 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/transformers/EmptyAddressFilter.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services.transformers 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 4 | 5 | class EmptyAddressFilter : ServiceInstancesTransformer { 6 | 7 | override fun transform(services: Sequence): Sequence = 8 | services.map { serviceInstances -> serviceInstances.withoutEmptyAddressInstances() } 9 | } 10 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotChangeAuditor.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot 2 | 3 | import reactor.core.publisher.Mono 4 | 5 | interface SnapshotChangeAuditor { 6 | fun audit(previousUpdateResult: UpdateResult, actualUpdateResult: UpdateResult): Mono 7 | } 8 | 9 | object NoopSnapshotChangeAuditor: SnapshotChangeAuditor { 10 | override fun audit(previousUpdateResult: UpdateResult, actualUpdateResult: UpdateResult): Mono { 11 | return Mono.empty() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/markdown-links-check.yaml: -------------------------------------------------------------------------------- 1 | name: Markdown Links Check 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * MON" # runs every monday at 8 am 6 | workflow_dispatch: 7 | push: 8 | paths: 9 | - 'docs/**' 10 | 11 | jobs: 12 | check-links: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: gaurav-nelson/github-action-markdown-link-check@v1 17 | with: 18 | use-verbose-mode: 'yes' 19 | folder-path: 'docs' 20 | config-file: '.github/workflows/markdown-links-config.json' 21 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/RoutesMatchers.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes 2 | 3 | import io.envoyproxy.envoy.config.route.v3.HeaderMatcher 4 | import pl.allegro.tech.servicemesh.envoycontrol.protocol.HttpMethod 5 | 6 | fun httpMethodMatcher(method: HttpMethod): HeaderMatcher = exactHeader(":method", method.name) 7 | 8 | fun exactHeader(name: String, value: String): HeaderMatcher = HeaderMatcher.newBuilder() 9 | .setName(name) 10 | .setExactMatch(value) 11 | .build() 12 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/chaos/storage/SimpleChaosDataStore.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.chaos.storage 2 | 3 | class SimpleChaosDataStore : ChaosDataStore { 4 | 5 | private val dataStore: MutableMap = mutableMapOf() 6 | 7 | override fun save(item: NetworkDelay): NetworkDelay = item.also { dataStore[item.id] = item } 8 | override fun get(): List = dataStore.map { item -> item.value } 9 | override fun delete(id: String) { 10 | dataStore.remove(id) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/transformers/RegexServiceInstancesFilter.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services.transformers 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 4 | 5 | class RegexServiceInstancesFilter(private val excludedRegexes: Collection) : ServiceInstancesTransformer { 6 | 7 | override fun transform(services: Sequence): Sequence = 8 | services.filter { (serviceName, _) -> 9 | !excludedRegexes.any { serviceName.matches(it) } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/Logger.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | import kotlin.reflect.full.companionObject 6 | 7 | fun R.logger(): Lazy = lazy { LoggerFactory.getLogger(unwrapCompanionClass(this.javaClass).name) } 8 | 9 | fun unwrapCompanionClass(ofClass: Class): Class<*> { 10 | return if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) { 11 | ofClass.enclosingClass 12 | } else { 13 | ofClass 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/assertions/AwaitAssertions.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.assertions 2 | 3 | import org.assertj.core.api.Assertions 4 | import org.awaitility.Awaitility 5 | import java.time.Duration 6 | 7 | fun untilAsserted( 8 | poll: Duration = Duration.ofMillis(500), 9 | wait: Duration = Duration.ofSeconds(90), 10 | fn: () -> (T) 11 | ): T { 12 | var lastResult: T? = null 13 | Awaitility.await().atMost(wait) 14 | .pollInterval(poll) 15 | .untilAsserted { lastResult = fn() } 16 | Assertions.assertThat(lastResult).isNotNull 17 | return lastResult!! 18 | } 19 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/HttpResponseCloser.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.envoy 2 | 3 | import okhttp3.Response 4 | 5 | object HttpResponseCloser { 6 | 7 | private val responses = mutableListOf() 8 | 9 | fun closeResponses() { 10 | responses.forEach(this::closeResponse) 11 | responses.clear() 12 | } 13 | 14 | private fun closeResponse(response: Response) { 15 | runCatching { response.close() } 16 | } 17 | 18 | fun Response.addToCloseableResponses() : Response { 19 | responses.add(this) 20 | return this 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/synchronization/SyncProperties.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package pl.allegro.tech.servicemesh.envoycontrol.synchronization 4 | 5 | import java.time.Duration 6 | 7 | class SyncProperties { 8 | var enabled = false 9 | var pollingInterval: Long = 1 10 | var cacheDuration = Duration.ofMinutes(2) 11 | var connectionTimeout: Duration = Duration.ofMillis(1000) 12 | var readTimeout: Duration = Duration.ofMillis(500) 13 | var envoyControlAppName = "envoy-control" 14 | var combineServiceChangesExperimentalFlow = false 15 | var blackListedRemoteClusters: Set = setOf() 16 | } 17 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/java/pl/allegro/tech/servicemesh/envoycontrol/v3/SimpleCacheNoInitialResourcesHandling.java: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.v3; 2 | 3 | import io.envoyproxy.controlplane.cache.NodeGroup; 4 | import io.envoyproxy.controlplane.cache.v3.Snapshot; 5 | // TODO #920 - remove this class after deploying and testing on production 6 | public class SimpleCacheNoInitialResourcesHandling extends pl.allegro.tech.servicemesh.envoycontrol.SimpleCacheNoInitialResourcesHandling { 7 | public SimpleCacheNoInitialResourcesHandling(NodeGroup nodeGroup, Boolean shouldSendMissingEndpoints) { 8 | super(nodeGroup, shouldSendMissingEndpoints); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /envoy-control-source-consul/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/consul/services/ServiceWatchPolicy.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.consul.services 2 | 3 | interface ServiceWatchPolicy { 4 | fun shouldBeWatched(service: String, tags: List): Boolean 5 | } 6 | 7 | object NoOpServiceWatchPolicy : ServiceWatchPolicy { 8 | override fun shouldBeWatched(service: String, tags: List): Boolean = true 9 | } 10 | 11 | class TagBlacklistServiceWatchPolicy( 12 | private val blacklistedTags: List 13 | ) : ServiceWatchPolicy { 14 | override fun shouldBeWatched(service: String, tags: List): Boolean = 15 | blacklistedTags.any { tags.contains(it) }.not() 16 | } 17 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoyControl.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.springframework.boot.CommandLineRunner 4 | import org.springframework.boot.SpringApplication 5 | import org.springframework.boot.autoconfigure.SpringBootApplication 6 | 7 | @SpringBootApplication 8 | class EnvoyControl( 9 | val controlPlane: ControlPlane 10 | ) : CommandLineRunner { 11 | 12 | override fun run(vararg args: String?) { 13 | controlPlane.start() 14 | } 15 | 16 | companion object { 17 | @JvmStatic 18 | fun main(args: Array) { 19 | SpringApplication.run(EnvoyControl::class.java, *args) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/utils/Ports.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.utils 2 | 3 | import java.net.ServerSocket 4 | import pl.allegro.tech.servicemesh.envoycontrol.logger 5 | 6 | object Ports { 7 | private val usedPorts: MutableSet = mutableSetOf() 8 | val logger by logger() 9 | 10 | @Synchronized 11 | fun nextAvailable(): Int { 12 | var randomPort: Int 13 | do { 14 | randomPort = ServerSocket(0).use { it.localPort } 15 | } while (usedPorts.contains(randomPort)) 16 | usedPorts.add(randomPort) 17 | logger.info("Generated port: {}", randomPort) 18 | logger.info("Used ports: {}", usedPorts) 19 | 20 | return randomPort 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/host_ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # Figuring out the IP of the docker host machine is a convoluted process. On Mac we pull that value from the special 6 | # host.docker.internal hostname. Linux does not support that yet, so we have to use the routing table. 7 | # 8 | # See https://github.com/docker/for-linux/issues/264 to track host.docker.internal support on linux. 9 | # Update 2025-02: don't use `getent hosts` as it started returning ipv6 address that don't work (at least on macos) 10 | HOST_DOMAIN_IP="$(getent ahostsv4 host.docker.internal | head -n 1 | awk '{ print $1 }')" 11 | 12 | if [[ ! -z "${HOST_DOMAIN_IP}" ]]; then 13 | printf "${HOST_DOMAIN_IP}" 14 | else 15 | printf "$(ip route | awk '/default/ { print $3 }')" 16 | fi 17 | -------------------------------------------------------------------------------- /tools/service/register_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o pipefail 4 | set -o errexit 5 | 6 | port=80 7 | service_name=http-echo 8 | instance_id="${service_name}-1" 9 | 10 | echo "Registering instance of ${service_name} in consul" 11 | echo "=============================" 12 | echo 13 | echo 14 | 15 | ip="$(hostname -i)" 16 | 17 | body=' 18 | { 19 | "ID": "'${instance_id}'", 20 | "Name": "'${service_name}'", 21 | "Tags": [ 22 | "primary" 23 | ], 24 | "Address": "'${ip}'", 25 | "Port": '${port}', 26 | "Check": { 27 | "DeregisterCriticalServiceAfter": "90m", 28 | "http": "http://'${ip}:${port}'", 29 | "Interval": "10s" 30 | } 31 | } 32 | ' 33 | curl -X PUT --fail --data "${body}" -s consul:8500/v1/agent/service/register 34 | 35 | cd /app 36 | node ./index.js 37 | 38 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/transformers/IpAddressFilter.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services.transformers 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 4 | 5 | /** 6 | * TODO https://github.com/allegro/envoy-control/issues/9 7 | * Envoy & Envoy Control supports only IP and not hostnames 8 | */ 9 | class IpAddressFilter : ServiceInstancesTransformer { 10 | 11 | override fun transform(services: Sequence): Sequence = 12 | services.filter { (_, instances) -> 13 | instances.all { isIpAddress(it.address.orEmpty()) } 14 | } 15 | 16 | private fun isIpAddress(address: String): Boolean = address.all { !it.isLetter() } 17 | } 18 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/ClusterState.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services 2 | 3 | enum class Locality { 4 | LOCAL, REMOTE 5 | } 6 | 7 | data class ClusterState( 8 | val servicesState: ServicesState, 9 | val locality: Locality, 10 | val cluster: String 11 | ) 12 | 13 | data class MultiClusterState(private val l: List = listOf()) : Collection by l { 14 | 15 | constructor(state: ClusterState) : this(listOf(state)) 16 | 17 | companion object { 18 | fun empty() = MultiClusterState(emptyList()) 19 | fun ClusterState.toMultiClusterState() = MultiClusterState(this) 20 | fun List.toMultiClusterState() = MultiClusterState(this) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoyControlProperties.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package pl.allegro.tech.servicemesh.envoycontrol 4 | 5 | import pl.allegro.tech.servicemesh.envoycontrol.server.ServerProperties 6 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties 7 | import pl.allegro.tech.servicemesh.envoycontrol.synchronization.SyncProperties 8 | 9 | class EnvoyControlProperties { 10 | var server = ServerProperties() 11 | var envoy = EnvoyProperties() 12 | var sync = SyncProperties() 13 | var serviceFilters = ServiceFilters() 14 | } 15 | 16 | class EnvoyProperties { 17 | var snapshot = SnapshotProperties() 18 | } 19 | 20 | class ServiceFilters { 21 | var excludedNamesPatterns: List = emptyList() 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-source-consul/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':envoy-control-core') 3 | 4 | implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib' 5 | implementation group: 'io.projectreactor', name: 'reactor-core' 6 | api group: 'pl.allegro.tech.discovery', name: 'consul-recipes', version: versions.consul_recipes 7 | api group: 'com.ecwid.consul', name: 'consul-api', version: versions.ecwid_consul 8 | 9 | testImplementation group: 'org.mockito', name: 'mockito-core' 10 | testImplementation group: 'net.bytebuddy', name: 'byte-buddy', version: versions.bytebuddy 11 | 12 | testImplementation group: 'io.projectreactor', name: 'reactor-test' 13 | testImplementation group: 'org.testcontainers', name: 'testcontainers' 14 | testImplementation project(path: ':envoy-control-tests') 15 | } 16 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/RedisContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.testcontainers.containers.Network 4 | import org.testcontainers.containers.wait.strategy.Wait 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 6 | 7 | class RedisContainer: GenericContainer("redis:alpine"), ServiceContainer { 8 | 9 | override fun configure() { 10 | super.configure() 11 | withExposedPorts(PORT) 12 | withNetwork(Network.SHARED) 13 | waitingFor(Wait.forListeningPort()) 14 | } 15 | 16 | fun address(): String = "${ipAddress()}:$PORT" 17 | 18 | override fun port() = PORT 19 | 20 | companion object { 21 | const val PORT = 6379 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/infrastructure/health/EnvoyControlHealthIndicator.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.infrastructure.health 2 | 3 | import org.springframework.boot.actuate.health.AbstractHealthIndicator 4 | import org.springframework.boot.actuate.health.Health 5 | import org.springframework.stereotype.Component 6 | import pl.allegro.tech.servicemesh.envoycontrol.services.LocalClusterStateChanges 7 | 8 | @Component 9 | class EnvoyControlHealthIndicator(private val localClusterStateChanges: LocalClusterStateChanges) 10 | : AbstractHealthIndicator() { 11 | override fun doHealthCheck(builder: Health.Builder?) { 12 | if (localClusterStateChanges.isInitialStateLoaded()) { 13 | builder!!.up() 14 | } else { 15 | builder!!.down() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/SanUriMatcherFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.TlsAuthenticationProperties 6 | 7 | internal class SanUriMatcherFactoryTest { 8 | 9 | @Test 10 | fun `should create SAN URI wildcard matcher regex for lua`() { 11 | // when 12 | val factory = SanUriMatcherFactory(TlsAuthenticationProperties().also { 13 | it.sanUriFormat = "spiffe://{service-name}?env=dev" 14 | it.serviceNameWildcardRegex = ".+" 15 | }) 16 | 17 | // then 18 | assertThat(factory.sanUriWildcardRegexForLua).isEqualTo("""^spiffe://(.+)%?env=dev$""") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/HttpsEchoExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.ResponseWithBody 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.ContainerExtension 6 | 7 | class HttpsEchoExtension : ContainerExtension(), ServiceExtension, UpstreamService { 8 | override fun id(): String = container.id() 9 | override fun isSourceOf(response: ResponseWithBody): Boolean = container.isSourceOf(response) 10 | override val container: HttpsEchoContainer = HttpsEchoContainer() 11 | override fun container(): HttpsEchoContainer = container 12 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 13 | } 14 | -------------------------------------------------------------------------------- /docs/assets/extra.js: -------------------------------------------------------------------------------- 1 | // source: https://github.com/squidfunk/mkdocs-material/issues/767 2 | document.addEventListener("DOMContentLoaded", function() { 3 | load_navpane(); 4 | }); 5 | 6 | function load_navpane() { 7 | var width = window.innerWidth; 8 | if (width <= 1200) { 9 | return; 10 | } 11 | 12 | var nav = document.getElementsByClassName("md-nav"); 13 | for(var i = 0; i < nav.length; i++) { 14 | if (typeof nav.item(i).style === "undefined") { 15 | continue; 16 | } 17 | 18 | if (nav.item(i).getAttribute("data-md-level") && nav.item(i).getAttribute("data-md-component")) { 19 | nav.item(i).style.display = 'block'; 20 | nav.item(i).style.overflow = 'visible'; 21 | } 22 | } 23 | 24 | var nav = document.getElementsByClassName("md-nav__toggle"); 25 | for(var i = 0; i < nav.length; i++) { 26 | nav.item(i).checked = true; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/containers/SSLGenericContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.containers 2 | 3 | import org.testcontainers.containers.BindMode 4 | import org.testcontainers.images.builder.dockerfile.DockerfileBuilder 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 6 | 7 | open class SSLGenericContainer>( 8 | dockerfileBuilder: DockerfileBuilder, 9 | private val sslDir: String = "testcontainers/ssl/", 10 | private val sslDirDestination: String = "/app/" 11 | ) : GenericContainer(dockerfileBuilder.statements) { 12 | constructor(dockerImageName: String) : this(DockerfileBuilder().from(dockerImageName)) 13 | 14 | override fun configure() { 15 | super.configure() 16 | withClasspathResourceMapping(sslDir, sslDirDestination, BindMode.READ_ONLY) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/SnapshotUtils.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.groups.IncomingRateLimitEndpoint 4 | import java.math.BigInteger 5 | import java.security.MessageDigest 6 | 7 | fun getRuleId(serviceName: String, endpoint: IncomingRateLimitEndpoint): String { 8 | val methods = endpoint.methods.sorted().joinToString() 9 | val clients = endpoint.clients.sorted().joinToString(transform = { "${it.name},${it.selector}" }) 10 | val key = "$serviceName,${endpoint.path},${endpoint.pathMatchingType},$methods,$clients" 11 | 12 | return "${serviceName.replace("-", "_")}_${key.md5()}" 13 | } 14 | 15 | @Suppress("MagicNumber") 16 | private fun String.md5(): String { 17 | val md = MessageDigest.getInstance("MD5") 18 | return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0') 19 | } 20 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/containers/ProxyOperations.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.containers 2 | 3 | import okhttp3.HttpUrl.Companion.toHttpUrl 4 | import okhttp3.OkHttpClient 5 | import okhttp3.Request 6 | import okhttp3.Response 7 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.HttpResponseCloser.addToCloseableResponses 8 | import java.time.Duration 9 | 10 | class ProxyOperations(val address: String) { 11 | private val client = OkHttpClient.Builder() 12 | .readTimeout(Duration.ofSeconds(20)) 13 | .build() 14 | 15 | fun call(pathAndQuery: String): Response { 16 | return client.newCall( 17 | Request.Builder() 18 | .get() 19 | .url(address.toHttpUrl().newBuilder(pathAndQuery)!!.build()) 20 | .build() 21 | ) 22 | .execute().addToCloseableResponses() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /envoy-control-runner/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | } 4 | 5 | mainClassName = 'pl.allegro.tech.servicemesh.envoycontrol.EnvoyControl' 6 | 7 | dependencies { 8 | api project(':envoy-control-source-consul') 9 | 10 | implementation group: 'org.springframework.boot', name: 'spring-boot-starter' 11 | api group: 'org.springframework.boot', name: 'spring-boot-starter-web' 12 | api group: 'org.springframework.boot', name: 'spring-boot-starter-actuator' 13 | api group: 'org.springframework.boot', name: 'spring-boot-starter-security' 14 | implementation group: 'io.micrometer', name: 'micrometer-registry-prometheus' 15 | 16 | implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin' 17 | implementation group: 'net.openhft', name: 'zero-allocation-hashing', version: versions.xxhash 18 | } 19 | 20 | test { 21 | maxParallelForks = 1 22 | useJUnitPlatform() 23 | } 24 | 25 | run { 26 | systemProperties.putIfAbsent("spring.profiles.active", "local") 27 | } 28 | 29 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/GenericServiceExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 5 | import pl.allegro.tech.servicemesh.envoycontrol.logger 6 | 7 | open class GenericServiceExtension(private val container: T) : ServiceExtension { 8 | 9 | private val logger by logger() 10 | 11 | override fun container() = container 12 | 13 | override fun beforeAllOnce(context: ExtensionContext) { 14 | logger.info("Generic service is starting.") 15 | container.start() 16 | logger.info("Generic service extension started.") 17 | } 18 | 19 | override fun afterAllOnce(context: ExtensionContext) { 20 | container.stop() 21 | } 22 | 23 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yaml: -------------------------------------------------------------------------------- 1 | name: Check changelog 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '.github/**' 7 | 8 | jobs: 9 | changelog: 10 | name: Check changelog 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | ref: ${{ github.head_ref }} 18 | 19 | - uses: jitterbit/get-changed-files@v1 20 | id: files 21 | continue-on-error: true 22 | 23 | - name: Printing modified files 24 | run: | 25 | echo "Added+Modified:" 26 | echo "${{ steps.files.outputs.added_modified }}" 27 | - name: Check if changelog is updated. 28 | run: | 29 | for changed_file in ${{ steps.files.outputs.added_modified }}; do 30 | if [ ${changed_file} == "CHANGELOG.md" ] 31 | then 32 | exit 0 33 | fi 34 | done 35 | echo "::error:: Changelog not present in modified files" 36 | exit 1 37 | 38 | 39 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/CallStats.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.envoy 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.UpstreamService 4 | 5 | class CallStats(private val upstreamServices: List) { 6 | var failedHits: Int = 0 7 | var totalHits: Int = 0 8 | 9 | private var containerHits: MutableMap = 10 | upstreamServices.associate { it.id() to 0 }.toMutableMap() 11 | 12 | fun hits(upstreamService: UpstreamService) = containerHits[upstreamService.id()] ?: 0 13 | 14 | fun addResponse(response: ResponseWithBody) { 15 | upstreamServices 16 | .firstOrNull { it.isSourceOf(response) } 17 | .let { it ?: if (response.isOk()) throw AssertionError("response from unknown instance") else null } 18 | ?.let { containerHits.compute(it.id()) { _, i -> i?.inc() } } 19 | if (!response.isOk()) failedHits++ 20 | totalHits++ 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/synchronization/StateController.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.synchronization 2 | 3 | import org.springframework.web.bind.annotation.GetMapping 4 | import org.springframework.web.bind.annotation.PathVariable 5 | import org.springframework.web.bind.annotation.RestController 6 | import pl.allegro.tech.servicemesh.envoycontrol.services.LocalClusterStateChanges 7 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 8 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServicesState 9 | 10 | @RestController 11 | class StateController(val localClusterStateChanges: LocalClusterStateChanges) { 12 | 13 | @GetMapping("/state") 14 | fun getState(): ServicesState = localClusterStateChanges.latestServiceState.get() 15 | 16 | @GetMapping("/state/{serviceName}") 17 | fun getStateByServiceName(@PathVariable("serviceName") serviceName: String): ServiceInstances? = 18 | localClusterStateChanges.latestServiceState.get()[serviceName] 19 | } 20 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/testcontainers/LogRecorder.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers 2 | 3 | import org.testcontainers.containers.output.BaseConsumer 4 | import org.testcontainers.containers.output.OutputFrame 5 | 6 | class LogRecorder : BaseConsumer() { 7 | 8 | private var recorder: (frame: OutputFrame?) -> Unit = {} 9 | private var recordedLogs: MutableList = mutableListOf() 10 | 11 | override fun accept(frame: OutputFrame?) { 12 | recorder(frame) 13 | } 14 | 15 | fun recordLogs(logPredicate: (line: String) -> Boolean) { 16 | recorder = { frame -> 17 | val line = frame?.utf8String ?: "" 18 | if (logPredicate(line)) { 19 | recordedLogs.add(line) 20 | } 21 | } 22 | } 23 | 24 | fun stopRecording() { 25 | recorder = {} 26 | recordedLogs = mutableListOf() 27 | } 28 | 29 | fun getRecordedLogs(): List = recordedLogs 30 | } 31 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/resources/lua/egress_auto_service_tags.lua: -------------------------------------------------------------------------------- 1 | function envoy_on_request(handle) 2 | rejectServiceTagDuplicatingAutoServiceTag(handle) 3 | end 4 | 5 | function rejectServiceTagDuplicatingAutoServiceTag(handle) 6 | local autoServiceTagPreference = handle:metadata():get("auto_service_tag_preference") 7 | if autoServiceTagPreference == nil then 8 | return 9 | end 10 | local requestServiceTag = (handle:streamInfo():dynamicMetadata():get("envoy.lb") or {})["%SERVICE_TAG_METADATA_KEY%"] 11 | if (requestServiceTag == nil) then 12 | return 13 | end 14 | 15 | for i = 1, #autoServiceTagPreference do 16 | if requestServiceTag == autoServiceTagPreference[i] then 17 | local message = "Request service-tag '" .. requestServiceTag .. "' duplicates auto service-tag preference. " 18 | .. "Remove service-tag parameter from the request" 19 | handle:respond({ [":status"] = 400 }, message) 20 | end 21 | end 22 | end 23 | 24 | function envoy_on_response(handle) 25 | end 26 | -------------------------------------------------------------------------------- /envoy-control-source-consul/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/consul/ConsulProperties.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package pl.allegro.tech.servicemesh.envoycontrol.consul 4 | 5 | import java.time.Duration 6 | 7 | class ConsulProperties { 8 | var host: String = "localhost" 9 | var port = 8500 10 | var subscriptionDelay: Duration = Duration.ofMillis(20) // max 50 subscription/s 11 | var watcher = ConsulWatcherOkHttpProperties() 12 | var tags = TagsProperties() 13 | var blacklist = BlacklistProperties() 14 | } 15 | 16 | class ConsulWatcherOkHttpProperties { 17 | var readTimeout: Duration = Duration.ofMinutes(6) 18 | var connectTimeout: Duration = Duration.ofSeconds(2) 19 | var dispatcherMaxPoolSize = 2000 20 | var dispatcherPoolKeepAliveTime: Duration = Duration.ofSeconds(30) 21 | } 22 | 23 | class TagsProperties { 24 | var weight = "weight" 25 | var defaultWeight = 50 26 | var canary = "canary" 27 | } 28 | 29 | class BlacklistProperties { 30 | var serviceTags: List = listOf() 31 | } 32 | -------------------------------------------------------------------------------- /tools/envoy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM envoyproxy/envoy:v1.34.0 2 | 3 | ENV PORT=9999:9999 4 | ENV PORT=80:80 5 | ENV ENVOY_NODE_ID=front-proxy-id 6 | ENV ENVOY_NODE_CLUSTER=front-proxy 7 | ENV ENVOY_EGRESS_LISTENER_PORT=31000 8 | ENV ENVOY_INGRESS_LISTENER_PORT=31001 9 | ENV ENVOY_ADMIN_PORT=9999 10 | ENV ENVOY_XDS_PORT=50000 11 | ENV ENVOY_XDS_HOST=host.docker.internal 12 | 13 | ADD envoy-template.yaml /etc/envoy/envoy.yaml 14 | ADD ingress-access.log /home/envoy/ingress-access.log 15 | RUN sed -i "s/{{.EgressListenerPort}}/${ENVOY_EGRESS_LISTENER_PORT}/g" /etc/envoy/envoy.yaml 16 | RUN sed -i "s/{{.IngressListenerPort}}/${ENVOY_INGRESS_LISTENER_PORT}/g" /etc/envoy/envoy.yaml 17 | RUN sed -i "s/{{.XdsHost}}/${ENVOY_XDS_HOST}/g" /etc/envoy/envoy.yaml 18 | RUN sed -i "s/{{.XdsPort}}/${ENVOY_XDS_PORT}/g" /etc/envoy/envoy.yaml 19 | RUN sed -i "s/{{.AdminPort}}/${ENVOY_ADMIN_PORT}/g" /etc/envoy/envoy.yaml 20 | 21 | EXPOSE 80 443 9999 22 | 23 | RUN mkdir envoy 24 | 25 | CMD ["envoy", "-c", "/etc/envoy/envoy.yaml", "--service-cluster", "${ENVOY_NODE_CLUSTER}", "--service-node", "${ENVOY_NODE_ID}"] 26 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/sharing/ContainerPool.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.sharing 2 | 3 | import org.testcontainers.containers.GenericContainer 4 | import java.util.LinkedList 5 | import java.util.Queue 6 | 7 | class ContainerPool>(private val containerFactory: () -> CONTAINER) { 8 | 9 | private val freeContainers: Queue = LinkedList() 10 | private val usedContainers = mutableMapOf() 11 | 12 | fun acquire(owner: OWNER): CONTAINER { 13 | val container = freeContainers.poll() ?: containerFactory() 14 | container.start() 15 | usedContainers[owner] = container 16 | return container 17 | } 18 | 19 | fun release(owner: OWNER) { 20 | val container = usedContainers.remove(owner) ?: throw ContainerNotFound(owner.toString()) 21 | freeContainers.add(container) 22 | } 23 | 24 | class ContainerNotFound(owner: String) : RuntimeException("container owned by $owner not found") 25 | } 26 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/device-csr_echo2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICojCCAYoCAQAwXTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNV 3 | BAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRlY2ggSW5jMRcwFQYDVQQDDA5teS5l 4 | eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKFJNfI3 5 | 8hB2sloqR/ud2GEX4mpvRIt2gPnMHsbeoiaWppFm25VfKo5Zgnkkv5NwHmRcpmb4 6 | 3c5NLUTuCKQFojp35XByIVahB5qjDMnnic8/T/3Ijr264jJei6NJoGEhckprCKrJ 7 | cpVFYfIpesMe3s2HyJVKt2xdUX+eYPgtPpZb5GVZVnQciniGOed5uES+Vy+3DIdy 8 | aeiHpUTZNpBhopWgqUl/rS1qzEVyWq/1GyrGxeThBUHJbvrux2gdOFx6bralTfCx 9 | TqteEHE0Cv7NWyHoH0brKxV9TXg8vIat01it9nyUD3B9jNuI3JZbFkk6VilDlZsq 10 | dMAg8zhC7NKSOR8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAry80LPCPjvjYH 11 | h4QbrVbmSBLGkzznQ2rUH8ua/v9Csev7EUk5T8BPxZy49fEnQ7lXF97qj4YJ+v9s 12 | Oeq2n9oE0K5c7KnZ/pSOovVa0aOmkJmImZBQUmQB804z+rXC3tBYsCvOIWZG8wNc 13 | OOBkUrQ7PNLHiNjFZ0bSqeWxrWeDT7HK+/04N/Ooydf+a4ZiZgxAoP4ogD2LHWxr 14 | l3QFa2mofcv+WinbpbldiRYjFBF+QN8L/GUCFElJn0VrY28Z35TYfYYzD19fvHCm 15 | 8c/mBVQEcz5FL9hKNa5K6gWz003r0pFxaqAI4dhA2xnsuHZlv0U8y1IP7Uzbv8QY 16 | oQ1GzbQC 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/device-csr_echo3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICojCCAYoCAQAwXTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNV 3 | BAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRlY2ggSW5jMRcwFQYDVQQDDA5teS5l 4 | eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4hPhXy 5 | OK2iClN3LjRuB47Lpse2ThooQomO2e+e289WkapnO0VoxAjXE7UvBJPTvBeioTw0 6 | 9GuyVJxBrJGWz/2SibXzg0stzWFcMVWr7+pNXPOxVbheZY7FFml5/KfTL7Zy9pp3 7 | TSWXCqzW6iwkGJIkyfBMmMdZDCQAFKBJeY9yVMNSHYcF7rMPCijyD53QgCbVMchf 8 | edIz6UOeBAEE26SJah9KnAX5mlLNykISWH75vUCS7iiEEQXQ4KpvDXQR+vNzzPXK 9 | rQMe0aektC3dKtKKJtNzNzz7566wSP1irYUDNV3dwfuX9zgNgzQojgBNSsoRYI7M 10 | U5yWVQ8tWuCQfEECAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQCJJxRQ8xKnTWGk 11 | Y5z1R38dQDIBY6+33DKcoTEgi5fbW4piY+ok4vhIGIKS0dPmY2tQi6feYZhhb3fs 12 | bnf6Tmiau9P95goGMciyaMjpOMU2e0nbzUtyS13rALfOqdj+wOI36KnoUjbxZK42 13 | PYi+jaE6hmexgJdVW/MlXCyzcHPurgC7mqJToVwkAbgQTIm85i7U6O/wVJlmRgsa 14 | 1/WA9U/Wgrqf7WMeMSUAVOj8gm4EosuYrQBhi7P+Q/9f21U+wZE3lLVZCs+FLQKy 15 | k0KrhbFITAPW4RGd/h1wwNs81K6hSUtkl/Nm03nOwR8+V0ZMveg4bBA4a4tiYRjh 16 | NplIEbHa 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/RedirectServiceContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.testcontainers.containers.Network 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 5 | import java.util.UUID 6 | 7 | class RedirectServiceContainer( 8 | private val redirectTo: String 9 | ) : GenericContainer("schmunk42/nginx-redirect:latest"), ServiceContainer { 10 | 11 | val response = UUID.randomUUID().toString() 12 | 13 | override fun configure() { 14 | super.configure() 15 | withExposedPorts(PORT) 16 | withNetwork(Network.SHARED) 17 | withEnv(mapOf( 18 | "SERVER_REDIRECT" to redirectTo, 19 | "SERVER_REDIRECT_PATH" to "/", 20 | "SERVER_REDIRECT_CODE" to "302" 21 | )) 22 | } 23 | 24 | fun address(): String = "${ipAddress()}:$PORT" 25 | 26 | override fun port() = PORT 27 | 28 | companion object { 29 | const val PORT = 80 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/device-csr_root-ca2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICojCCAYoCAQAwXTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNV 3 | BAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRlY2ggSW5jMRcwFQYDVQQDDA5teS5l 4 | eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALdGDN6G 5 | UyxG1MyktuRoZrwJmvxP9o8X6OL9e4Ci2APx3z85T/ufKPUczgv+bVY4QouOBcY9 6 | K7K7sKezPVxC5A8STpnE+iF+vmSpi+5bA+syXnarNsINXFyRcPLCWgG6Tlk8x55d 7 | fLJOJf6+BD42xFRcrScFQb9bcZ6/id1ffdYVrtqT+IYinANG1RjoupOgTxgwovVU 8 | 0pGjHjLPUbHxWVajZYfvPMrO7t2jbgngCI0zmvy212xcr8cMWlCO0dgzgWWTDn73 9 | gMoYdl06sFmxq+dHlCLmMLkqeOXattXWbO84NzT6/VMelOSQSAKBBNgpRdVURMkM 10 | 0Bc/u/76UuAyy6UCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQCNlq7DrXWhQiJU 11 | pmW5uqzdNerVu1ADmeTYiaTHOwba54zud+AMdDopdUne9CMjtEiDTdPC84r1MyG7 12 | AJni4PAQl7SQVo3FN30TgVV+zikDPseMvMrT9PVH7dTcBZqEM2zk/y30UgmrMLHc 13 | oJ2WnhPDq9Z7jLbw7wL6Gc7p3Bigh6UkTVZK7C5ECKsFZWWxAmxy3oHHQhuJ8Tvo 14 | SMrnCkhYsZRROzI+hcaq7UxWh7u/LavD0xVZrbqRFlZcghDx8gIqjeEvPw1Ymyqb 15 | K+0ouPfIAUlbIRauPnC4BAJdcQxP8/YjeRoTfyzRZGqvwsavU0h51vM+hdfWRFzV 16 | ichtw9GE 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/ServiceInstances.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services 2 | 3 | data class ServiceInstance( 4 | val id: String, 5 | val tags: Set, 6 | val address: String?, 7 | val port: Int?, 8 | val regular: Boolean = true, 9 | val canary: Boolean = false, 10 | val weight: Int = 1 11 | ) 12 | 13 | data class ServiceInstances( 14 | val serviceName: String, 15 | val instances: Set 16 | ) { 17 | fun withoutEmptyAddressInstances(): ServiceInstances = 18 | if (instances.any { it.address.isNullOrEmpty() }) { 19 | copy(instances = instances.asSequence() 20 | .filter { !it.address.isNullOrEmpty() } 21 | .toSet()) 22 | } else this 23 | 24 | fun withoutInvalidPortInstances(): ServiceInstances = 25 | if (instances.any { it.port == null }) { 26 | copy(instances = instances.asSequence() 27 | .filter { it.port != null } 28 | .toSet()) 29 | } else this 30 | } 31 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/resources/lua/ingress_service_tag_preference.lua: -------------------------------------------------------------------------------- 1 | local defaultServiceTagPreference = os.getenv("%DEFAULT_SERVICE_TAG_PREFERENCE_ENV%") or "%DEFAULT_SERVICE_TAG_PREFERENCE_FALLBACK%" 2 | local defaultServiceTagPreferenceLength = #defaultServiceTagPreference 3 | 4 | local isSuffixOfDefaultPreference = function(requestPreference) 5 | local requestPreferenceLength = #requestPreference 6 | if requestPreferenceLength >= defaultServiceTagPreferenceLength then 7 | return false 8 | end 9 | return string.sub(defaultServiceTagPreference, -requestPreferenceLength - 1) == "|" .. requestPreference 10 | end 11 | 12 | function envoy_on_request(handle) 13 | local requestPreference = handle:headers():getAtIndex("%SERVICE_TAG_PREFERENCE_HEADER%", 0) 14 | if not requestPreference then 15 | handle:headers():add("%SERVICE_TAG_PREFERENCE_HEADER%", defaultServiceTagPreference) 16 | return 17 | end 18 | if isSuffixOfDefaultPreference(requestPreference) then 19 | handle:headers():replace("%SERVICE_TAG_PREFERENCE_HEADER%", defaultServiceTagPreference) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/synchronization/RemoteClusterStateChanges.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.synchronization 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.EnvoyControlProperties 4 | import pl.allegro.tech.servicemesh.envoycontrol.services.ClusterStateChanges 5 | import pl.allegro.tech.servicemesh.envoycontrol.services.MultiClusterState 6 | import pl.allegro.tech.servicemesh.envoycontrol.utils.CHECKPOINT_TAG 7 | import pl.allegro.tech.servicemesh.envoycontrol.utils.SERVICES_STATE_METRIC 8 | import reactor.core.publisher.Flux 9 | 10 | class RemoteClusterStateChanges( 11 | val properties: EnvoyControlProperties, 12 | private val remoteServices: RemoteServices 13 | ) : ClusterStateChanges { 14 | override fun stream(): Flux = 15 | remoteServices 16 | .getChanges(properties.sync.pollingInterval) 17 | .startWith(MultiClusterState.empty()) 18 | .distinctUntilChanged() 19 | .name(SERVICES_STATE_METRIC) 20 | .tag(CHECKPOINT_TAG, "cross-dc") 21 | .metrics() 22 | } 23 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/assertions/HttpsEchoResponseAssertions.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.assertions 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.assertj.core.api.ObjectAssert 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.HttpsEchoContainer 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.HttpsEchoResponse 7 | import java.util.function.Consumer 8 | 9 | fun ObjectAssert.isOk(): ObjectAssert { 10 | matches { it.response.isOk() } 11 | return this 12 | } 13 | 14 | fun ObjectAssert.hasSNI(serverName: String): ObjectAssert = satisfies(Consumer { 15 | val actualServerName = HttpsEchoResponse.objectMapper.readTree(it.response.body).at("/connection/servername").textValue() 16 | assertThat(actualServerName).isEqualTo(serverName) 17 | }) 18 | 19 | fun ObjectAssert.isFrom(container: HttpsEchoContainer) = satisfies(Consumer { 20 | assertThat(it.isFrom(container)).describedAs("response is not from the container").isTrue() 21 | }) 22 | -------------------------------------------------------------------------------- /envoy-control-source-consul/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/consul/synchronization/SimpleConsulInstanceFetcher.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.consul.synchronization 2 | 3 | import com.ecwid.consul.v1.ConsulClient 4 | import com.ecwid.consul.v1.QueryParams 5 | import com.ecwid.consul.v1.health.model.HealthService 6 | import pl.allegro.tech.servicemesh.envoycontrol.synchronization.ControlPlaneInstanceFetcher 7 | import java.net.URI 8 | 9 | class SimpleConsulInstanceFetcher( 10 | private val consulClient: ConsulClient, 11 | private val envoyControlAppName: String 12 | ) : ControlPlaneInstanceFetcher { 13 | 14 | override fun instances(cluster: String): List = toServiceUri(findInstances(cluster)) 15 | 16 | private fun toServiceUri(instances: MutableList) = 17 | instances.map { instance -> createURI(instance.service.address, instance.service.port) } 18 | 19 | private fun findInstances(nonLocalDc: String) = 20 | consulClient.getHealthServices(envoyControlAppName, true, QueryParams(nonLocalDc)).value 21 | 22 | private fun createURI(host: String, port: Int) = URI.create("http://$host:$port/") 23 | } 24 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/TcpProxyFilterFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | internal class TcpProxyFilterFactoryTest { 7 | 8 | private val tcpProxyFilterFactory: TcpProxyFilterFactory = TcpProxyFilterFactory() 9 | 10 | @Test 11 | fun `should create tcp proxy filter`() { 12 | // when 13 | val filter = tcpProxyFilterFactory.createFilter( 14 | "cluster_tcp", 15 | isSsl = true, 16 | host = "test.org", 17 | statsPrefix = "stats_test" 18 | ) 19 | 20 | // then 21 | assertThat(filter.filterChainMatch.transportProtocol).isEqualTo("tls") 22 | assertThat(filter.filterChainMatch.serverNamesCount).isEqualTo(1) 23 | assertThat(filter.filterChainMatch.serverNamesList[0]).isEqualTo("test.org") 24 | assertThat(filter.filtersList.size).isEqualTo(1) 25 | assertThat(filter.filtersList[0].name).isEqualTo("envoy.tcp_proxy") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /envoy-control-services/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/MultiClusterStateTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import java.util.concurrent.ConcurrentHashMap 6 | 7 | internal class MultiClusterStateTest { 8 | 9 | @Test 10 | fun `MultiClusterStates should implement equality`() { 11 | // given 12 | val multiClusterState1 = createMultiClusterState() 13 | val multiClusterState2 = createMultiClusterState() 14 | 15 | // then 16 | assertThat(multiClusterState1).isEqualTo(multiClusterState2) 17 | } 18 | 19 | private fun createMultiClusterState(): MultiClusterState { 20 | val serviceInstance = ServiceInstance("1", address = "0.0.0.0", port = 1, tags = setOf("a")) 21 | val serviceInstances = ServiceInstances("a", setOf(serviceInstance)) 22 | val services = ConcurrentHashMap() 23 | services["a"] = serviceInstances 24 | val state = listOf(ClusterState(ServicesState(services), Locality.REMOTE, "dc1")) 25 | return MultiClusterState(state) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tools/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | consul: 5 | container_name: consul 6 | image: hashicorp/consul:1.11.11 7 | ports: 8 | - "18500:8500" 9 | - "18300:8300" 10 | volumes: 11 | - ./tmp/config:/config 12 | - ./tmp/_data/consul:/data 13 | command: agent -server -data-dir=/data -bind 0.0.0.0 -client 0.0.0.0 -bootstrap-expect=1 -ui 14 | 15 | http-echo: 16 | depends_on: 17 | - consul 18 | build: 19 | context: ./service 20 | dockerfile: Dockerfile 21 | 22 | envoy: 23 | build: 24 | context: ./envoy 25 | dockerfile: Dockerfile 26 | ports: 27 | - "9999:9999" 28 | - "31000:31000" 29 | - "31001:31001" 30 | 31 | envoy-control: 32 | container_name: envoy-control 33 | build: 34 | context: ../ 35 | dockerfile: tools/envoy-control/Dockerfile 36 | ports: 37 | - "8080:8080" 38 | - "50000:50000" 39 | # here you can define path to your own config 40 | volumes: 41 | - "../envoy-control-runner/src/main/resources/application-docker.yaml:/var/tmp/config/application.yaml" 42 | depends_on: 43 | - consul 44 | environment: 45 | - ENVOY_CONTROL_PROPERTIES= 46 | -------------------------------------------------------------------------------- /docs/features/service_transformers.md: -------------------------------------------------------------------------------- 1 | # Service Transformers 2 | 3 | Service Transformers are a way to filter out and modify services received from the discovery service before sending it to Envoy. 4 | Transformers are only applied to the local state of discovery. Remote state of discovery is already transformed by other 5 | instance of Envoy Control. 6 | 7 | ## Available Transformers 8 | 9 | There are couple of available transformers 10 | 11 | ### Empty Address Filter 12 | 13 | Exclude instances that have an empty address. 14 | 15 | ### IP Address Filter 16 | 17 | Exclude instances that contain hostname. Envoy does not support endpoints sent via EDS that has a hostname. 18 | 19 | ### Regex Service Instances Filter 20 | 21 | Exclude services with a given name using defined regex. 22 | 23 | ## Custom Transformers 24 | 25 | To provide custom Transformer implement `ServiceInstancesTransformer` interface. With Envoy Control Runner, every 26 | transformer available in Spring Context will be picked up and used. With pure Envoy Control, you have to provide 27 | a list of transformers to `LocalClusterStateChanges` class. 28 | 29 | ## Configuration 30 | 31 | You can see a list of settings [here](../configuration.md#service-filters) 32 | 33 | -------------------------------------------------------------------------------- /docs/integrations/consul.md: -------------------------------------------------------------------------------- 1 | # Integration - Consul 2 | 3 | [Consul](https://www.consul.io/) is a highly available and distributed service discovery. Envoy Control provides 4 | first-class integration with Consul. 5 | 6 | ## Performance 7 | 8 | Popular Service Mesh solutions provide integration with Consul by polling periodically the state of all services. 9 | Assuming we polled the state each second in order to minimize change propagation latency, we would have to send a request 10 | for a [list of services](https://www.consul.io/api-docs/catalog#list-services) and then a 11 | [request per each service](https://www.consul.io/api-docs/catalog.html#list-nodes-for-service). 12 | With 1,000 services, this would generate 1,000 rps per one instance of Control Plane. 13 | 14 | Integration in Envoy Control is based on [blocking queries](https://www.consul.io/api-docs/features/blocking.html). This way 15 | Consul will notify Envoy Control (via long-lasting HTTP requests) that the state of discovery changed. 16 | The implementation used in Envoy Control is available as a 17 | [Consul Recipes library](https://github.com/allegro/consul-recipes/). 18 | 19 | ## Configuration 20 | 21 | You can see a list of settings [here](../configuration.md#consul) 22 | -------------------------------------------------------------------------------- /docs/deployment/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Dependencies 4 | 5 | Envoy Control requires a Consul cluster to run. See [Consul Configuration](../integrations/consul.md) section on 6 | how to connect to a cluster. 7 | 8 | ## Scalability 9 | 10 | Envoy Control is a stateless application, which means that there can be as many instances running in the same cluster as needed. 11 | 12 | ## Envoy Configuration 13 | 14 | Example Envoy configuration that is compatible with Envoy Control is available in [tests](https://github.com/allegro/envoy-control/blob/master/envoy-control-tests/src/main/resources/envoy/config_ads.yaml). 15 | 16 | ## Envoy Control Configuration 17 | 18 | When running Envoy Control Runner, you can configure the app in Spring's way. 19 | 20 | ### Environment variables 21 | 22 | Use `ENVOY_CONTROL_RUNNER_OPTS` environment variable to override configuration. 23 | 24 | Example 25 | ```bash 26 | export ENVOY_CONTROL_RUNNER_OPTS="-Denvoy-control.consul-host=127.0.0.1 -Denvoy-control.source.consul.port=18500" 27 | ``` 28 | 29 | ### External configuration 30 | 31 | Instead of overriding every property, it is possible to provide a YAML configuration file. 32 | ```bash 33 | export SPRING_CONFIG_LOCATION="file://path/properties.yaml" 34 | ``` -------------------------------------------------------------------------------- /envoy-control-core/src/main/resources/lua/ingress_client_name_header.lua: -------------------------------------------------------------------------------- 1 | function envoy_on_request(handle) 2 | local streamInfo = handle:streamInfo() 3 | local trusted_header_name = handle:metadata():get("trusted_client_identity_header") or "" 4 | if trusted_header_name == "" then 5 | return 6 | end 7 | 8 | if handle:headers():get(trusted_header_name) ~= nil then 9 | handle:headers():remove(trusted_header_name) 10 | end 11 | 12 | if handle:connection():ssl() and streamInfo:downstreamSslConnection() then 13 | local uriSanPeerCertificate = handle:streamInfo():downstreamSslConnection():uriSanPeerCertificate() 14 | if uriSanPeerCertificate ~= nil and next(uriSanPeerCertificate) ~= nil then 15 | local san_uri_format = handle:metadata():get("san_uri_lua_pattern") 16 | 17 | for _, entry in pairs(uriSanPeerCertificate) do 18 | local clientName = string.match(entry, san_uri_format) 19 | if clientName ~= nil and clientName ~= '' then 20 | handle:headers():add(trusted_header_name, clientName) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | 27 | function envoy_on_response(handle) 28 | end 29 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/device-csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIDBDCCAewCAQAwXTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNV 3 | BAcMBVByb3ZvMRYwFAYDVQQKDA1BY21lIFRlY2ggSW5jMRcwFQYDVQQDDA5teS5l 4 | eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALdGDN6G 5 | UyxG1MyktuRoZrwJmvxP9o8X6OL9e4Ci2APx3z85T/ufKPUczgv+bVY4QouOBcY9 6 | K7K7sKezPVxC5A8STpnE+iF+vmSpi+5bA+syXnarNsINXFyRcPLCWgG6Tlk8x55d 7 | fLJOJf6+BD42xFRcrScFQb9bcZ6/id1ffdYVrtqT+IYinANG1RjoupOgTxgwovVU 8 | 0pGjHjLPUbHxWVajZYfvPMrO7t2jbgngCI0zmvy212xcr8cMWlCO0dgzgWWTDn73 9 | gMoYdl06sFmxq+dHlCLmMLkqeOXattXWbO84NzT6/VMelOSQSAKBBNgpRdVURMkM 10 | 0Bc/u/76UuAyy6UCAwEAAaBiMGAGCSqGSIb3DQEJDjFTMFEwCwYDVR0PBAQDAgQw 11 | MBMGA1UdJQQMMAoGCCsGAQUFBwMBMC0GA1UdEQQmMCSCDm15LmV4YW1wbGUuY29t 12 | ghJ3d3cubXkuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAHVkV0AXutgB 13 | +DgOKucBbOVboWaeXHpkc10PB1SEzeqBxkOBOPlrEq9Rixpl+hAgMCqBJm34DXeq 14 | shAhpy/VaFnK09c6Dw0NdJpZvbH/aW7R3G+GMzjhHa76SJ2vOIPOAxDYlJ/FcJOr 15 | pMqT7fQxo8qExP12iQ+gEXOP8iWFh0k9mqp4YrkeR9JENEkGkeNeWeonztFbkuAV 16 | TAGdxPtcDpG3CWBd0/E+XKJGne+hK2FF2Yh36R/9i5JeJwIxTLPBWqlzRTK9gpJj 17 | jq17Chdb5DHzw7LqUew8V8yKqpsabKe8aHiuyPz7NZDdddG9thTewIHxp64ocabW 18 | V+P/KAafGZQ= 19 | -----END CERTIFICATE REQUEST----- 20 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/containers/ToxiproxyExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.containers 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.extension.AfterAllCallback 5 | import org.junit.jupiter.api.extension.BeforeAllCallback 6 | import org.junit.jupiter.api.extension.ExtensionContext 7 | import org.testcontainers.containers.Network 8 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 9 | 10 | class ToxiproxyExtension(exposedPortsCount: Int = 0) : BeforeAllCallback, AfterAllCallback { 11 | private var started = false 12 | val container = ToxiproxyContainer(exposedPortsCount = exposedPortsCount).withNetwork(Network.SHARED) 13 | 14 | override fun beforeAll(context: ExtensionContext) { 15 | if (started) { 16 | return 17 | } 18 | 19 | container.start() 20 | awaitReady() 21 | started = true 22 | } 23 | 24 | private fun awaitReady() { 25 | untilAsserted { 26 | 27 | assertThat(container.containerInfo).isNotNull() 28 | } 29 | } 30 | 31 | override fun afterAll(context: ExtensionContext) { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/EchoServiceExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.ResponseWithBody 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.ContainerPool 7 | 8 | class EchoServiceExtension : ServiceExtension, UpstreamService { 9 | 10 | companion object { 11 | private val pool = ContainerPool { EchoContainer() } 12 | } 13 | 14 | private var container: EchoContainer? = null 15 | 16 | override fun container() = container!! 17 | 18 | override fun beforeAllOnce(context: ExtensionContext) { 19 | container = pool.acquire(this) 20 | } 21 | 22 | override fun afterAllOnce(context: ExtensionContext) { 23 | pool.release(this) 24 | } 25 | 26 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 27 | override fun id(): String = container().id() 28 | override fun isSourceOf(response: ResponseWithBody): Boolean = container().isSourceOf(response) 29 | } 30 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/root-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDSjCCAjICCQC4tp81kODMYTANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJV 3 | UzENMAsGA1UECAwEVXRhaDEOMAwGA1UEBwwFUHJvdm8xIzAhBgNVBAoMGkFDTUUg 4 | U2lnbmluZyBBdXRob3JpdHkgSW5jMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0y 5 | MDA0MjAxNTIyNTFaFw00NzA5MDUxNTIyNTFaMGcxCzAJBgNVBAYTAlVTMQ0wCwYD 6 | VQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwaQUNNRSBTaWduaW5n 7 | IEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAua0Add0guHDMLoEf6A1hj5ZxZfVd+swPncPw 9 | 8ZL1fSzV0pngxMTE/3vkefhz3jE+R4b0DQLsfYVjohDMETfqRERfwZ5HcpsZx0pC 10 | Fxob7Icty2tPwRijLnutcZBg4ENBqYeC3/DQuO+7ADV5A1ulFdOcJuXNBmNpsH2R 11 | mEuvtroGjxAMJWvoUu899snHaIsrcpAJEdgcYF7kjHEfU4Kv2pmLHx1nHAF3qb53 12 | mjMP0RdoeFDG/AuEQgB6VmHc/9Nz416o9E2KkrB5GGl8t9WVptKHQfqEU+Snzyfb 13 | y9rRgw90GUzRLylBiHCGUNpQ4fenD+lVnpuko5yqEDaJVYsYzwIDAQABMA0GCSqG 14 | SIb3DQEBCwUAA4IBAQAoTvbZwneK5Ukt1rqWog+REbpcSA6JZRqY7rq3qKKpOD56 15 | Pxw4tDAR/w5cV+q4FPEdXX+ziwz63rOqD1gQmuC/pPA76NF5ibyaDLSrtw/dWqiU 16 | yXsLMDaIha+iRUt5gM970g/9WdlCrcb0/vIsRSv28YCKO+v8qI8umVcVkpX6dzX+ 17 | 9U+RyYhVAfvOK1Dt/rzyWBsVivc+PH42C8Os9cjfxqOhrtaXrz4de27+PBUACyDF 18 | ykAC3mQFAbtsAf8/ZK6qjcKp6JnEcg4aoSAYxO7pt6boquYm9mYvXomkqJcWEsoX 19 | jgBm0WkjAezYrNjUJqnWrS2kqlMF6MrmMlHsxWat 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/root-ca2.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDSjCCAjICCQD4RNRr6stDfjANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJV 3 | UzENMAsGA1UECAwEVXRhaDEOMAwGA1UEBwwFUHJvdm8xIzAhBgNVBAoMGkFDTUUg 4 | U2lnbmluZyBBdXRob3JpdHkgSW5jMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0y 5 | MDA1MTIxMzM2MDNaFw00NzA5MjcxMzM2MDNaMGcxCzAJBgNVBAYTAlVTMQ0wCwYD 6 | VQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwaQUNNRSBTaWduaW5n 7 | IEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjJYWl6coMa2V4WV1ZwEfVK+UCtcPN/NBxRB 9 | MIYpOIYtHKZPileFSZE6JsmP7E3KL5DMo81hqtxsKpJpqEmXwmnYpujvyAVK8QWT 10 | /G1JZ8vSaAgQ+8vDIYrALYidN5sXRSqI/+QXmXs5VM4QUm78kS83LgosQxPXItRZ 11 | 2kVVheb5nb8NQSGWGVnkd+Ci3S8ztCCw6n6cUYP/K6uUm9KjfhFD0l1GU3yGE/oM 12 | b2fE5q/bl8tUhQlyEDhV66rZzJ8fK+GoCxxzGWbYSNeht743O2rDvP0kvjAMCVY5 13 | QFV5gOw3/5+Kntzxeb5eP/Odk3bvZVWxuARRVCrE38ysihKm+wIDAQABMA0GCSqG 14 | SIb3DQEBCwUAA4IBAQBqzyDpzN1N4td5SXLUue9p/Kiua6HaFEdNUosFbImBG4PM 15 | D7/DXZroE+SdxO1mmnaJTaTxiPbXBe5iraTn5RmHwU1wNOBWrPFW8hRt9WOAIW8p 16 | jSwACYLtLa3QtVI2dqzWXQbWTZtrIe9VF+R4sfiJuw7EwWkFMwzFVkGKjKNdQGWz 17 | qXiHEVTKiiIsJ9EQowfIxo2qdXMJOBnB1T5/Mk5pfPid0py2OvA3fiDglsE0c1lv 18 | dqLPthzNtFZhykWQilH12cPRAKpb/iKBOOZyqsnIeZ/gxk9huIGKNRIOhyJskOk2 19 | rlysUK27H5iOkJyu+GthCVVWYgt0GM9NLi2ealV6 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/EchoContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.testcontainers.containers.Network 4 | import org.testcontainers.containers.wait.strategy.Wait 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.ResponseWithBody 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 7 | import java.util.Locale 8 | import java.util.UUID 9 | 10 | class EchoContainer(val response: String = UUID.randomUUID().toString()) : 11 | GenericContainer("jxlwqq/http-echo"), ServiceContainer, UpstreamService { 12 | 13 | override fun configure() { 14 | super.configure() 15 | withExposedPorts(PORT) 16 | withNetwork(Network.SHARED) 17 | withCommand(String.format(Locale.getDefault(), "--text=%s --addr=:%s", response, PORT)) 18 | waitingFor(Wait.forHttp("/").forStatusCode(200)) 19 | } 20 | 21 | fun address(): String = "${ipAddress()}:$PORT" 22 | 23 | override fun port() = PORT 24 | override fun isSourceOf(response: ResponseWithBody) = response.body.contains(this.response) 25 | override fun id(): String = containerId 26 | 27 | companion object { 28 | const val PORT = 5678 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/OAuthServerContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.testcontainers.containers.Network 4 | import org.testcontainers.containers.wait.strategy.Wait 5 | import org.testcontainers.images.builder.ImageFromDockerfile 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 7 | 8 | class OAuthServerContainer : 9 | GenericContainer(ImageFromDockerfile().withFileFromClasspath("Dockerfile", "oauth/Dockerfile")), 10 | ServiceContainer { 11 | 12 | override fun configure() { 13 | super.configure() 14 | withEnv("PORT", OAUTH_PORT.toString()) 15 | withNetwork(Network.SHARED) 16 | withNetworkAliases(NETWORK_ALIAS) 17 | addExposedPort(OAUTH_PORT) 18 | waitingFor(Wait.forHttp("/").forStatusCode(200)) 19 | } 20 | 21 | fun address(): String = "http://${ipAddress()}:${getMappedPort(OAUTH_PORT)}" 22 | 23 | override fun port(): Int = getMappedPort(OAUTH_PORT) 24 | 25 | fun oAuthPort() = OAUTH_PORT 26 | 27 | fun networkAlias() = NETWORK_ALIAS 28 | 29 | companion object { 30 | const val NETWORK_ALIAS = "oauth" 31 | const val OAUTH_PORT = 9997 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/envoy/launch_envoy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | echo server=`cat /etc/resolv.conf | grep "^nameserver" | sed -n -e 's/^nameserver //p'` >> /etc/dnsmasq.conf 6 | cat /etc/resolv.conf | sed 's/^nameserver.*/nameserver 127.0.0.1/' > /tmp/resolv.conf 7 | cat /tmp/resolv.conf > /etc/resolv.conf 8 | /etc/init.d/dnsmasq restart 9 | 10 | HOST_IP=$(bash /usr/local/bin/host_ip.sh) 11 | HOST_PORT=$1 12 | HOST2_PORT=$2 13 | 14 | CONFIG=$(cat $3) 15 | CONFIG_DIR=$(mktemp -d) 16 | CONFIG_FILE="$CONFIG_DIR/envoy.yaml" 17 | 18 | LOCAL_SERVICE_IP="$4" 19 | TRUSTED_CA="$5" 20 | CERTIFICATE_CHAIN="$6" 21 | PRIVATE_KEY="$7" 22 | SERVICE_NAME="$8" 23 | WRAPPER_SERVICE_IP="$9" 24 | 25 | echo "debug: " "$@" 26 | 27 | echo "${CONFIG}" | sed \ 28 | -e "s;HOST_IP;${HOST_IP};g" \ 29 | -e "s;HOST_PORT;${HOST_PORT};g" \ 30 | -e "s;HOST2_PORT;${HOST2_PORT};g" \ 31 | -e "s;LOCAL_SERVICE_IP;${LOCAL_SERVICE_IP};g" \ 32 | -e "s;TRUSTED_CA;${TRUSTED_CA};g" \ 33 | -e "s;CERTIFICATE_CHAIN;${CERTIFICATE_CHAIN};g" \ 34 | -e "s;PRIVATE_KEY;${PRIVATE_KEY};g" \ 35 | -e "s;SERVICE_NAME;${SERVICE_NAME};g" \ 36 | -e "s;WRAPPER_SERVICE_IP;${WRAPPER_SERVICE_IP};g" \ 37 | > "${CONFIG_FILE}" 38 | cat "${CONFIG_FILE}" 39 | 40 | shift 9 41 | /usr/local/bin/envoy --drain-time-s 1 -c "${CONFIG_FILE}" "$@" 42 | 43 | rm -rf "${CONFIG_DIR}" 44 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/cert_echo.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYzCCAkugAwIBAgIJAPUyAfpLGbt3MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMTIxMzkwNVoXDTQ3MDkyNjIxMzkwNVowXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BY21lIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBALdGDN6GUyxG1MyktuRoZrwJmvxP9o8X6OL9e4Ci2APx 9 | 3z85T/ufKPUczgv+bVY4QouOBcY9K7K7sKezPVxC5A8STpnE+iF+vmSpi+5bA+sy 10 | XnarNsINXFyRcPLCWgG6Tlk8x55dfLJOJf6+BD42xFRcrScFQb9bcZ6/id1ffdYV 11 | rtqT+IYinANG1RjoupOgTxgwovVU0pGjHjLPUbHxWVajZYfvPMrO7t2jbgngCI0z 12 | mvy212xcr8cMWlCO0dgzgWWTDn73gMoYdl06sFmxq+dHlCLmMLkqeOXattXWbO84 13 | NzT6/VMelOSQSAKBBNgpRdVURMkM0Bc/u/76UuAyy6UCAwEAAaMcMBowGAYDVR0R 14 | BBEwD4YNc3BpZmZlOi8vZWNobzANBgkqhkiG9w0BAQUFAAOCAQEAhAf+yDRl6lNv 15 | UMyLvjAL7XiNIQA6K1LWgec+JR4iVCCw3ttGFScoleVg7B/iGmkuNYANyrtBDaJQ 16 | g2l7YR1oKbG0mrx/A0OZozF2OEiFQbqvnVfidSvHsp/aKsfyyd3plVz0sp7N3Vfd 17 | ZTPY8smq+ZqJ0f9xp4FXpXEOakWYxRIQO2Jiu0dIBq6i1eE3npyGzf/ogumz623z 18 | 9Vf/bFNOimlJAdTlQRxdyqgAyb4LDm4/13wKTqzZ4wUfgZ47VxO7x41oskxQYGty 19 | a6qw0tr4La1ukdqCG4R+QTi5xxNU+VtlRR+XrBheHlt+K0PYMBUGsRzGXR0v3jPg 20 | P7/7tLUzDg== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/cert_echo2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkygAwIBAgIJAPUyAfpLGbt4MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMTIxNDU0OVoXDTQ3MDkyNjIxNDU0OVowXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBAKFJNfI38hB2sloqR/ud2GEX4mpvRIt2gPnMHsbeoiaW 9 | ppFm25VfKo5Zgnkkv5NwHmRcpmb43c5NLUTuCKQFojp35XByIVahB5qjDMnnic8/ 10 | T/3Ijr264jJei6NJoGEhckprCKrJcpVFYfIpesMe3s2HyJVKt2xdUX+eYPgtPpZb 11 | 5GVZVnQciniGOed5uES+Vy+3DIdyaeiHpUTZNpBhopWgqUl/rS1qzEVyWq/1GyrG 12 | xeThBUHJbvrux2gdOFx6bralTfCxTqteEHE0Cv7NWyHoH0brKxV9TXg8vIat01it 13 | 9nyUD3B9jNuI3JZbFkk6VilDlZsqdMAg8zhC7NKSOR8CAwEAAaMdMBswGQYDVR0R 14 | BBIwEIYOc3BpZmZlOi8vZWNobzIwDQYJKoZIhvcNAQEFBQADggEBABWTLxxtQMhc 15 | GJ/qjHUHsjRZjHnQ4i5DH5/1nE4Eu6i6LDxcA/Tt2pjyOE8/WeW6ktcC5NVF7UL6 16 | a+7xOoG4TNM5s83iu99i7MekLTF+A0XPr4XkEn1fIalFqzRUw1MDeKm27pGF42Qa 17 | QxcZSF6pHjV+QTzbQ7iwL9tT3yH+O13cYkqND8cnJtGg0iTx09xiAbxD+DYecT1/ 18 | HcckqO1nca/FZlHA6Z1ElcQ60GrFmOOJ4xszFNg6Q7ZkyXMvhKIJx8qXrUiRvPOn 19 | fz0jr1V2TX9pCaxL+60O9lhKrxMKTnX9uj7+nB4qa5pNxEqjSW/+k1bJyJrGcOSm 20 | kt05a8hMw4k= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/cert_echo3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkygAwIBAgIJAPUyAfpLGbt5MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMjA4MzY0OVoXDTQ3MDkyNzA4MzY0OVowXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBAN4hPhXyOK2iClN3LjRuB47Lpse2ThooQomO2e+e289W 9 | kapnO0VoxAjXE7UvBJPTvBeioTw09GuyVJxBrJGWz/2SibXzg0stzWFcMVWr7+pN 10 | XPOxVbheZY7FFml5/KfTL7Zy9pp3TSWXCqzW6iwkGJIkyfBMmMdZDCQAFKBJeY9y 11 | VMNSHYcF7rMPCijyD53QgCbVMchfedIz6UOeBAEE26SJah9KnAX5mlLNykISWH75 12 | vUCS7iiEEQXQ4KpvDXQR+vNzzPXKrQMe0aektC3dKtKKJtNzNzz7566wSP1irYUD 13 | NV3dwfuX9zgNgzQojgBNSsoRYI7MU5yWVQ8tWuCQfEECAwEAAaMdMBswGQYDVR0R 14 | BBIwEIYOc3BpZmZlOi8vZWNobzMwDQYJKoZIhvcNAQEFBQADggEBAIfjcRyfJ5zv 15 | Ua4nf9U9e1OC5yiotNrsXmTermpe+ail909k64qlBSbnAwwxVjuZe4zcBvPlO8MN 16 | /Q7cptSvs80YZiPyGvt1v/3+dQWC5e1DIejJacHt2x6dDZgYmNH+Jf1O5m9a5qqR 17 | BX1qhRnjXsQ+JA7WzGM49Dwj+cFyRp+YhZjSgbcHlxlgocpombc1stDxXze8/7y9 18 | 6tpAwXUgLnIN4aFlb+pZO2UjrxA+kPeuCJeZRFwy5E4sIeuPcpE1vxGq007SbWkB 19 | RRaxz2eoJuWx6Va6wCSpQdmUdlJWr8/Rdp17DO+JasO+hZJKWLgRfIhEiQzapTn0 20 | zSy9vSMh/9A= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/cert_echo_root-ca2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYzCCAkugAwIBAgIJAK8MD2DjPpBUMA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMjEzMzc0M1oXDTQ3MDkyNzEzMzc0M1owXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBALdGDN6GUyxG1MyktuRoZrwJmvxP9o8X6OL9e4Ci2APx 9 | 3z85T/ufKPUczgv+bVY4QouOBcY9K7K7sKezPVxC5A8STpnE+iF+vmSpi+5bA+sy 10 | XnarNsINXFyRcPLCWgG6Tlk8x55dfLJOJf6+BD42xFRcrScFQb9bcZ6/id1ffdYV 11 | rtqT+IYinANG1RjoupOgTxgwovVU0pGjHjLPUbHxWVajZYfvPMrO7t2jbgngCI0z 12 | mvy212xcr8cMWlCO0dgzgWWTDn73gMoYdl06sFmxq+dHlCLmMLkqeOXattXWbO84 13 | NzT6/VMelOSQSAKBBNgpRdVURMkM0Bc/u/76UuAyy6UCAwEAAaMcMBowGAYDVR0R 14 | BBEwD4YNc3BpZmZlOi8vZWNobzANBgkqhkiG9w0BAQUFAAOCAQEAAPiDzvrJrTNO 15 | 58LaUPnq9OOdieIWKN6PekSMrfJZIqPc38C3Yb+gvofkpEcdcaD6XP3CmUeWmjKj 16 | TKNa4PPnXhM0CY9mj6TOrK25+ruj/Ua4xLf97gz1Kw811f7qg8j2VIargQa7zrl6 17 | YOmZYzeSBADlwmHqD7eiTLGL11S7wNWGmccXxf1mpgb5tlkwz7XzJs4H503mw90u 18 | PWMgQvilveM2c8OQEKarh6kDLkdGaLss2yBODWYQ/D9NJ8elJLUZIU69VPM6GorQ 19 | lcF/n/c2dY2TMHJBpgPjvStQtLLJe2pH3UuXIKd5FsXRtZblCHTV5ppnRiH6j9vY 20 | GyXoeoFntQ== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/fullchain_echo.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYzCCAkugAwIBAgIJAPUyAfpLGbt3MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMTIxMzkwNVoXDTQ3MDkyNjIxMzkwNVowXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BY21lIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBALdGDN6GUyxG1MyktuRoZrwJmvxP9o8X6OL9e4Ci2APx 9 | 3z85T/ufKPUczgv+bVY4QouOBcY9K7K7sKezPVxC5A8STpnE+iF+vmSpi+5bA+sy 10 | XnarNsINXFyRcPLCWgG6Tlk8x55dfLJOJf6+BD42xFRcrScFQb9bcZ6/id1ffdYV 11 | rtqT+IYinANG1RjoupOgTxgwovVU0pGjHjLPUbHxWVajZYfvPMrO7t2jbgngCI0z 12 | mvy212xcr8cMWlCO0dgzgWWTDn73gMoYdl06sFmxq+dHlCLmMLkqeOXattXWbO84 13 | NzT6/VMelOSQSAKBBNgpRdVURMkM0Bc/u/76UuAyy6UCAwEAAaMcMBowGAYDVR0R 14 | BBEwD4YNc3BpZmZlOi8vZWNobzANBgkqhkiG9w0BAQUFAAOCAQEAhAf+yDRl6lNv 15 | UMyLvjAL7XiNIQA6K1LWgec+JR4iVCCw3ttGFScoleVg7B/iGmkuNYANyrtBDaJQ 16 | g2l7YR1oKbG0mrx/A0OZozF2OEiFQbqvnVfidSvHsp/aKsfyyd3plVz0sp7N3Vfd 17 | ZTPY8smq+ZqJ0f9xp4FXpXEOakWYxRIQO2Jiu0dIBq6i1eE3npyGzf/ogumz623z 18 | 9Vf/bFNOimlJAdTlQRxdyqgAyb4LDm4/13wKTqzZ4wUfgZ47VxO7x41oskxQYGty 19 | a6qw0tr4La1ukdqCG4R+QTi5xxNU+VtlRR+XrBheHlt+K0PYMBUGsRzGXR0v3jPg 20 | P7/7tLUzDg== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/fullchain_echo2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkygAwIBAgIJAPUyAfpLGbt4MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMTIxNDU0OVoXDTQ3MDkyNjIxNDU0OVowXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBAKFJNfI38hB2sloqR/ud2GEX4mpvRIt2gPnMHsbeoiaW 9 | ppFm25VfKo5Zgnkkv5NwHmRcpmb43c5NLUTuCKQFojp35XByIVahB5qjDMnnic8/ 10 | T/3Ijr264jJei6NJoGEhckprCKrJcpVFYfIpesMe3s2HyJVKt2xdUX+eYPgtPpZb 11 | 5GVZVnQciniGOed5uES+Vy+3DIdyaeiHpUTZNpBhopWgqUl/rS1qzEVyWq/1GyrG 12 | xeThBUHJbvrux2gdOFx6bralTfCxTqteEHE0Cv7NWyHoH0brKxV9TXg8vIat01it 13 | 9nyUD3B9jNuI3JZbFkk6VilDlZsqdMAg8zhC7NKSOR8CAwEAAaMdMBswGQYDVR0R 14 | BBIwEIYOc3BpZmZlOi8vZWNobzIwDQYJKoZIhvcNAQEFBQADggEBABWTLxxtQMhc 15 | GJ/qjHUHsjRZjHnQ4i5DH5/1nE4Eu6i6LDxcA/Tt2pjyOE8/WeW6ktcC5NVF7UL6 16 | a+7xOoG4TNM5s83iu99i7MekLTF+A0XPr4XkEn1fIalFqzRUw1MDeKm27pGF42Qa 17 | QxcZSF6pHjV+QTzbQ7iwL9tT3yH+O13cYkqND8cnJtGg0iTx09xiAbxD+DYecT1/ 18 | HcckqO1nca/FZlHA6Z1ElcQ60GrFmOOJ4xszFNg6Q7ZkyXMvhKIJx8qXrUiRvPOn 19 | fz0jr1V2TX9pCaxL+60O9lhKrxMKTnX9uj7+nB4qa5pNxEqjSW/+k1bJyJrGcOSm 20 | kt05a8hMw4k= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/fullchain_echo3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkygAwIBAgIJAPUyAfpLGbt5MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDUxMjA4MzY0OVoXDTQ3MDkyNzA4MzY0OVowXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BQ01FIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBAN4hPhXyOK2iClN3LjRuB47Lpse2ThooQomO2e+e289W 9 | kapnO0VoxAjXE7UvBJPTvBeioTw09GuyVJxBrJGWz/2SibXzg0stzWFcMVWr7+pN 10 | XPOxVbheZY7FFml5/KfTL7Zy9pp3TSWXCqzW6iwkGJIkyfBMmMdZDCQAFKBJeY9y 11 | VMNSHYcF7rMPCijyD53QgCbVMchfedIz6UOeBAEE26SJah9KnAX5mlLNykISWH75 12 | vUCS7iiEEQXQ4KpvDXQR+vNzzPXKrQMe0aektC3dKtKKJtNzNzz7566wSP1irYUD 13 | NV3dwfuX9zgNgzQojgBNSsoRYI7MU5yWVQ8tWuCQfEECAwEAAaMdMBswGQYDVR0R 14 | BBIwEIYOc3BpZmZlOi8vZWNobzMwDQYJKoZIhvcNAQEFBQADggEBAIfjcRyfJ5zv 15 | Ua4nf9U9e1OC5yiotNrsXmTermpe+ail909k64qlBSbnAwwxVjuZe4zcBvPlO8MN 16 | /Q7cptSvs80YZiPyGvt1v/3+dQWC5e1DIejJacHt2x6dDZgYmNH+Jf1O5m9a5qqR 17 | BX1qhRnjXsQ+JA7WzGM49Dwj+cFyRp+YhZjSgbcHlxlgocpombc1stDxXze8/7y9 18 | 6tpAwXUgLnIN4aFlb+pZO2UjrxA+kPeuCJeZRFwy5E4sIeuPcpE1vxGq007SbWkB 19 | RRaxz2eoJuWx6Va6wCSpQdmUdlJWr8/Rdp17DO+JasO+hZJKWLgRfIhEiQzapTn0 20 | zSy9vSMh/9A= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/OAuthServerExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.ContainerPool 6 | 7 | class OAuthServerExtension : ServiceExtension { 8 | 9 | private var container: OAuthServerContainer? = null 10 | 11 | override fun container(): OAuthServerContainer = container!! 12 | 13 | override fun beforeAllOnce(context: ExtensionContext) { 14 | container = pool.acquire(this) 15 | } 16 | 17 | override fun afterAllOnce(context: ExtensionContext) { 18 | pool.release(this) 19 | } 20 | 21 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 22 | 23 | fun getTokenAddress(provider: String = "auth") = "http://localhost:${container().port()}/$provider/token" 24 | fun getJwksAddress(provider: String = "auth") = 25 | "http://${container().networkAlias()}:${container().oAuthPort()}/$provider/jwks" 26 | 27 | companion object { 28 | private val pool = ContainerPool { OAuthServerContainer() } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tools/envoy-control/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:8.3-jdk17 AS builder 2 | COPY --chown=gradle:gradle settings.gradle build.gradle gradle.properties /home/gradle/src/ 3 | COPY --chown=gradle:gradle envoy-control-core/ /home/gradle/src/envoy-control-core/ 4 | COPY --chown=gradle:gradle envoy-control-runner/ /home/gradle/src/envoy-control-runner/ 5 | COPY --chown=gradle:gradle envoy-control-services/ /home/gradle/src/envoy-control-services/ 6 | COPY --chown=gradle:gradle envoy-control-source-consul/ /home/gradle/src/envoy-control-source-consul/ 7 | 8 | WORKDIR /home/gradle/src 9 | RUN gradle :envoy-control-runner:assemble --parallel --no-daemon 10 | 11 | FROM eclipse-temurin:17-jre 12 | 13 | RUN mkdir /tmp/envoy-control-dist /tmp/envoy-control /bin/envoy-control /etc/envoy-control /var/tmp/config 14 | COPY --from=builder /home/gradle/src/envoy-control-runner/build/distributions/ /tmp/envoy-control-dist 15 | COPY ./envoy-control-runner/src/main/resources/application-docker.yaml /etc/envoy-control/application.yaml 16 | RUN tar -xf /tmp/envoy-control-dist/envoy-control-runner-0.1.0*.tar -C /tmp/envoy-control \ 17 | && mv /tmp/envoy-control/envoy-control-runner*/ /bin/envoy-control/envoy-control-runner 18 | 19 | COPY tools/envoy-control/run.sh /usr/local/bin/run.sh 20 | VOLUME /var/tmp/config 21 | WORKDIR /usr/local/bin/ 22 | 23 | # APP_PORT: 8080 24 | # XDS_PORT: 50000 25 | CMD ["sh", "run.sh"] 26 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/utils/EndpointsOperations.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.utils 2 | 3 | import io.envoyproxy.envoy.config.cluster.v3.Cluster 4 | import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment 5 | import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint 6 | import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints 7 | 8 | fun createLoadAssignments( 9 | clusters: List, 10 | endpoints: List 11 | ): List { 12 | return clusters.map { 13 | ClusterLoadAssignment.newBuilder() 14 | .setClusterName(it.name) 15 | .addAllEndpoints(endpoints) 16 | .build() 17 | } 18 | } 19 | 20 | fun createEndpoints(): List = 21 | listOf( 22 | createEndpoint(CURRENT_ZONE), 23 | createEndpoint(TRAFFIC_SPLITTING_ZONE) 24 | ) 25 | 26 | fun createEndpoint(zone: String): LocalityLbEndpoints { 27 | return LocalityLbEndpoints.newBuilder() 28 | .setLocality( 29 | io.envoyproxy.envoy.config.core.v3.Locality 30 | .newBuilder() 31 | .setZone(zone) 32 | .build() 33 | ) 34 | .addAllLbEndpoints(listOf(LbEndpoint.getDefaultInstance())) 35 | .setPriority(DEFAULT_PRIORITY) 36 | .build() 37 | } 38 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/fullchain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDhzCCAm+gAwIBAgIJAPUyAfpLGbt2MA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV 3 | BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa 4 | QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1wbGUuY29t 5 | MB4XDTIwMDQyMDE2MTkxM1oXDTQ3MDkwNTE2MTkxM1owXTELMAkGA1UEBhMCVVMx 6 | DTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1BY21lIFRl 7 | Y2ggSW5jMRcwFQYDVQQDDA5teS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBALdGDN6GUyxG1MyktuRoZrwJmvxP9o8X6OL9e4Ci2APx 9 | 3z85T/ufKPUczgv+bVY4QouOBcY9K7K7sKezPVxC5A8STpnE+iF+vmSpi+5bA+sy 10 | XnarNsINXFyRcPLCWgG6Tlk8x55dfLJOJf6+BD42xFRcrScFQb9bcZ6/id1ffdYV 11 | rtqT+IYinANG1RjoupOgTxgwovVU0pGjHjLPUbHxWVajZYfvPMrO7t2jbgngCI0z 12 | mvy212xcr8cMWlCO0dgzgWWTDn73gMoYdl06sFmxq+dHlCLmMLkqeOXattXWbO84 13 | NzT6/VMelOSQSAKBBNgpRdVURMkM0Bc/u/76UuAyy6UCAwEAAaNAMD4wPAYDVR0R 14 | BDUwM4IObXkuZXhhbXBsZS5jb22CEnd3dy5teS5leGFtcGxlLmNvbYYNc3BpZmZl 15 | Oi8vZWNobzANBgkqhkiG9w0BAQUFAAOCAQEAPQVr7cYnOMg8iMj/QobOenIJ6+La 16 | Q1eaiT9PQXqfidC1HJ1XNa5rGTwEa2W8WKctZ+qv8x9Zp77OOi2cQ8Dh5tJNzlFS 17 | MqGWPyf+hoAd6LMac5om9/rIgz6q6RrlKSZeLxx9MpYW8idyRsx9x78FVtYByfrr 18 | w4CsqfDSImjj6pfXoITEQyGyKnfBKIeT0erN+ixa+yRsm1Y4dLfi2033tuGZebGn 19 | dzhJhYrUGl2EhNcF7KrxQXMbWsBP/6CDr5WEX6TRYy2EqUuijNM9kYM97aB31Xu/ 20 | xykfrPa0mo4DRx1ms3KKL9SVwWqxYPk9W4atlcEp4LtlyTODF9RxHi5kYg== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/TcpProxyFilterFactory.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters 2 | 3 | import io.envoyproxy.envoy.config.listener.v3.Filter 4 | import io.envoyproxy.envoy.config.listener.v3.FilterChain 5 | import io.envoyproxy.envoy.config.listener.v3.FilterChainMatch 6 | import io.envoyproxy.envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy 7 | 8 | class TcpProxyFilterFactory { 9 | 10 | fun createFilter( 11 | clusterName: String, 12 | isSsl: Boolean, 13 | host: String = "", 14 | statsPrefix: String = clusterName 15 | ): FilterChain { 16 | val filterChainMatch = FilterChainMatch.newBuilder() 17 | if (isSsl) { 18 | filterChainMatch.setTransportProtocol("tls") 19 | .addServerNames(host) 20 | } 21 | val filter = Filter.newBuilder() 22 | .setName("envoy.tcp_proxy") 23 | .setTypedConfig( 24 | com.google.protobuf.Any.pack( 25 | TcpProxy.newBuilder() 26 | .setStatPrefix(statsPrefix) 27 | .setCluster(clusterName) 28 | .build() 29 | ) 30 | ) 31 | return FilterChain.newBuilder().setFilterChainMatch(filterChainMatch).addFilters(filter).build() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/sharing/BeforeAndAfterAllOnce.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.sharing 2 | 3 | import org.junit.jupiter.api.extension.AfterAllCallback 4 | import org.junit.jupiter.api.extension.BeforeAllCallback 5 | import org.junit.jupiter.api.extension.ExtensionContext 6 | 7 | interface BeforeAndAfterAllOnce : BeforeAllCallback, AfterAllCallback { 8 | fun beforeAllOnce(context: ExtensionContext) 9 | fun afterAllOnce(context: ExtensionContext) 10 | override fun beforeAll(context: ExtensionContext) { 11 | if (ctx.id != null && !ctx.terminated) { 12 | return 13 | } 14 | if (ctx.terminated) { 15 | ctx.terminated = false 16 | } 17 | ctx.id = context.uniqueId 18 | beforeAllOnce(context) 19 | } 20 | 21 | override fun afterAll(context: ExtensionContext) { 22 | require(!ctx.terminated) { "" + 23 | "afterAll called after termination. It should not happen, test hierarchy ordering bug" } 24 | // terminate only on the last test context, which is the first context beforeAll() was called with. 25 | if (context.uniqueId == ctx.id) { 26 | ctx.terminated = true 27 | afterAllOnce(context) 28 | } 29 | } 30 | 31 | val ctx: Context 32 | 33 | class Context(var id: String? = null, var terminated: Boolean = false) 34 | } 35 | -------------------------------------------------------------------------------- /envoy-control-source-consul/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/consul/services/ServiceWatchPolicyTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.consul.services 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | class ServiceWatchPolicyTest { 7 | 8 | @Test 9 | fun `should return true when service tag is not present of tag blacklist`() { 10 | // given 11 | val tagsBlacklist = listOf("testTag") 12 | val serviceName = "envoy-control" 13 | val serviceTags = listOf("envoy", "application") 14 | val serviceWatchPolicy = TagBlacklistServiceWatchPolicy(tagsBlacklist) 15 | 16 | // when 17 | val shouldBeWatched = serviceWatchPolicy.shouldBeWatched(serviceName, serviceTags) 18 | 19 | // then 20 | assertThat(shouldBeWatched).isTrue 21 | } 22 | 23 | @Test 24 | fun `should return false when service tag is present of tag blacklist`() { 25 | // given 26 | val tagsBlacklist = listOf("envoy") 27 | val serviceName = "envoy-control" 28 | val serviceTags = listOf("envoy", "application") 29 | val serviceWatchPolicy = TagBlacklistServiceWatchPolicy(tagsBlacklist) 30 | 31 | // when 32 | val shouldBeWatched = serviceWatchPolicy.shouldBeWatched(serviceName, serviceTags) 33 | 34 | // then 35 | assertThat(shouldBeWatched).isFalse 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Envoy Control documentation 2 | site_description: Envoy Control - Service Mesh Control Plane for Envoy Proxy 3 | site_author: Allegro.pl 4 | 5 | docs_dir: docs 6 | 7 | #repo_url: https://github.com/allegro/envoy-control 8 | #repo_name: 'envoy-control' 9 | 10 | copyright: 'Allegro.pl' 11 | 12 | theme: 13 | name: material 14 | logo: 15 | icon: code 16 | palette: 17 | primary: purple 18 | accent: purple 19 | 20 | extra_javascript: 21 | - 'assets/extra.js' 22 | 23 | markdown_extensions: 24 | - codehilite 25 | - admonition 26 | - toc: 27 | permalink: "#" 28 | 29 | nav: 30 | - About: index.md 31 | - Changelog: changelog_symlink.md 32 | - Quickstart: quickstart.md 33 | - Architecture: architecture.md 34 | - Configuration: configuration.md 35 | - Integrations: 36 | - Envoy: integrations/envoy.md 37 | - Consul: integrations/consul.md 38 | - Features: 39 | - Multi DC support: features/multi_dc_support.md 40 | - Permissions: features/permissions.md 41 | - Service Transformers: features/service_transformers.md 42 | - Weighted Load Balancing & Canary: features/load_balancing.md 43 | - Service tags: features/service_tags.md 44 | - Development: development.md 45 | - Performance: performance.md 46 | - Deployment: 47 | - Observability: deployment/observability.md 48 | - Deployment: deployment/deployment.md 49 | - Envoy Control vs other software: ec_vs_other_software.md 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | forceVersion: 7 | description: 'Force version' 8 | required: false 9 | default: '' 10 | release: 11 | types: [ created ] 12 | 13 | jobs: 14 | publish: 15 | 16 | runs-on: ubuntu-latest 17 | environment: ci 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - uses: gradle/actions/wrapper-validation@v4 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v3 26 | with: 27 | distribution: 'temurin' 28 | java-version: '17' 29 | - name: Release 30 | if: github.ref == 'refs/heads/master' 31 | run: ./gradlew release -Prelease.customPassword=${GITHUB_TOKEN} -Prelease.customUsername=${GITHUB_ACTOR} -Prelease.forceVersion=${FORCE_VERSION} 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | FORCE_VERSION: ${{ github.event.inputs.forceVersion }} 35 | - name: Publish to maven central 36 | run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 37 | env: 38 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 39 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 40 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} 41 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 42 | GPG_PRIVATE_KEY_PASSWORD: ${{ secrets.GPG_PRIVATE_KEY_PASSWORD }} 43 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/assertions/ResponseAssertions.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.assertions 2 | 3 | import okhttp3.Response 4 | import org.assertj.core.api.ObjectAssert 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 6 | 7 | fun ObjectAssert.isOk(): ObjectAssert { 8 | matches { it.isSuccessful } 9 | return this 10 | } 11 | 12 | fun ObjectAssert.isForbidden(): ObjectAssert { 13 | matches({ 14 | it.body?.close() 15 | it.code == 403 16 | }, "is forbidden") 17 | return this 18 | } 19 | 20 | fun ObjectAssert.isUnreachable(): ObjectAssert { 21 | matches({ 22 | it.body?.close() 23 | it.code == 503 || it.code == 504 24 | }, "is unreachable") 25 | return this 26 | } 27 | 28 | fun ObjectAssert.isFrom(echoServiceExtension: EchoServiceExtension): ObjectAssert { 29 | matches { 30 | it.body?.use { it.string().contains(echoServiceExtension.container().response) } ?: false 31 | } 32 | return this 33 | } 34 | 35 | fun ObjectAssert.isEitherFrom(vararg echoContainers: EchoServiceExtension): ObjectAssert { 36 | matches { 37 | val serviceResponse = it.body?.string() ?: "" 38 | echoContainers.any { containerExtension -> serviceResponse.contains(containerExtension.container().response) } 39 | } 40 | return this 41 | } 42 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.consul 2 | 3 | import org.junit.jupiter.api.extension.AfterEachCallback 4 | import org.junit.jupiter.api.extension.ExtensionContext 5 | import org.testcontainers.containers.Network 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 7 | import pl.allegro.tech.servicemesh.envoycontrol.logger 8 | 9 | class ConsulExtension(private val deregisterAfterEach: Boolean = true) : BeforeAndAfterAllOnce, AfterEachCallback { 10 | 11 | companion object { 12 | private val SHARED_CONSUL = ConsulSetup( 13 | Network.SHARED, 14 | ConsulServerConfig(1, "dc1", expectNodes = 1) 15 | ) 16 | } 17 | 18 | val server = SHARED_CONSUL 19 | private val logger by logger() 20 | 21 | override fun beforeAllOnce(context: ExtensionContext) { 22 | logger.info("Consul extension is starting.") 23 | server.container.start() 24 | logger.info("Consul extension started.") 25 | } 26 | 27 | override fun afterEach(context: ExtensionContext) { 28 | if (deregisterAfterEach) { 29 | server.operations.deregisterAll() 30 | } 31 | } 32 | 33 | override fun afterAllOnce(context: ExtensionContext) { 34 | server.operations.deregisterAll() 35 | } 36 | 37 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 38 | } 39 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/chaos/domain/ChaosService.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.chaos.domain 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.chaos.storage.ChaosDataStore 4 | import pl.allegro.tech.servicemesh.envoycontrol.chaos.storage.NetworkDelay as NetworkDelayEntity 5 | 6 | class ChaosService(val chaosDataStore: ChaosDataStore) { 7 | 8 | fun submitNetworkDelay( 9 | networkDelay: NetworkDelay 10 | ): NetworkDelay = chaosDataStore.save(item = networkDelay.toEntity()).toDomainObject() 11 | 12 | fun getExperimentsList(): List = chaosDataStore.get().map { it.toDomainObject() } 13 | 14 | fun deleteNetworkDelay( 15 | networkDelayId: String 16 | ) { 17 | chaosDataStore.delete(id = networkDelayId) 18 | } 19 | } 20 | 21 | data class NetworkDelay( 22 | val id: String, 23 | val affectedService: String, 24 | val delay: String, 25 | val duration: String, 26 | val targetService: String 27 | ) { 28 | fun toEntity(): NetworkDelayEntity = NetworkDelayEntity( 29 | id = id, 30 | affectedService = affectedService, 31 | delay = delay, 32 | duration = duration, 33 | targetService = targetService 34 | ) 35 | } 36 | 37 | fun NetworkDelayEntity.toDomainObject(): NetworkDelay = NetworkDelay( 38 | id = id, 39 | affectedService = affectedService, 40 | delay = delay, 41 | duration = duration, 42 | targetService = targetService 43 | ) 44 | -------------------------------------------------------------------------------- /.github/workflows/flaky.yaml: -------------------------------------------------------------------------------- 1 | name: Flaky tests 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | paths-ignore: 8 | - 'readme.md' 9 | 10 | jobs: 11 | flaky_test: 12 | name: flaky_test 13 | runs-on: ubuntu-latest 14 | env: 15 | GRADLE_OPTS: '-Dfile.encoding=utf-8 -Dorg.gradle.daemon=false' 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | ref: ${{ github.head_ref }} 22 | 23 | - uses: gradle/actions/wrapper-validation@v4 24 | 25 | - uses: actions/setup-java@v3 26 | with: 27 | distribution: 'temurin' 28 | java-version: '17' 29 | 30 | - name: Cache Gradle packages 31 | uses: actions/cache@v3 32 | with: 33 | path: | 34 | ~/.gradle/caches 35 | ~/.gradle/wrapper 36 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 37 | restore-keys: | 38 | ${{ runner.os }}-gradle- 39 | 40 | - name: Flaky tests 41 | run: ./gradlew clean -Penvironment=integration :envoy-control-tests:flakyTest 42 | 43 | - name: Junit report 44 | uses: mikepenz/action-junit-report@v5 45 | if: always() 46 | with: 47 | report_paths: '**/build/test-results/test/TEST-*.xml' 48 | 49 | - name: Cleanup Gradle Cache 50 | run: | 51 | rm -f ~/.gradle/caches/modules-2/modules-2.lock 52 | rm -f ~/.gradle/caches/modules-2/gc.properties 53 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/routing/ServiceTagsAndCanaryTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.routing 2 | 3 | import org.junit.jupiter.api.extension.RegisterExtension 4 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 7 | 8 | class ServiceTagsAndCanaryTest : ServiceTagsAndCanaryTestBase { 9 | companion object { 10 | 11 | @JvmField 12 | @RegisterExtension 13 | val consul = ConsulExtension() 14 | 15 | @JvmField 16 | @RegisterExtension 17 | val envoyControl = EnvoyControlExtension(consul, mapOf( 18 | "envoy-control.envoy.snapshot.routing.service-tags.enabled" to true, 19 | "envoy-control.envoy.snapshot.routing.service-tags.metadata-key" to "tag", 20 | "envoy-control.envoy.snapshot.load-balancing.canary.enabled" to true, 21 | "envoy-control.envoy.snapshot.load-balancing.canary.metadata-key" to "canary", 22 | "envoy-control.envoy.snapshot.load-balancing.canary.metadata-value" to "1" 23 | )) 24 | 25 | @JvmField 26 | @RegisterExtension 27 | val envoy = EnvoyExtension(envoyControl) 28 | } 29 | 30 | override fun consul() = consul 31 | 32 | override fun envoyControl() = envoyControl 33 | 34 | override fun envoy() = envoy 35 | } 36 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/utils/TestData.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.utils 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties 4 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.ZoneWeights 5 | 6 | const val INGRESS_HOST = "ingress-host" 7 | const val INGRESS_PORT = 3380 8 | const val EGRESS_HOST = "egress-host" 9 | const val EGRESS_PORT = 3380 10 | const val DEFAULT_IDLE_TIMEOUT = 100L 11 | const val DEFAULT_SERVICE_NAME = "service-name" 12 | const val DEFAULT_DISCOVERY_SERVICE_NAME = "discovery-service-name" 13 | const val CLUSTER_NAME = "cluster-name" 14 | const val CLUSTER_NAME1 = "cluster-1" 15 | const val CLUSTER_NAME2 = "cluster-2" 16 | const val TRAFFIC_SPLITTING_ZONE = "dc2" 17 | const val CURRENT_ZONE = "dc1" 18 | const val DEFAULT_PRIORITY = 1 19 | const val HIGHEST_PRIORITY = 0 20 | 21 | val DEFAULT_CLUSTER_WEIGHTS = zoneWeights(mapOf(CURRENT_ZONE to 60, TRAFFIC_SPLITTING_ZONE to 40)) 22 | 23 | val SNAPSHOT_PROPERTIES_WITH_WEIGHTS = SnapshotProperties().also { 24 | it.dynamicListeners.enabled = false 25 | it.loadBalancing.trafficSplitting.weightsByService = mapOf( 26 | DEFAULT_SERVICE_NAME to DEFAULT_CLUSTER_WEIGHTS 27 | ) 28 | it.loadBalancing.trafficSplitting.zoneName = TRAFFIC_SPLITTING_ZONE 29 | it.loadBalancing.trafficSplitting.zonesAllowingTrafficSplitting = listOf(CURRENT_ZONE) 30 | } 31 | 32 | fun zoneWeights(weightByZone: Map) = ZoneWeights().also { 33 | it.weightByZone = weightByZone 34 | } 35 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyHttpFilters.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters 2 | 3 | import io.envoyproxy.envoy.config.core.v3.Metadata 4 | import pl.allegro.tech.servicemesh.envoycontrol.groups.Group 5 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties 6 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.HttpFilterFactory 7 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes.IngressMetadataFactory 8 | 9 | class EnvoyHttpFilters( 10 | val ingressFilters: List, 11 | val egressFilters: List, 12 | val ingressMetadata: IngressMetadataFactory = { _: Group, _: String -> Metadata.getDefaultInstance() } 13 | ) { 14 | companion object { 15 | val emptyFilters = EnvoyHttpFilters(listOf(), listOf()) { _, _ -> Metadata.getDefaultInstance() } 16 | 17 | fun defaultFilters( 18 | snapshotProperties: SnapshotProperties, 19 | customLuaMetadata: LuaMetadataProperty.StructPropertyLua = LuaMetadataProperty.StructPropertyLua() 20 | ): EnvoyHttpFilters { 21 | val defaultFilters = EnvoyDefaultFilters(snapshotProperties, customLuaMetadata) 22 | return EnvoyHttpFilters( 23 | defaultFilters.ingressFilters(), 24 | defaultFilters.defaultEgressFilters, 25 | defaultFilters.defaultIngressMetadata 26 | ) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /envoy-control-source-consul/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/consul/services/ConsulClusterStateChangesDisposeTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.consul.services 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import org.mockito.Mockito 6 | import org.mockito.Mockito.`when` 7 | import org.mockito.Mockito.times 8 | import org.mockito.Mockito.verify 9 | import pl.allegro.tech.discovery.consul.recipes.watch.Canceller 10 | import pl.allegro.tech.discovery.consul.recipes.watch.ConsulWatcher 11 | import pl.allegro.tech.servicemesh.envoycontrol.server.ReadinessStateHandler 12 | 13 | class ConsulClusterStateChangesDisposeTest { 14 | 15 | @Test 16 | fun `should start watching and stop watching after dispose`() { 17 | val watcher = Mockito.mock(ConsulWatcher::class.java) 18 | val callbackCanceller = Canceller() 19 | val readinessStateHandler = Mockito.spy(ReadinessStateHandler::class.java) 20 | `when`(watcher.watchEndpoint(Mockito.eq("/v1/catalog/services"), Mockito.any(), Mockito.any())).thenReturn( 21 | callbackCanceller 22 | ) 23 | 24 | val recipes = ConsulServiceChanges(watcher = watcher, readinessStateHandler = readinessStateHandler) 25 | recipes.watchState().subscribe().dispose() 26 | 27 | verify(readinessStateHandler, times(2)).unready() 28 | verify(watcher).watchEndpoint(Mockito.eq("/v1/catalog/services"), Mockito.any(), Mockito.any()) 29 | assertThat(callbackCanceller.isCancelled).isTrue() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/containers/ToxiproxyContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.containers 2 | 3 | import eu.rekawek.toxiproxy.ToxiproxyClient 4 | import org.testcontainers.containers.wait.strategy.Wait 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 6 | import java.util.LinkedList 7 | 8 | class ToxiproxyContainer(exposedPortsCount: Int = 0) : 9 | GenericContainer("shopify/toxiproxy:2.1.4") { 10 | 11 | companion object { 12 | const val internalToxiproxyPort = 8474 13 | } 14 | 15 | val client by lazy { ToxiproxyClient("localhost", getMappedPort(internalToxiproxyPort)) } 16 | 17 | private val freeExposedPorts = (1..exposedPortsCount).map { portOffset -> 18 | val port = internalToxiproxyPort + portOffset 19 | this.addExposedPort(port) 20 | port 21 | }.let { LinkedList(it) } 22 | 23 | override fun configure() { 24 | super.configure() 25 | this.addExposedPort(internalToxiproxyPort) 26 | waitingFor(Wait.forHttp("/version").forPort(internalToxiproxyPort)) 27 | } 28 | 29 | fun createProxy(targetIp: String, targetPort: Int): String { 30 | val listenPort = freeExposedPorts.pop() 31 | client.createProxy( 32 | "$targetIp:$targetPort", 33 | "$allInterfaces:$listenPort", 34 | "$targetIp:$targetPort" 35 | ).enable() 36 | return "http://$containerIpAddress:${getMappedPort(listenPort)}" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/resilence.yaml: -------------------------------------------------------------------------------- 1 | name: Resilence test 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | paths-ignore: 8 | - 'readme.md' 9 | 10 | jobs: 11 | resilence_test: 12 | name: resilence_test 13 | runs-on: ubuntu-latest 14 | env: 15 | GRADLE_OPTS: '-Dfile.encoding=utf-8 -Dorg.gradle.daemon=false' 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | ref: ${{ github.head_ref }} 22 | 23 | - uses: gradle/actions/wrapper-validation@v4 24 | 25 | - uses: actions/setup-java@v3 26 | with: 27 | distribution: 'temurin' 28 | java-version: '17' 29 | 30 | - name: Cache Gradle packages 31 | uses: actions/cache@v3 32 | with: 33 | path: | 34 | ~/.gradle/caches 35 | ~/.gradle/wrapper 36 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 37 | restore-keys: | 38 | ${{ runner.os }}-gradle- 39 | 40 | - name: Reliability tests 41 | run: ./gradlew clean -Penvironment=integration :envoy-control-tests:reliabilityTest -DRELIABILITY_FAILURE_DURATION_SECONDS=20 42 | 43 | - name: Junit report 44 | uses: mikepenz/action-junit-report@v5 45 | if: always() 46 | with: 47 | report_paths: '**/build/test-results/test/TEST-*.xml' 48 | 49 | - name: Cleanup Gradle Cache 50 | run: | 51 | rm -f ~/.gradle/caches/modules-2/modules-2.lock 52 | rm -f ~/.gradle/caches/modules-2/gc.properties 53 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Envoy Control is a [Kotlin](https://kotlinlang.org/) application, it requires JDK 8+ to run it. 4 | 5 | ## Running 6 | ```bash 7 | ./gradlew run 8 | ``` 9 | 10 | ## Testing 11 | * All tests (unit and integration) 12 | ```./gradlew test``` 13 | * Unit 14 | ```./gradlew unitTest``` 15 | * Integration 16 | ```./gradlew integrationTest``` 17 | * Reliability tests 18 | ```./gradlew clean -i -Penvironment=integration :envoy-control-tests:reliabilityTest -DRELIABILITY_FAILURE_DURATION_SECONDS=20``` 19 | * Flaky tests 20 | ```./gradlew -Penvironment=integration :envoy-control-tests:flakyTest``` 21 | 22 | ## Running Lua tests locally (not inside docker) for debugging purposes 23 | 24 | If for some reason `busted` exists with non-zero code and does not give any output you can try running it locally. 25 | 26 | ### Requirements on macOS 27 | 28 | ```bash 29 | brew info luarocks 30 | luarocks install busted 31 | ``` 32 | 33 | ### Running Lua tests 34 | 35 | ``` 36 | busted --lpath="./envoy-control-core/src/main/resources/lua/?.lua" envoy-control-tests/src/main/resources/lua_spec/ 37 | ``` 38 | 39 | ## Packaging 40 | To build a distribution package run 41 | ``` 42 | ./gradle distZip 43 | ``` 44 | The package should be available in `{root}/envoy-control-runner/build/distributions/envoy-control-runner-{version}.zip` 45 | 46 | ## Formatter 47 | To apply [ktlint](https://ktlint.github.io/) formatting rules to IntelliJ IDEA. Run: `./gradlew ktlintApplyToIdea` 48 | 49 | ## Linter 50 | A linter - [detekt](https://detekt.github.io/detekt/) runs when Envoy Control is built. You can run it separately: 51 | `./gradlew detekt`. 52 | 53 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/LuaTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.junit.jupiter.api.DynamicTest 4 | import org.junit.jupiter.api.DynamicTest.dynamicTest 5 | import org.junit.jupiter.api.TestFactory 6 | import org.junit.jupiter.api.fail 7 | import pl.allegro.tech.servicemesh.envoycontrol.config.containers.LuaTestsContainer 8 | 9 | internal class LuaTest { 10 | 11 | val logger by logger() 12 | 13 | @TestFactory 14 | fun luaTests(): List { 15 | val luaContainer = LuaTestsContainer() 16 | 17 | val results = luaContainer.runLuaTests() 18 | val output = results.stdout 19 | 20 | val passedTests = output.successes.map { passedTest(it.name) } 21 | val failedTests = (output.failures + output.errors + output.pendings).map { failedTest(it.message, it.name) } 22 | 23 | val additionalErrors = mutableListOf() 24 | if (failedTests.isEmpty() && !results.exitCodeSuccess) { 25 | additionalErrors.add(failedTest("Lua tests failed")) 26 | } 27 | if (passedTests.isEmpty() && failedTests.isEmpty()) { 28 | additionalErrors.add(failedTest("no tests executed")) 29 | } 30 | 31 | logger.info("Lua tests stderr output:\n${results.stderr}") 32 | 33 | return passedTests + failedTests + additionalErrors 34 | } 35 | 36 | private fun passedTest(name: String) = dynamicTest(name) {} 37 | private fun failedTest(message: String, name: String = "") = dynamicTest( 38 | name.ifEmpty { "LuaTest" } 39 | ) { fail("$name:\n$message") } 40 | } 41 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulConfig.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.consul 2 | 3 | import java.io.File 4 | 5 | sealed class ConsulConfig( 6 | val id: Int, 7 | val dc: String, 8 | val config: Map, 9 | val jsonFiles: List = listOf() 10 | ) { 11 | fun launchCommand(): String { 12 | val base = mapOf( 13 | "datacenter" to dc 14 | ) 15 | 16 | return "consul agent " + (config + base).map { (key, value) -> format(key, value) }.joinToString(" ") 17 | } 18 | 19 | private fun format(key: String, value: String): String { 20 | return if (value.isEmpty()) "-$key" else "-$key=$value" 21 | } 22 | } 23 | 24 | val defaultConfig = mapOf( 25 | "data-dir" to "/data", 26 | "pid-file" to ConsulContainer.pidFile, 27 | "config-dir" to ConsulContainer.configDir, 28 | "bind" to "0.0.0.0", 29 | "client" to "0.0.0.0" 30 | ) 31 | 32 | class ConsulClientConfig(id: Int, dc: String, serverAddress: String, jsonFiles: List = listOf()) : ConsulConfig( 33 | id, 34 | dc, 35 | defaultConfig + mapOf( 36 | "retry-join" to serverAddress, 37 | "node" to "consul-client-$id" 38 | ), 39 | jsonFiles 40 | ) 41 | 42 | class ConsulServerConfig(id: Int, dc: String, expectNodes: Int = 3, jsonFiles: List = listOf()) : ConsulConfig( 43 | id, 44 | dc, 45 | defaultConfig + mapOf( 46 | "server" to "", 47 | "bootstrap-expect" to expectNodes.toString(), 48 | "ui" to "", 49 | "node" to "consul-server-$dc-$id" 50 | ), 51 | jsonFiles 52 | ) 53 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/transformers/InstanceMerger.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services.transformers 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstance 4 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 5 | 6 | class InstanceMerger : ServiceInstancesTransformer { 7 | 8 | override fun transform(services: Sequence): Sequence = services.map { 9 | 10 | val containsDuplicates = it.instances 11 | .groupingBy { it.address to it.port }.eachCount() 12 | .any { it.value > 1 } 13 | 14 | if (containsDuplicates) { 15 | it.copy(instances = merge(it.instances)) 16 | } else { 17 | it 18 | } 19 | } 20 | 21 | private fun merge(instances: Set): Set = instances 22 | .groupBy { it.address to it.port } 23 | .map { (target, instances) -> 24 | if (instances.size == 1) { 25 | instances[0] 26 | } else { 27 | ServiceInstance( 28 | id = instances.map { it.id }.joinToString(","), 29 | tags = instances.map { it.tags }.reduce { s1, s2 -> s1 + s2 }, 30 | address = target.first, 31 | port = target.second, 32 | regular = instances.any { it.regular }, 33 | canary = instances.any { it.canary }, 34 | weight = instances.sumOf { it.weight } 35 | ) 36 | } 37 | } 38 | .toSet() 39 | } 40 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulSetup.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.consul 2 | 3 | import org.testcontainers.containers.Network 4 | import org.testcontainers.junit.jupiter.Testcontainers 5 | import pl.allegro.tech.servicemesh.envoycontrol.utils.Ports 6 | 7 | @Testcontainers 8 | class ConsulSetup( 9 | network: Network, 10 | consulConfig: ConsulConfig, 11 | val port: Int = Ports.nextAvailable() 12 | ) { 13 | val container: ConsulContainer = ConsulContainer(consulConfig.dc, port, consulConfig.id, consulConfig) 14 | .withNetwork(network) 15 | val operations = ConsulOperations(port) 16 | } 17 | 18 | class ConsulClusterSetup(val consulSetups: List) { 19 | val container = consulSetups.first() 20 | val operations = consulSetups.first().operations 21 | val port = consulSetups.first().port 22 | 23 | fun start() { 24 | consulSetups.forEach { consul -> 25 | consul.container.start() 26 | } 27 | consulSetups.forEach { consul -> 28 | val consulContainerNames = consulSetups.map { it.container.containerName() }.toTypedArray() 29 | val args = arrayOf("consul", "join", *consulContainerNames) 30 | consul.container.execInContainer(*args) 31 | } 32 | } 33 | 34 | fun joinWith(other: ConsulClusterSetup) { 35 | consulSetups.forEach { consul -> 36 | val consulInDc2ContainerNames = other.consulSetups.map { it.container.containerName() }.toTypedArray() 37 | val args = arrayOf("consul", "join", "-wan", *consulInDc2ContainerNames) 38 | consul.container.execInContainer(*args) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/DynamicForwardProxyFilter.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters 2 | 3 | import com.google.protobuf.UInt32Value 4 | import com.google.protobuf.util.Durations 5 | import io.envoyproxy.envoy.extensions.common.dynamic_forward_proxy.v3.DnsCacheConfig 6 | import io.envoyproxy.envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig 7 | import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter 8 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.DynamicForwardProxyProperties 9 | 10 | class DynamicForwardProxyFilter( 11 | val properties: DynamicForwardProxyProperties 12 | ) { 13 | val filter: HttpFilter = HttpFilter.newBuilder() 14 | .setName("envoy.filters.http.dynamic_forward_proxy") 15 | .setTypedConfig( 16 | com.google.protobuf.Any.pack( 17 | FilterConfig.newBuilder() 18 | .setDnsCacheConfig( 19 | DnsCacheConfig.newBuilder() 20 | .setName("dynamic_forward_proxy_cache_config") 21 | .setDnsLookupFamily(properties.dnsLookupFamily) 22 | .setHostTtl( 23 | Durations.fromMillis( 24 | properties.maxHostTtl.toMillis() 25 | ) 26 | ) 27 | .setMaxHosts( 28 | UInt32Value.of(properties.maxCachedHosts) 29 | ) 30 | ).build() 31 | ) 32 | ).build() 33 | } 34 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | ## Requirements 4 | * Java 8+ 5 | * Docker & Docker Compose 6 | 7 | ## Setting up an environment 8 | 9 | ### Dependencies 10 | At this moment, Envoy Control requires Consul to run. 11 | In the future, there will be support for other discovery service systems. 12 | Additionally, to test the system it's convenient to have Envoy that connects to 13 | Envoy Control and a service registered in Consul that will be propagated to Envoy. 14 | 15 | You can run all dependencies with docker-compose 16 | ``` 17 | git clone https://github.com/allegro/envoy-control.git 18 | cd tools 19 | ./run-with-local-ec.sh 20 | ``` 21 | 22 | To check the environment, go to Consul UI: [http://localhost:18500](http://localhost:18500) and see whether there is 23 | a registered _http-echo_ service. 24 | 25 | Additionally, you can check Envoy Admin at [http://localhost:9999](http://localhost:9999) 26 | 27 | _http-echo_ service is not available outside of docker's network. 28 | 29 | Envoy listener is available on [http://localhost:31000](http://localhost:31000) but the _http-echo_ service location 30 | is not yet propagated to Envoy. 31 | 32 | ### Run Envoy Control 33 | Run Envoy Control `./gradlew run` in a cloned catalog. 34 | 35 | ## Test the system 36 | After a while, Envoy Control should read the state of Consul and propagate it to Envoy. 37 | 38 | You can check it by sending curl request to _http-echo_ through a proxy. 39 | The request will be sent to Envoy which will be redirected to the _http-echo_ service. 40 | ``` 41 | curl -x localhost:31000 http://http-echo/test -v 42 | ``` 43 | 44 | Instead of using the proxy feature you can also send a request to Envoy with a Host header. 45 | ``` 46 | curl -H "Host: http-echo" http://localhost:31000/status/info 47 | ``` -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoyControlMetrics.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import io.micrometer.core.instrument.MeterRegistry 4 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 5 | import java.util.concurrent.atomic.AtomicInteger 6 | 7 | interface EnvoyControlMetrics { 8 | fun serviceRemoved() 9 | fun serviceAdded() 10 | fun instanceChanged() 11 | fun snapshotChanged() 12 | fun setCacheGroupsCount(count: Int) 13 | fun errorWatchingServices() 14 | val meterRegistry: MeterRegistry 15 | } 16 | 17 | data class DefaultEnvoyControlMetrics( 18 | val servicesRemoved: AtomicInteger = AtomicInteger(), 19 | val servicesAdded: AtomicInteger = AtomicInteger(), 20 | val instanceChanges: AtomicInteger = AtomicInteger(), 21 | val snapshotChanges: AtomicInteger = AtomicInteger(), 22 | val cacheGroupsCount: AtomicInteger = AtomicInteger(), 23 | val errorWatchingServices: AtomicInteger = AtomicInteger(), 24 | override val meterRegistry: MeterRegistry = SimpleMeterRegistry() 25 | ) : EnvoyControlMetrics { 26 | 27 | override fun errorWatchingServices() { 28 | errorWatchingServices.incrementAndGet() 29 | } 30 | 31 | override fun serviceRemoved() { 32 | servicesRemoved.incrementAndGet() 33 | } 34 | 35 | override fun serviceAdded() { 36 | servicesAdded.incrementAndGet() 37 | } 38 | 39 | override fun instanceChanged() { 40 | instanceChanges.incrementAndGet() 41 | } 42 | 43 | override fun snapshotChanged() { 44 | snapshotChanges.incrementAndGet() 45 | } 46 | 47 | override fun setCacheGroupsCount(count: Int) { 48 | cacheGroupsCount.set(count) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/ControlPlaneTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | import pl.allegro.tech.servicemesh.envoycontrol.server.ExecutorType 7 | import reactor.core.publisher.Flux 8 | 9 | class ControlPlaneTest { 10 | 11 | @Test 12 | fun shouldUseSimpleCacheWithInitialResourcesHandling() { 13 | val meterRegistry = SimpleMeterRegistry() 14 | val envoyControlProperties = EnvoyControlProperties().also { 15 | it.server.executorGroup.type = ExecutorType.PARALLEL 16 | it.server.groupSnapshotUpdateScheduler.type = ExecutorType.PARALLEL 17 | it.server.enableInitialResourcesHandling = true 18 | } 19 | 20 | val controlPlane = ControlPlane.builder(envoyControlProperties, meterRegistry).build(Flux.empty()) 21 | assertThat(controlPlane.cache).isInstanceOf(SimpleCache::class.java) 22 | } 23 | 24 | @Test 25 | fun shouldUseSimpleCacheWithoutInitialResourcesHandling() { 26 | val meterRegistry = SimpleMeterRegistry() 27 | val envoyControlProperties = EnvoyControlProperties().also { 28 | it.server.executorGroup.type = ExecutorType.PARALLEL 29 | it.server.groupSnapshotUpdateScheduler.type = ExecutorType.PARALLEL 30 | it.server.enableInitialResourcesHandling = false 31 | } 32 | 33 | val controlPlane = ControlPlane.builder(envoyControlProperties, meterRegistry).build(Flux.empty()) 34 | assertThat(controlPlane.cache).isInstanceOf(SimpleCacheNoInitialResourcesHandling::class.java) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/reliability/EnvoyControlDownTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.reliability 2 | 3 | import org.junit.jupiter.api.BeforeAll 4 | import org.junit.jupiter.api.Test 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlRunnerTestApp 6 | 7 | internal class EnvoyControlDownTest : ReliabilityTest() { 8 | 9 | companion object { 10 | @JvmStatic 11 | @BeforeAll 12 | fun setup() { 13 | setup( 14 | appFactoryForEc1 = { 15 | EnvoyControlRunnerTestApp( 16 | consulPort = Toxiproxy.externalConsulPort, 17 | grpcPort = Toxiproxy.toxiproxyGrpcPort 18 | ) 19 | }, 20 | envoyConnectGrpcPort = Toxiproxy.externalEnvoyControl1GrpcPort 21 | ) 22 | } 23 | } 24 | 25 | @Test 26 | fun `is resilient to EnvoyControl failure in one dc`() { 27 | // given 28 | registerService(name = "service-1") 29 | assertReachableThroughEnvoy("service-1") 30 | 31 | // when 32 | makeEnvoyControlUnavailable() 33 | 34 | // Service registration is not affected by injected Consul faults, it bypasses toxiproxy 35 | registerService(name = "service-2") 36 | 37 | // then 38 | holdAssertionsTrue { 39 | assertReachableThroughEnvoy("service-1") 40 | assertUnreachableThroughEnvoy("service-2") 41 | } 42 | 43 | // and when 44 | makeEnvoyControlAvailable() 45 | 46 | // then 47 | assertReachableThroughEnvoy("service-1") 48 | assertReachableThroughEnvoy("service-2") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/features/timeouts.md: -------------------------------------------------------------------------------- 1 | # Timeouts 2 | 3 | Envoy Control provides a simple and fine-grained way to configure timeouts between services. Using 4 | Envoy's [metadata](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/base.proto#config-core-v3-metadata) 5 | section you can provide additional configuration to the Control Plane. The information provided 6 | in `metadata.proxy_settings` section is interpreted by Control Plane and it will create a 7 | corresponding configuration for `Envoy`. This means that Envoy Control is stateless but in the 8 | future there will be an override mechanism that uses a database to save the configuration. 9 | 10 | An example configuration: 11 | 12 | ```yaml 13 | metadata: 14 | proxy_settings: 15 | outgoing: 16 | dependencies: 17 | - service: "*" 18 | timeoutPolicy: 19 | idleTimeout: 20s 20 | requestTimeout: 30s 21 | - service: "service-a" 22 | timeoutPolicy: 23 | idleTimeout: 40s 24 | requestTimeout: 50s 25 | ``` 26 | In the `outgoing` section this configuration defines `timeoutPolicy` : 27 | 28 | * `idleTimeout` - The idle timeout is defined as the period in which there are no active requests. 29 | * `requestTimeout` - The amount of time that Envoy will wait for the entire request to be received. 30 | 31 | More over we have option to indicate `service-a` or use `*` which will be default properties for all 32 | other services. Last but not least if you don't provide this configuration it will use [default 33 | properties](https://github.com/allegro/envoy-control/blob/master/docs/configuration.md#snapshot-properties): 34 | 35 | * `envoy-control.envoy.snapshot.egress.common-http.idle-timeout` 36 | * `envoy-control.envoy.snapshot.egress.common-http.request-timeout` 37 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/OriginalDestinationTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.extension.RegisterExtension 6 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.isFrom 7 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.isOk 8 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 12 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 13 | 14 | class OriginalDestinationTest { 15 | 16 | companion object { 17 | 18 | @JvmField 19 | @RegisterExtension 20 | val consul = ConsulExtension() 21 | 22 | @JvmField 23 | @RegisterExtension 24 | val envoyControl = EnvoyControlExtension(consul) 25 | 26 | @JvmField 27 | @RegisterExtension 28 | val service = EchoServiceExtension() 29 | 30 | @JvmField 31 | @RegisterExtension 32 | val envoy = EnvoyExtension(envoyControl) 33 | } 34 | 35 | @Test 36 | fun `should send direct request when host envoy-original-destination and header x-envoy-original-dst-host with IP provided`() { 37 | untilAsserted { 38 | // when 39 | val response = envoy.egressOperations.callServiceWithOriginalDst(service) 40 | 41 | // then 42 | assertThat(response).isOk().isFrom(service) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/privkey_echo2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAoUk18jfyEHayWipH+53YYRfiam9Ei3aA+cwext6iJpamkWbb 3 | lV8qjlmCeSS/k3AeZFymZvjdzk0tRO4IpAWiOnflcHIhVqEHmqMMyeeJzz9P/ciO 4 | vbriMl6Lo0mgYSFySmsIqslylUVh8il6wx7ezYfIlUq3bF1Rf55g+C0+llvkZVlW 5 | dByKeIY553m4RL5XL7cMh3Jp6IelRNk2kGGilaCpSX+tLWrMRXJar/UbKsbF5OEF 6 | Qclu+u7HaB04XHputqVN8LFOq14QcTQK/s1bIegfRusrFX1NeDy8hq3TWK32fJQP 7 | cH2M24jcllsWSTpWKUOVmyp0wCDzOELs0pI5HwIDAQABAoIBAFUDMdwqgP0Mk5XT 8 | E34dBSCoZj+Txp18KR+B5/cLAo00ezfI75UcRGIj7BHOvOwJ/PsJmuxL4R4Mr//V 9 | N9i833XSHK3Yepoe5tMrfmIuGQWUaaVrQVHgX7oM+61l8ZNA/e0b3cWnyS3FFIuA 10 | MaUHcIkFOUT3zRbhWUPbR2GI96RolOWCOOuaICnkXcMOBuZ3kJGWLaNhadvwq4F0 11 | 5wL14iF/mamXFwE1g1tI4HUXejPkLJcLQE1m8dB756Kj6CBEkrkonfgUWBQeBmYj 12 | 8ZNutSgYlQ+wrlhIyh++66R9mwgkQiQC13E3NXR+56l4S/giEwdow8qBnDQLaHGF 13 | Jqf524ECgYEA080yQ8bP6EZgNWIP1FwFy6LkhLNX4H7SF28Qb40COPnM7n8+fJul 14 | Ok0Plj04Vwnu1urQMvF0OE55FD+TN2eiU2cugQsi/nXNue/7W7j8FPNLWr4x7dbf 15 | iDTdmGW3ktXu8iSPBdfxNu5tvmgZufO0ZMgpT9PmozD4aLDU299E38UCgYEAwvFi 16 | uh5lVAa3ka/k5Uxz1sbBOX/OE87GN7wrXiLRUugOk/LNGhj4pDmCsfc+X0GzSm6v 17 | EAbDfh5OoQcEDffzl/Kn1kJDX639OpvxPwwwTm6KSpRNHWaFzQaYU+N47GZLNo7D 18 | gAVKPaaFvm4MUWwRELshiobW6yEQ2jut6qwnf5MCgYAccwyUZAkbNxDHOuPLb4zr 19 | vHabiId+RSqtMSLxCOzKgGtRgc//5tw7x4wXbyO5NUFapvHOVfZ5dbj3yk5Y+9en 20 | Ak4R0VAjA+ndVunZeTCLE98EUlXcj6c44Xj+dnNBCvFsnxKDA22IJmChWdRS0PRj 21 | MSuzaFArjXFghpgg7I2QzQKBgHWqyhLZogP0Af24FAur+Afd9GfGkxc1qkOeGvc5 22 | QOwwEgyPc+maxQorhI3zVPyeZaA56wVbb8jCdmeQGoxr6+b9mv0jBhPMq5si4Z1S 23 | uB2/b03Q8jMa3QyCPJP2K9lVbXTC+5JcnxFTYEsvldPXzP0yVu6MsVE/pjJz72hE 24 | /ZptAoGAbXuqPRbel/dsklCmvCdn925vsMTzJnWjbpJJFrGblTWk+QfrYAdCm1M+ 25 | D9sSIwjqX1aijNVkMSTQl5+uvTyaY+x40U3bw9QFu00VwAQaQ7l94PBZofm72mG2 26 | r8eryh/h8bwiv5IDqdImtHVEQ/lD6oy6YKoDcxQGWs5tYA0zpRQ= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/privkey_echo4.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAy2GZkj5d1H8m8LkeCqi5DTghmxApzIQGRsbfkxvM9nN9RN/V 3 | 5w8iy3cdZabOr3DqSNU7pJAWFPLoJPntnrqnzAmzhniMHGz517xMNl25dwTL6FJn 4 | riOqFYHP+kauHG9bul8FHsrkVO4X+0GUfAVvteZOYZ5tfm/f2uWVs7Lm5x97TRbV 5 | PUBOkLL6SmdJU21UKWGfLnZU40VbnjRxbU1UMval44GThZNO5iov6q3Z3Eycyz3G 6 | fbZV8jP8Ie1ctAlchHuHnw7X9GveFAc+hd9ZgLulQbcuI3RD9u3sJL8ySOQCq9PK 7 | FqXHpLDI5yHS9rdpc3J3dQxZsKbvxSt/B8N2lQIDAQABAoIBAEv41GYFuAUlzkUD 8 | 0Y4HwsCGZD9JRWPpOXL83Q1VyDWTpIqy1nNuev/oKeoL6o8srcexJ4tsa2M97avK 9 | VJIDhaS5Rv+HTkdcCeQlqY5xalNkTkslZSdumR1ZlXgXKPjkPC6dSgtbnPmAyyKH 10 | N9EwMq+PLV5X0Oz3G3boZghvXsOcmqLLmaXF/XZ8p1mCVJLK715v4OkEkBrgykK7 11 | AsOTZ+XKIc91oi8bMDSbHPa+0X/jDlRBZmCS6oKBbSNJOF9PjCyHUxXm0zFND5gs 12 | Hzhn0E8P1CSfPRLZVDY2C4Tyx1XLFJvdRzbVyZCOeoaiFqU7YLs9MRCIJtihjsRi 13 | N4Em6k0CgYEA++iQObssz483a7FxoTlaohSChPpb+eqjbtgsHNUrVNqumc+stJcV 14 | I85YSMXobsPkQvF2wf4v99hoN/sdtF/9xpA26ItcfmU+pZfhM87atQtLbUUd7rvI 15 | +3ntx2f07efptoGRDi6SuwaJJ7kD7Ohit5Rjkq43WcNTw8tiQbyTJxMCgYEAzq9C 16 | mbHz1wyrpqBNxxW9DOW/z6VDvkRigE6kIB1NU6B/bp5vg5TJvQt3ftM//mhN7yIB 17 | 4mXZgtJX5Rra3wC8Be2J92UoiO23bt6tooPUkE8p+e38b7SQ+PTbZm3Qp0anI3d3 18 | b9nXoL/TgGAFpVATBlD6f8j2iBn+TEHjTLbMWLcCgYEA8k6GhHGNre0FkxpwwXMI 19 | wgTWcxDa7e8L27Al5moJrypWbm77og39cJ6n/wAXDoxxAQ+AeyOEgnNv9AEhVoJK 20 | +fd4SYDbrFy4wNHx9kKPzzuZBvdHzn5k2bgzxu4xA7Ji9YF1xN15mFq2DTaDFxuE 21 | 8S7UBKB9b2NaLGhzD+ZS6W8CgYAXnKAQLOs9cx4dAA1CpDIfyhN6plex4eAa5mEL 22 | pi1SU3Cbc5AryRCu/yNcoseZydK1cf4jHh2WO299Je9BMLVKPBPZ5n2V/wpIqTnO 23 | 6h6bid+yxMRlAozZVCfIcN04bvLjM1+6sHlYzPFdphqfAyHZQ+EKPiwj6kxJ6/EK 24 | r4tyywKBgGRyiz+9uIcYvQim7ZbKWISiV/+u7xNze1ckhwtfDp6OUlONSKH9pWWC 25 | yClCO8gA6vle1+PIQlHfEd8KQ9aOEQ+NG3e+ypvR2Vwq57tgp8lVkYM3oqGicECt 26 | wVkOqvLaS8yb3mJK9yyQ3ueb4yZ3LbJi8m8KPbdPIg0gM/LrPO2h 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/root-ca.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAua0Add0guHDMLoEf6A1hj5ZxZfVd+swPncPw8ZL1fSzV0png 3 | xMTE/3vkefhz3jE+R4b0DQLsfYVjohDMETfqRERfwZ5HcpsZx0pCFxob7Icty2tP 4 | wRijLnutcZBg4ENBqYeC3/DQuO+7ADV5A1ulFdOcJuXNBmNpsH2RmEuvtroGjxAM 5 | JWvoUu899snHaIsrcpAJEdgcYF7kjHEfU4Kv2pmLHx1nHAF3qb53mjMP0RdoeFDG 6 | /AuEQgB6VmHc/9Nz416o9E2KkrB5GGl8t9WVptKHQfqEU+Snzyfby9rRgw90GUzR 7 | LylBiHCGUNpQ4fenD+lVnpuko5yqEDaJVYsYzwIDAQABAoIBAFPCUz7YLfaGC9W+ 8 | siIAR05601TnRynn+/NzfLG9VbjODPcgK2EYUrXdscfD6KEHHc0ud9GWzBAjqDpn 9 | 0WbDDo8VOQ0i1aBBj3NzcDTztj0RDfuBwUyeTJ6fdDKSquD/9hL/6m4TOFT08Cq7 10 | 9IcrRGGAQCTb7Y1AM2FGg/Nr0SxgIHF0og5SPzENqnry18vH8WKzRcxLLEVSJP5u 11 | wUkDzKgHHLKmyx/coXYCieozqBUMONbufNbssT2q/XX7KOGHxZAhswuGoE7YSKWn 12 | 5cKxn9SRF3hm0IaKyRCOWo4DdSEZ9goJrvWK3nKcJ6dbuRL65cmVDNAF2kY73WCz 13 | BHSmNGECgYEA9viJ7AdX0uxpLvIk85dUY8+l03EUpoPoTqALhsKjbRdF+BexB82E 14 | 0QHX4k5gKdDQ4XJxADbcBOln987ECWdeUpgYsq0oSa0lVX0SSjoaFVOIGpYMdufu 15 | rcSRbw127OfFLK1BkBju12MUJC9Desrp6h8cbB/Kz5P6oPXl08VVPZMCgYEAwHbJ 16 | kUAFEqK3AR+dSZgQztFkQ9Ux8ldeN0p47PpDI3zO3/EfJtNj39F2+/zWp3DpVpuv 17 | C2hzJDsnDCrsW5ujcnu8h9hunZJgKMkXTCtNZ2cPcBZz4XS62vF7I4S8vlzh9ev+ 18 | kY38cxqitf2tI3rOwwhULrC0RY91ZGZ6XgEsHVUCgYAYq/2DQbyJSqq7UN9WIlEA 19 | 45aKR+qrM9Q6PozIOpt+42tO/Hbn86UICCob5n5+zuh/DSKyxcg3CWYkgFhfJB9t 20 | Gtqkxt2WdqCbKLJyDdnbNYwMM98s5cCXRWLN+EdgJUsySmCZV5RMmg5CCyKvmqPB 21 | irgZKRfmor7P46DBBh6c1wKBgDgMH6Th3NhRdDOqjjZZR0PDLIyocDQfhztYv6Bb 22 | POP/u4rxf93hn2sVZ634MlZuhjUHf1E2KJm7dCKR+WSwDUgQipWQzJ2se75E2TkA 23 | PzlGhPNiRnq4cJXDztVIGWLiT5c7E1Y13/dxIUMYTaxQXhfjvAggw06ieVA0m5v2 24 | gW9FAoGBAIv82kftlGZsBnVAwUpplahVKztH3aQGyrP2jTruBwjEbQv1moKRxwXq 25 | NA+zhni2+vlo6c/PYNPc3qNmAeFkESPL716e6TAWfvptwWMK1x+bvSL2tM6nzMTv 26 | Ky/+G7oy2PHMo2HFxRTzdJQBPacqXYXkY7+4QOv+UOESda6CBnhk 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/root-ca2.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAzjJYWl6coMa2V4WV1ZwEfVK+UCtcPN/NBxRBMIYpOIYtHKZP 3 | ileFSZE6JsmP7E3KL5DMo81hqtxsKpJpqEmXwmnYpujvyAVK8QWT/G1JZ8vSaAgQ 4 | +8vDIYrALYidN5sXRSqI/+QXmXs5VM4QUm78kS83LgosQxPXItRZ2kVVheb5nb8N 5 | QSGWGVnkd+Ci3S8ztCCw6n6cUYP/K6uUm9KjfhFD0l1GU3yGE/oMb2fE5q/bl8tU 6 | hQlyEDhV66rZzJ8fK+GoCxxzGWbYSNeht743O2rDvP0kvjAMCVY5QFV5gOw3/5+K 7 | ntzxeb5eP/Odk3bvZVWxuARRVCrE38ysihKm+wIDAQABAoIBADbmLqH4+MjRv3C0 8 | D32tKYDYi/U6ZjeK+sK8wNKTldwH1XEAUsMppEI/GXWESv6Gg6OAcQvXvf5I67+1 9 | 9c2psm8/0UA3WsgtqJNdgdY+nsG1AdNS+nqf+NcwYxR8gCdy8pImzg0bhLEHnu8W 10 | YkAgbryVDqMCcGB3otsSIM49kZSxvRRmRcK6nStfJZuTUcDuFMg4S/RpXWfv5B5m 11 | 3ui+mB9g2HmP9jujkwDu90B2gcQUMVDjYaWfCHmEqSc+p7dpSm7sNGBP36a4qQjf 12 | Nex8bU80jd1Gb5E/hzB11B6JZw/RJGvfag+KIXzZqmqxpFv65svHH0FSxoDpAvy3 13 | 0aGa/eECgYEA8oOLWiAFh558uN7PkLEZkC7gh2K5JVf1ow5bR8YhQLZZRuqMwDHI 14 | OXXbWidvHks4l0daDhzxPDaXXAfBbXuJQIDFaOucj/MRSiLT5+Xd8Jj+jhVVBSZR 15 | 6v22Oz9zi7SdkHSQ2cPlfQEFXwMTWsTdAf4idsHtF3NbKsAV5geXDmsCgYEA2anI 16 | +mcSWHt5unTq5/HIeMdMv2b56tv1rKuj79L8ar9A+evXvsiULKBwPKehw4uT032T 17 | b8tdj3CYRCSsgiHMtMdUIBIo30hO9O2SAuq1oR4xKA6dnv0vvUBPN9FavQ6EFX1q 18 | W7fieX1MP80Qoa1pMUcDLmVm0hGG4LYr2PXCzbECgYB4fidUxig5M+OgLwROTc/3 19 | tXLVkZWQGl3vgAiWZrjK4E8HTy5Tp+hltEsiRgmNsa4Sa98wt+ycEDuv/CJ89S5C 20 | oDh1YutNEmX6wccrpyhYjIudqLevcVSuPxS151bPiRPoXCJEMHLrYwB6LpsFNF7i 21 | yJPzEXNtfWFEol+/BPJmtQKBgA6CrBxNXMK7UI0mmDZoPvYWSz6DTevjSAh/1Mj7 22 | Jsqy/1Dp8RMN6hrjgzf38OfJWUyDFZ4hT5Ztaik4zKtMN4phs3ED5OeluWXIpLA/ 23 | F2arTZmfB9D+jf0u2VkeQs9RtWp9VubQZm+0861ZLV+p4NZhJowkRGuCsZwvaNLo 24 | 51ixAoGBAJYEriHW1Da6lI99zdisRh6iqc00dlrxZDZWbl+lr6gKw51mxgwDkmLq 25 | mvkb4V2xU2zVcUcvqt3NskO6Tmo9babX3WnXIpqFS8MI+qyX4L2eNAHCnoCduf19 26 | HPYWVv4/rX8OPMmfU12D9k6eMqC2SrgiPXl41fs0/DDTD8mhuWz9 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoyControlV3SmokeTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.extension.RegisterExtension 6 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.isFrom 7 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.isOk 8 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.Ads 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 12 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 13 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 14 | 15 | class EnvoyControlV3SmokeTest { 16 | 17 | companion object { 18 | 19 | @JvmField 20 | @RegisterExtension 21 | val consul = ConsulExtension() 22 | 23 | @JvmField 24 | @RegisterExtension 25 | val envoyControl = EnvoyControlExtension(consul) 26 | 27 | @JvmField 28 | @RegisterExtension 29 | val serviceEnvoyV3 = EchoServiceExtension() 30 | 31 | @JvmField 32 | @RegisterExtension 33 | val envoyV3 = EnvoyExtension(envoyControl, serviceEnvoyV3, Ads) 34 | } 35 | 36 | @Test 37 | fun `should create a server listening on a port`() { 38 | untilAsserted { 39 | // when 40 | val ingressRootEnvoyV3 = envoyV3.ingressOperations.callLocalService("") 41 | 42 | // then 43 | assertThat(ingressRootEnvoyV3).isFrom(serviceEnvoyV3).isOk() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/privkey_echo3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA3iE+FfI4raIKU3cuNG4Hjsumx7ZOGihCiY7Z757bz1aRqmc7 3 | RWjECNcTtS8Ek9O8F6KhPDT0a7JUnEGskZbP/ZKJtfODSy3NYVwxVavv6k1c87FV 4 | uF5ljsUWaXn8p9MvtnL2mndNJZcKrNbqLCQYkiTJ8EyYx1kMJAAUoEl5j3JUw1Id 5 | hwXusw8KKPIPndCAJtUxyF950jPpQ54EAQTbpIlqH0qcBfmaUs3KQhJYfvm9QJLu 6 | KIQRBdDgqm8NdBH683PM9cqtAx7Rp6S0Ld0q0oom03M3PPvnrrBI/WKthQM1Xd3B 7 | +5f3OA2DNCiOAE1KyhFgjsxTnJZVDy1a4JB8QQIDAQABAoIBAGArmEu8IRnbrAjS 8 | 8qg0cwU52q1gmpPsllIkDOsXpicwXcSfCSRV5XnGVHv/LUYrR6Yb/R6p9qCiBsvY 9 | CzTFTKuGRCUIlvF3lRHv7lQfKrIQaIwBXRGDnQig/8EO1Xp7jo65W1cpC7eYm0CV 10 | k7Ekt5aeYues4gB85yq29jcnWH/nu4V/wnU3npoeQuKW5d11yZ5fl3ai0ROaQOgx 11 | sgDwrCQ+XkFKiEx3A2s2QGKnd4IhFp1pMPUWJp2RbfBfgssCY2x6cstXEEhfvpUA 12 | v8gGf3OLxFC/OZ3PzJ364Tj5oUQskmQJ5NETgwjh6g73xviFF246reBb9hr+tTOU 13 | 0tWGFyECgYEA9Tv3VV1cwwF/Vrh0qpXTUMdkq2Z644mpXj/DoeVaP/5gdldXrlNe 14 | hprpygFYE/Jh6dIMvTtAJazmqPUdwmchDK5ySFNvdpWWwOViQKhBdOOBbhEfKoMt 15 | +3sDQ/dVFB5tQv6D5lilRsNu+nYX3VO/1FuM1SK3PFdo+ScUHLLvnV0CgYEA5+Ge 16 | 4geulfFuhpc3NvpZsMB0dcwZ90Bx9ikORylf0jCcvZPv4fWqgZvDDhW/Gi8Uggux 17 | 9talM3A6bkJXfXHWHMfr2l3OSXhXDCaTdv9fDJV8Prx8aZMMtt0eufWEOQs6GB3P 18 | ZLH57JhNmGEVF/WK9HGjdEePt+nYfjmbMm2SCDUCgYAf3b7x9MPNAzDM0AM492cS 19 | JBbMvvBRCN5dROPi8a6cii12szrNiD/MNe0TNsF/NgvLGmRVYpGfU6xVYCSR0lzV 20 | DQYEp/Lf8eg5AJWX2UVILxfueYMXPxyGhSGTf1wq1RlVj0UMdZBkdZjCKv5G4E8Z 21 | BRxzxaMR0Dnvxkgywn2ocQKBgQCaKINqndTR1sq2K/4HTPUn7yr9zY1NtciN8MmP 22 | QdB0euEZoCqQvLR4qkdJK+f6zmYB9yh/hEAcLHaMKwrjWTURuU/xwv+MFLc6WsMb 23 | D7fvM8qAIutLfPms8OCmnLUk/3PF6LuipDgVtUORFKnjXdjdnlcezRydFphZgo7N 24 | Pc8iKQKBgQDWqUBhpc82dlNgl3/GyFIdEGYJxSIEoTe5p/9VAiNra+PaU8z4Xw7H 25 | O5JX9nJQDQ3ubqB9F6SpMj69pf2qiwRSi3Vksu+dqDwy6RXLm379UXzL1mD+a8Ds 26 | GXJvHAxx+RQEaMan9hIe70hCo9sG+FGdwHZc7E5zLezPtLMES62RpA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/privkey_echo5.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA4rXKFPpUQZNSAHw6mxV3bswzzuHlCsiF7bumk7zCzNZgljhk 3 | A0lwiOy1EEaR8PUKZMGj9UQ0aQoZFoZJPiFCHWjN7MzZDISMOWAQJ9rLyZG9oJBq 4 | ELFcdWLnSZUWWncFfvufnGg/BABDm9F7Ic3jn1f+MkKv+Xwq3G1Vouw+lHLcWz/n 5 | YN3gCSBxXIldMqLgJx8t40MYy6dZTAeykYWpZBdPR8uHlaI7Yr4d8Ic1SuMijKcN 6 | 7kovIIOo9jPNpRcgeALsmiVb4zM5YsX1qRI7JUapp7gPwwy+dEOR9eB+LCNVZVio 7 | CYAZqe+pB+cGobzs8nKtsdg2rOu0DATF3bKukQIDAQABAoIBAQCbjnDihAFcHWGe 8 | w57S2p/gfmzt/0HRbFW5EZDyvgpL0qzjZj35o2J8ES8Lw+BPnVNMXzHJEMuFZ1Bf 9 | jDSlVNywoUDqwoQLesdcdvWe4xoBbObYTT75xTjTstFQQ+PanuA+iRizkTPr4uKA 10 | //Ok9kjBBPd+NRPZ88DVDpA8Ai3OXNcJXaDQr9957P71ZuOLvTSt4elrparR0Pdg 11 | weo+7c6eaS2FBoI0MuE/Pgdu2EP6NS2T/7u3ffKawWZ9bnr7EbwhtSEmV3nMHR6J 12 | bWA4E1sDpRFSd7EzRxn+BDq5ukWlICpqMxXVZjHVmu03M0wSGzMNwiOYHP3Mw43F 13 | G/mC6f2BAoGBAPKPKIm2QSdxs8LE45xB6MbSpnkBoleonf+fSsShnZ5Dhaa3sVvq 14 | H+rQn3E/fjskr9S3VMWmz8aq5aPWVL2rOMmDNdUZvp1Pt99F+m4zhuR6JRyxzEOj 15 | BEd6a2ZYunyVdj+oJVP2htbGWteX29GdRN8jY8jiKbeICAZwr/GVBQsNAoGBAO9F 16 | zW+2FdH+mcPY+BdGniIC3Zbh0Lhw5PRI/YnfjlY0VgUGxAG1f+pb7VvHTrvU5hBA 17 | dHy0RaggEFO9Pcpgc3/KB1NfGwE7OYbMhm4mx3viGgH26tHwEvSOR+JkPHZ05TJ2 18 | qviTEInS+fRELrZgMp3EMHOfcp5sKECSZRibAECVAoGBAJvj9iHLyMQZB3c+Iyri 19 | EUD3UZajvjqoXCNVtS/6ztpQey1TEeII7spzmoWmUPKh+X+08/6z3wXIAB70OTJN 20 | QQoCEi8LhL6F5Z7R0snQw/lDp2Zxvt4Zfz6RJ0V38SLwzDbNUnBMGQ0gHnJBXz2w 21 | 3fqrPA53jGgwPTgmZG3XYI5hAoGAWIZTzovooMv4qdwBVeM7qEu1Hhin90VVgAft 22 | PfBnIf+0/6EULamwDM48ECO6PoYzJDoknuq3hs9uGv09+j0bHmFpum/KdvcpfnT3 23 | G5PfZDcv9iAbmtaevLpTYDBDqnPvRG9hLByFFujmr3f2bGVE9NfcMTsB1hkf/1vq 24 | aWMXgc0CgYBW5JDN/oMp0I0gmZ50Xni9jd4Bgu527WZeHWYIFl/BoAVg7ycKuTVx 25 | +lN95jopzrN/jcbIJtl7lvFa6wpbZBnQ0/HyNHovn03AR6QNqUMJyw1O/PFwhs9H 26 | KSu/BDjo7g2hf0bbHweCpdeDGdBNsKHzV77S7og3wflHDP7FbTSe3Q== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/CustomRoutesFactory.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes 2 | 3 | import io.envoyproxy.envoy.config.route.v3.Route 4 | import io.envoyproxy.envoy.config.route.v3.RouteAction 5 | import io.envoyproxy.envoy.config.route.v3.RouteMatch 6 | import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher 7 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.RoutesProperties 8 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StringMatcherType 9 | 10 | class CustomRoutesFactory(properties: RoutesProperties) { 11 | 12 | val routes: List = properties.customs.filter { it.enabled }.map { 13 | val matcher = when (it.path.type) { 14 | StringMatcherType.REGEX -> RouteMatch.newBuilder() 15 | .setSafeRegex( 16 | RegexMatcher.newBuilder() 17 | .setRegex(it.path.value) 18 | .setGoogleRe2(RegexMatcher.GoogleRE2.getDefaultInstance()) 19 | ) 20 | StringMatcherType.EXACT -> RouteMatch.newBuilder().setPath(it.path.value) 21 | StringMatcherType.PREFIX -> RouteMatch.newBuilder().setPrefix(it.path.value) 22 | } 23 | RouteMatch.newBuilder() 24 | Route.newBuilder() 25 | .setName(it.cluster) 26 | .setRoute(RouteAction.newBuilder() 27 | .setCluster(it.cluster) 28 | .also { route -> 29 | if (it.prefixRewrite != "") { 30 | route.setPrefixRewrite(it.prefixRewrite) 31 | } 32 | } 33 | ) 34 | .setMatch(matcher) 35 | .build() 36 | } 37 | 38 | fun generateCustomRoutes() = routes 39 | } 40 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/resources/testcontainers/ssl/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3RgzehlMsRtTM 3 | pLbkaGa8CZr8T/aPF+ji/XuAotgD8d8/OU/7nyj1HM4L/m1WOEKLjgXGPSuyu7Cn 4 | sz1cQuQPEk6ZxPohfr5kqYvuWwPrMl52qzbCDVxckXDywloBuk5ZPMeeXXyyTiX+ 5 | vgQ+NsRUXK0nBUG/W3Gev4ndX33WFa7ak/iGIpwDRtUY6LqToE8YMKL1VNKRox4y 6 | z1Gx8VlWo2WH7zzKzu7do24J4AiNM5r8ttdsXK/HDFpQjtHYM4Flkw5+94DKGHZd 7 | OrBZsavnR5Qi5jC5Knjl2rbV1mzvODc0+v1THpTkkEgCgQTYKUXVVETJDNAXP7v+ 8 | +lLgMsulAgMBAAECggEAUVVJCMfbyV51uYuvjpW2YeRxX1+tL7uQoouTb3bVHosM 9 | Y+ZvVF9BseVim3gB1a2J+pzAe0VSSzN4JjQpGvSkePvK3frIdmzk2Y9UhibmCk6E 10 | FW3OEzgSsRHqahFOGE4xzvBmNiWO3SJJQk/ZWjK71iR6w0JV9zIBrk2aRR/HnGWq 11 | vGx+C5sQMS4T+E+nHuNUPr4yV3hTuyL8Mb7uvpOQsqlnxDNIM2i7cAOtrPZnPvbv 12 | iNOur+yDf4sbEEMYPjC7/uq3q5aNF+apdbQXmnw2nPnyfISeZicMvqkf1LjWEAeJ 13 | kNdsGk5+1fxZV7DOc4FE+8Q5xx11uiJaeTHPJLocAQKBgQDamCGk/zpWAd/qcKAe 14 | 0sSwpjTzbGK71OsiB8AA+x1tIyO+BRBWeWPt3jYtIe754Xv8Ia2yY6LP068KzrRk 15 | e4zfa0Tc7UPp/H0LGsStzVrEwRfhw1PNnAIocPFd9zXO/TNiefLRiJM2oSgtmSSE 16 | cxyZRs4WSfRJUq5ORg9qzc1yQQKBgQDWoqSC/KtmZSCYbL/mO1yGPSYsMW3sdOep 17 | vS6le41ED8djOmKTTuj0UPEhEm/7S0wthaCLfm9F++WvUJPmgKkT/EnXGTvx757b 18 | gCrmJcd0UwBqqtrtCS58RqqeftrTvHPvPTlHRhFTczoOeb/gOqk9yDhShTqQxUWD 19 | lziAtoa4ZQKBgQCxmg8j3paIEKfb87u0r2xNVg0JhhNGJv0Pvho68cv7wyQkHDsk 20 | 9yXAut1rl/lxHsm8laRpnthGYOgEMOOSDGvtjlr54PBf6NuzrQEFcSaBW51KuYea 21 | M1nxf6orvVIDpecc6JXntj5dkVxyh82Kq3gd9NA2fcmz1TB3uiaGkUWNwQKBgQCU 22 | xJzEkb93tKZ4IACO4xxJiz7XKNepKoqcEx2u48lRoKIx+/jxY6OCHExWAQKPKmy9 23 | rL5Pka4s0uErt+0bupf220qPBdWP5uez+s1BQnRSA2nphU1DOLb4ur5uJz0jv56X 24 | 91apOT6vGdHm0KqXD/HYedvYDrI+QA3jnMA0Ls+IJQKBgQC1yj2S5qojBqnV7/40 25 | 2cYy0ZJ9xaoR1Hwx8bG4XwkE8gc0Isyq+3pFpcLeQ1sIDbOBWiZHxuthYIFUb4ua 26 | kRVLYoVzxaq+aPbAkpD/Oa+iI/ao2uQ+mGYFrBwilfFOdvVdNX6JkAGhX40R2o6M 27 | e6KVw5M58pqJ+qUd3LClocywRg== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/RedisBasedRateLimitContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import org.testcontainers.containers.BindMode 4 | import org.testcontainers.containers.Network 5 | import org.testcontainers.containers.wait.strategy.HttpWaitStrategy 6 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 7 | 8 | class RedisBasedRateLimitContainer( 9 | private val redis: RedisContainer 10 | ): GenericContainer("envoyproxy/ratelimit:4d2efd61"), HttpContainer { 11 | override fun configure() { 12 | super.configure() 13 | withExposedPorts(HTTP_PORT, GRPC_PORT, DEBUG_PORT) 14 | withNetwork(Network.SHARED) 15 | withEnv(mapOf( 16 | "BACKEND_TYPE" to "redis", 17 | "REDIS_SOCKET_TYPE" to "tcp", 18 | "REDIS_URL" to redis.address(), 19 | "USE_STATSD" to "false", 20 | "LOG_LEVEL" to "trace", 21 | "RUNTIME_ROOT" to "/", 22 | "RUNTIME_SUBDIRECTORY" to "tmp", 23 | "DEBUG_PORT" to "$DEBUG_PORT", 24 | "PORT" to "$HTTP_PORT", 25 | "GRPC_TRACE" to "all", 26 | "GRPC_PORT" to "$GRPC_PORT")) 27 | withClasspathResourceMapping("ratelimit_config.yaml", "/tmp/config.yaml", BindMode.READ_ONLY) 28 | withCommand("/bin/ratelimit") 29 | waitingFor(HttpWaitStrategy().forPath("/healthcheck").forPort(HTTP_PORT)) 30 | } 31 | 32 | fun address(): String = "${ipAddress()}:$GRPC_PORT" 33 | 34 | override fun port() = GRPC_PORT 35 | 36 | override fun httpPort() = HTTP_PORT 37 | 38 | companion object { 39 | const val DEBUG_PORT = 5698 40 | const val HTTP_PORT = 5699 41 | const val GRPC_PORT = 5700 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/utils/ClusterOperations.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.utils 2 | 3 | import com.google.protobuf.Duration 4 | import com.google.protobuf.util.Durations 5 | import io.envoyproxy.envoy.config.cluster.v3.Cluster 6 | import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource 7 | import io.envoyproxy.envoy.config.core.v3.ConfigSource 8 | import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions 9 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.ClusterConfiguration 10 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties 11 | 12 | fun createCluster( 13 | defaultProperties: SnapshotProperties = SnapshotProperties(), 14 | clusterName: String = CLUSTER_NAME, 15 | idleTimeout: Long = DEFAULT_IDLE_TIMEOUT 16 | ): Cluster { 17 | return Cluster.newBuilder().setName(clusterName) 18 | .setType(Cluster.DiscoveryType.EDS) 19 | .setConnectTimeout(Durations.fromMillis(defaultProperties.edsConnectionTimeout.toMillis())) 20 | .setEdsClusterConfig( 21 | Cluster.EdsClusterConfig.newBuilder() 22 | .setEdsConfig( 23 | ConfigSource.newBuilder().setAds( 24 | AggregatedConfigSource.newBuilder() 25 | ) 26 | ).setServiceName(clusterName) 27 | ) 28 | .setLbPolicy(defaultProperties.loadBalancing.policy) 29 | .setCommonHttpProtocolOptions( 30 | HttpProtocolOptions.newBuilder() 31 | .setIdleTimeout(Duration.newBuilder().setSeconds(idleTimeout).build()) 32 | ) 33 | .build() 34 | } 35 | 36 | fun createClusterConfigurations(vararg clusters: Cluster): Map { 37 | return clusters.associate { it.name to ClusterConfiguration(it.name, false) } 38 | } 39 | -------------------------------------------------------------------------------- /envoy-control-services/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/services/ServicesState.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.services 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | typealias ServiceName = String 6 | 7 | data class ServicesState( 8 | // TODO this field should be private but right now jackson ignores it and it cannot be instantiate. 9 | // Will fix this i next pr 10 | val serviceNameToInstances: ConcurrentHashMap = ConcurrentHashMap() 11 | ) { 12 | operator fun get(serviceName: ServiceName): ServiceInstances? = serviceNameToInstances[serviceName] 13 | 14 | fun hasService(serviceName: String): Boolean = serviceNameToInstances.containsKey(serviceName) 15 | fun serviceNames(): Set = serviceNameToInstances.keys 16 | fun allInstances(): Collection = serviceNameToInstances.values 17 | 18 | fun removeServicesWithoutInstances(): ServicesState { 19 | serviceNameToInstances.entries.retainAll { (_, value) -> value.instances.isNotEmpty() } 20 | return this 21 | } 22 | 23 | fun remove(serviceName: ServiceName): Boolean { 24 | return serviceNameToInstances.remove(serviceName) != null 25 | } 26 | 27 | fun add(serviceName: ServiceName): Boolean { 28 | return if (serviceNameToInstances.containsKey(serviceName)) { 29 | false 30 | } else { 31 | change(ServiceInstances(serviceName, instances = emptySet())) 32 | } 33 | } 34 | 35 | fun change(serviceInstances: ServiceInstances): Boolean { 36 | return if (serviceNameToInstances[serviceInstances.serviceName] == serviceInstances) { 37 | false 38 | } else { 39 | serviceNameToInstances[serviceInstances.serviceName] = serviceInstances 40 | true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | envoyVersion: 7 | type: string 8 | description: "envoy version to run tests on, e.g. 'v1.24.0'. Special values: 'max' - max supported version, 'min' - min supported version" 9 | default: max 10 | 11 | push: 12 | paths-ignore: 13 | - 'readme.md' 14 | 15 | workflow_call: 16 | inputs: 17 | envoyVersion: 18 | type: string 19 | default: max 20 | 21 | jobs: 22 | ci: 23 | name: CI 24 | runs-on: ubuntu-latest 25 | env: 26 | GRADLE_OPTS: '-Dfile.encoding=utf-8 -Dorg.gradle.daemon=false' 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | ref: ${{ github.head_ref }} 33 | 34 | - uses: gradle/actions/wrapper-validation@v4 35 | 36 | - uses: actions/setup-java@v3 37 | with: 38 | distribution: 'temurin' 39 | java-version: '17' 40 | 41 | - name: Cache Gradle packages 42 | uses: actions/cache@v3 43 | with: 44 | path: | 45 | ~/.gradle/caches 46 | ~/.gradle/wrapper 47 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 48 | restore-keys: | 49 | ${{ runner.os }}-gradle- 50 | 51 | - name: Test with Gradle (envoyVersion=${{ inputs.envoyVersion }}) 52 | run: ./gradlew clean check -PenvoyVersion=${{ inputs.envoyVersion }} 53 | 54 | - name: Junit report 55 | uses: mikepenz/action-junit-report@v5 56 | if: always() 57 | with: 58 | report_paths: '**/build/test-results/test/TEST-*.xml' 59 | 60 | - name: Cleanup Gradle Cache 61 | run: | 62 | rm -f ~/.gradle/caches/modules-2/modules-2.lock 63 | rm -f ~/.gradle/caches/modules-2/gc.properties 64 | -------------------------------------------------------------------------------- /envoy-control-core/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':envoy-control-services') 3 | 4 | implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib' 5 | api group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner' 6 | api group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin' 7 | implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect' 8 | api group: 'io.dropwizard.metrics', name: 'metrics-core', version: versions.dropwizard 9 | api group: 'io.micrometer', name: 'micrometer-core' 10 | 11 | implementation group: 'com.google.re2j', name: 're2j', version: versions.re2j 12 | implementation("com.github.ben-manes.caffeine:caffeine:3.2.0") 13 | 14 | api group: 'io.envoyproxy.controlplane', name: 'server', version: versions.java_controlplane 15 | 16 | implementation group: 'io.grpc', name: 'grpc-netty', version: versions.grpc 17 | 18 | implementation group: 'io.projectreactor', name: 'reactor-core' 19 | 20 | implementation group: 'org.slf4j', name: 'jcl-over-slf4j' 21 | implementation group: 'ch.qos.logback', name: 'logback-classic' 22 | 23 | testImplementation group: 'io.grpc', name: 'grpc-testing', version: versions.grpc 24 | testImplementation group: 'io.projectreactor', name: 'reactor-test' 25 | testImplementation group: 'org.mockito', name: 'mockito-core' 26 | testImplementation group: 'net.bytebuddy', name: 'byte-buddy', version: versions.bytebuddy 27 | 28 | testImplementation group: 'org.awaitility', name: 'awaitility' 29 | 30 | testImplementation group: 'org.testcontainers', name: 'testcontainers' 31 | testImplementation group: 'org.testcontainers', name: 'junit-jupiter' 32 | } 33 | 34 | tasks.withType(GroovyCompile) { 35 | groovyOptions.optimizationOptions.indy = true 36 | options.encoding = 'UTF-8' 37 | } 38 | 39 | test { 40 | maxParallelForks = 1 41 | useJUnitPlatform() 42 | } 43 | 44 | -------------------------------------------------------------------------------- /envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/synchronization/RestTemplateControlPlaneClient.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.synchronization 2 | 3 | import io.micrometer.core.instrument.MeterRegistry 4 | import io.micrometer.core.instrument.Tags 5 | import org.springframework.web.client.RestTemplate 6 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServicesState 7 | import java.net.URI 8 | import java.util.concurrent.CompletableFuture 9 | import java.util.concurrent.Executor 10 | 11 | class RestTemplateControlPlaneClient( 12 | private val restTemplate: RestTemplate, 13 | private val meterRegistry: MeterRegistry, 14 | private val executors: Executor 15 | ) : ControlPlaneClient { 16 | override fun getState(uri: URI): CompletableFuture { 17 | return CompletableFuture.supplyAsync({ 18 | metered { 19 | restTemplate.getForEntity("$uri/state", ServicesState::class.java).body!! 20 | } 21 | }, executors) 22 | } 23 | 24 | private fun metered(function: () -> T): T { 25 | try { 26 | val response = timed { function() } 27 | success() 28 | return response 29 | } catch (e: Exception) { 30 | failure() 31 | throw e 32 | } 33 | } 34 | 35 | private fun timed(function: () -> T): T { 36 | return meterRegistry.timer("cross.dc.synchronization.seconds", Tags.of("operation", "get-state")) 37 | .record(function) 38 | } 39 | 40 | private fun success() { 41 | meterRegistry.counter("cross.dc.synchronization", Tags.of("operation", "get-state", "status", "success")) 42 | .increment() 43 | } 44 | 45 | private fun failure() { 46 | meterRegistry.counter("cross.dc.synchronization", Tags.of("operation", "get-state", "status", "failure")) 47 | .increment() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/resources/lua/egress_service_tag_preference.lua: -------------------------------------------------------------------------------- 1 | 2 | local defaultServiceTagPreference = os.getenv("%DEFAULT_SERVICE_TAG_PREFERENCE_ENV%") or "%DEFAULT_SERVICE_TAG_PREFERENCE_FALLBACK%" 3 | 4 | local fallbackToAnyIfDefaultPreferenceEqualTo = "%FALLBACK_TO_ANY_IF_DEFAULT_PREFERENCE_EQUAL_TO%" 5 | local fallbackToAny = false 6 | if fallbackToAnyIfDefaultPreferenceEqualTo ~= "" then 7 | if fallbackToAnyIfDefaultPreferenceEqualTo == defaultServiceTagPreference then 8 | fallbackToAny = true 9 | end 10 | end 11 | 12 | local parseServiceTagPreferenceToFallbackList = function(preferenceString) 13 | local fallbackList = {} 14 | local i = 1 15 | for tag in string.gmatch(preferenceString, "[^|]+") do 16 | fallbackList[i] = {["%SERVICE_TAG_METADATA_KEY%"] = tag} 17 | i = i + 1 18 | end 19 | if fallbackToAny then 20 | fallbackList[i] = {} 21 | end 22 | return fallbackList 23 | end 24 | 25 | local defaultServiceTagPreferenceFallbackList = parseServiceTagPreferenceToFallbackList(defaultServiceTagPreference) 26 | 27 | function envoy_on_request(handle) 28 | local requestPreference = handle:headers():getAtIndex("%SERVICE_TAG_PREFERENCE_HEADER%", 0) 29 | if not requestPreference then 30 | handle:headers():add("%SERVICE_TAG_PREFERENCE_HEADER%", defaultServiceTagPreference) 31 | end 32 | 33 | local serviceTag = handle:headers():get("%SERVICE_TAG_HEADER%") 34 | if not serviceTag or serviceTag == "" then 35 | local fallbackList = defaultServiceTagPreferenceFallbackList 36 | if requestPreference then 37 | fallbackList = parseServiceTagPreferenceToFallbackList(requestPreference) 38 | end 39 | 40 | if next(fallbackList) ~= nil then 41 | local dynMetadata = handle:streamInfo():dynamicMetadata() 42 | dynMetadata:set("envoy.lb", "fallback_list", fallbackList) 43 | end 44 | end 45 | end 46 | 47 | 48 | function envoy_on_response(handle) 49 | end 50 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/reliability/LocalConsulAgentToMasterCutOff.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.reliability 2 | 3 | import org.junit.jupiter.api.BeforeAll 4 | import org.junit.jupiter.api.Test 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlRunnerTestApp 6 | 7 | internal class LocalConsulAgentToMasterCutOff : ReliabilityTest() { 8 | companion object { 9 | @JvmStatic 10 | @BeforeAll 11 | fun setup() { 12 | setup( 13 | appFactoryForEc1 = { 14 | EnvoyControlRunnerTestApp( 15 | consulPort = Toxiproxy.externalConsulPort, 16 | grpcPort = Toxiproxy.toxiproxyGrpcPort 17 | ) 18 | }, 19 | envoyConnectGrpcPort = Toxiproxy.externalEnvoyControl1GrpcPort 20 | ) 21 | } 22 | } 23 | 24 | @Test 25 | fun `should register service when communication between local agent and master is restored`() { 26 | // given 27 | registerService(name = "service-1") 28 | assertReachableThroughEnvoy("service-1") 29 | assertUnreachableThroughEnvoy("service-2") 30 | 31 | // when 32 | consulMastersInDc1.forEach { 33 | it.container.blockExternalTraffic() 34 | } 35 | 36 | // and 37 | registerService(name = "service-2", consulOps = consulAgentInDc1.operations) 38 | 39 | // then 40 | holdAssertionsTrue { 41 | assertReachableThroughEnvoy("service-1") 42 | assertUnreachableThroughEnvoy("service-2") 43 | } 44 | 45 | // when 46 | consulMastersInDc1.forEach { 47 | it.container.unblockExternalTraffic() 48 | } 49 | 50 | // then 51 | holdAssertionsTrue { 52 | assertReachableThroughEnvoy("service-1") 53 | assertReachableThroughEnvoy("service-2") 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/metrics/ThreadPoolMetricTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.metrics 2 | 3 | import io.micrometer.core.instrument.Tag 4 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | import pl.allegro.tech.servicemesh.envoycontrol.ControlPlane 8 | import pl.allegro.tech.servicemesh.envoycontrol.EnvoyControlProperties 9 | import pl.allegro.tech.servicemesh.envoycontrol.server.ExecutorType 10 | import reactor.core.publisher.Flux 11 | 12 | class ThreadPoolMetricTest { 13 | 14 | @Test 15 | fun `should bind metrics for default executors`() { 16 | // given 17 | val meterRegistry = SimpleMeterRegistry() 18 | val envoyControlProperties = EnvoyControlProperties().also { 19 | it.server.executorGroup.type = ExecutorType.PARALLEL 20 | it.server.groupSnapshotUpdateScheduler.type = ExecutorType.PARALLEL 21 | } 22 | 23 | val controlPlane = ControlPlane.builder(envoyControlProperties, meterRegistry).build(Flux.empty()) 24 | 25 | // when 26 | controlPlane.start() 27 | 28 | // then 29 | val metricNames = listOf("executor.completed", "executor.active", "executor.queued", "executor.pool.size") 30 | 31 | val executorNames = listOf( 32 | "grpc-server-worker", 33 | "grpc-worker-event-loop", 34 | "snapshot-update", 35 | "group-snapshot" 36 | ).associateWith { metricNames } 37 | 38 | assertThat(executorNames.entries).allSatisfy { 39 | assertThat(it.value.all { metricName -> 40 | meterRegistry.meters.any { meter -> 41 | meter.id.name == metricName && meter.id.tags.contains( 42 | Tag.of("executor", it.key) 43 | ) 44 | } 45 | }).isTrue() 46 | } 47 | 48 | // and 49 | controlPlane.close() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoycontrol/EnvoyControlClusteredExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol 2 | 3 | import org.assertj.core.api.Assertions 4 | 5 | import org.awaitility.Awaitility 6 | import org.junit.jupiter.api.extension.BeforeAllCallback 7 | import org.junit.jupiter.api.extension.ExtensionContext 8 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulClusterSetup 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.sharing.BeforeAndAfterAllOnce 10 | import java.util.UUID 11 | import java.util.concurrent.TimeUnit 12 | 13 | class EnvoyControlClusteredExtension( 14 | private val consul: ConsulClusterSetup, 15 | override val app: EnvoyControlTestApp, 16 | private val dependencies: List = listOf() 17 | ) : 18 | EnvoyControlExtensionBase { 19 | 20 | constructor( 21 | consul: ConsulClusterSetup, 22 | propertiesProvider: () -> Map = { mapOf() }, 23 | dependencies: List = listOf() 24 | ) : this( 25 | consul, 26 | EnvoyControlRunnerTestApp(propertiesProvider = propertiesProvider, consulPort = consul.port), 27 | dependencies 28 | ) 29 | 30 | override fun beforeAllOnce(context: ExtensionContext) { 31 | 32 | dependencies.forEach { it.beforeAll(context) } 33 | app.run() 34 | waitUntilHealthy() 35 | val id = UUID.randomUUID().toString() 36 | consul.operations.registerService(id, app.appName, "localhost", app.appPort) 37 | } 38 | 39 | private fun waitUntilHealthy() { 40 | Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted { 41 | Assertions.assertThat(app.isHealthy()).isTrue() 42 | } 43 | } 44 | 45 | override fun afterAllOnce(context: ExtensionContext) { 46 | app.stop() 47 | } 48 | 49 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 50 | } 51 | 52 | typealias ExtensionDependency = BeforeAllCallback 53 | -------------------------------------------------------------------------------- /envoy-control-tests/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':envoy-control-runner') 3 | 4 | implementation group: 'org.assertj', name: 'assertj-core' 5 | implementation group: 'org.junit.jupiter', name: 'junit-jupiter-api' 6 | implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params' 7 | implementation group: 'org.awaitility', name: 'awaitility' 8 | implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: versions.okhttp 9 | 10 | implementation group: 'org.apache.httpcomponents.core5', name: 'httpcore5' 11 | implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5' 12 | 13 | implementation group: 'eu.rekawek.toxiproxy', name: 'toxiproxy-java', version: versions.toxiproxy 14 | runtimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine' 15 | implementation group: 'org.testcontainers', name: 'junit-jupiter' 16 | implementation group: 'org.testcontainers', name: 'testcontainers' 17 | } 18 | 19 | test { 20 | useJUnitPlatform { 21 | excludeTags ('reliability', 'flaky') 22 | } 23 | maxParallelForks = 1 24 | testClassesDirs = project.sourceSets.main.output.classesDirs 25 | } 26 | 27 | task reliabilityTest(type: Test) { 28 | systemProperty 'RELIABILITY_FAILURE_DURATION_SECONDS', System.getProperty('RELIABILITY_FAILURE_DURATION_SECONDS', '300') 29 | useJUnitPlatform { 30 | includeTags 'reliability' 31 | } 32 | 33 | testLogging { 34 | events "passed", "skipped", "failed" 35 | exceptionFormat = 'full' 36 | } 37 | testClassesDirs = project.sourceSets.main.output.classesDirs 38 | } 39 | 40 | task flakyTest(type: Test) { 41 | useJUnitPlatform { 42 | includeTags 'flaky' 43 | } 44 | 45 | testLogging { 46 | events "passed", "skipped", "failed" 47 | exceptionFormat = 'full' 48 | } 49 | testClassesDirs = project.sourceSets.main.output.classesDirs 50 | } 51 | 52 | tasks.withType(Test).configureEach { 53 | project.findProperty("envoyVersion")?.with { systemProperty("pl.allegro.tech.servicemesh.envoyVersion", it) } 54 | } 55 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/GlobalSnapshot.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.snapshot 2 | 3 | import io.envoyproxy.controlplane.cache.SnapshotResources 4 | import io.envoyproxy.envoy.config.cluster.v3.Cluster 5 | import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment 6 | 7 | data class GlobalSnapshot( 8 | val clusters: Map, 9 | val allServicesNames: Set, 10 | val endpoints: Map, 11 | val clusterConfigurations: Map, 12 | val securedClusters: Map 13 | ) 14 | 15 | @Suppress("LongParameterList") 16 | fun globalSnapshot( 17 | clusters: Iterable = emptyList(), 18 | endpoints: Iterable = emptyList(), 19 | properties: OutgoingPermissionsProperties = OutgoingPermissionsProperties(), 20 | clusterConfigurations: Map = emptyMap(), 21 | securedClusters: List = emptyList() 22 | ): GlobalSnapshot { 23 | val clusters = SnapshotResources.create(clusters, "").resources() 24 | val securedClusters = SnapshotResources.create(securedClusters, "").resources() 25 | val allServicesNames = getClustersForAllServicesGroups(clusters, properties) 26 | val endpoints = SnapshotResources.create(endpoints, "").resources() 27 | return GlobalSnapshot( 28 | clusters = clusters, 29 | securedClusters = securedClusters, 30 | endpoints = endpoints, 31 | allServicesNames = allServicesNames, 32 | clusterConfigurations = clusterConfigurations 33 | ) 34 | } 35 | 36 | private fun getClustersForAllServicesGroups( 37 | clusters: Map, 38 | properties: OutgoingPermissionsProperties 39 | ): Set { 40 | val blacklist = properties.allServicesDependencies.notIncludedByPrefix 41 | if (blacklist.isEmpty()) { 42 | return clusters.keys 43 | } else { 44 | return clusters.filter { (serviceName) -> blacklist.none { serviceName.startsWith(it) } }.keys 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/reliability/NoConsulLeaderTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.reliability 2 | 3 | import org.junit.jupiter.api.BeforeAll 4 | import org.junit.jupiter.api.Test 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlRunnerTestApp 6 | 7 | internal class NoConsulLeaderTest : ReliabilityTest() { 8 | companion object { 9 | @JvmStatic 10 | @BeforeAll 11 | fun setup() { 12 | setup( 13 | appFactoryForEc1 = { 14 | EnvoyControlRunnerTestApp( 15 | consulPort = Toxiproxy.externalConsulPort, 16 | grpcPort = Toxiproxy.toxiproxyGrpcPort 17 | ) 18 | }, 19 | envoyConnectGrpcPort = Toxiproxy.externalEnvoyControl1GrpcPort 20 | ) 21 | } 22 | } 23 | 24 | @Test 25 | fun `is resilient to consul cluster without a leader`() { 26 | // given 27 | registerService(name = "service-1") 28 | assertReachableThroughEnvoy("service-1") 29 | 30 | // when 31 | makeConsulClusterLoseLeader() 32 | assertConsulHasNoLeader() 33 | 34 | registerService(name = "service-2") 35 | 36 | // then 37 | holdAssertionsTrue { 38 | assertConsulHasNoLeader() 39 | assertReachableThroughEnvoy("service-1") 40 | assertUnreachableThroughEnvoy("service-2") 41 | } 42 | 43 | // when 44 | makeConsulClusterRegainLeader() 45 | 46 | // then 47 | assertConsulHasALeader() 48 | assertReachableThroughEnvoy("service-1") 49 | assertReachableThroughEnvoy("service-2") 50 | } 51 | 52 | private fun makeConsulClusterRegainLeader() { 53 | consulMastersInDc1.drop(1).forEach { consul -> 54 | consul.container.sigcont() 55 | } 56 | } 57 | 58 | private fun makeConsulClusterLoseLeader() { 59 | consulMastersInDc1.drop(1).forEach { consul -> 60 | consul.container.sigstop() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/HealthIndicatorTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.assertj.core.api.ObjectAssert 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.extension.RegisterExtension 7 | import org.springframework.boot.actuate.health.Status 8 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.Health 12 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServicesState 13 | 14 | class HealthIndicatorTest { 15 | 16 | companion object { 17 | 18 | @JvmField 19 | @RegisterExtension 20 | val consul = ConsulExtension() 21 | 22 | @JvmField 23 | @RegisterExtension 24 | val envoyControl = EnvoyControlExtension(consul, mapOf("management.endpoint.health.show-details" to "ALWAYS")) 25 | } 26 | 27 | @Test 28 | fun `should application state be healthy after state of applications is loaded from consul`() { 29 | // when 30 | untilAsserted { 31 | assertThat(envoyControl.app.getState()).hasServiceStateChanged() 32 | } 33 | 34 | // then 35 | val healthStatus = envoyControl.app.getHealthStatus() 36 | assertThat(healthStatus).isStatusHealthy().hasEnvoyControlCheckPassed() 37 | } 38 | 39 | fun ObjectAssert.hasServiceStateChanged(): ObjectAssert { 40 | matches { it.serviceNames().isNotEmpty() } 41 | return this 42 | } 43 | 44 | fun ObjectAssert.isStatusHealthy(): ObjectAssert { 45 | matches { it.status == Status.UP } 46 | return this 47 | } 48 | 49 | fun ObjectAssert.hasEnvoyControlCheckPassed(): ObjectAssert { 50 | matches { it.components.get("envoyControl")?.status == Status.UP } 51 | return this 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # ![logo](assets/images/logo.png) Envoy Control 2 | 3 | Envoy Control is a production-ready Control Plane for Service Mesh based on [Envoy Proxy](https://www.envoyproxy.io/) 4 | Data Plane that is platform agnostic. 5 | 6 | ## Features 7 | 8 | * Exposing data from Service Discovery to [Envoy via gRPC xDS v3 API](integrations/envoy.md) 9 | * Scalable [integration with Consul](integrations/consul.md) 10 | * [Multi-DC support](features/multi_dc_support.md) 11 | * [Permission management](features/permissions.md) 12 | * [Observability](deployment/observability.md) 13 | * [Weighted load balancing and canary support](features/load_balancing.md) 14 | * [Service tags routing support](features/service_tags.md) 15 | * [Access log filter](features/access_log_filter.md) 16 | * [Local reply modification](features/local_reply_mapper.md) 17 | 18 | ## Why another Control Plane? 19 | In the past, our use case for Service Mesh was running 800 microservices on [Mesos](https://mesos.apache.org/) / Marathon stack. 20 | Some of these services were run on Virtual Machines using the [OpenStack](https://www.openstack.org/) platform. 21 | Most solutions on the market at that time assumed that the platform is [Kubernetes](https://kubernetes.io/). 22 | After evaluating current solutions on the market we decided to build our own Control Plane. 23 | [See comparision](ec_vs_other_software.md) with other popular alternatives. 24 | 25 | ## Performance 26 | 27 | Envoy Control is built with [performance in mind](performance.md). It was tested on a real-world production system. 28 | Currently, at [allegro.tech](https://allegro.tech/) there are 800+ microservices which converts to 10k+ Envoys running 29 | across all the environments. With a proper configuration, a single instance of Envoy Control with 2 CPU and 2GB RAM 30 | can easily handle 1k+ Envoys connected to it. 31 | 32 | ## Reliability 33 | Envoy Control includes a [suite of reliability tests](https://github.com/allegro/envoy-control/tree/master/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/reliability) that checks the behavior of the system under unusual circumstances. 34 | Additionally, there are multiple metrics that help to observe the current condition of the Control Plane. 35 | -------------------------------------------------------------------------------- /envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/server/ServerProperties.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package pl.allegro.tech.servicemesh.envoycontrol.server 4 | 5 | import java.time.Duration 6 | 7 | class ServerProperties { 8 | var port = 50000 9 | var nioEventLoopThreadCount = 0 // if set to 0, default Netty value will be used: * 2 10 | var nioBossEventLoopThreadCount = 0 // if set to 0, default Netty value will be used: * 2 11 | var serverPoolSize = 16 12 | var serverPoolKeepAlive: Duration = Duration.ofMinutes(10) 13 | var executorGroup = ExecutorProperties() 14 | var netty = NettyProperties() 15 | /** 16 | * Minimum size = 2, to work correctly with reactor operators merge and combineLatest 17 | */ 18 | var globalSnapshotUpdatePoolSize = 5 19 | var globalSnapshotAuditPoolSize = 2 20 | var groupSnapshotUpdateScheduler = ExecutorProperties().apply { 21 | type = ExecutorType.DIRECT 22 | parallelPoolSize = 1 23 | } 24 | var snapshotCleanup = SnapshotCleanupProperties() 25 | var reportProtobufCacheMetrics = false 26 | var logFullRequest = false 27 | var logFullResponse = false 28 | // todo #920 remove after deploying and testing on production 29 | var enableInitialResourcesHandling = true 30 | } 31 | 32 | enum class ExecutorType { 33 | DIRECT, PARALLEL 34 | } 35 | 36 | class ExecutorProperties { 37 | var type = ExecutorType.DIRECT 38 | var parallelPoolSize = 4 39 | } 40 | 41 | class NettyProperties { 42 | /** 43 | * @see io.grpc.netty.NettyServerBuilder.keepAliveTime 44 | */ 45 | var keepAliveTime: Duration = Duration.ofSeconds(15) 46 | 47 | /** 48 | * @see io.grpc.netty.NettyServerBuilder.permitKeepAliveTime 49 | */ 50 | var permitKeepAliveTime: Duration = Duration.ofSeconds(10) 51 | 52 | /** 53 | * @see io.grpc.netty.NettyServerBuilder.permitKeepAliveWithoutCalls 54 | */ 55 | var permitKeepAliveWithoutCalls = true 56 | } 57 | 58 | class SnapshotCleanupProperties { 59 | var collectAfterMillis: Duration = Duration.ofSeconds(10) 60 | var collectionIntervalMillis: Duration = Duration.ofSeconds(10) 61 | } 62 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/StatusRouteTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.extension.RegisterExtension 6 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 7 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 8 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.snapshot.EndpointMatch 12 | 13 | class StatusRouteTest { 14 | companion object { 15 | 16 | @JvmField 17 | @RegisterExtension 18 | val consul = ConsulExtension() 19 | 20 | @JvmField 21 | @RegisterExtension 22 | val envoyControl = EnvoyControlExtension(consul, mapOf( 23 | "envoy-control.envoy.snapshot.routes.status.enabled" to true, 24 | "envoy-control.envoy.snapshot.routes.status.endpoints" to mutableListOf(EndpointMatch().also { it.path = "/my-status/" }), 25 | "envoy-control.envoy.snapshot.routes.status.createVirtualCluster" to true 26 | )) 27 | 28 | @JvmField 29 | @RegisterExtension 30 | val service = EchoServiceExtension() 31 | 32 | @JvmField 33 | @RegisterExtension 34 | val envoy = EnvoyExtension(envoyControl, service) 35 | } 36 | 37 | @Test 38 | fun `should allow defining custom status prefix`() { 39 | untilAsserted { 40 | // when 41 | val ingressRoot = envoy.ingressOperations.callLocalService(endpoint = "/my-status/abc") 42 | 43 | // then 44 | val statusUpstreamOk = envoy.container.admin().statValue( 45 | "vhost.secured_local_service.vcluster.status.upstream_rq_200" 46 | )?.toInt() 47 | assertThat(statusUpstreamOk).isGreaterThan(0) 48 | assertThat(ingressRoot.code).isEqualTo(200) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/ec_vs_other_software.md: -------------------------------------------------------------------------------- 1 | # Envoy Control vs other software 2 | 3 | ### Istio 4 | [Istio](https://istio.io/) is the most popular complete Service Mesh solution based on Envoy. 5 | The problem with Istio is that it's almost Kubernetes only. 6 | 7 | The integration with Consul did not scale properly for our use case (see [Integration - Consul](integrations/consul.md)) 8 | 9 | ### Linkerd 10 | [Linkerd](https://linkerd.io/) is an alternative to Envoy based Service Meshes. It includes both Data Plane and 11 | Control Plane (Namerd). 12 | 13 | Linkerd v1 Data Plane is built using Scala with Twitter's Finagle library. We feel like Scala is not the best tool for 14 | this job, because of the JRE runtime. This means higher memory footprint and latency due to GC pauses. 15 | 16 | Linkerd v2 was rewritten in Rust to get better performance. Unfortunately, just like Istio - it's Kubernetes only. 17 | 18 | ### Consul Connect 19 | [Consul Connect](https://www.consul.io/docs/connect) is a simple way to deploy Envoy to current 20 | Consul based infrastructure. 21 | The problem with Consul Connect is that versions prior to 1.6.0 had very limited traffic control capabilities. 22 | We want to have a fallback to instances from other DCs, canary deployment and other features specific to our 23 | infrastructure. This was not possible in the version of Consul (1.5.1) that was available when Envoy Control was developed. 24 | 25 | ### Rotor 26 | [Rotor](https://github.com/turbinelabs/rotor) is a Control Plane built by Turbine Labs. 27 | The project is no longer maintained because Turbine Labs was shut down. 28 | 29 | The integration with Consul did not scale properly for our use case (see [Integration - Consul](integrations/consul.md)) 30 | 31 | ### Go Control Plane / Java Control Plane 32 | [Go Control Plane](https://github.com/envoyproxy/go-control-plane) and 33 | [Java Control Plane](https://github.com/envoyproxy/java-control-plane) are projects that you can base your 34 | Control Plane implementation on. They're not a sufficient Control Plane by themselves as they require connecting to your 35 | Discovery Service. 36 | 37 | Envoy Control is based on Java Control Plane and integrates with Consul by default. It also adds features like 38 | Cross DC Synchronization or Permission management. 39 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/LocalServiceCustomHealthCheckRouteTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import okhttp3.Headers.Companion.headersOf 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.extension.RegisterExtension 7 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 8 | import pl.allegro.tech.servicemesh.envoycontrol.config.AdsCustomHealthCheck 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 12 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 13 | 14 | class LocalServiceCustomHealthCheckRouteTest { 15 | 16 | companion object { 17 | 18 | @JvmField 19 | @RegisterExtension 20 | val consul = ConsulExtension() 21 | 22 | @JvmField 23 | @RegisterExtension 24 | val envoyControl = EnvoyControlExtension(consul) 25 | 26 | @JvmField 27 | @RegisterExtension 28 | val service = EchoServiceExtension() 29 | 30 | @JvmField 31 | @RegisterExtension 32 | val envoy = EnvoyExtension(envoyControl, service, AdsCustomHealthCheck) 33 | } 34 | 35 | @Test 36 | fun `should health check be routed to custom cluster`() { 37 | untilAsserted { 38 | // when 39 | envoy.ingressOperations.callLocalService(endpoint = "/status/custom", headers = headersOf()) 40 | 41 | // then 42 | assertThat(envoy.container.admin().statValue("cluster.local_service_health_check.upstream_rq_200")?.toInt()).isGreaterThan(0) 43 | assertThat(envoy.container.admin().statValue("cluster.local_service.upstream_rq_200")?.toInt()).isEqualTo(-1) 44 | } 45 | 46 | // and 47 | envoy.ingressOperations.callLocalService(endpoint = "/status/ping", headers = headersOf()) 48 | 49 | // then 50 | assertThat(envoy.container.admin().statValue("cluster.local_service.upstream_rq_200")?.toInt()).isEqualTo(1) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/service/HttpsEchoContainer.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.service 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.convertValue 6 | import okhttp3.Response 7 | import org.testcontainers.containers.Network 8 | import pl.allegro.tech.servicemesh.envoycontrol.config.containers.SSLGenericContainer 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.ResponseWithBody 10 | 11 | class HttpsEchoContainer : SSLGenericContainer("mendhak/http-https-echo@$hash"), 12 | ServiceContainer, UpstreamService { 13 | 14 | companion object { 15 | // We need to use hash because the image doesn't use tags and the tests will fail if there is an older version 16 | // of the image pulled locally 17 | const val hash = "sha256:cd9025b7cdb6b2e8dd6e4a403d50b2dea074835948411167fc86566cb4ae77b6" 18 | const val PORT = 5678 19 | } 20 | 21 | override fun configure() { 22 | super.configure() 23 | withEnv("HTTP_PORT", "$PORT") 24 | withNetwork(Network.SHARED) 25 | } 26 | 27 | override fun port() = PORT 28 | override fun id(): String = containerId 29 | 30 | override fun isSourceOf(response: ResponseWithBody) = HttpsEchoResponse(response).isFrom(this) 31 | } 32 | 33 | class HttpsEchoResponse(val response: ResponseWithBody) { 34 | companion object { 35 | val objectMapper: ObjectMapper = ObjectMapper() 36 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 37 | } 38 | 39 | constructor(response: Response) : this(ResponseWithBody(response)) 40 | 41 | val requestHeaders by lazy> { 42 | objectMapper.convertValue(objectMapper.readTree(response.body).at("/headers")) 43 | } 44 | 45 | val hostname by lazy { objectMapper.readTree(response.body).at("/os/hostname").textValue() } 46 | 47 | fun isFrom(container: HttpsEchoContainer): Boolean { 48 | return container.containerName() == hostname 49 | } 50 | } 51 | 52 | fun Response.asHttpsEchoResponse() = HttpsEchoResponse(this) 53 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/LocalServiceTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import okhttp3.Headers.Companion.headersOf 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.extension.RegisterExtension 7 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.isOk 8 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 9 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 12 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 13 | 14 | class LocalServiceTest { 15 | 16 | companion object { 17 | private const val prefix = "envoy-control.envoy.snapshot" 18 | 19 | @JvmField 20 | @RegisterExtension 21 | val consul = ConsulExtension() 22 | 23 | @JvmField 24 | @RegisterExtension 25 | val envoyControl = EnvoyControlExtension( 26 | consul, mapOf( 27 | "$prefix.ingress.add-service-name-header-to-response" to true, 28 | "$prefix.ingress.add-requested-authority-header-to-response" to true 29 | ) 30 | ) 31 | 32 | @JvmField 33 | @RegisterExtension 34 | val service = EchoServiceExtension() 35 | 36 | @JvmField 37 | @RegisterExtension 38 | val envoy = EnvoyExtension(envoyControl, service) 39 | } 40 | 41 | @Test 42 | fun `should add header with service name to response`() { 43 | // given 44 | consul.server.operations.registerService(service, name = "service-1") 45 | 46 | // when 47 | untilAsserted { 48 | // when 49 | val response = envoy.ingressOperations.callLocalService("/", headers = headersOf("host", "test-service")) 50 | 51 | assertThat(response).isOk() 52 | assertThat(response.header("x-service-name")).isEqualTo("echo2") 53 | assertThat(response.header("x-requested-authority")).isEqualTo("test-service") 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/features/access_log_filter.md: -------------------------------------------------------------------------------- 1 | # Access log filter configuration 2 | 3 | Using Envoy's [metadata](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/base.proto.html#core-metadata) 4 | section you can provide additional configuration to the Control Plane. 5 | Configuration provided in `metadata.access_log_filter` will be used to set up an access log filter for `Envoy`. 6 | 7 | ## Filter logs by status code 8 | 9 | Envoy allows filtering access logs by status code, request duration, response flag, traceable and not a health check 10 | filters. 11 | Note that when adding multiple filters they are applied using AND operator. 12 | 13 | * Variable `metadata.access_log_filter.status_code_filter` and `metadata.access_log_filter.duration_filter` should 14 | * contain operator (case insensitive) and status code/duration. 15 | 16 | Expected format is: `{operator}`:`{status code}`. 17 | 18 | ####Allowed operators are: 19 | 20 | * `le` - lower equal 21 | * `eq` - equal 22 | * `ge` - greater equal 23 | 24 | ####An example configuration: 25 | 26 | ```yaml 27 | metadata: 28 | access_log_filter: 29 | status_code_filter: "GE:500" 30 | ``` 31 | 32 | * Variable `metadata.access_log_filter.not_health_check_filter` 33 | 34 | ####An example configuration: 35 | ```yaml 36 | metadata: 37 | access_log_filter: 38 | not_health_check_filter: true 39 | ``` 40 | 41 | * Variable `metadata.access_log_filter.header_filter` should contain header name and regex to match corresponding 42 | header value. 43 | 44 | Expected format {header name}:{regex} 45 | 46 | ####An example configuration 47 | 48 | ```yaml 49 | metadata: 50 | access_log_filter: 51 | header_filter: "X-Canary:^1$" 52 | ``` 53 | 54 | * Variable `metadata.access_log_filter.response_flag_filter` should contain flags that are limited to the ones listed 55 | [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-response-flags) 56 | 57 | Multiple flags should be delimited by comma. 58 | 59 | *Importand note: this filter hasn't been tested* 60 | 61 | ####An example configuration 62 | 63 | ```yaml 64 | metadata: 65 | access_log_filter: 66 | response_flag_filter: "UPE,DT" 67 | ``` 68 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/sharing/ContainerExtension.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config.sharing 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext 4 | import org.testcontainers.lifecycle.Startables 5 | import pl.allegro.tech.servicemesh.envoycontrol.config.testcontainers.GenericContainer 6 | import pl.allegro.tech.servicemesh.envoycontrol.logger 7 | 8 | abstract class ContainerExtension : BeforeAndAfterAllOnce { 9 | companion object { 10 | val logger by logger() 11 | } 12 | 13 | abstract val container: GenericContainer<*> 14 | protected open fun preconditions(context: ExtensionContext) {} 15 | protected open fun waitUntilHealthy() {} 16 | 17 | override fun beforeAllOnce(context: ExtensionContext) { 18 | preconditions(context) 19 | logAndThrowError { 20 | container.start() 21 | waitUntilHealthy() 22 | } 23 | } 24 | 25 | override fun afterAllOnce(context: ExtensionContext) { 26 | container.stop() 27 | } 28 | 29 | private fun logAndThrowError(action: ContainerExtension.() -> Unit) = try { 30 | action(this) 31 | } catch (e: Exception) { 32 | logger.error("Logs from failed container: ${container.logs}", e) 33 | throw e 34 | } 35 | 36 | class Parallel(private vararg val extensions: ContainerExtension) : BeforeAndAfterAllOnce { 37 | override fun beforeAllOnce(context: ExtensionContext) { 38 | extensions.forEach { extension -> 39 | extension.logAndThrowError { preconditions(context) } 40 | } 41 | try { 42 | Startables.deepStart(extensions.map { it.container }).join() 43 | } catch (e: Exception) { 44 | logger.error("Starting containers in parallel failed", e) 45 | } 46 | extensions.forEach { extension -> 47 | extension.logAndThrowError { waitUntilHealthy() } 48 | } 49 | } 50 | 51 | override fun afterAllOnce(context: ExtensionContext) { 52 | extensions.forEach { extension -> extension.afterAll(context) } 53 | } 54 | 55 | override val ctx: BeforeAndAfterAllOnce.Context = BeforeAndAfterAllOnce.Context() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /envoy-control-source-consul/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/consul/services/ConsulLocalClusterStateChanges.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.consul.services 2 | 3 | import pl.allegro.tech.servicemesh.envoycontrol.services.LocalClusterStateChanges 4 | import pl.allegro.tech.servicemesh.envoycontrol.services.Locality 5 | import pl.allegro.tech.servicemesh.envoycontrol.services.ClusterState 6 | import pl.allegro.tech.servicemesh.envoycontrol.services.MultiClusterState 7 | import pl.allegro.tech.servicemesh.envoycontrol.services.MultiClusterState.Companion.toMultiClusterState 8 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances 9 | import pl.allegro.tech.servicemesh.envoycontrol.services.ServicesState 10 | import pl.allegro.tech.servicemesh.envoycontrol.services.transformers.ServiceInstancesTransformer 11 | import reactor.core.publisher.Flux 12 | import java.util.concurrent.ConcurrentHashMap 13 | import java.util.concurrent.atomic.AtomicReference 14 | 15 | class ConsulLocalClusterStateChanges( 16 | private val consulChanges: ConsulServiceChanges, 17 | private val locality: Locality, 18 | private val cluster: String, 19 | private val transformers: List = emptyList(), 20 | override val latestServiceState: AtomicReference = AtomicReference(ServicesState()) 21 | ) : LocalClusterStateChanges { 22 | override fun stream(): Flux = 23 | consulChanges 24 | .watchState() 25 | .map { state -> 26 | transformers 27 | .fold(state.allInstances().asSequence()) { instancesSequence, transformer -> 28 | transformer.transform(instancesSequence) 29 | } 30 | .associateBy { it.serviceName } 31 | .toConcurrentHashMap() 32 | .let(::ServicesState) 33 | } 34 | .doOnNext { latestServiceState.set(it) } 35 | .map { 36 | ClusterState(it, locality, cluster).toMultiClusterState() 37 | } 38 | 39 | override fun isInitialStateLoaded(): Boolean = latestServiceState.get() != ServicesState() 40 | 41 | private fun Map.toConcurrentHashMap() = ConcurrentHashMap(this) 42 | } 43 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/AddUpstreamHeaderTest.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol 2 | 3 | import okhttp3.Response 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.assertj.core.api.ObjectAssert 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.extension.RegisterExtension 8 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.isOk 9 | import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted 10 | import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension 11 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension 12 | import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension 13 | import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension 14 | 15 | class AddUpstreamHeaderTest { 16 | 17 | companion object { 18 | @JvmField 19 | @RegisterExtension 20 | val consul = ConsulExtension() 21 | 22 | @JvmField 23 | @RegisterExtension 24 | val envoyControl = EnvoyControlExtension(consul) 25 | 26 | @JvmField 27 | @RegisterExtension 28 | val envoy = EnvoyExtension(envoyControl) 29 | 30 | @JvmField 31 | @RegisterExtension 32 | val echoContainer = EchoServiceExtension() 33 | } 34 | 35 | @Test 36 | fun `should add x-envoy-upstream-remote-address header with address of upstream service`() { 37 | // given 38 | consul.server.operations.registerService(echoContainer, name = "service-1") 39 | 40 | untilAsserted { 41 | // when 42 | val response = envoy.egressOperations.callService(service = "service-1", pathAndQuery = "/endpoint") 43 | 44 | // then 45 | assertThat(response).isOk().hasXEnvoyUpstreamRemoteAddressFrom(echoContainer) 46 | } 47 | } 48 | 49 | private fun ObjectAssert.hasXEnvoyUpstreamRemoteAddressFrom( 50 | echoServiceExtension: EchoServiceExtension 51 | ): ObjectAssert { 52 | matches { 53 | it 54 | .headers("x-envoy-upstream-remote-address") 55 | .contains(echoServiceExtension.container().address()) 56 | } 57 | return this 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | ## Envoy 4 | 5 | Here are some tips to improve the performance of Service Mesh with Envoy Control. 6 | In the future, [Incremental xDS](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#incremental-xds) 7 | should solve most of the performance issues. 8 | 9 | ### Use permissions 10 | 11 | It is recommended to only send to Envoy the data that will be used. 12 | If service A communicates only with B and C, the A's Envoy should only follow B and C clusters. 13 | 14 | This approach solves most of the performance problems. 15 | 16 | During our tests - Envoy that follows 1,000 clusters will use almost 200 MB of RAM in comparison to about 10 MB when 17 | following only a few services. 18 | 19 | Additionally, the network usage is significantly higher. With 1,000 clusters, we've seen snapshot size go up to 300 KB. 20 | Assuming that a new snapshot is generated every second. 1,000 Envoys with 1,000 clusters can generate a load of 21 | 300 MB/s. When following only a few services, the snapshot is about 5 KB and it's sent much less frequently. 22 | 23 | ### Use ADS 24 | 25 | With xDS, Envoy set up a gRPC stream to Envoy Control per cluster. Let's say there are 1,000 Envoys and 1,000 clusters. 26 | Envoy Control will have to handle 1,000,000 open gRPC streams. This puts pressure on memory, which converts to more 27 | frequent GC runs and higher CPU usage. 28 | 29 | With ADS, each Envoy sets up a single gRPC stream for all clusters. With 1,000 Envoys, there are 1,000 streams which 30 | reduces memory usage dramatically. 31 | 32 | ### Sampling 33 | 34 | Envoy Control by default follows changes from the discovery service, batches them and sends to Envoys at most once every second. 35 | This can be set to a longer time which will decrease the workload of Envoy Control at the cost of higher latency of 36 | changes in Envoy. When setting it to a longer time, consider using Outlier Detection - this can passively eliminate 37 | old instances. 38 | 39 | ### Use G1 GC 40 | 41 | In Java 9 and onwards, G1 GC is the default Garbage Collection algorithm. When using Java 8, consider switching from 42 | Concurrent Mark and Sweep GC to G1 GC. 43 | 44 | ### DOS prevention 45 | 46 | We are currently working on a mechanism that would [allow rate limiting to Envoy Control](https://github.com/envoyproxy/java-control-plane/pull/102). 47 | -------------------------------------------------------------------------------- /envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/ClientsFactory.kt: -------------------------------------------------------------------------------- 1 | package pl.allegro.tech.servicemesh.envoycontrol.config 2 | 3 | import okhttp3.OkHttpClient 4 | import org.apache.hc.client5.http.ssl.NoopHostnameVerifier 5 | import org.apache.hc.client5.http.ssl.TrustAllStrategy 6 | import org.apache.hc.core5.ssl.SSLContextBuilder 7 | import java.security.KeyStore 8 | import java.time.Duration 9 | import javax.net.ssl.SSLSocketFactory 10 | import javax.net.ssl.TrustManagerFactory 11 | import javax.net.ssl.X509TrustManager 12 | 13 | object ClientsFactory { 14 | 15 | // envoys default timeout is 15 seconds while OkHttp is 10 16 | private const val TIMEOUT_SECONDS: Long = 20 17 | private var insecureOkHttpClient: OkHttpClient? = null 18 | private var okHttpClient: OkHttpClient? = null 19 | 20 | fun createClient(): OkHttpClient = if (okHttpClient == null) { 21 | OkHttpClient.Builder() 22 | .connectTimeout(Duration.ofSeconds(TIMEOUT_SECONDS)) 23 | .readTimeout(Duration.ofSeconds(TIMEOUT_SECONDS)) 24 | .build().also { okHttpClient = it } 25 | } else { 26 | okHttpClient!! 27 | } 28 | 29 | fun createInsecureClient(): OkHttpClient = if (insecureOkHttpClient == null) { 30 | OkHttpClient.Builder() 31 | .hostnameVerifier(NoopHostnameVerifier()) 32 | .sslSocketFactory(getInsecureSSLSocketFactory(), getInsecureTrustManager()) 33 | .connectTimeout(Duration.ofSeconds(TIMEOUT_SECONDS)) 34 | .readTimeout(Duration.ofSeconds(TIMEOUT_SECONDS)) 35 | .build().also { insecureOkHttpClient = it } 36 | } else { 37 | insecureOkHttpClient!! 38 | } 39 | 40 | private fun getInsecureSSLSocketFactory(): SSLSocketFactory { 41 | val builder = SSLContextBuilder() 42 | builder.loadTrustMaterial(null, TrustAllStrategy()) 43 | return builder.build().socketFactory 44 | } 45 | 46 | private fun getInsecureTrustManager(): X509TrustManager { 47 | val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance( 48 | TrustManagerFactory.getDefaultAlgorithm()) 49 | trustManagerFactory.init(null as KeyStore?) 50 | val trustManagers = trustManagerFactory.trustManagers 51 | return trustManagers[0] as X509TrustManager 52 | } 53 | } 54 | --------------------------------------------------------------------------------