From 283152eff4a7d5cb489d70a70fbb943720ad6fc4 Mon Sep 17 00:00:00 2001 From: "radoslaw.chrzanowski" Date: Fri, 7 Mar 2025 11:49:41 +0100 Subject: [PATCH] feature white list for separated status routes --- CHANGELOG.md | 4 + docs/configuration.md | 1 + .../snapshot/SnapshotProperties.kt | 7 + .../routes/EnvoyIngressRoutesFactory.kt | 30 +++- .../routes/EnvoyIngressRoutesFactoryTest.kt | 163 +++++++++++++++++- 5 files changed, 197 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b7f6000..f56f4dfe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Lists all changes with user impact. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## [0.22.11] +### Changed +- white list for enabling separated routes for status endpoints + ## [0.22.10] ### Changed - changes for `x-envoy-upstream-service-tags` response header: diff --git a/docs/configuration.md b/docs/configuration.md index e5feb7bda..e190eed31 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -76,6 +76,7 @@ Property **envoy-control.envoy.snapshot.local-service.response-timeout** | Response timeout for localService | 15s **envoy-control.envoy.snapshot.local-service.connection-idle-timeout** | Connection idle timeout for localService | 120s **envoy-control.envoy.snapshot.routes.status.enabled** | Enable status route | false +**envoy-control.envoy.snapshot.routes.status.separated-route-white-list** | List of services for which we create a separated route for status endpoints | empty list **envoy-control.envoy.snapshot.routes.status.endpoints** | List of endpoints with path or prefix of status routes | /status **envoy-control.envoy.snapshot.routes.status.create-virtual-cluster** | Create virtual cluster for status route | false **envoy-control.envoy.snapshot.state-sample-duration** | Duration of state sampling (this is used to prevent surges in consul events overloading control plane) | 1s diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotProperties.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotProperties.kt index 410f973bd..8bfb5d517 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotProperties.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotProperties.kt @@ -222,6 +222,7 @@ class AdminRouteProperties { class StatusRouteProperties { var enabled = false + var separatedRouteWhiteList = FeatureWhiteList(emptyList()) var endpoints: MutableList = mutableListOf() var createVirtualCluster = false } @@ -459,3 +460,9 @@ data class ResetHeader(val name: String, val format: String) typealias ProviderName = String typealias TokenField = String + +data class FeatureWhiteList(val services: List) { + fun enabledFor(serviceName: String): Boolean { + return services.contains("*") || services.contains(serviceName) + } +} diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactory.kt index d7fc9a1db..a0c484239 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactory.kt @@ -181,15 +181,22 @@ class EnvoyIngressRoutesFactory( .toMap() private fun ingressRoutes(proxySettings: ProxySettings, group: Group): List { - return ingressRoute(proxySettings, group, RoutingPriority.HIGH, "/status/") + - ingressRoute(proxySettings, group, RoutingPriority.DEFAULT, "/") + val defaultRoute = ingressRoute(proxySettings, group, RoutingPriority.DEFAULT, PathMatchingType.PATH_PREFIX,"/") + if (properties.routes.status.separatedRouteWhiteList.enabledFor(group.serviceName)) { + val statusRoutes = statusEndpointsMatch.flatMap { + ingressRoute(proxySettings, group, RoutingPriority.HIGH, it.matchingType, it.path) + } + return statusRoutes + defaultRoute + } + return ingressRoute(proxySettings, group, RoutingPriority.DEFAULT, PathMatchingType.PATH_PREFIX,"/") } private fun ingressRoute( proxySettings: ProxySettings, group: Group, priority: RoutingPriority, - prefix: String + matchingType: PathMatchingType, + path: String ): List { val localRouteAction = clusterRouteAction( proxySettings.incoming.timeoutPolicy.responseTimeout, @@ -201,17 +208,15 @@ class EnvoyIngressRoutesFactory( val nonRetryRoute = Route.newBuilder() .setMatch( - RouteMatch.newBuilder() - .setPrefix(prefix) + routeMatcher(matchingType, path) ) .setRoute(localRouteAction) val retryRoutes = perMethodRetryPolicies .map { (method, retryPolicy) -> Route.newBuilder() .setMatch( - RouteMatch.newBuilder() + routeMatcher(matchingType, path) .addHeaders(httpMethodMatcher(method)) - .setPrefix(prefix) ) .setRoute(clusterRouteActionWithRetryPolicy(retryPolicy, localRouteAction)) } @@ -220,6 +225,17 @@ class EnvoyIngressRoutesFactory( } } + private fun routeMatcher( + matchingType: PathMatchingType, + path: String + ): RouteMatch.Builder { + return when (matchingType) { + PathMatchingType.PATH -> RouteMatch.newBuilder().setPath(path) + PathMatchingType.PATH_PREFIX -> RouteMatch.newBuilder().setPrefix(path) + PathMatchingType.PATH_REGEX -> RouteMatch.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder().setRegex(path)) + } + } private fun customHealthCheckRoute(proxySettings: ProxySettings): List { if (proxySettings.incoming.healthCheck.hasCustomHealthCheck()) { val healthCheckRouteAction = clusterRouteAction( diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactoryTest.kt index 8d16b0dcd..dbdd7a1fe 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyIngressRoutesFactoryTest.kt @@ -41,6 +41,7 @@ import pl.allegro.tech.servicemesh.envoycontrol.groups.publicAccess import pl.allegro.tech.servicemesh.envoycontrol.groups.toCluster import pl.allegro.tech.servicemesh.envoycontrol.snapshot.CustomRuteProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.EndpointMatch +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.FeatureWhiteList import pl.allegro.tech.servicemesh.envoycontrol.snapshot.LocalRetryPoliciesProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.LocalRetryPolicyProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SecuredRoute @@ -94,7 +95,15 @@ internal class EnvoyIngressRoutesFactoryTest { // given val routesFactory = EnvoyIngressRoutesFactory(SnapshotProperties().apply { routes.status.enabled = true - routes.status.endpoints = mutableListOf(EndpointMatch()) + routes.status.apply { + enabled = true + endpoints = mutableListOf(EndpointMatch().apply { + path = "/status/" + matchingType = PathMatchingType.PATH_PREFIX + }) + createVirtualCluster = true + separatedRouteWhiteList = FeatureWhiteList(listOf("service_1")) + } routes.status.createVirtualCluster = true localService.retryPolicy = retryPolicyProps routes.admin.publicAccessEnabled = true @@ -111,6 +120,7 @@ internal class EnvoyIngressRoutesFactoryTest { value = "/status/wrapper/" } }) + }, currentZone = currentZone) val responseTimeout = Durations.fromSeconds(777) val idleTimeout = Durations.fromSeconds(61) @@ -221,6 +231,157 @@ internal class EnvoyIngressRoutesFactoryTest { } } + + @Test + @Suppress("LongMethod") + fun `should create not create routes for status endpoints if the service is not whitelisted`() { + // given + val routesFactory = EnvoyIngressRoutesFactory(SnapshotProperties().apply { + routes.status.enabled = true + routes.status.apply { + enabled = true + endpoints = mutableListOf(EndpointMatch().apply { + path = "/status/" + matchingType = PathMatchingType.PATH_PREFIX + }) + createVirtualCluster = true + separatedRouteWhiteList = FeatureWhiteList(listOf("service_123")) + } + routes.status.createVirtualCluster = true + localService.retryPolicy = retryPolicyProps + routes.admin.publicAccessEnabled = true + routes.admin.token = "test_token" + routes.admin.securedPaths.add(SecuredRoute().apply { + pathPrefix = "/config_dump" + method = "GET" + }) + }, currentZone = currentZone) + val proxySettingsOneEndpoint = ProxySettings( + + ) + val group = ServicesGroup( + communicationMode = CommunicationMode.XDS, + serviceName = "service_1", + discoveryServiceName = "service_1", + proxySettings = proxySettingsOneEndpoint + ) + + // when + val routeConfig = routesFactory.createSecuredIngressRouteConfig( + "service_1", + proxySettingsOneEndpoint, + group + ) + + // then + routeConfig + .hasSingleVirtualHostThat { + hasStatusVirtualClusters() + hasOneDomain("*") + hasOnlyRoutesInOrder( + *adminRoutes, + { + ingressServiceRoute() + matchingOnMethod("GET") + matchingRetryPolicy(retryPolicyProps.perHttpMethod["GET"]!!) + }, + { + ingressServiceRoute() + matchingOnMethod("HEAD") + matchingRetryPolicy(retryPolicyProps.perHttpMethod["HEAD"]!!) + }, + { + ingressServiceRoute() + matchingOnAnyMethod() + hasNoRetryPolicy() + } + ) + matchingRetryPolicy(retryPolicyProps.default) + } + } + + @Test + @Suppress("LongMethod") + fun `should create create routes for status endpoints when whitelist contains wildcard`() { + // given + val routesFactory = EnvoyIngressRoutesFactory(SnapshotProperties().apply { + routes.status.enabled = true + routes.status.apply { + enabled = true + endpoints = mutableListOf(EndpointMatch().apply { + path = "/status/" + matchingType = PathMatchingType.PATH_PREFIX + }) + createVirtualCluster = true + separatedRouteWhiteList = FeatureWhiteList(listOf("service_123", "*")) + } + routes.status.createVirtualCluster = true + localService.retryPolicy = retryPolicyProps + routes.admin.publicAccessEnabled = true + routes.admin.token = "test_token" + routes.admin.securedPaths.add(SecuredRoute().apply { + pathPrefix = "/config_dump" + method = "GET" + }) + }, currentZone = currentZone) + val proxySettingsOneEndpoint = ProxySettings( + + ) + val group = ServicesGroup( + communicationMode = CommunicationMode.XDS, + serviceName = "service_1", + discoveryServiceName = "service_1", + proxySettings = proxySettingsOneEndpoint + ) + + // when + val routeConfig = routesFactory.createSecuredIngressRouteConfig( + "service_1", + proxySettingsOneEndpoint, + group + ) + + // then + routeConfig + .hasSingleVirtualHostThat { + hasStatusVirtualClusters() + hasOneDomain("*") + hasOnlyRoutesInOrder( + *adminRoutes, + { + ingresStatusRoute() + matchingOnMethod("GET") + matchingRetryPolicy(retryPolicyProps.perHttpMethod["GET"]!!) + }, + { + ingresStatusRoute() + matchingOnMethod("HEAD") + matchingRetryPolicy(retryPolicyProps.perHttpMethod["HEAD"]!!) + }, + { + ingresStatusRoute() + matchingOnAnyMethod() + hasNoRetryPolicy() + }, + { + ingressServiceRoute() + matchingOnMethod("GET") + matchingRetryPolicy(retryPolicyProps.perHttpMethod["GET"]!!) + }, + { + ingressServiceRoute() + matchingOnMethod("HEAD") + matchingRetryPolicy(retryPolicyProps.perHttpMethod["HEAD"]!!) + }, + { + ingressServiceRoute() + matchingOnAnyMethod() + hasNoRetryPolicy() + } + ) + matchingRetryPolicy(retryPolicyProps.default) + } + } @Test fun `should create route config with headers to remove and add`() { // given