diff --git a/CHANGELOG.md b/CHANGELOG.md index aad3d2cef..b1e073f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,39 @@ Lists all changes with user impact. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). -## [Unreleased] +## [0.20.0] ### Changed - Remove xds support +## [0.19.29] + +### Changed +- add mechanism to store custom data in group + +## [0.19.28] + +### Changed +- update envoy version to 1.24.0 + +## [0.19.27] + +### Changed +- flaky test fixed +- Auto service tags (proxy settings outgoing.routingPolicy +- remove duplicated routes + +## [0.19.26] + +### Changed +- Bump consul recipes to fix index handling behavior in edge cases + +## [0.19.25] + +### Changed +- Prefix for negating values from jwt token used in rbac +- Configurable default clients lists + ## [0.19.24] ### Changed diff --git a/build.gradle b/build.gradle index 69fc46345..9b68f1f27 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ allprojects { project.ext.versions = [ kotlin : '1.6.10', - java_controlplane : '0.1.35', + java_controlplane : '1.0.37', spring_boot : '2.3.4.RELEASE', grpc : '1.48.1', jaxb : '2.3.1', @@ -58,12 +58,12 @@ allprojects { awaitility : '4.0.3', embedded_consul : '2.0.0', junit : '5.6.2', - assertj : '3.16.1', + assertj : '3.17.2', jackson : '2.11.2', toxiproxy : '2.1.3', testcontainers : '1.16.0', reactor : '3.3.10.RELEASE', - consul_recipes : '0.9.0', + consul_recipes : '0.9.1', mockito : '3.3.3', cglib : '3.2.9', logback : '1.2.3', diff --git a/docs/configuration.md b/docs/configuration.md index 74e57d1ca..6c2642937 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -123,6 +123,8 @@ Property **envoy-control.envoy.snapshot.outgoing-permissions.all-services-dependencies.identifier** | Special value (wildcard) that signifies that the service depends on all other services | * **envoy-control.envoy.snapshot.outgoing-permissions.all-services-dependencies.not-included-by-prefix** | Services not included in dependencies for services with wildcard in outgoing.dependency field. Matched by service name prefix. | empty list **envoy-control.envoy.snapshot.outgoing-permissions.services-allowed-to-use-wildcard** | Services that are allowed to have wildcard in outgoing.dependency field | empty set +**envoy-control.envoy.snapshot.outgoing-permissions.rbac.clients-lists.default-clients-list** | List of clients which will be applied to each rbac policy, if none of the lists defined in `custom-clients-lists` have been matched | empty list +**envoy-control.envoy.snapshot.outgoing-permissions.rbac.clients-lists.custom-clients-lists** | Lists of clients which will be applied to each rbac policy, only if key for defined list is present in clients for defined endpoint | empty map ## Load Balancing Property | Description | Default value @@ -135,15 +137,16 @@ Property **envoy-control.envoy.snapshot.load-balancing.use-keys-subset-fallback-policy** | KEYS_SUBSET fallback policy is used by default when canary and service-tags are enabled. It is not supported in Envoy <= 1.12.x. Set to false for compatibility with Envoy 1.12.x | true ## Routing -Property | Description | Default value -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- -**envoy-control.envoy.snapshot.routing.service-tags.enabled** | If set to true, service tags routing will be enabled | false -**envoy-control.envoy.snapshot.routing.service-tags.metadata-key** | What key to use in endpoint metadata to store its service tags | tag -**envoy-control.envoy.snapshot.routing.service-tags.header** | What header to use in service tag rules | x-service-tag -**envoy-control.envoy.snapshot.routing.service-tags.routing-excluded-tags** | List of tags predicates that cannot be used for routing. This supports an exact matching (just "string" - EXACT matching) prefixes (PREFIX matching) and regexes (REGEX matching) | empty list -**envoy-control.envoy.snapshot.routing.service-tags.allowed-tags-combinations** | List of rules, which tags can be conbined together and requested together. Details below | empty list -**(...).allowed-tags-combinations[].service-name** | The rule will apply only for this service | "" -**(...).allowed-tags-combinations[].tags** | List of tag patterns, that can be combined and requested together | empty list +Property | Description | Default value +------------------------------------------------------------------------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --------- +**envoy-control.envoy.snapshot.routing.service-tags.enabled** | If set to true, service tags routing will be enabled | false +**envoy-control.envoy.snapshot.routing.service-tags.metadata-key** | What key to use in endpoint metadata to store its service tags | tag +**envoy-control.envoy.snapshot.routing.service-tags.header** | What header to use in service tag rules | x-service-tag +**envoy-control.envoy.snapshot.routing.service-tags.routing-excluded-tags** | List of tags predicates that cannot be used for routing. This supports an exact matching (just "string" - EXACT matching) prefixes (PREFIX matching) and regexes (REGEX matching) | empty list +**envoy-control.envoy.snapshot.routing.service-tags.allowed-tags-combinations** | List of rules, which tags can be conbined together and requested together. Details below | empty list +**(...).allowed-tags-combinations[].service-name** | The rule will apply only for this service | "" +**(...).allowed-tags-combinations[].tags** | List of tag patterns, that can be combined and requested together | empty list +**envoy-control.envoy.snapshot.routing.service-tags.auto-service-tag-enabled** | Enable auto service tag feature. (`enabled` needs also be true) | false ## Outlier detection Property | Description | Default value @@ -180,6 +183,7 @@ Where `` is one of the following: ### Outgoing traffic Property | Description | Default value --------------------------------------------------------------------------------------------------------| ----------------------------------------------------------------------------------------------------------------------------- | --------- +**envoy-control.envoy.snapshot.retryPolicy.enabled** | Flag which enables default retries | true **envoy-control.envoy.snapshot.retryPolicy.numberOfRetries** | Number of retries | 1 **envoy-control.envoy.snapshot.retryPolicy.hostSelectionRetryMaxAttempts** | The maximum number of times host selection will be reattempted before request being routed to last selected host | 3 **envoy-control.envoy.snapshot.retryPolicy.retryHostPredicate** | Specifies a collection of RetryHostPredicates that will be consulted when selecting a host for retries | a list with one entry "envoy.retry_host_predicates.previous_hosts" diff --git a/docs/features/service_tags.md b/docs/features/service_tags.md index 39b424527..817f4e1a5 100644 --- a/docs/features/service_tags.md +++ b/docs/features/service_tags.md @@ -94,3 +94,71 @@ Use this feature with caution, because tags combinations require a lot of additional memory for Envoy. +## Automatic service tags with fallback support using "routingPolicy" + +The mode described above is very flexible. +It allows using a different service tag for each request via a http header specified manually by the client. +When such flexibility is not needed, the client might configure it once for all +via `proxy_settings.outgoing.routingPolicy`. + +Another advantage of `routingPolicy` over manual, per-request service-tags, is a fallback mechanism. +The client lists possible service tags in preferred order and the best possible is going to be selected + +### Example: + +```yaml +metadata: + proxy_settings: + outgoing: + routingPolicy: + autoServiceTag: true + serviceTagPreference: ["ipsum", "lorem"] + dependencies: + - service: "echo" +``` + +* `outgoing.routingPolicy` applies to all `outgoing.dependencies`, unless overriden + on specific dependency level, like + below, where only a subset of `routingPolicy` fields is overridden for a service `echo`: + +```yaml +metadata: + proxy_settings: + outgoing: + routingPolicy: + autoServiceTag: false + serviceTagPreference: ["dolom", "est"] + dependencies: + - service: "echo" + routingPolicy: + autoServiceTag: true + fallbackToAnyInstance: true +``` + +Then effective `routingPolicy` for service `echo` is: + +```yaml +routingPolicy: + autoServiceTag: true + serviceTagPreference: ["dolom", "est"] + fallbackToAnyInstance: true +``` + +### `routingPolicy` fields + +`autoServiceTag` - Enables automatic service tag routing.
+Type: boolean
+Default: `false` + +`serviceTagPreference` - Service tag list in priority order. Instances with the + left-most service tag will be selected. If there is no instance with a preferred service-tag, + the next tag from the list is considered.
+Type: list of strings
+Default: empty list + +`fallbackToAnyInstance` - When no instance with service-tag from the `serviceTagPreference` is found, selects any.
+Type: boolean
+Default: `false` + +### Hints +`routingPolicy` tags can be combined with manual, per-request service-tags diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt index 802affbee..1033f0fb9 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt @@ -6,7 +6,10 @@ import com.google.protobuf.Value import com.google.protobuf.util.Durations import io.envoyproxy.controlplane.server.exception.RequestException import io.grpc.Status +import pl.allegro.tech.servicemesh.envoycontrol.groups.ClientWithSelector.Companion.decomposeClient import pl.allegro.tech.servicemesh.envoycontrol.snapshot.AccessLogFiltersProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.CommonHttpProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.RetryPolicyProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties import pl.allegro.tech.servicemesh.envoycontrol.utils.AccessLogFilterParser import pl.allegro.tech.servicemesh.envoycontrol.utils.ComparisonFilterSettings @@ -45,11 +48,13 @@ data class AccessLogFilterSettings(val proto: Value?, val properties: AccessLogF data class ProxySettings( val incoming: Incoming = Incoming(), - val outgoing: Outgoing = Outgoing() + val outgoing: Outgoing = Outgoing(), + val customData: Map = emptyMap() ) { constructor(proto: Value?, properties: SnapshotProperties) : this( incoming = proto?.field("incoming").toIncoming(properties), - outgoing = proto?.field("outgoing").toOutgoing(properties) + outgoing = proto?.field("outgoing").toOutgoing(properties), + customData = proto?.field("customData").toCustomData() ) fun withIncomingPermissionsDisabled(): ProxySettings = copy( @@ -78,33 +83,45 @@ fun Value?.toHeaderFilter(default: String? = null): HeaderFilterSettings? { AccessLogFilterParser.parseHeaderFilter(it) } } + private class RawDependency(val service: String?, val domain: String?, val domainPattern: String?, val value: Value) +private fun defaultRetryPolicy(retryPolicy: RetryPolicyProperties) = if (retryPolicy.enabled) { + RetryPolicy( + retryOn = retryPolicy.retryOn, + numberRetries = retryPolicy.numberOfRetries, + retryHostPredicate = retryPolicy.retryHostPredicate, + hostSelectionRetryMaxAttempts = retryPolicy.hostSelectionRetryMaxAttempts, + rateLimitedRetryBackOff = RateLimitedRetryBackOff( + retryPolicy.rateLimitedRetryBackOff.resetHeaders.map { ResetHeader(it.name, it.format) } + ), + retryBackOff = RetryBackOff( + Durations.fromMillis(retryPolicy.retryBackOff.baseInterval.toMillis()) + ), + ) +} else { + null +} + +private fun defaultTimeoutPolicy(commonHttpProperties: CommonHttpProperties) = Outgoing.TimeoutPolicy( + idleTimeout = Durations.fromMillis(commonHttpProperties.idleTimeout.toMillis()), + connectionIdleTimeout = Durations.fromMillis(commonHttpProperties.connectionIdleTimeout.toMillis()), + requestTimeout = Durations.fromMillis(commonHttpProperties.requestTimeout.toMillis()) +) + +private fun defaultDependencySettings(properties: SnapshotProperties) = DependencySettings( + handleInternalRedirect = properties.egress.handleInternalRedirect, + timeoutPolicy = defaultTimeoutPolicy(properties.egress.commonHttp), + retryPolicy = defaultRetryPolicy(properties.retryPolicy) +) + fun Value?.toOutgoing(properties: SnapshotProperties): Outgoing { val allServiceDependenciesIdentifier = properties.outgoingPermissions.allServicesDependencies.identifier val rawDependencies = this?.field("dependencies")?.list().orEmpty().map(::toRawDependency) val allServicesDependencies = toAllServiceDependencies(rawDependencies, allServiceDependenciesIdentifier) - val defaultSettingsFromProperties = DependencySettings( - handleInternalRedirect = properties.egress.handleInternalRedirect, - timeoutPolicy = Outgoing.TimeoutPolicy( - idleTimeout = Durations.fromMillis(properties.egress.commonHttp.idleTimeout.toMillis()), - connectionIdleTimeout = Durations.fromMillis(properties.egress.commonHttp.connectionIdleTimeout.toMillis()), - requestTimeout = Durations.fromMillis(properties.egress.commonHttp.requestTimeout.toMillis()) - ), - retryPolicy = RetryPolicy( - retryOn = properties.retryPolicy.retryOn, - numberRetries = properties.retryPolicy.numberOfRetries, - retryHostPredicate = properties.retryPolicy.retryHostPredicate, - hostSelectionRetryMaxAttempts = properties.retryPolicy.hostSelectionRetryMaxAttempts, - rateLimitedRetryBackOff = RateLimitedRetryBackOff( - properties.retryPolicy.rateLimitedRetryBackOff.resetHeaders.map { ResetHeader(it.name, it.format) } - ), - retryBackOff = RetryBackOff( - Durations.fromMillis(properties.retryPolicy.retryBackOff.baseInterval.toMillis()) - ), - ) - ) - val allServicesDefaultSettings = allServicesDependencies?.value.toSettings(defaultSettingsFromProperties) + val defaultSettingsFromProperties = defaultDependencySettings(properties) + val defaultSettings = this.toSettings(defaultSettingsFromProperties) + val allServicesDefaultSettings = allServicesDependencies?.value.toSettings(defaultSettings) val services = rawDependencies.filter { it.service != null && it.service != allServiceDependenciesIdentifier } .map { ServiceDependency( @@ -114,10 +131,10 @@ fun Value?.toOutgoing(properties: SnapshotProperties): Outgoing { } val domains = rawDependencies.filter { it.domain != null } .onEach { validateDomainFormat(it, allServiceDependenciesIdentifier) } - .map { DomainDependency(it.domain.orEmpty(), it.value.toSettings(defaultSettingsFromProperties)) } + .map { DomainDependency(it.domain.orEmpty(), it.value.toSettings(defaultSettings)) } val domainPatterns = rawDependencies.filter { it.domainPattern != null } .onEach { validateDomainPatternFormat(it) } - .map { DomainPatternDependency(it.domainPattern.orEmpty(), it.value.toSettings(defaultSettingsFromProperties)) } + .map { DomainPatternDependency(it.domainPattern.orEmpty(), it.value.toSettings(defaultSettings)) } return Outgoing( serviceDependencies = services, domainDependencies = domains, @@ -200,17 +217,14 @@ private fun Value?.toSettings(defaultSettings: DependencySettings): DependencySe val handleInternalRedirect = this?.field("handleInternalRedirect")?.boolValue val timeoutPolicy = this?.field("timeoutPolicy")?.toOutgoingTimeoutPolicy(defaultSettings.timeoutPolicy) val rewriteHostHeader = this?.field("rewriteHostHeader")?.boolValue - val retryPolicy = this?.field("retryPolicy")?.let { retryPolicy -> - mapProtoToRetryPolicy( - retryPolicy, - defaultSettings.retryPolicy - ) - } + val retryPolicy = this?.field("retryPolicy")?.toRetryPolicy(defaultSettings.retryPolicy) + val routingPolicy = this?.field("routingPolicy")?.toRoutingPolicy(defaultSettings.routingPolicy) val shouldAllBeDefault = handleInternalRedirect == null && - rewriteHostHeader == null && - timeoutPolicy == null && - retryPolicy == null + rewriteHostHeader == null && + timeoutPolicy == null && + retryPolicy == null && + routingPolicy == null return if (shouldAllBeDefault) { defaultSettings @@ -219,39 +233,51 @@ private fun Value?.toSettings(defaultSettings: DependencySettings): DependencySe handleInternalRedirect = handleInternalRedirect ?: defaultSettings.handleInternalRedirect, timeoutPolicy = timeoutPolicy ?: defaultSettings.timeoutPolicy, rewriteHostHeader = rewriteHostHeader ?: defaultSettings.rewriteHostHeader, - retryPolicy = retryPolicy ?: defaultSettings.retryPolicy + retryPolicy = retryPolicy ?: defaultSettings.retryPolicy, + routingPolicy = routingPolicy ?: defaultSettings.routingPolicy ) } } -private fun mapProtoToRetryPolicy(value: Value, defaultRetryPolicy: RetryPolicy): RetryPolicy { +private fun Value.toRetryPolicy(defaultRetryPolicy: RetryPolicy?): RetryPolicy { return RetryPolicy( - retryOn = value.field("retryOn")?.listValue?.valuesList?.map { it.stringValue } ?: defaultRetryPolicy.retryOn, - hostSelectionRetryMaxAttempts = value.field("hostSelectionRetryMaxAttempts")?.numberValue?.toLong() - ?: defaultRetryPolicy.hostSelectionRetryMaxAttempts, - numberRetries = value.field("numberRetries")?.numberValue?.toInt() ?: defaultRetryPolicy.numberRetries, - retryHostPredicate = value.field("retryHostPredicate")?.listValue?.valuesList?.map { - RetryHostPredicate(it.field("name")!!.stringValue) - }?.toList() ?: defaultRetryPolicy.retryHostPredicate, - perTryTimeoutMs = value.field("perTryTimeoutMs")?.numberValue?.toLong(), - retryBackOff = value.field("retryBackOff")?.structValue?.let { + retryOn = this.field("retryOn")?.listValue?.valuesList?.map { it.stringValue } ?: defaultRetryPolicy?.retryOn, + hostSelectionRetryMaxAttempts = this.field("hostSelectionRetryMaxAttempts")?.numberValue?.toLong() + ?: defaultRetryPolicy?.hostSelectionRetryMaxAttempts, + numberRetries = this.field("numberRetries")?.numberValue?.toInt() ?: defaultRetryPolicy?.numberRetries, + retryHostPredicate = this.field("retryHostPredicate")?.listValue?.valuesList?.mapNotNull { + RetryHostPredicate.parse(it.field("name")!!.stringValue) + }?.toList() ?: defaultRetryPolicy?.retryHostPredicate, + perTryTimeoutMs = this.field("perTryTimeoutMs")?.numberValue?.toLong(), + retryBackOff = this.field("retryBackOff")?.structValue?.let { RetryBackOff( baseInterval = it.fieldsMap["baseInterval"]?.toDuration(), maxInterval = it.fieldsMap["maxInterval"]?.toDuration() ) - } ?: defaultRetryPolicy.retryBackOff, - rateLimitedRetryBackOff = value.field("rateLimitedRetryBackOff")?.structValue?.let { + } ?: defaultRetryPolicy?.retryBackOff, + rateLimitedRetryBackOff = this.field("rateLimitedRetryBackOff")?.structValue?.let { RateLimitedRetryBackOff( it.fieldsMap["resetHeaders"]?.listValue?.valuesList?.mapNotNull(::mapProtoToResetHeader) ) - } ?: defaultRetryPolicy.rateLimitedRetryBackOff, - retryableStatusCodes = value.field("retryableStatusCodes")?.listValue?.valuesList?.map { + } ?: defaultRetryPolicy?.rateLimitedRetryBackOff, + retryableStatusCodes = this.field("retryableStatusCodes")?.listValue?.valuesList?.map { it.numberValue.toInt() }, - retryableHeaders = value.field("retryableHeaders")?.listValue?.valuesList?.map { + retryableHeaders = this.field("retryableHeaders")?.listValue?.valuesList?.map { it.stringValue }, - methods = mapProtoToMethods(value) + methods = mapProtoToMethods(this) + ) +} + +private fun Value.toRoutingPolicy(defaultRoutingPolicy: RoutingPolicy): RoutingPolicy { + return RoutingPolicy( + autoServiceTag = this.field("autoServiceTag")?.boolValue + ?: defaultRoutingPolicy.autoServiceTag, + serviceTagPreference = this.field("serviceTagPreference")?.list()?.map { it.stringValue } + ?: defaultRoutingPolicy.serviceTagPreference, + fallbackToAnyInstance = this.field("fallbackToAnyInstance")?.boolValue + ?: defaultRoutingPolicy.fallbackToAnyInstance ) } @@ -361,15 +387,6 @@ fun Value.toIncomingRateLimitEndpoint(): IncomingRateLimitEndpoint { fun isMoreThanOnePropertyDefined(vararg properties: String?): Boolean = properties.filterNotNull().count() > 1 -private fun decomposeClient(client: ClientComposite): ClientWithSelector { - val parts = client.split(":", ignoreCase = false, limit = 2) - return if (parts.size == 2) { - ClientWithSelector(parts[0], parts[1]) - } else { - ClientWithSelector(client, null) - } -} - private fun Value?.toIncomingTimeoutPolicy(): Incoming.TimeoutPolicy { val idleTimeout: Duration? = this?.field("idleTimeout")?.toDuration() val responseTimeout: Duration? = this?.field("responseTimeout")?.toDuration() @@ -441,7 +458,7 @@ fun Value.toDuration(): Duration? { return when (this.kindCase) { Value.KindCase.NUMBER_VALUE -> throw NodeMetadataValidationException( "Timeout definition has number format" + - " but should be in string format and ends with 's'" + " but should be in string format and ends with 's'" ) Value.KindCase.STRING_VALUE -> { try { @@ -454,6 +471,28 @@ fun Value.toDuration(): Duration? { } } +fun Value?.toCustomData(): Map { + return when (this?.kindCase) { + Value.KindCase.STRUCT_VALUE -> this.toMap() + else -> emptyMap() + } +} + +private fun Value.toMap(): Map { + return this.structValue.fieldsMap.map { it.key to it.value.toCustomDataValue() }.toMap() +} + +private fun Value?.toCustomDataValue(): Any? { + return when (this?.kindCase) { + Value.KindCase.BOOL_VALUE -> this.boolValue + Value.KindCase.LIST_VALUE -> this.listValue.valuesList.map { it.toCustomDataValue() } + Value.KindCase.STRING_VALUE -> this.stringValue + Value.KindCase.NUMBER_VALUE -> this.numberValue + Value.KindCase.STRUCT_VALUE -> this.toMap() + else -> null + } +} + data class Incoming( val endpoints: List = emptyList(), val rateLimitEndpoints: List = emptyList(), @@ -567,11 +606,12 @@ data class DependencySettings( val handleInternalRedirect: Boolean = false, val timeoutPolicy: Outgoing.TimeoutPolicy = Outgoing.TimeoutPolicy(), val rewriteHostHeader: Boolean = false, - val retryPolicy: RetryPolicy = RetryPolicy() + val retryPolicy: RetryPolicy? = RetryPolicy(), + val routingPolicy: RoutingPolicy = RoutingPolicy() ) data class RetryPolicy( - val retryOn: List = emptyList(), + val retryOn: List? = emptyList(), val hostSelectionRetryMaxAttempts: Long? = null, val numberRetries: Int? = null, val retryHostPredicate: List? = null, @@ -583,6 +623,12 @@ data class RetryPolicy( val methods: Set? = null ) +data class RoutingPolicy( + val autoServiceTag: Boolean = false, + val serviceTagPreference: List = emptyList(), + val fallbackToAnyInstance: Boolean = false +) + data class RetryBackOff( val baseInterval: Duration? = null, val maxInterval: Duration? = null @@ -599,9 +645,17 @@ data class RateLimitedRetryBackOff( data class ResetHeader(val name: String, val format: String) -data class RetryHostPredicate( - val name: String -) +enum class RetryHostPredicate(val predicateName: String) { + OMIT_CANARY_HOST("envoy.retry_host_predicates.omit_canary_hosts"), + OMIT_HOST_METADATA("envoy.retry_host_predicates.omit_host_metadata"), + PREVIOUS_HOSTS("envoy.retry_host_predicates.previous_hosts"); + + companion object { + fun parse(value: String): RetryHostPredicate? { + return values().find { it.predicateName.equals(value, true) || it.name.equals(value, true) } + } + } +} data class Role( val name: String?, @@ -622,13 +676,39 @@ data class HealthCheck( typealias ClientComposite = String -data class ClientWithSelector( +data class ClientWithSelector private constructor( val name: String, - val selector: String? = null + val selector: String? = null, + val negated: Boolean = false ) : Comparable { + + companion object { + const val NEGATION_PREFIX = "!" + fun create(name: String, selector: String? = null): ClientWithSelector { + return ClientWithSelector( + name, selector?.removePrefix(NEGATION_PREFIX), selector?.startsWith( + NEGATION_PREFIX + ) ?: false + ) + } + + fun decomposeClient(client: ClientComposite): ClientWithSelector { + val parts = client.split(":", ignoreCase = false, limit = 2) + return if (parts.size == 2) { + ClientWithSelector.create(parts[0], parts[1]) + } else { + ClientWithSelector.create(client, null) + } + } + } + fun compositeName(): ClientComposite { return if (selector != null) { - "$name:$selector" + if (negated) { + "$name:$NEGATION_PREFIX$selector" + } else { + "$name:$selector" + } } else { name } diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactory.kt index cd4d7ce9d..ba65df397 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactory.kt @@ -215,12 +215,22 @@ class EnvoySnapshotFactory( globalSnapshot: GlobalSnapshot, egressRouteSpecifications: Collection ): List { - val egressRouteClusters = egressRouteSpecifications.map(RouteSpecification::clusterName) + val egressLoadAssignments = egressRouteSpecifications.mapNotNull { routeSpec -> + globalSnapshot.endpoints[routeSpec.clusterName]?.let { endpoints -> + // TODO: create a cache in GlobalSnapshot where a key is a pair (serviceName, serviceTag) and a value + // is ClusterLoadAssignment (simple mutable map should be enough). + // endpointsFactory.filterEndpoints() can use this cache to prevent computing the same + // ClusterLoadAssignments many times - it may reduce MEM, CPU and latency if some serviceTags are + // commonly used + endpointsFactory.filterEndpoints(endpoints, routeSpec.settings.routingPolicy) + } + } + val rateLimitClusters = if (rateLimitEndpoints.isNotEmpty()) listOf(properties.rateLimit.serviceName) else emptyList() - val allClusters = egressRouteClusters + rateLimitClusters + val rateLimitLoadAssignments = rateLimitClusters.mapNotNull { name -> globalSnapshot.endpoints[name] } - return allClusters.mapNotNull { name -> globalSnapshot.endpoints[name] } + return egressLoadAssignments + rateLimitLoadAssignments } private fun newSnapshotForGroup( 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 467a84625..c28464f76 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 @@ -94,6 +94,7 @@ class IncomingPermissionsProperties { var selectorMatching: MutableMap = mutableMapOf() var tlsAuthentication = TlsAuthenticationProperties() var clientsAllowedToAllEndpoints = mutableListOf() + var clientsLists = ClientsListsProperties() var overlappingPathsFix = false // TODO: to be removed when proved it did not mess up anything } @@ -116,6 +117,10 @@ class TlsAuthenticationProperties { var sanUriFormat: String = "spiffe://{service-name}" } +class ClientsListsProperties { + var defaultClientsList: List = emptyList() + var customClientsLists: Map> = mapOf() +} class TlsProtocolProperties { var cipherSuites: List = listOf("ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256") var minimumVersion = TlsParameters.TlsProtocol.TLSv1_2 @@ -232,6 +237,7 @@ class ServiceTagsProperties { var header = "x-service-tag" var routingExcludedTags: MutableList = mutableListOf() var allowedTagsCombinations: MutableList = mutableListOf() + var autoServiceTagEnabled = false } class StringMatcher { @@ -361,11 +367,12 @@ data class RateLimitProperties( ) data class RetryPolicyProperties( + var enabled: Boolean = true, var retryOn: List = emptyList(), var numberOfRetries: Int = 1, var hostSelectionRetryMaxAttempts: Long = 3, var retryHostPredicate: List = - listOf(RetryHostPredicate("envoy.retry_host_predicates.previous_hosts")), + listOf(RetryHostPredicate.PREVIOUS_HOSTS), var retryBackOff: RetryBackOffProperties = RetryBackOffProperties( baseInterval = Duration.ofMillis(25) ), diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClustersFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClustersFactory.kt index d39c54d31..69f7fa875 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClustersFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClustersFactory.kt @@ -27,6 +27,7 @@ import io.envoyproxy.envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterC import io.envoyproxy.envoy.extensions.common.dynamic_forward_proxy.v3.DnsCacheConfig import io.envoyproxy.envoy.extensions.common.tap.v3.AdminConfig import io.envoyproxy.envoy.extensions.common.tap.v3.CommonExtensionConfig +import io.envoyproxy.envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer import io.envoyproxy.envoy.extensions.transport_sockets.tap.v3.Tap import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext @@ -580,7 +581,10 @@ class EnvoyClustersFactory( .setName("plaintext_match") .setTransportSocket( wrapTransportSocket(clusterName) { - TransportSocket.newBuilder().setName("envoy.transport_sockets.raw_buffer").build() + TransportSocket.newBuilder() + .setName("envoy.transport_sockets.raw_buffer") + .setTypedConfig(Any.pack(RawBuffer.newBuilder().build())) + .build() } ) .build() diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactory.kt index 8b8488f6f..5101cf7a1 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactory.kt @@ -12,6 +12,7 @@ import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment import io.envoyproxy.envoy.config.endpoint.v3.Endpoint import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints +import pl.allegro.tech.servicemesh.envoycontrol.groups.RoutingPolicy import pl.allegro.tech.servicemesh.envoycontrol.services.MultiClusterState import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstance import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances @@ -47,6 +48,70 @@ class EnvoyEndpointsFactory( } } + fun filterEndpoints( + clusterLoadAssignment: ClusterLoadAssignment, + routingPolicy: RoutingPolicy + ): ClusterLoadAssignment { + if (!routingPolicy.autoServiceTag || !isAutoServiceTagEnabled()) { + return clusterLoadAssignment + } + + val filteredLoadAssignment = routingPolicy.serviceTagPreference.firstNotNullOfOrNull { serviceTag -> + filterEndpoints(clusterLoadAssignment, serviceTag) + } + + return when { + filteredLoadAssignment != null -> filteredLoadAssignment + routingPolicy.fallbackToAnyInstance -> clusterLoadAssignment + else -> createEmptyLoadAssignment(clusterLoadAssignment) + } + } + + private fun isAutoServiceTagEnabled() = properties.routing.serviceTags.run { enabled && autoServiceTagEnabled } + + private fun filterEndpoints(loadAssignment: ClusterLoadAssignment, tag: String): ClusterLoadAssignment? { + var allEndpointMatched = true + val filteredEndpoints = loadAssignment.endpointsList.mapNotNull { localityLbEndpoint -> + val (matchedEndpoints, unmatchedEndpoints) = localityLbEndpoint.lbEndpointsList.partition { + metadataContainsServiceTag(it.metadata, tag) + } + when { + matchedEndpoints.isNotEmpty() && unmatchedEndpoints.isNotEmpty() -> { // SOME + allEndpointMatched = false + localityLbEndpoint.toBuilder() + .clearLbEndpoints() + .addAllLbEndpoints(matchedEndpoints) + .build() + } + matchedEndpoints.isNotEmpty() -> { // ALL + localityLbEndpoint + } + else -> { // NONE + allEndpointMatched = false + null + } + } + } + return when { + allEndpointMatched -> loadAssignment // ALL + filteredEndpoints.isNotEmpty() -> loadAssignment.toBuilder() // SOME + .clearEndpoints() + .addAllEndpoints(filteredEndpoints) + .build() + else -> null // NONE + } + } + + private fun createEmptyLoadAssignment(loadAssignment: ClusterLoadAssignment): ClusterLoadAssignment { + return loadAssignment.toBuilder().clearEndpoints().build() + } + + private fun metadataContainsServiceTag(metadata: Metadata, serviceTag: String) = metadata + .filterMetadataMap["envoy.lb"]?.fieldsMap + ?.get(properties.routing.serviceTags.metadataKey) + ?.listValue?.valuesList.orEmpty() + .any { it.stringValue == serviceTag } + private fun createEndpointsGroup( serviceInstances: ServiceInstances?, zone: String, diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt index ce1607fdc..aab1293fe 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt @@ -2,6 +2,7 @@ package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.fil import com.google.protobuf.Any import io.envoyproxy.envoy.extensions.filters.http.header_to_metadata.v3.Config +import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter import pl.allegro.tech.servicemesh.envoycontrol.groups.Group import pl.allegro.tech.servicemesh.envoycontrol.snapshot.GlobalSnapshot @@ -140,6 +141,8 @@ class EnvoyDefaultFilters( private fun envoyRouterHttpFilter(): HttpFilter = HttpFilter .newBuilder() .setName("envoy.filters.http.router") + .setTypedConfig(Any.pack(Router.newBuilder() + .build())) .build() private fun headerToMetadataHttpFilter(headerToMetadataConfig: Config.Builder): HttpFilter { diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt index 4f3bd6b52..572d7ad0c 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt @@ -48,7 +48,7 @@ class RBACFilterFactory( private val anyPrincipal = Principal.newBuilder().setAny(true).build() private val denyForAllPrincipal = Principal.newBuilder().setNotId(anyPrincipal).build() private val fullAccessClients = incomingPermissionsProperties.clientsAllowedToAllEndpoints.map { - ClientWithSelector(name = it) + ClientWithSelector.create(name = it) } private val sanUriMatcherFactory = SanUriMatcherFactory(incomingPermissionsProperties.tlsAuthentication) @@ -60,6 +60,13 @@ class RBACFilterFactory( } } + private val defaultClientsList = incomingPermissionsProperties.clientsLists + .defaultClientsList.map { ClientWithSelector.decomposeClient(it) } + private val customClientsLists = incomingPermissionsProperties.clientsLists + .customClientsLists.mapValues { + it.value.map(ClientWithSelector::decomposeClient) + } + companion object { private val logger by logger() private const val ALLOW_UNLISTED_POLICY_NAME = "ALLOW_UNLISTED_POLICY" @@ -82,9 +89,9 @@ class RBACFilterFactory( ): List { val principalCache = mutableMapOf>() return incomingPermissions.endpoints.map { incomingEndpoint -> - val clientsWithSelectors = resolveClientsWithSelectors(incomingEndpoint, roles) - - val principals = clientsWithSelectors + val (clientsWithNegatedSelectors, clientsWithNotNegatedSelectors) = + resolveClientsWithSelectors(incomingEndpoint, roles).partition { it.negated } + val notNegatedPrincipals = clientsWithNotNegatedSelectors .flatMap { client -> getPrincipals( principalCache, @@ -95,7 +102,22 @@ class RBACFilterFactory( ).map { mergeWithOAuthPolicy(client, it, incomingEndpoint.oauth?.policy) } } .toSet() - .ifEmpty { + val negatedPrincipals = clientsWithNegatedSelectors.flatMap { client -> + getPrincipals( + principalCache, + client, + snapshot, + incomingEndpoint.unlistedClientsPolicy, + incomingEndpoint.oauth + ) + } + + val principals = if (negatedPrincipals.isNotEmpty()) { + val mergedNegatedPrincipals = mergePrincipals(negatedPrincipals) + notNegatedPrincipals.map { principal -> mergePrincipals(listOf(principal, mergedNegatedPrincipals)) } + .ifEmpty { negatedPrincipals } + } else { + notNegatedPrincipals.ifEmpty { setOf( oAuthPolicyForEmptyClients( incomingEndpoint.oauth?.policy, @@ -103,6 +125,7 @@ class RBACFilterFactory( ) ) } + } val policy = Policy.newBuilder().addAllPrincipals(principals) val combinedPermissions = rBACFilterPermissions.createCombinedPermissions(incomingEndpoint) @@ -275,8 +298,21 @@ class RBACFilterFactory( val clients = incomingEndpoint.clients.flatMap { clientOrRole -> roles.find { it.name == clientOrRole.name }?.clients ?: setOf(clientOrRole) } + val clientsWithPredefinedList = mutableListOf() + var anyCustomListApplied = false + for (client in clients) { + if (customClientsLists.containsKey(client.compositeName())) { + clientsWithPredefinedList.addAll(customClientsLists.getOrDefault(client.compositeName(), emptyList())) + anyCustomListApplied = true + } else { + clientsWithPredefinedList.add(client) + } + } + if (!anyCustomListApplied) { + clientsWithPredefinedList.addAll(defaultClientsList) + } // sorted order ensures that we do not duplicate rules - return clients.toSortedSet() + return clientsWithPredefinedList.toSortedSet() } private fun mapClientWithSelectorToPrincipals( @@ -326,26 +362,18 @@ class RBACFilterFactory( return principal // don't merge if client has OAuth selector } else { return when (policy) { - OAuth.Policy.ALLOW_MISSING -> { - Principal.newBuilder().setAndIds( - Principal.Set.newBuilder().addAllIds( - listOf( - allowMissingPolicyPrincipal, - principal - ) - ) - ).build() - } - OAuth.Policy.STRICT -> { - Principal.newBuilder().setAndIds( - Principal.Set.newBuilder().addAllIds( - listOf( - strictPolicyPrincipal, - principal - ) - ) - ).build() - } + OAuth.Policy.ALLOW_MISSING -> mergePrincipals( + listOf( + allowMissingPolicyPrincipal, + principal + ) + ) + OAuth.Policy.STRICT -> mergePrincipals( + listOf( + strictPolicyPrincipal, + principal + ) + ) OAuth.Policy.ALLOW_MISSING_OR_FAILED -> { principal } @@ -356,6 +384,15 @@ class RBACFilterFactory( } } + private fun mergePrincipals(principals: Collection) = if (principals.size > 1) { + Principal.newBuilder() + .setAndIds( + Principal.Set.newBuilder().addAllIds(principals) + ).build() + } else { + principals.firstOrNull() ?: Principal.getDefaultInstance() + } + private val strictPolicyPrincipal = Principal.newBuilder().setAndIds( Principal.Set.newBuilder().addAllIds( listOf( @@ -426,7 +463,8 @@ class RBACFilterFactory( ) ) ) - ).build() + ).setInvert(client.negated) + .build() ).build() } @@ -464,13 +502,14 @@ class RBACFilterFactory( ).build() ) } + private fun tlsPrincipals(client: String): Principal { val stringMatcher = sanUriMatcherFactory.createSanUriMatcher(client) return Principal.newBuilder().setAuthenticated( - Principal.Authenticated.newBuilder() - .setPrincipalName(stringMatcher) - ).build() + Principal.Authenticated.newBuilder() + .setPrincipalName(stringMatcher) + ).build() } private fun originalDestinationPrincipals(client: String) = Principal.newBuilder() diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactory.kt index 5e8bb2139..f3e246cf1 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactory.kt @@ -1,5 +1,6 @@ package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes +import com.google.protobuf.Any import com.google.protobuf.BoolValue import com.google.protobuf.UInt32Value import com.google.protobuf.util.Durations @@ -16,6 +17,9 @@ import io.envoyproxy.envoy.config.route.v3.RouteAction import io.envoyproxy.envoy.config.route.v3.RouteConfiguration import io.envoyproxy.envoy.config.route.v3.RouteMatch import io.envoyproxy.envoy.config.route.v3.VirtualHost +import io.envoyproxy.envoy.extensions.retry.host.omit_canary_hosts.v3.OmitCanaryHostsPredicate +import io.envoyproxy.envoy.extensions.retry.host.omit_host_metadata.v3.OmitHostMetadataConfig +import io.envoyproxy.envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher import pl.allegro.tech.servicemesh.envoycontrol.groups.RateLimitedRetryBackOff import pl.allegro.tech.servicemesh.envoycontrol.groups.RetryBackOff @@ -72,6 +76,11 @@ class EnvoyEgressRoutesFactory( .setValue("%UPSTREAM_REMOTE_ADDRESS%").build() ) + private val defaultRouteMatch = RouteMatch + .newBuilder() + .setPrefix("/") + .build() + /** * @see TestResources.createRoute */ @@ -84,12 +93,7 @@ class EnvoyEgressRoutesFactory( val virtualHosts = routes .filter { it.routeDomains.isNotEmpty() } .map { routeSpecification -> - addMultipleRoutes( - VirtualHost.newBuilder() - .setName(routeSpecification.clusterName) - .addAllDomains(routeSpecification.routeDomains), - routeSpecification - ).build() + buildEgressRoute(routeSpecification) } var routeConfiguration = RouteConfiguration.newBuilder() @@ -120,37 +124,55 @@ class EnvoyEgressRoutesFactory( return routeConfiguration.build() } - private fun addMultipleRoutes( - addAllDomains: VirtualHost.Builder, - routeSpecification: RouteSpecification - ): VirtualHost.Builder { - routeSpecification.settings.retryPolicy.let { - buildRouteForRetryPolicy(addAllDomains, routeSpecification) + private fun buildEgressRoute(routeSpecification: RouteSpecification): VirtualHost { + val virtualHost = VirtualHost.newBuilder() + .setName(routeSpecification.clusterName) + .addAllDomains(routeSpecification.routeDomains) + val retryPolicy = routeSpecification.settings.retryPolicy + if (retryPolicy != null) { + buildEgressRouteWithRetryPolicy(virtualHost, retryPolicy, routeSpecification) + } else { + virtualHost.addRoutes( + Route.newBuilder() + .setMatch(defaultRouteMatch) + .setRoute(createRouteAction(routeSpecification, shouldAddRetryPolicy = false)) + .build() + ) } - buildDefaultRoute(addAllDomains, routeSpecification) - return addAllDomains + return virtualHost.build() } - private fun buildRouteForRetryPolicy( - addAllDomains: VirtualHost.Builder, + private fun buildEgressRouteWithRetryPolicy( + virtualHost: VirtualHost.Builder, + retryPolicy: EnvoyControlRetryPolicy, routeSpecification: RouteSpecification - ): VirtualHost.Builder? { - val regexAsAString = routeSpecification.settings.retryPolicy.methods?.joinToString(separator = "|") - val routeMatchBuilder = RouteMatch - .newBuilder() - .setPrefix("/") - .also { routeMatcher -> - regexAsAString?.let { - routeMatcher.addHeaders(buildMethodHeaderMatcher(it)) - } - } - - return addAllDomains.addRoutes( - Route.newBuilder() - .setMatch(routeMatchBuilder.build()) - .setRoute(createRouteAction(routeSpecification, shouldAddRetryPolicy = true)) + ): VirtualHost.Builder { + return if (retryPolicy.methods != null) { + val regexAsAString = retryPolicy.methods.joinToString(separator = "|") + val retryRouteMatch = RouteMatch + .newBuilder() + .setPrefix("/") + .addHeaders(buildMethodHeaderMatcher(regexAsAString)) .build() - ) + virtualHost.addRoutes( + Route.newBuilder() + .setMatch(retryRouteMatch) + .setRoute(createRouteAction(routeSpecification, shouldAddRetryPolicy = true)) + .build() + ).addRoutes( + Route.newBuilder() + .setMatch(defaultRouteMatch) + .setRoute(createRouteAction(routeSpecification, shouldAddRetryPolicy = false)) + .build() + ) + } else { + virtualHost.addRoutes( + Route.newBuilder() + .setMatch(defaultRouteMatch) + .setRoute(createRouteAction(routeSpecification, shouldAddRetryPolicy = true)) + .build() + ) + } } private fun buildMethodHeaderMatcher(regexAsAString: String) = HeaderMatcher.newBuilder() @@ -162,23 +184,6 @@ class EnvoyEgressRoutesFactory( .build() ) - private fun buildDefaultRoute( - addAllDomains: VirtualHost.Builder, - routeSpecification: RouteSpecification - ) { - addAllDomains.addRoutes( - Route.newBuilder() - .setMatch( - RouteMatch.newBuilder() - .setPrefix("/") - .build() - ) - .setRoute( - createRouteAction(routeSpecification) - ).build() - ) - } - /** * @see TestResources.createRoute */ @@ -194,11 +199,7 @@ class EnvoyEgressRoutesFactory( .addAllDomains(routeSpecification.routeDomains) .addRoutes( Route.newBuilder() - .setMatch( - RouteMatch.newBuilder() - .setPrefix("/") - .build() - ) + .setMatch(defaultRouteMatch) .setRoute( createRouteAction(routeSpecification) ).build() @@ -252,7 +253,7 @@ class RequestPolicyMapper private constructor() { return retryPolicy?.let { policy -> val retryPolicyBuilder = RetryPolicy.newBuilder() - policy.retryOn.let { retryPolicyBuilder.setRetryOn(it.joinToString { joined -> joined }) } + policy.retryOn?.let { retryPolicyBuilder.setRetryOn(it.joinToString { joined -> joined }) } policy.hostSelectionRetryMaxAttempts?.let { retryPolicyBuilder.setHostSelectionRetryMaxAttempts(it) } @@ -329,12 +330,25 @@ class RequestPolicyMapper private constructor() { retryPolicyBuilder: RetryPolicy.Builder ) { hostPredicates.map { - RetryPolicy.RetryHostPredicate.newBuilder().setName(it.name).build() + RetryPolicy.RetryHostPredicate.newBuilder() + .setName(it.predicateName) + .setTypedConfig(it.toRetryHostPredicate()) + .build() }.also { retryPolicyBuilder.addAllRetryHostPredicate(it) } } + private fun RetryHostPredicate.toRetryHostPredicate(): Any { + val any = when (this) { + RetryHostPredicate.PREVIOUS_HOSTS -> PreviousHostsPredicate.newBuilder().build() + RetryHostPredicate.OMIT_CANARY_HOST -> OmitCanaryHostsPredicate.newBuilder().build() + RetryHostPredicate.OMIT_HOST_METADATA -> OmitHostMetadataConfig.newBuilder() + .build() + } + return Any.pack(any) + } + private fun parseResetHeaderFormat(format: String): ResetHeaderFormat { return when (format) { "SECONDS" -> ResetHeaderFormat.SECONDS 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 5dc310ada..f293b9ad6 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 @@ -38,7 +38,7 @@ class EnvoyIngressRoutesFactory( ) { private val allClients = setOf( - ClientWithSelector(properties.incomingPermissions.tlsAuthentication.wildcardClientIdentifier) + ClientWithSelector.create(properties.incomingPermissions.tlsAuthentication.wildcardClientIdentifier) ) private val filterMetadata = envoyHttpFilters.ingressMetadata diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt index 526bd9cae..e5ed243ab 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt @@ -1,5 +1,8 @@ package pl.allegro.tech.servicemesh.envoycontrol.groups +import com.google.protobuf.ListValue +import com.google.protobuf.NullValue +import com.google.protobuf.Struct import com.google.protobuf.Value import com.google.protobuf.util.Durations import io.envoyproxy.envoy.config.accesslog.v3.ComparisonFilter @@ -52,6 +55,40 @@ class NodeMetadataTest { arguments(null, "/prefix", "/regex"), arguments("/path", "/prefix", "/regex") ) + + @JvmStatic + fun parsingCustomData() = listOf( + arguments("bool", Value.newBuilder().setBoolValue(true).build(), true), + arguments("string", Value.newBuilder().setStringValue("string").build(), "string"), + arguments("number", Value.newBuilder().setNumberValue(1.0).build(), 1.0), + arguments("not_set", Value.newBuilder().build(), null), + arguments("null", Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), null), + arguments("list", Value.newBuilder().setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setBoolValue(true).build()).build()) + .build(), listOf(true) + ), + arguments("struct", Value.newBuilder().setStructValue( + Struct.newBuilder() + .putFields("string", Value.newBuilder().setBoolValue(true).build()) + .build() + ).build(), mapOf("string" to true) + ) + ) + + @JvmStatic + fun parsingNotStructInCustomData() = listOf( + arguments(Value.newBuilder().setBoolValue(true).build(), true), + arguments(Value.newBuilder().setStringValue("string").build(), "string"), + arguments(Value.newBuilder().setNumberValue(1.0).build(), 1.0), + arguments(Value.newBuilder().build(), null), + arguments(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), null), + arguments(Value.newBuilder().setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setBoolValue(true).build()).build()) + .build(), listOf(true) + ) + ) } private fun snapshotProperties( @@ -360,7 +397,7 @@ class NodeMetadataTest { baseInterval = "7s", maxInterval = "8s" ), - retryHostPredicate = listOf(RetryHostPredicateInput(name = "givenHost")), + retryHostPredicate = listOf(RetryHostPredicateInput(name = "previous_hosts")), methods = setOf("GET", "POST", "PUT") ) val expectedRetryPolicy = RetryPolicy( @@ -374,7 +411,7 @@ class NodeMetadataTest { baseInterval = Durations.fromSeconds(7), maxInterval = Durations.fromSeconds(8) ), - retryHostPredicate = listOf(RetryHostPredicate(name = "givenHost")), + retryHostPredicate = listOf(RetryHostPredicate.PREVIOUS_HOSTS), methods = setOf("GET", "POST", "PUT"), rateLimitedRetryBackOff = RateLimitedRetryBackOff( listOf(ResetHeader("Retry-After", "SECONDS")) @@ -416,7 +453,7 @@ class NodeMetadataTest { baseInterval = Durations.fromMillis(25), maxInterval = Durations.fromMillis(250) ), - retryHostPredicate = listOf(RetryHostPredicate(name = "envoy.retry_host_predicates.previous_hosts")), + retryHostPredicate = listOf(RetryHostPredicate.PREVIOUS_HOSTS), methods = setOf("GET", "POST", "PUT"), rateLimitedRetryBackOff = RateLimitedRetryBackOff( listOf(ResetHeader("Retry-After", "SECONDS")) @@ -1151,6 +1188,115 @@ class NodeMetadataTest { assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } + @Test + fun `should use default routing policy`() { + // given + val proto = outgoingDependenciesProto { + withService("lorem") + } + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) + + // expects + val loremDependency = outgoing.getServiceDependencies().single() + assertThat(loremDependency.service).isEqualTo("lorem") + assertThat(loremDependency.settings.routingPolicy.autoServiceTag).isFalse + } + + @Test + fun `should use global and overriden routing policy`() { + // given + val proto = outgoingDependenciesProto { + routingPolicy = RoutingPolicyInput( + autoServiceTag = true, + serviceTagPreference = listOf("preferredGlobalTag", "fallbackGlobalTag"), + fallbackToAnyInstance = true + ) + withService("lorem") + withService("ipsum", routingPolicy = RoutingPolicyInput(autoServiceTag = false)) + withService("dolom", routingPolicy = RoutingPolicyInput(fallbackToAnyInstance = false)) + withService("est", routingPolicy = RoutingPolicyInput(serviceTagPreference = listOf("estTag")) + ) + } + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) + + // expects + val dependencies = outgoing.getServiceDependencies() + assertThat(dependencies).hasSize(4) + val loremDependency = dependencies[0] + assertThat(loremDependency.service).isEqualTo("lorem") + assertThat(loremDependency.settings.routingPolicy).satisfies { policy -> + assertThat(policy.autoServiceTag).isTrue + assertThat(policy.serviceTagPreference).isEqualTo(listOf("preferredGlobalTag", "fallbackGlobalTag")) + assertThat(policy.fallbackToAnyInstance).isTrue + } + val ipsumDependency = dependencies[1] + assertThat(ipsumDependency.service).isEqualTo("ipsum") + assertThat(ipsumDependency.settings.routingPolicy).satisfies { policy -> + assertThat(policy.autoServiceTag).isFalse + } + val dolomDependency = dependencies[2] + assertThat(dolomDependency.service).isEqualTo("dolom") + assertThat(dolomDependency.settings.routingPolicy).satisfies { policy -> + assertThat(policy.autoServiceTag).isTrue + assertThat(policy.serviceTagPreference).isEqualTo(listOf("preferredGlobalTag", "fallbackGlobalTag")) + assertThat(policy.fallbackToAnyInstance).isFalse + } + val estDependency = dependencies[3] + assertThat(estDependency.service).isEqualTo("est") + assertThat(estDependency.settings.routingPolicy).satisfies { policy -> + assertThat(policy.autoServiceTag).isTrue + assertThat(policy.serviceTagPreference).isEqualTo(listOf("estTag")) + assertThat(policy.fallbackToAnyInstance).isTrue + } + } + + @ParameterizedTest + @MethodSource("parsingNotStructInCustomData") + fun `should return empty custom data if is not a struct`(value: Value) { + // when + val customData = value.toCustomData() + + // then + assertThat(customData).isEmpty() + } + + @ParameterizedTest + @MethodSource("parsingCustomData") + fun `should parse custom data if it is a struct with value`(name: String, field: Value, expected: Any?) { + // given + val value = Value.newBuilder() + .setStructValue(Struct.newBuilder() + .putFields(name, field) + .build()) + .build() + + // when + val customData = value.toCustomData() + + // then + assertThat(customData).isEqualTo(mapOf(name to expected)) + } + + @Test + fun `should parse custom data if is a struct`() { + // given + val value = Value.newBuilder().setStructValue( + Struct.newBuilder() + .putFields("abc", Value.newBuilder().setBoolValue(true).build()) + .build() + ).build() + + // when + val customData = value.toCustomData() + + // then + assertThat(customData).isEqualTo(mapOf("abc" to true)) + } + fun ObjectAssert.hasTimeouts( idleTimeout: String, connectionIdleTimeout: String, diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/RoutesAssertions.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/RoutesAssertions.kt index 909a359c0..e667fe5c4 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/RoutesAssertions.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/RoutesAssertions.kt @@ -22,6 +22,11 @@ fun RouteConfiguration.hasSingleVirtualHostThat(condition: VirtualHost.() -> Uni return this } +fun RouteConfiguration.hasVirtualHostThat(name: String, condition: VirtualHost.() -> Unit): RouteConfiguration { + condition(this.virtualHostsList.find { it.name == name }!!) + return this +} + fun RouteConfiguration.hasRequestHeaderToAdd(key: String, value: String): RouteConfiguration { assertThat(this.requestHeadersToAddList).anySatisfy { assertThat(it.header.key).isEqualTo(key) @@ -136,15 +141,22 @@ fun Route.matchingOnPath(path: String): Route { return this } -fun Route.matchingOnHeader(name: String, value: String): Route { +fun Route.matchingOnHeader(name: String, value: String, isRegex: Boolean = false): Route { + val matcher = RegexMatcher.newBuilder().setRegex(value) + .setGoogleRe2(RegexMatcher.GoogleRE2.getDefaultInstance()) + .build() assertThat(this.match.headersList).anyMatch { - it.name == name && it.exactMatch == value + if (isRegex) { + it.name == name && it.safeRegexMatch == matcher + } else { + it.name == name && it.exactMatch == value + } } return this } -fun Route.matchingOnMethod(method: String): Route { - return this.matchingOnHeader(":method", method) +fun Route.matchingOnMethod(method: String, isRegex: Boolean = false): Route { + return this.matchingOnHeader(":method", method, isRegex) } fun Route.matchingOnAnyMethod(): Route { @@ -169,6 +181,8 @@ fun Route.toCluster(cluster: String): Route { return this } +// TODO: all below assertions that use ".satisfies { condition(it) }" doesn't work, because condition should throw +// assertion exception, not return true/false fun Route.directResponse(condition: (DirectResponseAction) -> Boolean) { assertThat(this.directResponse).satisfies { condition(it) } } @@ -207,6 +221,10 @@ fun Route.hasNoRetryPolicy() { assertThat(this.route.retryPolicy).isEqualTo(RetryPolicy.newBuilder().build()) } +fun Route.hasRetryPolicy() { + assertThat(this.route.hasRetryPolicy()).isTrue() +} + fun Route.ingressRoute() { this.matchingOnPrefix("/") .publicAccess() diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/TestNodeFactory.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/TestNodeFactory.kt index b56a72d1d..16bab8962 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/TestNodeFactory.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/TestNodeFactory.kt @@ -60,7 +60,7 @@ val addedProxySettings = ProxySettings( endpoints = listOf( IncomingEndpoint( path = "/endpoint", - clients = setOf(ClientWithSelector("client1")) + clients = setOf(ClientWithSelector.create("client1")) ) ), permissionsEnabled = true @@ -79,7 +79,7 @@ fun ProxySettings.with( ), retryPolicy = RetryPolicy( hostSelectionRetryMaxAttempts = 3, - retryHostPredicate = listOf(RetryHostPredicate("envoy.retry_host_predicates.previous_hosts")), + retryHostPredicate = listOf(RetryHostPredicate.PREVIOUS_HOSTS), numberRetries = 1, retryBackOff = RetryBackOff(Durations.fromMillis(25), Durations.fromMillis(250)), rateLimitedRetryBackOff = RateLimitedRetryBackOff(listOf(ResetHeader("Retry-After", "SECONDS"))) @@ -185,6 +185,12 @@ data class RetryHostPredicateInput( val name: String? ) +data class RoutingPolicyInput( + val autoServiceTag: Boolean? = null, + val serviceTagPreference: List? = null, + val fallbackToAnyInstance: Boolean? = null +) + class OutgoingDependenciesProtoScope { class Dependency( val service: String? = null, @@ -194,16 +200,18 @@ class OutgoingDependenciesProtoScope { val connectionIdleTimeout: String? = null, val requestTimeout: String? = null, val handleInternalRedirect: Boolean? = null, - val retryPolicy: RetryPolicyInput? = null + val retryPolicy: RetryPolicyInput? = null, + val routingPolicy: RoutingPolicyInput? = null ) val dependencies = mutableListOf() + var routingPolicy: RoutingPolicyInput? = null fun withServices( serviceDependencies: List = emptyList(), idleTimeout: String? = null, responseTimeout: String? = null - ) = serviceDependencies.forEach { withService(it, idleTimeout, responseTimeout) } + ) = serviceDependencies.forEach { withService(it, idleTimeout, responseTimeout) } // TODO: responseTimeout as connectionIdleTimeout, is this correct? fun withService( serviceName: String, @@ -211,7 +219,8 @@ class OutgoingDependenciesProtoScope { connectionIdleTimeout: String? = null, requestTimeout: String? = null, handleInternalRedirect: Boolean? = null, - retryPolicy: RetryPolicyInput? = null + retryPolicy: RetryPolicyInput? = null, + routingPolicy: RoutingPolicyInput? = null ) = dependencies.add( Dependency( service = serviceName, @@ -219,7 +228,8 @@ class OutgoingDependenciesProtoScope { connectionIdleTimeout = connectionIdleTimeout, requestTimeout = requestTimeout, handleInternalRedirect = handleInternalRedirect, - retryPolicy = retryPolicy + retryPolicy = retryPolicy, + routingPolicy = routingPolicy ) ) @@ -275,11 +285,15 @@ fun outgoingDependenciesProto( connectionIdleTimeout = it.connectionIdleTimeout, requestTimeout = it.requestTimeout, handleInternalRedirect = it.handleInternalRedirect, - retryPolicy = it.retryPolicy + retryPolicy = it.retryPolicy, + routingPolicy = it.routingPolicy ) ) } }) + scope.routingPolicy?.let { + putFields("routingPolicy", routingPolicyProto(it)) + } } } @@ -291,7 +305,8 @@ fun outgoingDependencyProto( idleTimeout: String? = null, connectionIdleTimeout: String? = null, requestTimeout: String? = null, - retryPolicy: RetryPolicyInput? = null + retryPolicy: RetryPolicyInput? = null, + routingPolicy: RoutingPolicyInput? = null ) = struct { service?.also { putFields("service", string(service)) } domain?.also { putFields("domain", string(domain)) } @@ -301,6 +316,7 @@ fun outgoingDependencyProto( if (idleTimeout != null || requestTimeout != null || connectionIdleTimeout != null) { putFields("timeoutPolicy", outgoingTimeoutPolicy(idleTimeout, connectionIdleTimeout, requestTimeout)) } + routingPolicy?.let { putFields("routingPolicy", routingPolicyProto(it)) } } private fun retryPolicyProto(retryPolicy: RetryPolicyInput) = struct { @@ -342,6 +358,14 @@ private fun retryHostPredicateListProto(retryHostPredicateList: List + putFields("serviceTagPreference", list { serviceTagList.forEach { addValues(string(it)) } }) + } + routingPolicy.fallbackToAnyInstance?.let { putFields("fallbackToAnyInstance", boolean(it)) } +} + fun outgoingTimeoutPolicy( idleTimeout: String? = null, connectionIdleTimeout: String? = null, diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotUpdaterTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotUpdaterTest.kt index 6298e9290..17413f656 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotUpdaterTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotUpdaterTest.kt @@ -204,7 +204,7 @@ class SnapshotUpdaterTest { baseInterval = Durations.fromMillis(123), maxInterval = Durations.fromMillis(234) ), - retryHostPredicate = listOf(RetryHostPredicate(name = "givenHost")), + retryHostPredicate = listOf(RetryHostPredicate.PREVIOUS_HOSTS), methods = setOf("POST") ) val allServicesGroup = AllServicesGroup( @@ -1326,7 +1326,7 @@ fun serviceDependencies(vararg serviceNames: String): Set = timeoutPolicy = outgoingTimeoutPolicy(), retryPolicy = pl.allegro.tech.servicemesh.envoycontrol.groups.RetryPolicy( hostSelectionRetryMaxAttempts = 3, - retryHostPredicate = listOf(RetryHostPredicate("envoy.retry_host_predicates.previous_hosts")), + retryHostPredicate = listOf(RetryHostPredicate.PREVIOUS_HOSTS), numberRetries = 1, retryBackOff = RetryBackOff(Durations.fromMillis(25), Durations.fromMillis(250)), rateLimitedRetryBackOff = EnvoyControlRateLimitedRetryBackOff( diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotsVersionsTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotsVersionsTest.kt index 1b5499b10..dc7d58071 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotsVersionsTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotsVersionsTest.kt @@ -144,14 +144,14 @@ internal class SnapshotsVersionsTest { path = endpointPath, pathMatchingType = PathMatchingType.PATH, methods = setOf("GET", "PUT"), - clients = setOf(ClientWithSelector("client1"), ClientWithSelector("role1")) + clients = setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("role1")) ) ), permissionsEnabled = true, roles = listOf( Role( name = "role1", - clients = setOf(ClientWithSelector("client2"), ClientWithSelector("client3")) + clients = setOf(ClientWithSelector.create("client2"), ClientWithSelector.create("client3")) ) ) ), diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactoryTest.kt new file mode 100644 index 000000000..3663fe1ed --- /dev/null +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/endpoints/EnvoyEndpointsFactoryTest.kt @@ -0,0 +1,232 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.endpoints + +import com.google.protobuf.util.JsonFormat +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import pl.allegro.tech.servicemesh.envoycontrol.groups.RoutingPolicy +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties + +internal class EnvoyEndpointsFactoryTest { + + private val endpointsFactory = EnvoyEndpointsFactory(SnapshotProperties().apply { + routing.serviceTags.enabled = true + routing.serviceTags.autoServiceTagEnabled = true + }) + + // language=json + private val globalLoadAssignmentJson = """{ + "cluster_name": "lorem-service", + "endpoints": [ + { + "locality": { "zone": "west" }, + "lb_endpoints": [ + { + "endpoint": { "address": { "socket_address": { "address": "1.2.3.4", "port_value": 111 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "canary": "1", "tag": [ "x64", "lorem" ] }, + "envoy.transport_socket_match": {} + }}, + "load_balancing_weight": 0 + }, + { + "endpoint": { "address": { "socket_address": { "address": "2.3.4.5", "port_value": 222 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "lb_regular": true, "tag": ["global"] }, + "envoy.transport_socket_match": {} + }}, + "load_balancing_weight": 50 + }, + { + "endpoint": { "address": { "socket_address": { "address": "3.4.5.6", "port_value": 333 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "lb_regular": true, "tag": ["lorem"] }, + "envoy.transport_socket_match": { "acceptMTLS": true } + }}, + "load_balancing_weight": 40 + } + ] + }, + { + "locality": { "zone": "east" }, + "lb_endpoints": [ + { + "endpoint": { "address": { "socket_address": { "address": "4.5.6.7", "port_value": 444 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "lb_regular": true, "tag": ["lorem", "ipsum"] }, + "envoy.transport_socket_match": { "acceptMTLS": true } + }}, + "load_balancing_weight": 60 + } + ], + "priority": 1 + }, + { + "locality": { "zone": "south" }, + "priority": 1 + } + ] + }""" + private val globalLoadAssignment = globalLoadAssignmentJson.toClusterLoadAssignment() + + @Test + fun `should not filter endpoints if auto service tags are disabled`() { + // given + val policy = RoutingPolicy(autoServiceTag = false) + + // when + val filtered = endpointsFactory.filterEndpoints(globalLoadAssignment, policy) + + // then + assertThat(filtered) + .isEqualTo(globalLoadAssignmentJson.toClusterLoadAssignment()) + .describedAs("unnecessary copy!").isSameAs(globalLoadAssignment) + } + + @Test + fun `should filter lorem endpoints from two localities and reuse objects in memory`() { + // given + val policy = RoutingPolicy(autoServiceTag = true, serviceTagPreference = listOf("lorem")) + // language=json + val expectedLoadAssignmentJson = """{ + "cluster_name": "lorem-service", + "endpoints": [ + { + "locality": { "zone": "west" }, + "lb_endpoints": [ + { + "endpoint": { "address": { "socket_address": { "address": "1.2.3.4", "port_value": 111 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "canary": "1", "tag": [ "x64", "lorem" ] }, + "envoy.transport_socket_match": {} + }}, + "load_balancing_weight": 0 + }, + { + "endpoint": { "address": { "socket_address": { "address": "3.4.5.6", "port_value": 333 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "lb_regular": true, "tag": ["lorem"] }, + "envoy.transport_socket_match": { "acceptMTLS": true } + }}, + "load_balancing_weight": 40 + } + ] + }, + { + "locality": { "zone": "east" }, + "lb_endpoints": [ + { + "endpoint": { "address": { "socket_address": { "address": "4.5.6.7", "port_value": 444 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "lb_regular": true, "tag": ["lorem", "ipsum"] }, + "envoy.transport_socket_match": { "acceptMTLS": true } + }}, + "load_balancing_weight": 60 + } + ], + "priority": 1 + } + ] + }""" + + // when + val filtered = endpointsFactory.filterEndpoints(globalLoadAssignment, policy) + + // then + assertThat(filtered) + .isNotEqualTo(globalLoadAssignmentJson.toClusterLoadAssignment()) + .isEqualTo(expectedLoadAssignmentJson.toClusterLoadAssignment()) + + val westEndpoints = filtered.getEndpoints(0).lbEndpointsList + val globalWestEndpoints = globalLoadAssignment.getEndpoints(0).lbEndpointsList + assertThat(westEndpoints[0]).describedAs("unnecessary copy!").isSameAs(globalWestEndpoints[0]) + assertThat(westEndpoints[1]).describedAs("unnecessary copy!").isSameAs(globalWestEndpoints[2]) + + val eastEndpoints = filtered.getEndpoints(1) + val globalEastEndpoints = globalLoadAssignment.getEndpoints(1) + assertThat(eastEndpoints).describedAs("unnecessary copy!").isSameAs(globalEastEndpoints) + + val southEndpoints = filtered.getEndpoints(1) + val globalSouthEndpoints = globalLoadAssignment.getEndpoints(1) + assertThat(southEndpoints).describedAs("unnecessary copy!").isSameAs(globalSouthEndpoints) + } + + @Test + fun `should filter ipsum endpoints as fallback and reuse objects in memory`() { + // given + val policy = RoutingPolicy(autoServiceTag = true, serviceTagPreference = listOf("est", "ipsum")) + // language=json + val expectedLoadAssignmentJson = """{ + "cluster_name": "lorem-service", + "endpoints": [ + { + "locality": { "zone": "east" }, + "lb_endpoints": [ + { + "endpoint": { "address": { "socket_address": { "address": "4.5.6.7", "port_value": 444 } } }, + "metadata": { "filter_metadata": { + "envoy.lb": { "lb_regular": true, "tag": ["lorem", "ipsum"] }, + "envoy.transport_socket_match": { "acceptMTLS": true } + }}, + "load_balancing_weight": 60 + } + ], + "priority": 1 + } + ] + }""" + + // when + val filtered = endpointsFactory.filterEndpoints(globalLoadAssignment, policy) + + // then + assertThat(filtered) + .isNotEqualTo(globalLoadAssignmentJson.toClusterLoadAssignment()) + .isEqualTo(expectedLoadAssignmentJson.toClusterLoadAssignment()) + + val eastEndpoints = filtered.getEndpoints(0) + val globalEastEndpoints = globalLoadAssignment.getEndpoints(1) + assertThat(eastEndpoints).describedAs("unnecessary copy!").isSameAs(globalEastEndpoints) + + val southEndpoints = filtered.getEndpoints(0) + val globalSouthEndpoints = globalLoadAssignment.getEndpoints(1) + assertThat(southEndpoints).describedAs("unnecessary copy!").isSameAs(globalSouthEndpoints) + } + + @Test + fun `should return all endpoints if preferred tag not found and fallback to any instance is true`() { + // given + val policy = + RoutingPolicy(autoServiceTag = true, serviceTagPreference = listOf("est"), fallbackToAnyInstance = true) + + // when + val filtered = endpointsFactory.filterEndpoints(globalLoadAssignment, policy) + + // then + assertThat(filtered) + .isEqualTo(globalLoadAssignmentJson.toClusterLoadAssignment()) + .describedAs("unnecessary copy!").isSameAs(globalLoadAssignment) + } + + @Test + fun `should return empty result if no matching instance is found`() { + // given + val policy = RoutingPolicy(autoServiceTag = true, serviceTagPreference = listOf("est")) + // language=json + val expectedLoadAssignmentJson = """{ + "cluster_name": "lorem-service" + }""" + + // when + val filtered = endpointsFactory.filterEndpoints(globalLoadAssignment, policy) + + // then + assertThat(filtered) + .isNotEqualTo(globalLoadAssignmentJson.toClusterLoadAssignment()) + .isEqualTo(expectedLoadAssignmentJson.toClusterLoadAssignment()) + } + + private fun String.toClusterLoadAssignment(): ClusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .also { builder -> JsonFormat.parser().merge(this, builder) } + .build() +} diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt index be6f7db64..fddd3f9e4 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt @@ -182,7 +182,7 @@ internal class JwtFilterFactoryTest { pathToProvider.map { (path, _) -> IncomingEndpoint( path, - clients = setOf(ClientWithSelector("oauth", "client")), + clients = setOf(ClientWithSelector.create("oauth", "client")), oauth = null ) } diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt index a9e189d06..aebc9238c 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt @@ -57,12 +57,12 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { val expectedRbacBuilder = getRBACFilterWithShadowRules( expectedPoliciesForOAuth( oAuthPolicyPrincipal, - "ClientWithSelector(name=$client, selector=null)", + "ClientWithSelector(name=$client, selector=null, negated=false)", "OAuth(provider=oauth-provider, verification=OFFLINE, policy=$policy)" ), expectedPoliciesForOAuth( oAuthPolicyPrincipal, - "ClientWithSelector(name=$client, selector=null)", + "ClientWithSelector(name=$client, selector=null, negated=false)", "OAuth(provider=oauth-provider, verification=OFFLINE, policy=$policy)" ) ) @@ -74,7 +74,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { "/oauth-protected", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector(client)), + setOf(ClientWithSelector.create(client)), oauth = OAuth("oauth-provider", policy = policy) ) ) @@ -92,16 +92,17 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { // given val selector = "team1" val client = "oauth-prefix" - val oAuthPrincipal = oAuthClientPrincipal(getTokenFieldForClientWithSelector("oauth-provider", client), selector) + val oAuthPrincipal = + oAuthClientPrincipal(getTokenFieldForClientWithSelector("oauth-provider", client), selector) val expectedRbacBuilder = getRBACFilterWithShadowRules( expectedPoliciesForOAuth( oAuthPrincipal, - "ClientWithSelector(name=$client, selector=$selector)", + "ClientWithSelector(name=$client, selector=$selector, negated=false)", "OAuth(provider=oauth-provider, verification=OFFLINE, policy=ALLOW_MISSING_OR_FAILED)" ), expectedPoliciesForOAuth( oAuthPrincipal, - "ClientWithSelector(name=$client, selector=$selector)", + "ClientWithSelector(name=$client, selector=$selector, negated=false)", "OAuth(provider=oauth-provider, verification=OFFLINE, policy=ALLOW_MISSING_OR_FAILED)" ) ) @@ -112,7 +113,52 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { "/oauth-protected", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector(client, selector)), + setOf(ClientWithSelector.create(client, selector)), + oauth = OAuth("oauth-provider", policy = OAuth.Policy.ALLOW_MISSING_OR_FAILED) + ) + ) + ) + + // when + val generated = rbacFilterFactoryWithOAuth.createHttpFilter(createGroup(incomingPermission), snapshot) + + // then + Assertions.assertThat(generated).isEqualTo(expectedRbacBuilder) + } + + @Test + fun `should generate RBAC rules for Clients with OAuth negated selectors`() { + // given + val selector = "team1" + val negatedSelector = "team2" + val client = "oauth-prefix" + val oAuthPrincipal = + oAuthClientPrincipal(getTokenFieldForClientWithSelector("oauth-provider", client), selector) + val negatedOAuthPrincipal = + oAuthClientPrincipal(getTokenFieldForClientWithSelector("oauth-provider", client), negatedSelector, true) + val expectedRbacBuilder = getRBACFilterWithShadowRules( + expectedPoliciesForOAuth( + conjunctionOfPrincipals(oAuthPrincipal, negatedOAuthPrincipal), + "ClientWithSelector(name=$client, selector=$selector, negated=false), ClientWithSelector(name=$client, selector=$negatedSelector, negated=true)", + "OAuth(provider=oauth-provider, verification=OFFLINE, policy=ALLOW_MISSING_OR_FAILED)" + ), + expectedPoliciesForOAuth( + conjunctionOfPrincipals(oAuthPrincipal, negatedOAuthPrincipal), + "ClientWithSelector(name=$client, selector=$selector, negated=false), ClientWithSelector(name=$client, selector=$negatedSelector, negated=true)", + "OAuth(provider=oauth-provider, verification=OFFLINE, policy=ALLOW_MISSING_OR_FAILED)" + ) + ) + val incomingPermission = Incoming( + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/oauth-protected", + PathMatchingType.PATH, + setOf("GET"), + setOf( + ClientWithSelector.create(client, selector), + ClientWithSelector.create(client, "!$negatedSelector") + ), oauth = OAuth("oauth-provider", policy = OAuth.Policy.ALLOW_MISSING_OR_FAILED) ) ) @@ -135,7 +181,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { "/oauth-protected", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client1")), + setOf(ClientWithSelector.create("client1")), oauth = OAuth( provider = "oauth-provider", verification = OAuth.Verification.OFFLINE, @@ -147,12 +193,12 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { val expectedRbacBuilder = getRBACFilterWithShadowRules( expectedPoliciesForOAuth( originalAndAuthenticatedPrincipal("client1"), - "ClientWithSelector(name=client1, selector=null)", + "ClientWithSelector(name=client1, selector=null, negated=false)", "OAuth(provider=oauth-provider, verification=OFFLINE, policy=ALLOW_MISSING_OR_FAILED)" ), expectedPoliciesForOAuth( originalAndAuthenticatedPrincipal("client1"), - "ClientWithSelector(name=client1, selector=null)", + "ClientWithSelector(name=client1, selector=null, negated=false)", "OAuth(provider=oauth-provider, verification=OFFLINE, policy=ALLOW_MISSING_OR_FAILED)" ) ) @@ -257,7 +303,8 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { private fun getTokenFieldForClientWithSelector(provider: String, client: String) = jwtProperties.providers[provider]!!.matchings[client]!! - private fun oAuthClientPrincipal(selectorMatching: String, selector: String) = """{ + private fun oAuthClientPrincipal(selectorMatching: String, selector: String, negated: Boolean = false) = + """{ "metadata": { "filter": "envoy.filters.http.jwt_authn", "path": [ @@ -277,6 +324,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { } } } + ${if (negated) ",\"invert\": true" else ""} } }""" @@ -310,6 +358,13 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { } """ + private fun conjunctionOfPrincipals(vararg principals: String) = """{ + "andIds": { + "ids": [${principals.joinToString(", ")}] + } + } + """ + private fun principalForOAuthPolicy(policy: OAuth.Policy, client: String): String = when (policy) { OAuth.Policy.STRICT -> """{ "andIds": { diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt index 3de5edaf9..e8ab1d68b 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt @@ -15,6 +15,7 @@ import pl.allegro.tech.servicemesh.envoycontrol.groups.Incoming import pl.allegro.tech.servicemesh.envoycontrol.groups.IncomingEndpoint import pl.allegro.tech.servicemesh.envoycontrol.groups.PathMatchingType import pl.allegro.tech.servicemesh.envoycontrol.groups.Role +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.ClientsListsProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.EndpointMatch import pl.allegro.tech.servicemesh.envoycontrol.snapshot.GlobalSnapshot import pl.allegro.tech.servicemesh.envoycontrol.snapshot.IncomingPermissionsProperties @@ -86,6 +87,20 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { StatusRouteProperties() ) + private val rbacFilterFactoryWithDefaultAndCustomClientsLists = RBACFilterFactory( + IncomingPermissionsProperties().also { + it.enabled = true + it.clientsLists = ClientsListsProperties().also { + it.defaultClientsList = listOf("default-client", "xyz") + it.customClientsLists = mapOf( + "custom1" to listOf("custom1-client", "xyz"), + "ad:custom2" to listOf("ad:custom2-client", "xyz") + ) + } + }, + StatusRouteProperties() + ) + val snapshot = GlobalSnapshot( SnapshotResources.create(listOf(), "").resources(), setOf(), @@ -211,7 +226,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate RBAC rules for incoming permissions with log unlisted clients and endpoints`() { // given val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + - "clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client2, selector=null)], " + + "clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], " + "unlistedClientsPolicy=LOG, oauth=null)" val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) @@ -226,7 +241,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), + setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")), Incoming.UnlistedPolicy.LOG ) ), @@ -244,7 +259,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate RBAC rules for incoming permissions with block unlisted endpoints and log clients`() { // given val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + - "clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client2, selector=null)], " + + "clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], " + "unlistedClientsPolicy=LOG, oauth=null)" val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) @@ -259,7 +274,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), + setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")), Incoming.UnlistedPolicy.LOG ) ), @@ -277,7 +292,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate RBAC rules for incoming permissions with log unlisted endpoints and block clients`() { // given val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + - "clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client2, selector=null)], " + + "clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], " + "unlistedClientsPolicy=BLOCKANDLOG, oauth=null)" val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) @@ -292,7 +307,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), + setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")), Incoming.UnlistedPolicy.BLOCKANDLOG ) ), @@ -310,7 +325,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate RBAC rules for incoming permissions with roles`() { // given val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + - "clients=[ClientWithSelector(name=role-1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)" + "clients=[ClientWithSelector(name=role-1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)" val expectedRbacBuilder = getRBACFilter(expectedSimpleEndpointPermissionsJson(policyName)) val incomingPermission = Incoming( permissionsEnabled = true, @@ -318,8 +333,8 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("role-1")) - )), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"), ClientWithSelector("client2")))) + setOf(ClientWithSelector.create("role-1")) + )), roles = listOf(Role("role-1", setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")))) ) // when @@ -340,17 +355,17 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { PathMatchingType.PATH, setOf("GET", "POST"), setOf( - ClientWithSelector("client1"), - ClientWithSelector("client1"), - ClientWithSelector("client1", "selector"), - ClientWithSelector("client1-duplicated", "selector"), - ClientWithSelector("client1-duplicated"), - ClientWithSelector("role-1") + ClientWithSelector.create("client1"), + ClientWithSelector.create("client1"), + ClientWithSelector.create("client1", "selector"), + ClientWithSelector.create("client1-duplicated", "selector"), + ClientWithSelector.create("client1-duplicated"), + ClientWithSelector.create("role-1") ) )), roles = listOf(Role("role-1", setOf( - ClientWithSelector("client1-duplicated"), - ClientWithSelector("client1-duplicated"), - ClientWithSelector("client2")) + ClientWithSelector.create("client1-duplicated"), + ClientWithSelector.create("client1-duplicated"), + ClientWithSelector.create("client2")) )) ) @@ -371,12 +386,12 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client1")) + setOf(ClientWithSelector.create("client1")) ), IncomingEndpoint( "/example2", PathMatchingType.PATH, setOf("POST"), - setOf(ClientWithSelector("client2")) + setOf(ClientWithSelector.create("client2")) )) ) @@ -396,13 +411,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("role-1")) + setOf(ClientWithSelector.create("role-1")) ), IncomingEndpoint( "/example2", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("client2"), ClientWithSelector("client1")) - )), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"), ClientWithSelector("client2")))) + setOf(ClientWithSelector.create("client2"), ClientWithSelector.create("client1")) + )), roles = listOf(Role("role-1", setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")))) ) val expectedRbacBuilder = getRBACFilter(expectedTwoClientsSimpleEndpointPermissionsJson( "${incomingPermission.endpoints[0]}", "${incomingPermission.endpoints[1]}" @@ -424,13 +439,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("client2"), ClientWithSelector("role-1")) + setOf(ClientWithSelector.create("client2"), ClientWithSelector.create("role-1")) ), IncomingEndpoint( "/example2", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("role-2"), ClientWithSelector("client1")) - )), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"))), Role("role-2", setOf(ClientWithSelector("client2")))) + setOf(ClientWithSelector.create("role-2"), ClientWithSelector.create("client1")) + )), roles = listOf(Role("role-1", setOf(ClientWithSelector.create("client1"))), Role("role-2", setOf(ClientWithSelector.create("client2")))) ) val expectedRbacBuilder = getRBACFilter(expectedTwoClientsSimpleEndpointPermissionsJson( "${incomingPermission.endpoints[0]}", "${incomingPermission.endpoints[1]}" @@ -453,7 +468,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")) + setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")) )) ) @@ -589,7 +604,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client1")) + setOf(ClientWithSelector.create("client1")) )) ) @@ -610,7 +625,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")) + setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("client2")) )) ) @@ -634,7 +649,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client2", "selector")) + setOf(ClientWithSelector.create("client2", "selector")) )) ) @@ -658,7 +673,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client1", "selector")) + setOf(ClientWithSelector.create("client1", "selector")) )) ) @@ -682,11 +697,11 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("role1")) + setOf(ClientWithSelector.create("role1")) )), roles = listOf(Role("role1", setOf( - ClientWithSelector("client1", "selector1"), - ClientWithSelector("client2", "selector2")) + ClientWithSelector.create("client1", "selector1"), + ClientWithSelector.create("client2", "selector2")) )) ) @@ -710,7 +725,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "/example", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("client1")) + setOf(ClientWithSelector.create("client1")) )) ) @@ -724,10 +739,56 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { assertThat(generated).isEqualTo(expectedRbacBuilder) } + @Test + fun `should generate RBAC rules for incoming permissions with default client list`() { + // given + val incomingPermission = Incoming( + permissionsEnabled = true, + endpoints = listOf(IncomingEndpoint( + "/default", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector.create("client1")) + )) + ) + + val rules = expectedPoliciesForDefaultAndCustomLists(listOf("client1"), listOf("client1", "default-client", "xyz"), "/default") + val expectedDefaultRbacBuilder = getRBACFilterWithShadowRules(rules, rules) + + // when + val generated = rbacFilterFactoryWithDefaultAndCustomClientsLists.createHttpFilter(createGroup(incomingPermission), snapshot) + + // then + assertThat(generated).isEqualTo(expectedDefaultRbacBuilder) + } + + @Test + fun `should generate RBAC rules for incoming permissions with custom client list`() { + // given + val incomingPermission = Incoming( + permissionsEnabled = true, + endpoints = listOf(IncomingEndpoint( + "/custom", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector.create("client1"), ClientWithSelector.create("custom1")) + )) + ) + + val rules = expectedPoliciesForDefaultAndCustomLists(listOf("client1", "custom1"), listOf("client1", "custom1-client", "xyz"), "/custom") + val expectedDefaultRbacBuilder = getRBACFilterWithShadowRules(rules, rules) + + // when + val generated = rbacFilterFactoryWithDefaultAndCustomClientsLists.createHttpFilter(createGroup(incomingPermission), snapshot) + + // then + assertThat(generated).isEqualTo(expectedDefaultRbacBuilder) + } + private val expectedEndpointPermissionsWithDifferentRulesForDifferentClientsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -747,7 +808,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { ${originalAndAuthenticatedPrincipal("client1")} ] }, - "IncomingEndpoint(path=/example2, pathMatchingType=PATH, methods=[POST], clients=[ClientWithSelector(name=client2, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example2, pathMatchingType=PATH, methods=[POST], clients=[ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -774,7 +835,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpFromDiscoveryWithSelectorAuthPermissionsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=selector)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=selector, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -809,7 +870,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpWithSelectorAuthPermissionsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client2, selector=selector)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client2, selector=selector, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -844,7 +905,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpWithStaticRangeAndSelectorAuthPermissionsAndRolesJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=role1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=role1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -888,7 +949,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpAuthPermissionsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client2, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -947,7 +1008,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { { "policies": { """ /* notice that duplicated clients occurs only once here */ + """ - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client1, selector=selector), ClientWithSelector(name=client1-duplicated, selector=selector), ClientWithSelector(name=client1-duplicated, selector=null), ClientWithSelector(name=role-1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client1, selector=selector, negated=false), ClientWithSelector(name=client1-duplicated, selector=selector, negated=false), ClientWithSelector(name=client1-duplicated, selector=null, negated=false), ClientWithSelector(name=role-1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1167,7 +1228,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpAuthWithStaticRangeJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1194,7 +1255,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpAuthWithStaticRangeAndSourceIpJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client2, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1231,7 +1292,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedEndpointPermissionsLogUnlistedEndpointsAndBlockUnlistedClients = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null), ClientWithSelector(name=client2, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1287,7 +1348,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private fun expectedPoliciesForAllowedClient(principals: String) = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1309,6 +1370,30 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { } """ + private fun expectedPoliciesForDefaultAndCustomLists(clients: List, principals: List, path: String) = """ + { + "policies": { + "IncomingEndpoint(path=$path, pathMatchingType=PATH, methods=[GET], clients=[${clients.joinToString(", ") { "ClientWithSelector(name=$it, selector=null, negated=false)" }}], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "permissions": [ + { + "and_rules": { + "rules": [ + ${pathRule(path)}, + { + "or_rules": { + "rules": [ + ${methodRule("GET")} + ] + } + } + ] + } + } + ], "principals": [ ${principals.joinToString(", ") { originalAndAuthenticatedPrincipal(it) }} ] + } + } + } + """ private val expectedRulesForAllowedClient = expectedPoliciesForAllowedClient( "${originalAndAuthenticatedPrincipal("client1")}, ${originalAndAuthenticatedPrincipal("allowed-client")}" ) diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactoryTest.kt index 7876fada6..eebb7f283 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/routes/EnvoyEgressRoutesFactoryTest.kt @@ -1,18 +1,26 @@ package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes import com.google.protobuf.util.Durations +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import pl.allegro.tech.servicemesh.envoycontrol.groups.DependencySettings import pl.allegro.tech.servicemesh.envoycontrol.groups.Outgoing +import pl.allegro.tech.servicemesh.envoycontrol.groups.RetryPolicy import pl.allegro.tech.servicemesh.envoycontrol.groups.hasCustomIdleTimeout import pl.allegro.tech.servicemesh.envoycontrol.groups.hasCustomRequestTimeout import pl.allegro.tech.servicemesh.envoycontrol.groups.hasHostRewriteHeader import pl.allegro.tech.servicemesh.envoycontrol.groups.hasNoRequestHeaderToAdd +import pl.allegro.tech.servicemesh.envoycontrol.groups.hasNoRetryPolicy import pl.allegro.tech.servicemesh.envoycontrol.groups.hasRequestHeaderToAdd import pl.allegro.tech.servicemesh.envoycontrol.groups.hasRequestHeadersToRemove import pl.allegro.tech.servicemesh.envoycontrol.groups.hasResponseHeaderToAdd +import pl.allegro.tech.servicemesh.envoycontrol.groups.hasRetryPolicy +import pl.allegro.tech.servicemesh.envoycontrol.groups.hasVirtualHostThat import pl.allegro.tech.servicemesh.envoycontrol.groups.hasVirtualHostsInOrder import pl.allegro.tech.servicemesh.envoycontrol.groups.hostRewriteHeaderIsEmpty +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.snapshot.RouteSpecification import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties @@ -28,7 +36,8 @@ internal class EnvoyEgressRoutesFactoryTest { idleTimeout = Durations.fromSeconds(10L), requestTimeout = Durations.fromSeconds(10L) ), - rewriteHostHeader = true + rewriteHostHeader = true, + retryPolicy = RetryPolicy() ) ) ) @@ -187,4 +196,86 @@ internal class EnvoyEgressRoutesFactoryTest { ) routeConfig.hasRequestHeadersToRemove(listOf("x-special-case-header", "x-custom")) } + + @Test + fun `should create route config and apply retry police only to specified methods`() { + // given + val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties()) + val retryPolicy = RetryPolicy(methods = setOf("GET", "POST")) + val routesSpecifications = listOf( + RouteSpecification("example", listOf("example.pl:1553"), DependencySettings(retryPolicy = retryPolicy)), + ) + + val routeConfig = routesFactory.createEgressRouteConfig( + "test", + routesSpecifications, + routeName = "retry", + addUpstreamAddressHeader = false + ) + + routeConfig.hasVirtualHostThat("example") { + assertEquals(2, routesCount) + val retryRoute = getRoutes(0) + val defaultRoute = getRoutes(1) + + retryRoute.hasRetryPolicy() + retryRoute.matchingOnMethod("GET|POST", true) + retryRoute.matchingOnPrefix("/") + + defaultRoute.hasNoRetryPolicy() + defaultRoute.matchingOnAnyMethod() + defaultRoute.matchingOnPrefix("/") + } + } + + @Test + fun `should create route config and apply retry policy without specified methods to default prefix and all method`() { + // given + val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties()) + val retryPolicy = RetryPolicy(numberRetries = 3) + val routesSpecifications = listOf( + RouteSpecification("example", listOf("example.pl:1553"), DependencySettings(retryPolicy = retryPolicy)), + ) + + val routeConfig = routesFactory.createEgressRouteConfig( + "test", + routesSpecifications, + routeName = "retry", + addUpstreamAddressHeader = false + ) + + routeConfig.hasVirtualHostThat("example") { + assertEquals(1, routesCount) + val defaultRoute = getRoutes(0) + + defaultRoute.hasRetryPolicy() + defaultRoute.matchingOnAnyMethod() + defaultRoute.matchingOnPrefix("/") + } + } + + @Test + fun `should create route config and not apply retry policy if it is not specified`() { + // given + val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties()) + val routesSpecifications = listOf( + RouteSpecification("example", listOf("example.pl:1553"), DependencySettings()), + ) + + val routeConfig = routesFactory.createEgressRouteConfig( + "test", + routesSpecifications, + routeName = "retry", + addUpstreamAddressHeader = false + ) + + routeConfig.hasVirtualHostThat("example") { + assertEquals(1, routesCount) + val defaultRoute = getRoutes(0) + + defaultRoute.hasNoRetryPolicy() + defaultRoute.matchingOnAnyMethod() + defaultRoute.matchingOnPrefix("/") + } + } } 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 1bdea5bcf..cd88a6f43 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 @@ -107,11 +107,11 @@ internal class EnvoyIngressRoutesFactoryTest { timeoutPolicy = TimeoutPolicy(idleTimeout, responseTimeout, connectionIdleTimeout), rateLimitEndpoints = listOf( IncomingRateLimitEndpoint("/hello", PathMatchingType.PATH_PREFIX, setOf("GET", "POST"), - setOf(ClientWithSelector("client-1", "selector")), "100/s"), + setOf(ClientWithSelector.create("client-1", "selector")), "100/s"), IncomingRateLimitEndpoint("/banned", PathMatchingType.PATH, setOf("GET"), - setOf(ClientWithSelector("*")), "0/m"), + setOf(ClientWithSelector.create("*")), "0/m"), IncomingRateLimitEndpoint("/a/.*", PathMatchingType.PATH_REGEX, emptySet(), - setOf(ClientWithSelector("client-2")), "0/m") + setOf(ClientWithSelector.create("client-2")), "0/m") ) ) ) diff --git a/envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/debug/SnapshotDebugController.kt b/envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/debug/SnapshotDebugController.kt index 19415d833..719bf7afd 100644 --- a/envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/debug/SnapshotDebugController.kt +++ b/envoy-control-runner/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/debug/SnapshotDebugController.kt @@ -14,7 +14,11 @@ import com.google.protobuf.util.JsonFormat.TypeRegistry import io.envoyproxy.envoy.config.rbac.v3.RBAC import io.envoyproxy.envoy.extensions.filters.http.header_to_metadata.v3.Config import io.envoyproxy.envoy.extensions.filters.http.lua.v3.Lua +import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager +import io.envoyproxy.envoy.extensions.retry.host.omit_canary_hosts.v3.OmitCanaryHostsPredicate +import io.envoyproxy.envoy.extensions.retry.host.omit_host_metadata.v3.OmitHostMetadataConfig +import io.envoyproxy.envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate import io.envoyproxy.envoy.extensions.transport_sockets.tap.v3.Tap import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext import io.envoyproxy.envoy.type.matcher.PathMatcher @@ -86,6 +90,10 @@ class SnapshotDebugController(val debugService: SnapshotDebugService) { .add(Any.getDescriptor()) .add(PathMatcher.getDescriptor()) .add(StringMatcher.getDescriptor()) + .add(Router.getDescriptor()) + .add(PreviousHostsPredicate.getDescriptor()) + .add(OmitCanaryHostsPredicate.getDescriptor()) + .add(OmitHostMetadataConfig.getDescriptor()) .add(Tap.getDescriptor()) .add(UpstreamTlsContext.getDescriptor()) .add(Lua.getDescriptor()) diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EndpointMetadataMergingTests.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EndpointMetadataMergingTests.kt index 2944cd5df..98eb5b8cc 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EndpointMetadataMergingTests.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EndpointMetadataMergingTests.kt @@ -3,6 +3,7 @@ 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.untilAsserted import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.CallStats import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension @@ -39,17 +40,16 @@ open class EndpointMetadataMergingTests { consul.server.operations.registerService(name = "echo", extension = service, tags = listOf("ipsum")) consul.server.operations.registerService(name = "echo", extension = service, tags = listOf("lorem", "dolom")) - // TODO: flaky test. I'm not sure why, but it fails time to time. - // Theoretically after one call service should be ready, - // but for some reason sometimes is not and returns 503. - repeat(3) { - envoy.waitForReadyServices("echo") + untilAsserted { + assertThat(consul.server.operations.getService("echo")).hasSize(2) } + envoy.waitForAvailableEndpoints("echo") + // when - val ipsumStats = callEchoServiceRepeatedly(repeat = 1, tag = "ipsum") - val loremStats = callEchoServiceRepeatedly(repeat = 1, tag = "lorem") - val dolomStats = callEchoServiceRepeatedly(repeat = 1, tag = "dolom") + val ipsumStats = callEchoServiceRepeatedly(service, repeat = 1, tag = "ipsum") + val loremStats = callEchoServiceRepeatedly(service, repeat = 1, tag = "lorem") + val dolomStats = callEchoServiceRepeatedly(service, repeat = 1, tag = "dolom") // then assertThat(ipsumStats.hits(service)).isEqualTo(1) @@ -58,8 +58,10 @@ open class EndpointMetadataMergingTests { } protected open fun callEchoServiceRepeatedly( + service: EchoServiceExtension, repeat: Int, - tag: String, + tag: String? = null, + assertNoErrors: Boolean = true ): CallStats { val stats = CallStats(listOf(service)) envoy.egressOperations.callServiceRepeatedly( @@ -67,8 +69,8 @@ open class EndpointMetadataMergingTests { stats = stats, minRepeat = repeat, maxRepeat = repeat, - headers = mapOf("x-service-tag" to tag), - assertNoErrors = true + headers = tag?.let { mapOf("x-service-tag" to it) } ?: emptyMap(), + assertNoErrors = assertNoErrors ) return stats } diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/JWTFilterTest.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/JWTFilterTest.kt index 0346999ca..960fe9d1d 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/JWTFilterTest.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/JWTFilterTest.kt @@ -9,6 +9,9 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import pl.allegro.tech.discovery.consul.recipes.internal.http.MediaType import pl.allegro.tech.servicemesh.envoycontrol.assertions.isForbidden import pl.allegro.tech.servicemesh.envoycontrol.assertions.isFrom @@ -24,9 +27,22 @@ import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtens import pl.allegro.tech.servicemesh.envoycontrol.config.service.OAuthServerExtension import pl.allegro.tech.servicemesh.envoycontrol.snapshot.OAuthProvider import java.net.URI +import java.util.stream.Stream class JWTFilterTest { companion object { + @JvmStatic + fun provideClientsForTestWithNegatedSelector(): Stream { + return Stream.of( + Arguments.of("/allow-team1-deny-team2", "team1", true), + Arguments.of("/allow-team1-deny-team2", "team2", false), + Arguments.of("/allow-team1-deny-team2", "team3", false), + Arguments.of("/allow-team1-deny-team2", "team1,team2", false), + Arguments.of("/allow-team1-deny-team2", "team3,team2", false), + Arguments.of("/non-team1-access", "team1", false), + Arguments.of("/non-team1-access", "team2", true) + ) + } @JvmField @RegisterExtension @@ -115,6 +131,12 @@ class JWTFilterTest { - path: '/team-access' clients: ['first-provider-prefix:team1'] unlistedClientsPolicy: blockAndLog + - path: '/non-team1-access' + clients: ['first-provider-prefix:!team1'] + unlistedClientsPolicy: blockAndLog + - path: '/allow-team1-deny-team2' + clients: ['first-provider-prefix:team1','first-provider-prefix:!team2'] + unlistedClientsPolicy: blockAndLog - pathPrefix: '/no-clients' clients: [] unlistedClientsPolicy: log @@ -145,7 +167,11 @@ class JWTFilterTest { @JvmField @RegisterExtension - val oauthEnvoy = EnvoyExtension(envoyControl, oAuthServer, config = OAuthEnvoyConfig.copy(serviceName = "oauth", configOverride = oauthConfig)) + val oauthEnvoy = EnvoyExtension( + envoyControl, + oAuthServer, + config = OAuthEnvoyConfig.copy(serviceName = "oauth", configOverride = oauthConfig) + ) @JvmField @RegisterExtension @@ -163,7 +189,11 @@ class JWTFilterTest { @JvmField @RegisterExtension - val echo2Envoy = EnvoyExtension(envoyControl, localService = service, config = Echo2EnvoyAuthConfig.copy(configOverride = echo2Config)) + val echo2Envoy = EnvoyExtension( + envoyControl, + localService = service, + config = Echo2EnvoyAuthConfig.copy(configOverride = echo2Config) + ) } @BeforeEach @@ -429,6 +459,26 @@ class JWTFilterTest { assertThat(response).isOk().isFrom(service) } + @ParameterizedTest + @MethodSource("provideClientsForTestWithNegatedSelector") + fun `should allow only clients without negated selector`(endpoint: String, authority: String, isAllowed: Boolean) { + + // given + registerClientWithAuthority("first-provider", authority, authority) + val token = tokenForProvider("first-provider", authority) + + val response = envoy.ingressOperations.callLocalService( + endpoint = endpoint, headers = headersOf("Authorization", "Bearer $token") + ) + + // then + if (isAllowed) { + assertThat(response).isOk() + } else { + assertThat(response).isForbidden() + } + } + @Test fun `should allow request with token when policy is strict, unlisted clients policy is log and there are no clients`() { // given @@ -481,7 +531,10 @@ class JWTFilterTest { } private fun tokenForProvider(provider: String, clientId: String = "client1") = - OkHttpClient().newCall(Request.Builder().post(FormBody.Builder().add("client_id", clientId).build()).url(oAuthServer.getTokenAddress(provider)).build()) + OkHttpClient().newCall( + Request.Builder().post(FormBody.Builder().add("client_id", clientId).build()) + .url(oAuthServer.getTokenAddress(provider)).build() + ) .execute().addToCloseableResponses() .body!!.string() @@ -491,7 +544,10 @@ class JWTFilterTest { "clientSecret": "secret", "authorities":["$authority"] }""" - return OkHttpClient().newCall(Request.Builder().put(RequestBody.create(MediaType.JSON_MEDIA_TYPE, body)).url("http://localhost:${oAuthServer.container().port()}/$provider/client").build()) + return OkHttpClient().newCall( + Request.Builder().put(RequestBody.create(MediaType.JSON_MEDIA_TYPE, body)) + .url("http://localhost:${oAuthServer.container().port()}/$provider/client").build() + ) .execute().close() } } diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/RoutingPolicyTest.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/RoutingPolicyTest.kt new file mode 100644 index 000000000..2f317c8e3 --- /dev/null +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/RoutingPolicyTest.kt @@ -0,0 +1,357 @@ +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.untilAsserted +import pl.allegro.tech.servicemesh.envoycontrol.config.RandomConfigFile +import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension +import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.CallStats +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 java.time.Duration + +class RoutingPolicyTest { + + companion object { + private val properties = mapOf( + "envoy-control.envoy.snapshot.routing.service-tags.enabled" to true, + "envoy-control.envoy.snapshot.routing.service-tags.metadata-key" to "tag", + "envoy-control.envoy.snapshot.routing.service-tags.auto-service-tag-enabled" to true + ) + + @JvmField + @RegisterExtension + val consul = ConsulExtension() + + @JvmField + @RegisterExtension + val envoyControl = EnvoyControlExtension(consul, properties) + + @JvmField + @RegisterExtension + val loremEchoService = EchoServiceExtension() + + @JvmField + @RegisterExtension + val ipsumEchoService = EchoServiceExtension() + + @JvmField + @RegisterExtension + val otherEchoService = EchoServiceExtension() + + // language=yaml + private var autoServiceTagEnabledSettings = """ + node: + metadata: + proxy_settings: + outgoing: + routingPolicy: + autoServiceTag: true + serviceTagPreference: ["ipsum", "lorem"] + dependencies: + - service: "echo" + """.trimIndent() + + @JvmField + @RegisterExtension + val autoServiceTagEnabledEnvoy = + EnvoyExtension(envoyControl, config = RandomConfigFile.copy(configOverride = autoServiceTagEnabledSettings)) + + @JvmField + @RegisterExtension + val autoServiceTagDisabledEnvoy = EnvoyExtension(envoyControl) + } + + @Test + fun `should filter service instances according to tag preference`() { + // given + waitForEcConsulStateSynchronized(listOf( + consul.server.operations.registerService( + name = "echo", + extension = ipsumEchoService, + tags = listOf("ipsum", "other") + ), + consul.server.operations.registerService( + name = "echo", + extension = loremEchoService, + tags = listOf("lorem") + ), + consul.server.operations.registerService(name = "echo", extension = otherEchoService, tags = emptyList()), + )) + waitForEndpointReady("echo", ipsumEchoService, autoServiceTagEnabledEnvoy) + + // when + val stats = callEchoTenTimes(autoServiceTagEnabledEnvoy) + + // then + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumEchoService)).isEqualTo(10) + assertThat(stats.hits(loremEchoService)).isEqualTo(0) + assertThat(stats.hits(otherEchoService)).isEqualTo(0) + } + + @Test + fun `should not filter service instances if autoServiceTag is false`() { + // given + waitForEcConsulStateSynchronized(listOf( + consul.server.operations.registerService( + name = "echo", + extension = ipsumEchoService, + tags = listOf("ipsum", "other") + ), + consul.server.operations.registerService( + name = "echo", + extension = loremEchoService, + tags = listOf("lorem") + ), + consul.server.operations.registerService(name = "echo", extension = otherEchoService, tags = emptyList()), + )) + waitForEndpointReady("echo", ipsumEchoService, autoServiceTagEnabledEnvoy) + + // when + val stats = callEchoTenTimes(autoServiceTagDisabledEnvoy) + + // then + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumEchoService)).isBetween(3, 4) + assertThat(stats.hits(loremEchoService)).isBetween(3, 4) + assertThat(stats.hits(otherEchoService)).isBetween(3, 4) + } + + @Test + fun `should change routing when instance with prefered tag appears`() { + // given + val otherEchoId = consul.server.operations.registerService( + name = "echo", extension = otherEchoService, tags = emptyList()) + waitForEcConsulStateSynchronized(listOf(otherEchoId)) + waitForEndpointReady("echo", otherEchoService, autoServiceTagDisabledEnvoy) + + // when + val statsAutoServiceTagAfterOther = callEchoTenTimes(autoServiceTagEnabledEnvoy, assertNoErrors = false) + val statsNoAutoServiceTagAfterOther = callEchoTenTimes(autoServiceTagDisabledEnvoy) + + // then + statsAutoServiceTagAfterOther.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.failedHits).isEqualTo(10) + } + statsNoAutoServiceTagAfterOther.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(otherEchoService)).isEqualTo(10) + } + + // when + val loremEchoId = consul.server.operations.registerService( + name = "echo", extension = loremEchoService, tags = listOf("lorem")) + waitForEcConsulStateSynchronized(listOf(otherEchoId, loremEchoId)) + waitForEndpointReady("echo", loremEchoService, autoServiceTagDisabledEnvoy) + waitForEndpointReady("echo", loremEchoService, autoServiceTagEnabledEnvoy) + + // and + val statsAutoServiceTagAfterLorem = callEchoTenTimes(autoServiceTagEnabledEnvoy) + val statsNoAutoServiceTagAfterLorem = callEchoTenTimes(autoServiceTagDisabledEnvoy) + + // then + statsAutoServiceTagAfterLorem.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(loremEchoService)).isEqualTo(10) + assertThat(stats.hits(otherEchoService)).isEqualTo(0) + } + statsNoAutoServiceTagAfterLorem.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(loremEchoService)).isEqualTo(5) + assertThat(stats.hits(otherEchoService)).isEqualTo(5) + } + + // when + val ipsumEchoId = consul.server.operations.registerService( + name = "echo", extension = ipsumEchoService, tags = listOf("noise", "ipsum")) + waitForEcConsulStateSynchronized(listOf(otherEchoId, loremEchoId, ipsumEchoId)) + waitForEndpointReady("echo", ipsumEchoService, autoServiceTagDisabledEnvoy) + waitForEndpointReady("echo", ipsumEchoService, autoServiceTagEnabledEnvoy) + + // and + val statsAutoServiceTagAfterIpsum = callEchoTenTimes(autoServiceTagEnabledEnvoy) + val statsNoAutoServiceTagAfterIpsum = callEchoTenTimes(autoServiceTagDisabledEnvoy) + + // then + statsAutoServiceTagAfterIpsum.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumEchoService)).isEqualTo(10) + assertThat(stats.hits(loremEchoService)).isEqualTo(0) + assertThat(stats.hits(otherEchoService)).isEqualTo(0) + } + statsNoAutoServiceTagAfterIpsum.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumEchoService)).isBetween(3, 4) + assertThat(stats.hits(loremEchoService)).isBetween(3, 4) + assertThat(stats.hits(otherEchoService)).isBetween(3, 4) + } + } + + @Test + fun `should change routing when instance with prefered tag disappers`() { + // given + val ipsumId = consul.server.operations.registerService( + name = "echo", + extension = ipsumEchoService, + tags = listOf("ipsum", "other") + ) + val otherId = consul.server.operations.registerService( + name = "echo", + extension = otherEchoService, + tags = listOf("other") + ) + + waitForEcConsulStateSynchronized(listOf(ipsumId, otherId)) + waitForEndpointReady("echo", otherEchoService, autoServiceTagDisabledEnvoy) + waitForEndpointReady("echo", ipsumEchoService, autoServiceTagEnabledEnvoy) + + // when + val statsNoAutoServiceTag = callEchoTenTimes(autoServiceTagDisabledEnvoy) + val statsAutoServiceTag = callEchoTenTimes(autoServiceTagEnabledEnvoy) + + // then + statsNoAutoServiceTag.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumEchoService)).isEqualTo(5) + assertThat(stats.hits(otherEchoService)).isEqualTo(5) + } + statsAutoServiceTag.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumEchoService)).isEqualTo(10) + assertThat(stats.hits(otherEchoService)).isEqualTo(0) + } + + // when + consul.server.operations.deregisterService(ipsumId) + + waitForEcConsulStateSynchronized(listOf(otherId)) + waitForEndpointRemoved("echo", ipsumEchoService, autoServiceTagDisabledEnvoy) + waitForEndpointRemoved("echo", ipsumEchoService, autoServiceTagEnabledEnvoy) + + // and + val statsNoAutoServiceTagAfterNoIpsum = callEchoTenTimes(autoServiceTagDisabledEnvoy) + val statsAutoServiceTagAfterNoIpsum = callEchoTenTimes(autoServiceTagEnabledEnvoy, assertNoErrors = false) + + // then + statsNoAutoServiceTagAfterNoIpsum.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(otherEchoService)).isEqualTo(10) + } + statsAutoServiceTagAfterNoIpsum.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.failedHits).isEqualTo(10) + } + } + + @Test + fun `should consider client side service-tag`() { + // given + val ipsumBetaEchoService = ipsumEchoService + val ipsumAlphaEchoService = otherEchoService + val loremBetaEchoService = loremEchoService + + val ipsumBetaId = consul.server.operations.registerService( + name = "echo", + extension = ipsumBetaEchoService, + tags = listOf("ipsum", "beta") + ) + val loremBetaId = consul.server.operations.registerService( + name = "echo", + extension = loremBetaEchoService, + tags = listOf("lorem", "beta") + ) + val ipsumAlphaId = consul.server.operations.registerService( + name = "echo", + extension = ipsumAlphaEchoService, + tags = listOf("ipsum", "alpha") + ) + waitForEcConsulStateSynchronized(listOf(ipsumBetaId, ipsumAlphaId, loremBetaId)) + waitForEndpointReady("echo", ipsumAlphaEchoService, autoServiceTagEnabledEnvoy) + waitForEndpointReady("echo", ipsumAlphaEchoService, autoServiceTagDisabledEnvoy) + + // when + val statsAutoServiceTag = callEchoTenTimes(autoServiceTagEnabledEnvoy, tag = "beta") + val statsNoAutoServiceTag = callEchoTenTimes(autoServiceTagDisabledEnvoy, tag = "beta") + + // then + statsAutoServiceTag.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumBetaEchoService)).isEqualTo(10) + } + statsNoAutoServiceTag.let { stats -> + assertThat(stats.totalHits).isEqualTo(10) + assertThat(stats.hits(ipsumBetaEchoService)).isEqualTo(5) + assertThat(stats.hits(loremBetaEchoService)).isEqualTo(5) + } + } + + private fun waitForEcConsulStateSynchronized(expectedInstancesIds: Collection) { + untilAsserted(wait = Duration.ofSeconds(5)) { + val echoInstances = envoyControl.app.getState()["echo"]?.instances.orEmpty() + assertThat(echoInstances.map { it.id }.toSet()) + .withFailMessage( + "EC instances state of 'echo' service not consistent with consul. Expected instances: %s, Found: %s", + expectedInstancesIds, echoInstances + ) + .isEqualTo(expectedInstancesIds.toSet()) + } + } + + private fun waitForEndpointReady( + serviceName: String, + serviceInstance: EchoServiceExtension, + envoy: EnvoyExtension + ) { + untilAsserted(wait = Duration.ofSeconds(5)) { + assertThat(envoy.container.admin().isEndpointHealthy(serviceName, serviceInstance.container().ipAddress())) + .withFailMessage { + "Expected to see healthy endpoint of cluster '$serviceName' with address " + + "'${serviceInstance.container().address()}' in envoy " + + "${serviceInstance.container().address()}/clusters, " + + "but it's not present. Found following endpoints: " + + "${envoy.container.admin().endpointsAddress(serviceName)}" + } + .isTrue() + } + } + + private fun waitForEndpointRemoved( + serviceName: String, + serviceInstance: EchoServiceExtension, + envoy: EnvoyExtension + ) { + untilAsserted(wait = Duration.ofSeconds(5)) { + assertThat(envoy.container.admin().isEndpointHealthy(serviceName, serviceInstance.container().ipAddress())) + .withFailMessage { + "Expected to not see endpoint of cluster '$serviceName' with address " + + "'${serviceInstance.container().address()}' in envoy " + + "${serviceInstance.container().address()}/clusters, " + + "but it's still present. Found following endpoints: " + + "${envoy.container.admin().endpointsAddress(serviceName)}" + } + .isFalse() + } + } + + private fun callStats() = CallStats(listOf(ipsumEchoService, loremEchoService, otherEchoService)) + + private fun callEchoTenTimes( + envoy: EnvoyExtension, + assertNoErrors: Boolean = true, + tag: String? = null + ): CallStats { + val stats = callStats() + envoy.egressOperations.callServiceRepeatedly( + service = "echo", + stats = stats, + maxRepeat = 10, + assertNoErrors = assertNoErrors, + headers = tag?.let { mapOf("x-service-tag" to it) } ?: emptyMap(), + ) + return stats + } +} diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulContainer.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulContainer.kt index 4292be2c4..d6988c116 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulContainer.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulContainer.kt @@ -13,7 +13,7 @@ class ConsulContainer( val internalPort: Int = 8500 ) : GenericContainer( ImageFromDockerfile().withDockerfileFromBuilder { - it.from("consul:latest") + it.from("consul:1.10.12") .run("apk", "add", "iproute2") .cmd(consulConfig.launchCommand()) .expose(internalPort) diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulOperations.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulOperations.kt index 3c8f5eddc..342c7b9a2 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulOperations.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/consul/ConsulOperations.kt @@ -1,7 +1,9 @@ package pl.allegro.tech.servicemesh.envoycontrol.config.consul import com.ecwid.consul.v1.ConsulClient +import com.ecwid.consul.v1.QueryParams import com.ecwid.consul.v1.agent.model.NewService +import com.ecwid.consul.v1.catalog.model.CatalogService import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyContainer import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension import pl.allegro.tech.servicemesh.envoycontrol.config.service.ServiceExtension @@ -72,6 +74,9 @@ class ConsulOperations(port: Int) { client.agentServiceDeregister(id) } + fun getService(serviceName: String, params: QueryParams = QueryParams.DEFAULT): List = + client.getCatalogService(serviceName, params).value ?: emptyList() + fun deregisterAll() { registeredServices().forEach { deregisterService(it) } } diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/EnvoyContainer.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/EnvoyContainer.kt index d276f589d..b6a7f7c3c 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/EnvoyContainer.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/config/envoy/EnvoyContainer.kt @@ -36,7 +36,7 @@ class EnvoyContainer( const val ENVOY_UID_ENV_NAME = "ENVOY_UID" const val EGRESS_LISTENER_CONTAINER_PORT = 5000 const val INGRESS_LISTENER_CONTAINER_PORT = 5001 - const val DEFAULT_IMAGE = "envoyproxy/envoy:v1.21.0" + const val DEFAULT_IMAGE = "envoyproxy/envoy:v1.24.0" private const val ADMIN_PORT = 10000 } diff --git a/envoy-control-tests/src/main/resources/envoy/bad_config.yaml b/envoy-control-tests/src/main/resources/envoy/bad_config.yaml index 614e22a3f..d0ab0840f 100644 --- a/envoy-control-tests/src/main/resources/envoy/bad_config.yaml +++ b/envoy-control-tests/src/main/resources/envoy/bad_config.yaml @@ -90,6 +90,8 @@ static_resources: ads: {} http_filters: - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - name: ingress_listener address: socket_address: @@ -107,3 +109,5 @@ static_resources: ads: {} http_filters: - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/envoy-control-tests/src/main/resources/envoy/config_ads_custom_health_check.yaml b/envoy-control-tests/src/main/resources/envoy/config_ads_custom_health_check.yaml index 84042c56f..e5cb31622 100644 --- a/envoy-control-tests/src/main/resources/envoy/config_ads_custom_health_check.yaml +++ b/envoy-control-tests/src/main/resources/envoy/config_ads_custom_health_check.yaml @@ -100,6 +100,8 @@ static_resources: ads: {} http_filters: - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - name: ingress_listener address: socket_address: @@ -117,3 +119,5 @@ static_resources: ads: {} http_filters: - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/envoy-control-tests/src/main/resources/envoy/config_ads_static_listeners.yaml b/envoy-control-tests/src/main/resources/envoy/config_ads_static_listeners.yaml index 2d694b8b0..7211e9fbb 100644 --- a/envoy-control-tests/src/main/resources/envoy/config_ads_static_listeners.yaml +++ b/envoy-control-tests/src/main/resources/envoy/config_ads_static_listeners.yaml @@ -85,6 +85,8 @@ static_resources: ads: {} http_filters: - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - name: ingress_listener address: socket_address: @@ -102,3 +104,5 @@ static_resources: ads: {} http_filters: - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/tools/envoy/Dockerfile b/tools/envoy/Dockerfile index 19bd7fec8..500e559c0 100644 --- a/tools/envoy/Dockerfile +++ b/tools/envoy/Dockerfile @@ -1,4 +1,4 @@ -FROM envoyproxy/envoy:v1.21.0 +FROM envoyproxy/envoy:v1.24.0 ENV PORT=9999:9999 ENV PORT=80:80