Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Custom routes #439

Merged
merged 12 commits into from
Dec 12, 2024
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Lists all changes with user impact.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## [0.22.5]
### Changed
- Add possibility to create custom routes

## [0.22.4]
- Added possibility for configuring priorities per service

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class RoutesProperties {
var admin = AdminRouteProperties()
var status = StatusRouteProperties()
var authorization = AuthorizationProperties()
var customs = emptyList<CustomRuteProperties>()
}

class ClusterOutlierDetectionProperties {
Expand Down Expand Up @@ -261,6 +262,13 @@ class AuthorizationProperties {
var unauthorizedResponseMessage = "You have to be authorized"
}

class CustomRuteProperties {
var enabled = false
var cluster = "custom"
var prefixRewrite = ""
var path = StringMatcher()
}

class ServiceTagsProperties {
var enabled = false
var metadataKey = "tag"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ class AdminRoutesFactory(
HttpMethod.POST
)

private val adminRoutes = guardAccessWithDisableHeader() +
generateSecuredAdminRoutes() +
listOfNotNull(
adminPostRoute.authorized.takeIf { properties.admin.publicAccessEnabled },
adminPostRoute.unauthorized.takeIf { properties.admin.publicAccessEnabled },
adminRoute.takeIf { properties.admin.publicAccessEnabled },
adminRedirectRoute.takeIf { properties.admin.publicAccessEnabled }
)

private fun generateSecuredAdminRoutes(): List<Route> {
return properties.admin.securedPaths
.flatMap {
Expand All @@ -55,16 +64,7 @@ class AdminRoutesFactory(
}
}

fun generateAdminRoutes(): List<Route> {
return guardAccessWithDisableHeader() +
generateSecuredAdminRoutes() +
listOfNotNull(
adminPostRoute.authorized.takeIf { properties.admin.publicAccessEnabled },
adminPostRoute.unauthorized.takeIf { properties.admin.publicAccessEnabled },
adminRoute.takeIf { properties.admin.publicAccessEnabled },
adminRedirectRoute.takeIf { properties.admin.publicAccessEnabled }
)
}
fun generateAdminRoutes() = adminRoutes

private fun createAuthorizedRoute(
pathPrefix: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes

import io.envoyproxy.envoy.config.route.v3.Route
import io.envoyproxy.envoy.config.route.v3.RouteAction
import io.envoyproxy.envoy.config.route.v3.RouteMatch
import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.RoutesProperties
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StringMatcherType

class CustomRoutesFactory(properties: RoutesProperties) {

val routes: List<Route> = properties.customs.filter { it.enabled }.map {
val matcher = when (it.path.type) {
StringMatcherType.REGEX -> RouteMatch.newBuilder()
.setSafeRegex(
RegexMatcher.newBuilder()
.setRegex(it.path.value)
.setGoogleRe2(RegexMatcher.GoogleRE2.getDefaultInstance())
)
StringMatcherType.EXACT -> RouteMatch.newBuilder().setPath(it.path.value)
StringMatcherType.PREFIX -> RouteMatch.newBuilder().setPrefix(it.path.value)
}
RouteMatch.newBuilder()
Route.newBuilder()
.setName(it.cluster)
.setRoute(RouteAction.newBuilder()
.setCluster(it.cluster)
.also { route ->
if (it.prefixRewrite != "") {
route.setPrefixRewrite(it.prefixRewrite)
}
}
)
.setMatch(matcher)
.build()
}

fun generateCustomRoutes() = routes
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class EnvoyIngressRoutesFactory(
envoyHttpFilters: EnvoyHttpFilters = EnvoyHttpFilters.emptyFilters,
private val currentZone: String
) {

private val adminRoutesFactory = AdminRoutesFactory(properties.routes)
private val customRoutesFactory = CustomRoutesFactory(properties.routes)
private val allClients = setOf(
ClientWithSelector.create(properties.incomingPermissions.tlsAuthentication.wildcardClientIdentifier)
)
Expand Down Expand Up @@ -231,12 +232,11 @@ class EnvoyIngressRoutesFactory(
emptyList()
}

val adminRoutesFactory = AdminRoutesFactory(properties.routes)

val virtualHost = VirtualHost.newBuilder()
.setName("secured_local_service")
.addDomains("*")
.addAllVirtualClusters(virtualClusters)
.addAllRoutes(customRoutesFactory.generateCustomRoutes())
.addAllRoutes(adminRoutesFactory.generateAdminRoutes())
.addAllRoutes(generateSecuredIngressRoutes(proxySettings, group))
.also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ import pl.allegro.tech.servicemesh.envoycontrol.groups.hasStatusVirtualClusters
import pl.allegro.tech.servicemesh.envoycontrol.groups.ingressRoute
import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingOnAnyMethod
import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingOnMethod
import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingOnPrefix
import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingRetryPolicy
import pl.allegro.tech.servicemesh.envoycontrol.groups.pathMatcher
import pl.allegro.tech.servicemesh.envoycontrol.groups.prefixPathMatcher
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.LocalRetryPoliciesProperties
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.LocalRetryPolicyProperties
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SecuredRoute
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StringMatcher
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StringMatcherType
import java.time.Duration

internal class EnvoyIngressRoutesFactoryTest {
Expand Down Expand Up @@ -95,6 +101,14 @@ internal class EnvoyIngressRoutesFactoryTest {
pathPrefix = "/config_dump"
method = "GET"
})
routes.customs = listOf(CustomRuteProperties().apply {
enabled = true
cluster = "wrapper"
path = StringMatcher().apply {
type = StringMatcherType.PREFIX
value = "/status/wrapper/"
}
})
}, currentZone = currentZone)
val responseTimeout = Durations.fromSeconds(777)
val idleTimeout = Durations.fromSeconds(61)
Expand Down Expand Up @@ -143,6 +157,11 @@ internal class EnvoyIngressRoutesFactoryTest {
hasStatusVirtualClusters()
hasOneDomain("*")
hasOnlyRoutesInOrder(
{
matchingOnPrefix("/status/wrapper/")
.toCluster("wrapper")
.publicAccess()
},
*adminRoutes,
{
ingressRoute()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package pl.allegro.tech.servicemesh.envoycontrol

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import pl.allegro.tech.servicemesh.envoycontrol.assertions.isFrom
import pl.allegro.tech.servicemesh.envoycontrol.assertions.isOk
import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.CustomRuteProperties
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StringMatcher
import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StringMatcherType

internal class CustomRouteTest {
companion object {

private val properties = mapOf(
"envoy-control.envoy.snapshot.routes.customs" to listOf(CustomRuteProperties().apply {
enabled = true
cluster = "wrapper"
path = StringMatcher().apply {
type = StringMatcherType.PREFIX
value = "/status/wrapper/"
}
}),
)

@JvmField
@RegisterExtension
val consul = ConsulExtension()

@JvmField
@RegisterExtension
val envoyControl = EnvoyControlExtension(consul, properties)

@JvmField
@RegisterExtension
val service = EchoServiceExtension()

@JvmField
@RegisterExtension
val wrapper = EchoServiceExtension()

@JvmField
@RegisterExtension
// val envoy = EnvoyExtension(envoyControl, service)
val envoy = EnvoyExtension(envoyControl, service, wrapperService = wrapper)
}
@Test
fun `should redirect to wrapper`() {
// when
val response = envoy.ingressOperations.callLocalService(
endpoint = "/status/wrapper/prometheus"
)
// then
assertThat(response).isOk()
.isFrom(wrapper)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class EnvoyContainer(
private val envoyControl1XdsPort: Int,
private val envoyControl2XdsPort: Int = envoyControl1XdsPort,
private val logLevel: String = "info",
image: String = DEFAULT_IMAGE
image: String = DEFAULT_IMAGE,
private val wrapperServiceIp: () -> String = { "127.0.0.1" },
) : SSLGenericContainer<EnvoyContainer>(
dockerfileBuilder = DockerfileBuilder()
.from(image)
Expand Down Expand Up @@ -72,14 +73,15 @@ class EnvoyContainer(

withCommand(
"/bin/sh", "/usr/local/bin/launch_envoy.sh",
Integer.toString(envoyControl1XdsPort),
Integer.toString(envoyControl2XdsPort),
envoyControl1XdsPort.toString(),
envoyControl2XdsPort.toString(),
CONFIG_DEST,
localServiceIp(),
config.trustedCa,
config.certificateChain,
config.privateKey,
config.serviceName,
wrapperServiceIp(),
"--config-yaml", config.configOverride,
"-l", logLevel
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import java.util.concurrent.TimeUnit
class EnvoyExtension(
private val envoyControl: EnvoyControlExtensionBase,
private val localService: ServiceExtension<*>? = null,
config: EnvoyConfig = RandomConfigFile
config: EnvoyConfig = RandomConfigFile,
private val wrapperService: ServiceExtension<*>? = null
) : BeforeAllCallback, AfterAllCallback, AfterEachCallback {

companion object {
Expand All @@ -31,14 +32,16 @@ class EnvoyExtension(
val container: EnvoyContainer = EnvoyContainer(
config,
{ localService?.container()?.ipAddress() ?: "127.0.0.1" },
envoyControl.app.grpcPort
envoyControl.app.grpcPort,
wrapperServiceIp = { wrapperService?.container()?.ipAddress() ?: "127.0.0.1" },
).withNetwork(Network.SHARED)

val ingressOperations: IngressOperations = IngressOperations(container)
val egressOperations: EgressOperations = EgressOperations(container)

override fun beforeAll(context: ExtensionContext) {
localService?.beforeAll(context)
wrapperService?.beforeAll(context)
envoyControl.beforeAll(context)
try {
container.start()
Expand Down
11 changes: 11 additions & 0 deletions envoy-control-tests/src/main/resources/envoy/bad_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ static_resources:
port_value: 10000
connect_timeout:
seconds: 1
- name: wrapper
type: STATIC
load_assignment:
cluster_name: wrapper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: WRAPPER_SERVICE_IP
port_value: 5678
listeners:
- name: default_listener
address:
Expand Down
11 changes: 11 additions & 0 deletions envoy-control-tests/src/main/resources/envoy/config_ads.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,14 @@ static_resources:
port_value: 10000
connect_timeout:
seconds: 1
- name: wrapper
type: STATIC
load_assignment:
cluster_name: wrapper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: WRAPPER_SERVICE_IP
port_value: 5678
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,14 @@ static_resources:
port_value: 10000
connect_timeout:
seconds: 1
- name: wrapper
type: STATIC
load_assignment:
cluster_name: wrapper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: WRAPPER_SERVICE_IP
port_value: 5678
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ static_resources:
port_value: 10000
connect_timeout:
seconds: 1
- name: wrapper
type: STATIC
load_assignment:
cluster_name: wrapper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: WRAPPER_SERVICE_IP
port_value: 5678
listeners:
- name: default_listener
address:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,14 @@ static_resources:
port_value: 10000
connect_timeout:
seconds: 1
- name: wrapper
type: STATIC
load_assignment:
cluster_name: wrapper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: WRAPPER_SERVICE_IP
port_value: 5678
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,14 @@ static_resources:
port_value: 10000
connect_timeout:
seconds: 1
- name: wrapper
type: STATIC
load_assignment:
cluster_name: wrapper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: WRAPPER_SERVICE_IP
port_value: 5678
Loading
Loading