diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1c6cb1d..ce4348dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Changed +- Add possibility to set dependency to group of services by tag mechanism - flaky test fixed - remove duplicated routes + ## [0.19.26] ### Changed diff --git a/docs/configuration.md b/docs/configuration.md index b33628585..c29102dfe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -95,7 +95,7 @@ Property ## Permissions Property | Description | Default value --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- +-------------------------------------------------------------------------------------------------------------------------------------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --------- **envoy-control.envoy.snapshot.incoming-permissions.enabled** | Enable incoming permissions | false **envoy-control.envoy.snapshot.incoming-permissions.client-identity-headers** | Headers that identify the client calling the endpoint. In most cases `client-identity-header` should include `service-name-header` value to correctly identify other services in the mesh. | [ x-service-name ] **envoy-control.envoy.snapshot.incoming-permissions.clients-allowed-to-all-endpoints** | Client names which are allowed to even call service if incoming permissions are enabled. | empty list @@ -127,7 +127,7 @@ Property **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 - +**envoy-control.envoy.snapshot.outgoing-permissions.tag-prefix** | Value that specify which tags are allowed to be used in dependencies by prefix | empty string ## Load Balancing Property | Description | Default value ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- diff --git a/docs/features/permissions.md b/docs/features/permissions.md index 06ee90807..433244a77 100644 --- a/docs/features/permissions.md +++ b/docs/features/permissions.md @@ -24,6 +24,7 @@ metadata: - service: service-b handleInternalRedirect: true - domain: http://www.example.com + - tag: tag-a incoming: endpoints: - path: /example diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/ControlPlane.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/ControlPlane.kt index ac431e297..3cffc8c62 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/ControlPlane.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/ControlPlane.kt @@ -26,6 +26,7 @@ import pl.allegro.tech.servicemesh.envoycontrol.server.callbacks.MetricsDiscover import pl.allegro.tech.servicemesh.envoycontrol.services.MultiClusterState import pl.allegro.tech.servicemesh.envoycontrol.snapshot.EnvoySnapshotFactory import pl.allegro.tech.servicemesh.envoycontrol.snapshot.NoopSnapshotChangeAuditor +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.RouteSpecificationFactory import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotChangeAuditor import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotUpdater import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotsVersions @@ -167,7 +168,10 @@ class ControlPlane private constructor( val snapshotProperties = properties.envoy.snapshot val envoySnapshotFactory = EnvoySnapshotFactory( ingressRoutesFactory = EnvoyIngressRoutesFactory(snapshotProperties, envoyHttpFilters), - egressRoutesFactory = EnvoyEgressRoutesFactory(snapshotProperties), + egressRoutesFactory = EnvoyEgressRoutesFactory( + snapshotProperties.egress, + snapshotProperties.incomingPermissions + ), clustersFactory = EnvoyClustersFactory(snapshotProperties), endpointsFactory = EnvoyEndpointsFactory( snapshotProperties, ServiceTagMetadataGenerator(snapshotProperties.routing.serviceTags) @@ -176,6 +180,7 @@ class ControlPlane private constructor( snapshotProperties, envoyHttpFilters ), + routeSpecificationFactory = RouteSpecificationFactory(snapshotProperties), // Remember when LDS change we have to send RDS again snapshotsVersions = snapshotsVersions, properties = snapshotProperties, 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 8dffdc81c..159829b66 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 @@ -95,7 +95,13 @@ fun Value?.toHeaderFilter(default: String? = null): HeaderFilterSettings? { } } -private class RawDependency(val service: String?, val domain: String?, val domainPattern: String?, val value: Value) +private class RawDependency( + val service: String?, + val domain: String?, + val domainPattern: String?, + val tag: String?, + val value: Value +) private fun defaultRetryPolicy(retryPolicy: RetryPolicyProperties) = if (retryPolicy.enabled) { RetryPolicy( @@ -145,10 +151,19 @@ fun Value?.toOutgoing(properties: SnapshotProperties): Outgoing { val domainPatterns = rawDependencies.filter { it.domainPattern != null } .onEach { validateDomainPatternFormat(it) } .map { DomainPatternDependency(it.domainPattern.orEmpty(), it.value.toSettings(defaultSettingsFromProperties)) } + + val tags = rawDependencies.filter { it.tag != null } + .map { + TagDependency( + tag = it.tag!!, + settings = it.value.toSettings(allServicesDefaultSettings) + ) + } return Outgoing( serviceDependencies = services, domainDependencies = domains, domainPatternDependencies = domainPatterns, + tagDependencies = tags, defaultServiceSettings = allServicesDefaultSettings, allServicesDependencies = allServicesDependencies != null ) @@ -159,6 +174,7 @@ private fun toRawDependency(it: Value): RawDependency { val service = it.field("service")?.stringValue val domain = it.field("domain")?.stringValue val domainPattern = it.field("domainPattern")?.stringValue + val tag = it.field("tag")?.stringValue var count = 0 if (!service.isNullOrBlank()) { count += 1 @@ -169,6 +185,9 @@ private fun toRawDependency(it: Value): RawDependency { if (!domainPattern.isNullOrBlank()) { count += 1 } + if (!tag.isNullOrBlank()) { + count += 1 + } if (count != 1) { throw NodeMetadataValidationException( "Define one of: 'service', 'domain' or 'domainPattern' as an outgoing dependency" @@ -178,6 +197,7 @@ private fun toRawDependency(it: Value): RawDependency { service = service, domain = domain, domainPattern = domainPattern, + tag = tag, value = it ) } @@ -499,27 +519,30 @@ data class Outgoing( private val serviceDependencies: List = emptyList(), private val domainDependencies: List = emptyList(), private val domainPatternDependencies: List = emptyList(), + private val tagDependencies: List = emptyList(), val allServicesDependencies: Boolean = false, val defaultServiceSettings: DependencySettings = DependencySettings() ) { // not declared in primary constructor to exclude from equals(), copy(), etc. - private val deduplicatedDomainDependencies: List = domainDependencies - .map { it.domain to it } - .toMap().values.toList() + private val deduplicatedDomainDependencies: List = + domainDependencies.associateBy { it.domain }.values.toList() + + private val deduplicatedServiceDependencies: List = + serviceDependencies.associateBy { it.service }.values.toList() - private val deduplicatedServiceDependencies: List = serviceDependencies - .map { it.service to it } - .toMap().values.toList() + private val deduplicatedDomainPatternDependencies: List = + domainPatternDependencies.associateBy { it.domainPattern }.values.toList() - private val deduplicatedDomainPatternDependencies: List = domainPatternDependencies - .map { it.domainPattern to it } - .toMap().values.toList() + private val deduplicatedTagDependency: List = + tagDependencies.associateBy { it.tag }.values.toList() fun getDomainDependencies(): List = deduplicatedDomainDependencies fun getServiceDependencies(): List = deduplicatedServiceDependencies fun getDomainPatternDependencies(): List = deduplicatedDomainPatternDependencies + fun getTagDependencies(): List = deduplicatedTagDependency + data class TimeoutPolicy( val idleTimeout: Duration? = null, val connectionIdleTimeout: Duration? = null, @@ -581,6 +604,20 @@ data class DomainPatternDependency( override fun useSsl() = DEFAULT_HTTPS_POLICY } +data class TagDependency( + val tag: String, + val settings: DependencySettings = DependencySettings() +) : Dependency { + companion object { + private const val DEFAULT_HTTP_PORT = 80 + private const val DEFAULT_HTTPS_POLICY = false + } + + override fun getPort() = DEFAULT_HTTP_PORT + + override fun useSsl() = DEFAULT_HTTPS_POLICY +} + data class DependencySettings( val handleInternalRedirect: Boolean = false, val timeoutPolicy: Outgoing.TimeoutPolicy = Outgoing.TimeoutPolicy(), diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidator.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidator.kt index feb5e20c2..f3d335f58 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidator.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidator.kt @@ -18,6 +18,10 @@ class AllDependenciesValidationException(serviceName: String?) : NodeMetadataVal "Blocked service $serviceName from using all dependencies. Only defined services can use all dependencies" ) +class TagDependencyValidationException(serviceName: String?, tags: List) : NodeMetadataValidationException( + "Blocked service $serviceName from using tag dependencies $tags. Only allowed tags are supported." +) + class WildcardPrincipalValidationException(serviceName: String?) : NodeMetadataValidationException( "Blocked service $serviceName from allowing everyone in incoming permissions. " + "Only defined services can use that." @@ -99,10 +103,17 @@ class NodeMetadataValidator( if (!properties.outgoingPermissions.enabled) { return } - validateEndpointPermissionsMethods(metadata) if (hasAllServicesDependencies(metadata) && !isAllowedToHaveAllServiceDependencies(metadata)) { throw AllDependenciesValidationException(metadata.serviceName) } + if (properties.outgoingPermissions.tagPrefix.isNotBlank()) { + val unsupportedTags = metadata.proxySettings.outgoing.getTagDependencies() + .filter { !it.tag.startsWith(properties.outgoingPermissions.tagPrefix) } + .map { it.tag } + if (unsupportedTags.isNotEmpty()) { + throw TagDependencyValidationException(metadata.serviceName, unsupportedTags) + } + } } private fun validateIncomingEndpoints(metadata: NodeMetadata) { @@ -110,6 +121,8 @@ class NodeMetadataValidator( return } + validateEndpointPermissionsMethods(metadata) + metadata.proxySettings.incoming.endpoints.forEach { incomingEndpoint -> val clients = incomingEndpoint.clients.map { it.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 1357b6eac..580811522 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 @@ -24,19 +24,32 @@ import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes.EnvoyEg import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.routes.EnvoyIngressRoutesFactory import java.util.SortedMap +@Suppress("LongParameterList") class EnvoySnapshotFactory( private val ingressRoutesFactory: EnvoyIngressRoutesFactory, private val egressRoutesFactory: EnvoyEgressRoutesFactory, private val clustersFactory: EnvoyClustersFactory, private val endpointsFactory: EnvoyEndpointsFactory, private val listenersFactory: EnvoyListenersFactory, + private val routeSpecificationFactory: RouteSpecificationFactory, private val snapshotsVersions: SnapshotsVersions, private val properties: SnapshotProperties, - private val meterRegistry: MeterRegistry + private val meterRegistry: MeterRegistry, ) { companion object { const val DEFAULT_HTTP_PORT = 80 + + internal fun extractTags(tagPrefix: String, servicesStates: MultiClusterState): Map> = + servicesStates.flatMap { it.servicesState.serviceNameToInstances.asIterable() } + .fold(emptyMap()) { + acc, entry -> + val value = acc.getOrDefault(entry.key, emptySet()) + val newValue = entry.value.instances + .flatMap { it.tags } + .filter { it.startsWith(tagPrefix) } + acc.plus(entry.key to (value + newValue)) + } } fun newSnapshot( @@ -62,7 +75,8 @@ class EnvoySnapshotFactory( clusters = clusters, securedClusters = securedClusters, endpoints = endpoints, - properties = properties.outgoingPermissions + properties = properties.outgoingPermissions, + tags = extractTags(properties.outgoingPermissions.tagPrefix, servicesStates) ) sample.stop(meterRegistry.timer("snapshot-factory.new-snapshot.time")) @@ -147,71 +161,9 @@ class EnvoySnapshotFactory( } } - fun getSnapshotForGroup(group: Group, globalSnapshot: GlobalSnapshot): Snapshot { - val groupSample = Timer.start(meterRegistry) - - val newSnapshotForGroup = newSnapshotForGroup(group, globalSnapshot) - groupSample.stop(meterRegistry.timer("snapshot-factory.get-snapshot-for-group.time")) - return newSnapshotForGroup - } - - private fun getDomainRouteSpecifications(group: Group): Map> { - return group.proxySettings.outgoing.getDomainDependencies().groupBy( - { DomainRoutesGrouper(it.getPort(), it.useSsl()) }, - { - RouteSpecification( - clusterName = it.getClusterName(), - routeDomains = listOf(it.getRouteDomain()), - settings = it.settings - ) - } - ) - } - - private fun getDomainPatternRouteSpecifications(group: Group): RouteSpecification { - return RouteSpecification( - clusterName = properties.dynamicForwardProxy.clusterName, - routeDomains = group.proxySettings.outgoing.getDomainPatternDependencies().map { it.domainPattern }, - settings = group.proxySettings.outgoing.defaultServiceSettings - ) - } - - private fun getServiceRouteSpecifications( - group: Group, - globalSnapshot: GlobalSnapshot - ): Collection { - val definedServicesRoutes = group.proxySettings.outgoing.getServiceDependencies().map { - RouteSpecification( - clusterName = it.service, - routeDomains = listOf(it.service) + getServiceWithCustomDomain(it.service), - settings = it.settings - ) - } - return when (group) { - is ServicesGroup -> { - definedServicesRoutes - } - is AllServicesGroup -> { - val servicesNames = group.proxySettings.outgoing.getServiceDependencies().map { it.service }.toSet() - val allServicesRoutes = globalSnapshot.allServicesNames.subtract(servicesNames).map { - RouteSpecification( - clusterName = it, - routeDomains = listOf(it) + getServiceWithCustomDomain(it), - settings = group.proxySettings.outgoing.defaultServiceSettings - ) - } - allServicesRoutes + definedServicesRoutes - } - } - } - - private fun getServiceWithCustomDomain(it: String): List { - return if (properties.egress.domains.isNotEmpty()) { - properties.egress.domains.map { domain -> "$it$domain" } - } else { - emptyList() - } - } + fun getSnapshotForGroup(group: Group, globalSnapshot: GlobalSnapshot): Snapshot = + meterRegistry.timer("snapshot-factory.get-snapshot-for-group.time") + .record { newSnapshotForGroup(group, globalSnapshot) } private fun getServicesEndpointsForGroup( rateLimitEndpoints: List, @@ -232,10 +184,14 @@ class EnvoySnapshotFactory( ): Snapshot { // TODO(dj): This is where serious refactoring needs to be done - val egressDomainRouteSpecifications = getDomainRouteSpecifications(group) - val egressServiceRouteSpecification = getServiceRouteSpecifications(group, globalSnapshot) + val egressDomainRouteSpecifications = routeSpecificationFactory.domainRouteSpecifications(group) + val egressServiceRouteSpecification = routeSpecificationFactory.serviceRouteSpecifications( + group, + globalSnapshot + ) val egressRouteSpecification = egressServiceRouteSpecification + - egressDomainRouteSpecifications.values.flatten().toSet() + getDomainPatternRouteSpecifications(group) + egressDomainRouteSpecifications.values.flatten().toSet() + + routeSpecificationFactory.domainPatternRouteSpecifications(group) val clusters: List = clustersFactory.getClustersForGroup(group, globalSnapshot) @@ -358,6 +314,81 @@ data class ClusterConfiguration( val http2Enabled: Boolean ) +class RouteSpecificationFactory( + private val properties: SnapshotProperties +) { + fun serviceRouteSpecifications( + group: Group, + globalSnapshot: GlobalSnapshot + ): Collection { + val definedServicesRoutes = group.proxySettings.outgoing.getServiceDependencies().map { + RouteSpecification( + clusterName = it.service, + routeDomains = listOf(it.service) + getServiceWithCustomDomain(it.service), + settings = it.settings + ) + } + val servicesNames = group.proxySettings.outgoing.getServiceDependencies().map { it.service }.toSet() + val definedTagsRoutes = globalSnapshot + .getTagsForDependency(group.proxySettings.outgoing) + .map { + RouteSpecification( + clusterName = it.clusterName, + routeDomains = listOf(it.clusterName) + getServiceWithCustomDomain(it.clusterName), + settings = it.settings + ) + }.distinctBy { it.clusterName } + + return when (group) { + is ServicesGroup -> { + definedServicesRoutes + definedTagsRoutes + } + is AllServicesGroup -> { + val allServicesRoutes = globalSnapshot.allServicesNames + .subtract(servicesNames) + .subtract(definedTagsRoutes.map { it.clusterName }.toSet()) + .map { + RouteSpecification( + clusterName = it, + routeDomains = listOf(it) + getServiceWithCustomDomain(it), + settings = group.proxySettings.outgoing.defaultServiceSettings + ) + } + allServicesRoutes + definedServicesRoutes + definedTagsRoutes + } + } + } + + fun domainRouteSpecifications(group: Group): Map> { + return group.proxySettings.outgoing.getDomainDependencies().groupBy( + { DomainRoutesGrouper(it.getPort(), it.useSsl()) }, + { + RouteSpecification( + clusterName = it.getClusterName(), + routeDomains = listOf(it.getRouteDomain()), + settings = it.settings + ) + } + ) + } + + fun domainPatternRouteSpecifications(group: Group): RouteSpecification { + return RouteSpecification( + clusterName = properties.dynamicForwardProxy.clusterName, + routeDomains = group.proxySettings.outgoing.getDomainPatternDependencies().map { it.domainPattern }, + settings = group.proxySettings.outgoing.defaultServiceSettings + ) + } + + private fun getServiceWithCustomDomain(it: String): List { + return if (properties.egress.domains.isNotEmpty()) { + properties.egress.domains.map { domain -> "$it$domain" } + } else { + emptyList() + } + } +} + class RouteSpecification( val clusterName: String, val routeDomains: List, diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/GlobalSnapshot.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/GlobalSnapshot.kt index ae618cd4f..4891930f6 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/GlobalSnapshot.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/GlobalSnapshot.kt @@ -3,13 +3,34 @@ package pl.allegro.tech.servicemesh.envoycontrol.snapshot import io.envoyproxy.controlplane.cache.SnapshotResources import io.envoyproxy.envoy.config.cluster.v3.Cluster import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment +import pl.allegro.tech.servicemesh.envoycontrol.groups.DependencySettings +import pl.allegro.tech.servicemesh.envoycontrol.groups.Outgoing + +typealias ClusterName = String data class GlobalSnapshot( - val clusters: Map, - val allServicesNames: Set, - val endpoints: Map, - val clusterConfigurations: Map, - val securedClusters: Map + val clusters: Map, + val allServicesNames: Set, + val endpoints: Map, + val clusterConfigurations: Map, + val securedClusters: Map, + val tags: Map> +) { + fun getTagsForDependency( + outgoing: Outgoing + ): List { + val serviceDependencies = outgoing.getServiceDependencies().map { it.service }.toSet() + return outgoing.getTagDependencies().flatMap { tagDependency -> + tags.filterKeys { !serviceDependencies.contains(it) } + .filterValues { it.contains(tagDependency.tag) } + .map { TagDependencySettings(it.key, tagDependency.settings) } + } + } +} + +data class TagDependencySettings( + val clusterName: ClusterName, + val settings: DependencySettings ) @Suppress("LongParameterList") @@ -17,8 +38,9 @@ fun globalSnapshot( clusters: Iterable = emptyList(), endpoints: Iterable = emptyList(), properties: OutgoingPermissionsProperties = OutgoingPermissionsProperties(), - clusterConfigurations: Map = emptyMap(), - securedClusters: List = emptyList() + clusterConfigurations: Map = emptyMap(), + securedClusters: List = emptyList(), + tags: Map> ): GlobalSnapshot { val clusters = SnapshotResources.create(clusters, "").resources() val securedClusters = SnapshotResources.create(securedClusters, "").resources() @@ -29,7 +51,8 @@ fun globalSnapshot( securedClusters = securedClusters, endpoints = endpoints, allServicesNames = allServicesNames, - clusterConfigurations = clusterConfigurations + clusterConfigurations = clusterConfigurations, + tags = tags ) } 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 55bc56ff5..c61da55b5 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 @@ -76,6 +76,7 @@ class OutgoingPermissionsProperties { var enabled = false var allServicesDependencies = AllServicesDependenciesProperties() var servicesAllowedToUseWildcard: MutableSet = mutableSetOf() + var tagPrefix = "" } class AllServicesDependenciesProperties { 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 1a25e5a7f..27f4282f6 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 @@ -110,7 +110,8 @@ class EnvoyClustersFactory( } fun getClustersForGroup(group: Group, globalSnapshot: GlobalSnapshot): List = - getEdsClustersForGroup(group, globalSnapshot) + getStrictDnsClustersForGroup(group) + clustersForJWT + + getEdsClustersForGroup(group, globalSnapshot) + + getStrictDnsClustersForGroup(group) + clustersForJWT + getRateLimitClusterForGroup(group, globalSnapshot) private fun clusterForOAuthProvider(provider: OAuthProvider): Cluster? { @@ -190,21 +191,31 @@ class EnvoyClustersFactory( globalSnapshot.clusters } - val serviceDependencies = group.proxySettings.outgoing.getServiceDependencies().associateBy { it.service } + val serviceSettings = + group.proxySettings.outgoing.getServiceDependencies() + .associate { it.service to it.settings } + + val serviceFromTagSettings = globalSnapshot + .getTagsForDependency(group.proxySettings.outgoing) + .distinctBy { it.clusterName } + .associate { it.clusterName to it.settings } val clustersForGroup = when (group) { - is ServicesGroup -> serviceDependencies.mapNotNull { - createClusterForGroup(it.value.settings, clusters[it.key]) + is ServicesGroup -> serviceSettings.mapNotNull { + createClusterForGroup(it.value, clusters[it.key]) + } + serviceFromTagSettings.mapNotNull { + createClusterForGroup(it.value, clusters[it.key]) } is AllServicesGroup -> { - globalSnapshot.allServicesNames.mapNotNull { - val dependency = serviceDependencies[it] - if (dependency != null && dependency.settings.timeoutPolicy.connectionIdleTimeout != null) { - createClusterForGroup(dependency.settings, clusters[it]) - } else { - createClusterForGroup(group.proxySettings.outgoing.defaultServiceSettings, clusters[it]) + globalSnapshot.allServicesNames + .mapNotNull { + val settings = serviceSettings[it] ?: serviceFromTagSettings[it] + if (settings != null && settings.timeoutPolicy.connectionIdleTimeout != null) { + createClusterForGroup(settings, clusters[it]) + } else { + createClusterForGroup(group.proxySettings.outgoing.defaultServiceSettings, clusters[it]) + } } - } } } diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactory.kt index 18170fe85..ccb27659a 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactory.kt @@ -22,7 +22,6 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.TlsParameters -import pl.allegro.tech.servicemesh.envoycontrol.groups.Dependency import pl.allegro.tech.servicemesh.envoycontrol.groups.DomainDependency import pl.allegro.tech.servicemesh.envoycontrol.groups.Group import pl.allegro.tech.servicemesh.envoycontrol.groups.ListenersConfig @@ -203,19 +202,20 @@ class EnvoyListenersFactory( { it.getPort() }, { it } ).toMap() - val httpProxy = (group.proxySettings.outgoing.getDomainDependencies() + - group.proxySettings.outgoing.getServiceDependencies()).filter { + val httpProxyPorts = (group.proxySettings.outgoing.getDomainDependencies() + + group.proxySettings.outgoing.getServiceDependencies() + + group.proxySettings.outgoing.getTagDependencies()).filter { !it.useSsl() - }.groupBy( - { it.getPort() }, { it } - ).toMutableMap() + } + .map { it.getPort() } + .toMutableSet() if (group.proxySettings.outgoing.allServicesDependencies) { - httpProxy[DEFAULT_HTTP_PORT] = group.proxySettings.outgoing.getServiceDependencies() + httpProxyPorts.add(DEFAULT_HTTP_PORT) } return createEgressTcpProxyVirtualListener(tcpProxy) + - createEgressHttpProxyVirtualListener(httpProxy.toMap(), group, globalSnapshot) + createEgressHttpProxyVirtualListener(httpProxyPorts, group, globalSnapshot) } private fun createEgressFilter( @@ -290,21 +290,21 @@ class EnvoyListenersFactory( } private fun createEgressHttpProxyVirtualListener( - portAndDomains: Map>, + ports: Set, group: Group, globalSnapshot: GlobalSnapshot ): List { - return portAndDomains.map { + return ports.map { port -> Listener.newBuilder() - .setName("$DOMAIN_PROXY_LISTENER_ADDRESS:${it.key}") + .setName("$DOMAIN_PROXY_LISTENER_ADDRESS:$port") .setAddress( Address.newBuilder().setSocketAddress( SocketAddress.newBuilder() - .setPortValue(it.key) + .setPortValue(port) .setAddress(DOMAIN_PROXY_LISTENER_ADDRESS) ) ) - .addFilterChains(createHttpProxyFilterChainForDomains(group, it.key, globalSnapshot)) + .addFilterChains(createHttpProxyFilterChainForDomains(group, port, globalSnapshot)) .setTrafficDirection(TrafficDirection.OUTBOUND) .setDeprecatedV1( Listener.DeprecatedV1.newBuilder() diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/AccessLogFilter.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/AccessLogFilter.kt index 8591841a8..e5c001032 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/AccessLogFilter.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/AccessLogFilter.kt @@ -17,14 +17,13 @@ import io.envoyproxy.envoy.config.route.v3.HeaderMatcher import io.envoyproxy.envoy.extensions.access_loggers.file.v3.FileAccessLog import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher import pl.allegro.tech.servicemesh.envoycontrol.groups.AccessLogFilterSettings -import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.AccessLogProperties import pl.allegro.tech.servicemesh.envoycontrol.utils.ComparisonFilterSettings import pl.allegro.tech.servicemesh.envoycontrol.utils.HeaderFilterSettings class AccessLogFilter( - snapshotProperties: SnapshotProperties + private val accessLog: AccessLogProperties ) { - private val accessLog = snapshotProperties.dynamicListeners.httpFilters.accessLog private val accessLogTimeFormat = stringValue(accessLog.timeFormat) private val accessLogMessageFormat = stringValue(accessLog.messageFormat) private val accessLogLevel = stringValue(accessLog.level) diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/HttpConnectionManagerFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/HttpConnectionManagerFactory.kt index 336ecc73d..a4b5099bc 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/HttpConnectionManagerFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/HttpConnectionManagerFactory.kt @@ -37,7 +37,7 @@ class HttpConnectionManagerFactory( ).filter private val defaultApiConfigSourceV3: ApiConfigSource = apiConfigSource() - private val accessLogFilter = AccessLogFilter(snapshotProperties) + private val accessLogFilter = AccessLogFilter(snapshotProperties.dynamicListeners.httpFilters.accessLog) @SuppressWarnings("LongParameterList") fun createFilter( 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 7daca8a02..01502311e 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 @@ -20,12 +20,14 @@ import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher import pl.allegro.tech.servicemesh.envoycontrol.groups.RateLimitedRetryBackOff import pl.allegro.tech.servicemesh.envoycontrol.groups.RetryBackOff import pl.allegro.tech.servicemesh.envoycontrol.groups.RetryHostPredicate +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.EgressProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.IncomingPermissionsProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.RouteSpecification -import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties import pl.allegro.tech.servicemesh.envoycontrol.groups.RetryPolicy as EnvoyControlRetryPolicy class EnvoyEgressRoutesFactory( - private val properties: SnapshotProperties + private val egressProperties: EgressProperties, + private val incomingPermissionsProperties: IncomingPermissionsProperties ) { /** @@ -44,8 +46,8 @@ class EnvoyEgressRoutesFactory( ) .setRoute( RouteAction.newBuilder() - .setIdleTimeout(Durations.fromMillis(properties.egress.commonHttp.idleTimeout.toMillis())) - .setTimeout(Durations.fromMillis(properties.egress.commonHttp.requestTimeout.toMillis())) + .setIdleTimeout(Durations.fromMillis(egressProperties.commonHttp.idleTimeout.toMillis())) + .setTimeout(Durations.fromMillis(egressProperties.commonHttp.requestTimeout.toMillis())) .setCluster("envoy-original-destination") ) ) @@ -62,7 +64,7 @@ class EnvoyEgressRoutesFactory( ) .setDirectResponse( DirectResponseAction.newBuilder() - .setStatus(properties.egress.clusterNotFoundStatusCode) + .setStatus(egressProperties.clusterNotFoundStatusCode) ) ) .build() @@ -97,20 +99,20 @@ class EnvoyEgressRoutesFactory( .addAllVirtualHosts( virtualHosts + originalDestinationRoute + wildcardRoute ).also { - if (properties.incomingPermissions.enabled) { + if (incomingPermissionsProperties.enabled) { it.addRequestHeadersToAdd( HeaderValueOption.newBuilder() .setHeader( HeaderValue.newBuilder() - .setKey(properties.incomingPermissions.serviceNameHeader) + .setKey(incomingPermissionsProperties.serviceNameHeader) .setValue(serviceName) ).setAppend(BoolValue.of(false)) ) } } - if (properties.egress.headersToRemove.isNotEmpty()) { - routeConfiguration.addAllRequestHeadersToRemove(properties.egress.headersToRemove) + if (egressProperties.headersToRemove.isNotEmpty()) { + routeConfiguration.addAllRequestHeadersToRemove(egressProperties.headersToRemove) } if (addUpstreamAddressHeader) { @@ -207,8 +209,8 @@ class EnvoyEgressRoutesFactory( .addAllVirtualHosts( virtualHosts + originalDestinationRoute + wildcardRoute ) - if (properties.egress.headersToRemove.isNotEmpty()) { - routeConfiguration.addAllRequestHeadersToRemove(properties.egress.headersToRemove) + if (egressProperties.headersToRemove.isNotEmpty()) { + routeConfiguration.addAllRequestHeadersToRemove(egressProperties.headersToRemove) } return routeConfiguration.build() } @@ -235,8 +237,8 @@ class EnvoyEgressRoutesFactory( routeAction.internalRedirectPolicy = InternalRedirectPolicy.newBuilder().build() } - if (properties.egress.hostHeaderRewriting.enabled && routeSpecification.settings.rewriteHostHeader) { - routeAction.hostRewriteHeader = properties.egress.hostHeaderRewriting.customHostHeader + if (egressProperties.hostHeaderRewriting.enabled && routeSpecification.settings.rewriteHostHeader) { + routeAction.hostRewriteHeader = egressProperties.hostHeaderRewriting.customHostHeader } return routeAction 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 f293b9ad6..583b34ed9 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 @@ -163,8 +163,8 @@ class EnvoyIngressRoutesFactory( addAllRetriableStatusCodes(retryProps.retriableStatusCodes) }.build() - val defaultRetryPolicy: RetryPolicy = retryPolicy(properties.localService.retryPolicy.default) - val perMethodRetryPolicies: Map = properties.localService.retryPolicy.perHttpMethod + private val defaultRetryPolicy: RetryPolicy = retryPolicy(properties.localService.retryPolicy.default) + private val perMethodRetryPolicies: Map = properties.localService.retryPolicy.perHttpMethod .filter { it.value.enabled } .map { HttpMethod.valueOf(it.key) to retryPolicy(it.value) } .toMap() diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoySnapshotFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoySnapshotFactoryTest.kt index 41fe92da5..55f8327b5 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoySnapshotFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoySnapshotFactoryTest.kt @@ -27,6 +27,7 @@ import pl.allegro.tech.servicemesh.envoycontrol.groups.ServicesGroup import pl.allegro.tech.servicemesh.envoycontrol.groups.with import pl.allegro.tech.servicemesh.envoycontrol.snapshot.EnvoySnapshotFactory import pl.allegro.tech.servicemesh.envoycontrol.snapshot.GlobalSnapshot +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.RouteSpecificationFactory import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotsVersions import pl.allegro.tech.servicemesh.envoycontrol.snapshot.outgoingTimeoutPolicy @@ -348,11 +349,12 @@ class EnvoySnapshotFactoryTest { emptyList(), emptyList() ) { Metadata.getDefaultInstance() } ) - val egressRoutesFactory = EnvoyEgressRoutesFactory(properties) + val egressRoutesFactory = EnvoyEgressRoutesFactory(properties.egress, properties.incomingPermissions) val clustersFactory = EnvoyClustersFactory(properties) val endpointsFactory = EnvoyEndpointsFactory(properties, ServiceTagMetadataGenerator()) val envoyHttpFilters = EnvoyHttpFilters.defaultFilters(properties) val listenersFactory = EnvoyListenersFactory(properties, envoyHttpFilters) + val routeSpecificationFactory = RouteSpecificationFactory(properties) val snapshotsVersions = SnapshotsVersions() val meterRegistry: MeterRegistry = SimpleMeterRegistry() @@ -362,6 +364,7 @@ class EnvoySnapshotFactoryTest { clustersFactory, endpointsFactory, listenersFactory, + routeSpecificationFactory, snapshotsVersions, properties, meterRegistry @@ -374,7 +377,8 @@ class EnvoySnapshotFactoryTest { clusters.map { it.name }.toSet(), SnapshotResources.create(emptyList(), "v1").resources(), emptyMap(), - SnapshotResources.create(clusters.toList(), "v3").resources() + SnapshotResources.create(clusters.toList(), "v3").resources(), + emptyMap() ) } 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..ed88bf5b0 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 @@ -54,21 +54,6 @@ class NodeMetadataTest { ) } - private fun snapshotProperties( - allServicesDependenciesIdentifier: String = "*", - handleInternalRedirect: Boolean = false, - idleTimeout: String = "120s", - connectionIdleTimeout: String = "120s", - requestTimeout: String = "120s" - ) = SnapshotProperties().apply { - outgoingPermissions.allServicesDependencies.identifier = allServicesDependenciesIdentifier - egress.handleInternalRedirect = handleInternalRedirect - egress.commonHttp.idleTimeout = Duration.ofNanos(Durations.toNanos(Durations.parse(idleTimeout))) - egress.commonHttp.connectionIdleTimeout = - Duration.ofNanos(Durations.toNanos(Durations.parse(connectionIdleTimeout))) - egress.commonHttp.requestTimeout = Duration.ofNanos(Durations.toNanos(Durations.parse(requestTimeout))) - } - @Test fun `should reject endpoint with both path and pathPrefix defined`() { // given @@ -270,82 +255,6 @@ class NodeMetadataTest { assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } - @Test - fun `should reject dependency with unsupported protocol in domain field `() { - // given - val proto = outgoingDependenciesProto { - withDomain(url = "ftp://domain") - } - - // expects - val exception = assertThrows { proto.toOutgoing(snapshotProperties()) } - assertThat(exception.status.description) - .isEqualTo("Unsupported protocol for domain dependency for domain ftp://domain") - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } - - @Test - fun `should reject domain dependency with unsupported all services dependencies identifier`() { - // given - val proto = outgoingDependenciesProto { - withDomain(url = "*") - } - val properties = snapshotProperties(allServicesDependenciesIdentifier = "*") - - // expects - val exception = assertThrows { proto.toOutgoing(properties) } - assertThat(exception.status.description) - .isEqualTo("Unsupported 'all serviceDependencies identifier' for domain dependency: *") - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } - - @Test - fun `should accept domain dependency`() { - // given - val proto = outgoingDependenciesProto { - withDomain(url = "http://domain") - } - - // when - val outgoing = proto.toOutgoing(snapshotProperties()) - - // expects - val dependency = outgoing.getDomainDependencies().single() - assertThat(dependency.domain).isEqualTo("http://domain") - } - - @Test - fun `should return correct host and default port for domain dependency`() { - // given - val proto = outgoingDependenciesProto { - withDomain(url = "http://domain") - } - - // when - val outgoing = proto.toOutgoing(snapshotProperties()) - - // expects - val dependency = outgoing.getDomainDependencies().single() - assertThat(dependency.getHost()).isEqualTo("domain") - assertThat(dependency.getPort()).isEqualTo(80) - } - - @Test - fun `should return correct host and default port for https domain dependency`() { - // given - val proto = outgoingDependenciesProto { - withDomain(url = "https://domain") - } - - // when - val outgoing = proto.toOutgoing(snapshotProperties()) - - // expects - val dependency = outgoing.getDomainDependencies().single() - assertThat(dependency.getHost()).isEqualTo("domain") - assertThat(dependency.getPort()).isEqualTo(443) - } - @Test fun `should return retry policy`() { // given @@ -438,217 +347,425 @@ class NodeMetadataTest { } @Test - fun `should deduplicate domains dependencies based on url`() { + fun `should accept incoming settings with custom healthCheckPath`() { // given - val proto = outgoingDependenciesProto { - withDomain(url = "http://domain", requestTimeout = "8s", idleTimeout = "8s") - withDomain(url = "http://domain", requestTimeout = "10s", idleTimeout = "10s", connectionIdleTimeout = "5s") - withDomain(url = "http://domain2") - } + val proto = proxySettingsProto( + incomingSettings = true, + path = "/path", + healthCheckPath = "/status/ping" + ) + val incoming = proto.structValue?.fieldsMap?.get("incoming").toIncoming(snapshotProperties()) - // when - val outgoing = proto.toOutgoing(snapshotProperties()) + // expects + assertThat(incoming.healthCheck.clusterName).isEqualTo("local_service_health_check") + assertThat(incoming.healthCheck.path).isEqualTo("/status/ping") + assertThat(incoming.healthCheck.hasCustomHealthCheck()).isTrue() + } + + @Test + fun `should set empty healthCheckPath for incoming settings when healthCheckPath is empty`() { + // given + val proto = proxySettingsProto( + incomingSettings = true, + path = "/path" + ) + val incoming = proto.structValue?.fieldsMap?.get("incoming").toIncoming(snapshotProperties()) // expects - val dependencies = outgoing.getDomainDependencies() - assertThat(dependencies).hasSize(2) - assertThat(dependencies[0].getHost()).isEqualTo("domain") - assertThat(dependencies[0].getPort()).isEqualTo(80) - assertThat(dependencies[0].settings).hasTimeouts( - idleTimeout = "10s", - requestTimeout = "10s", - connectionIdleTimeout = "5s" + assertThat(incoming.healthCheck.clusterName).isEqualTo("local_service_health_check") + assertThat(incoming.healthCheck.path).isEqualTo("") + assertThat(incoming.healthCheck.hasCustomHealthCheck()).isFalse() + } + + @Test + fun `should set healthCheckPath and healthCheckClusterName for incoming settings`() { + // given + val proto = proxySettingsProto( + incomingSettings = true, + path = "/path", + healthCheckPath = "/status/ping", + healthCheckClusterName = "local_service_health_check" ) - assertThat(dependencies[1].getHost()).isEqualTo("domain2") - assertThat(dependencies[1].getPort()).isEqualTo(80) + val incoming = proto.structValue?.fieldsMap?.get("incoming").toIncoming(snapshotProperties()) + + // expects + assertThat(incoming.healthCheck.clusterName).isEqualTo("local_service_health_check") + assertThat(incoming.healthCheck.path).isEqualTo("/status/ping") + assertThat(incoming.healthCheck.hasCustomHealthCheck()).isTrue() } @Test - fun `should deduplicate services dependencies based on serviceName`() { + fun `should reject configuration with number timeout format`() { // given - val proto = outgoingDependenciesProto { - withService(serviceName = "service-1", requestTimeout = "8s", idleTimeout = "8s") - withService( - serviceName = "service-1", - requestTimeout = "10s", - idleTimeout = "10s", - connectionIdleTimeout = "10s" - ) - withService(serviceName = "service-2") - } + val proto = Value.newBuilder().setNumberValue(10.0).build() // when - val outgoing = proto.toOutgoing(snapshotProperties()) + val exception = assertThrows { proto.toDuration() } - // expect - val dependencies = outgoing.getServiceDependencies() - assertThat(dependencies).hasSize(2) - assertThat(dependencies[0].service).isEqualTo("service-1") - assertThat(dependencies[0].settings).hasTimeouts( - idleTimeout = "10s", - requestTimeout = "10s", - connectionIdleTimeout = "10s" + // then + assertThat(exception.status.description).isEqualTo( + "Timeout definition has number format" + + " but should be in string format and ends with 's'" ) - assertThat(dependencies[1].service).isEqualTo("service-2") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } @Test - fun `should return custom port for domain dependency if it was defined`() { + fun `should reject configuration with incorrect string timeout format`() { // given - val proto = outgoingDependenciesProto { - withDomain(url = "http://domain:1234") - } + val proto = Value.newBuilder().setStringValue("20").build() // when - val outgoing = proto.toOutgoing(snapshotProperties()) + val exception = assertThrows { proto.toDuration() } - // expects - val dependency = outgoing.getDomainDependencies().single() - assertThat(dependency.getPort()).isEqualTo(1234) + // then + assertThat(exception.status.description).isEqualTo( + "Timeout definition has incorrect format: " + + "Invalid duration string: 20" + ) + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } @Test - fun `should return correct names for domain dependency without port specified`() { + fun `should return duration when correct configuration provided`() { // given - val proto = outgoingDependenciesProto { - withDomain(url = "http://domain.pl") - } + val proto = Value.newBuilder().setStringValue("20s").build() // when - val outgoing = proto.toOutgoing(snapshotProperties()) + val duration = proto.toDuration() - // expects - val dependency = outgoing.getDomainDependencies().single() - assertThat(dependency.getClusterName()).isEqualTo("domain_pl_80") - assertThat(dependency.getRouteDomain()).isEqualTo("domain.pl") + // then + assertThat(duration).isNotNull + assertThat(duration!!.seconds).isEqualTo(20L) } @Test - fun `should return correct names for domain dependency with port specified`() { + fun `should return null when empty value provided`() { // given - val proto = outgoingDependenciesProto { - withDomain(url = "http://domain.pl:80") - } + val proto = Value.newBuilder().build() // when - val outgoing = proto.toOutgoing(snapshotProperties()) + val duration = proto.toDuration() - // expects - val dependency = outgoing.getDomainDependencies().single() - assertThat(dependency.getClusterName()).isEqualTo("domain_pl_80") - assertThat(dependency.getRouteDomain()).isEqualTo("domain.pl:80") + // then + assertThat(duration).isNull() } - @Test - fun `should accept domain pattern dependency`() { + @ParameterizedTest + @MethodSource("validComparisonFilterData") + fun `should set statusCodeFilter for accessLogFilter`(input: String, op: ComparisonFilter.Op, code: Int) { // given - val proto = outgoingDependenciesProto { - withDomainPattern(pattern = "*.example.com") - } + val proto = accessLogFilterProto(value = input, fieldName = "status_code_filter") // when - val outgoing = proto.toOutgoing(snapshotProperties()) + val statusCodeFilterSettings = proto.structValue?.fieldsMap?.get("status_code_filter").toComparisonFilter() // expects - val dependency = outgoing.getDomainPatternDependencies().single() - assertThat(dependency.domainPattern).isEqualTo("*.example.com") + assertThat(statusCodeFilterSettings?.comparisonCode).isEqualTo(code) + assertThat(statusCodeFilterSettings?.comparisonOperator).isEqualTo(op) } - @Test - fun `should reject domain pattern dependency with schema`() { + @ParameterizedTest + @MethodSource("invalidComparisonFilterData") + fun `should throw exception for invalid status code filter data`(input: String) { // given - val proto = outgoingDependenciesProto { - withDomainPattern(pattern = "http://example.com") - } + val proto = accessLogFilterProto(value = input, fieldName = "status_code_filter") // expects - val exception = assertThrows { proto.toOutgoing(snapshotProperties()) } - assertThat(exception.status.description).isEqualTo( - "Unsupported format for domainPattern: domainPattern cannot contain a schema like http:// or https://" - ) + val exception = assertThrows { + proto.structValue?.fieldsMap?.get("status_code_filter").toComparisonFilter() + } + assertThat(exception.status.description) + .isEqualTo("Invalid access log comparison filter. Expected OPERATOR:VALUE") assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } - @Test - fun `should accept service dependency with redirect policy defined`() { + @ParameterizedTest + @MethodSource("errorMessages") + fun `should throw exception for null value comparison filter data`(filter: String, errorMessage: String) { // given - val proto = outgoingDependenciesProto { - withService(serviceName = "service-1", handleInternalRedirect = true) - } - - // when - val outgoing = proto.toOutgoing(snapshotProperties()) + val proto = accessLogFilterProto(value = null, fieldName = filter) - // expect - val dependency = outgoing.getServiceDependencies().single() - assertThat(dependency.service).isEqualTo("service-1") - assertThat(dependency.settings.handleInternalRedirect).isEqualTo(true) + // expects + val exception = assertThrows { + proto.structValue?.fieldsMap?.get(filter).toComparisonFilter() + } + assertThat(exception.status.description) + .isEqualTo(errorMessage) + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @ParameterizedTest + @MethodSource("validComparisonFilterData") + fun `should set duration filter for accessLogFilter`(input: String, op: ComparisonFilter.Op, code: Int) { + // given + val proto = accessLogFilterProto(value = input, fieldName = "duration_filter") + + // when + val durationFilterSettings = proto.structValue?.fieldsMap?.get("duration_filter").toComparisonFilter() + + // expects + assertThat(durationFilterSettings?.comparisonCode).isEqualTo(code) + assertThat(durationFilterSettings?.comparisonOperator).isEqualTo(op) + } + + @ParameterizedTest + @MethodSource("invalidComparisonFilterData") + fun `should throw exception for invalid duration filter data`(input: String) { + // given + val proto = accessLogFilterProto(value = input, fieldName = "duration_filter") + + // expects + val exception = assertThrows { + proto.structValue?.fieldsMap?.get("duration_filter").toComparisonFilter() + } + assertThat(exception.status.description) + .isEqualTo("Invalid access log comparison filter. Expected OPERATOR:VALUE") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } @Test - fun `should accept incoming settings with custom healthCheckPath`() { + fun `should throw exception for null value header filter data`() { // given - val proto = proxySettingsProto( - incomingSettings = true, - path = "/path", - healthCheckPath = "/status/ping" + val proto = accessLogFilterProto(value = null, fieldName = "header_filter") + + // expects + val exception = assertThrows { + proto.structValue?.fieldsMap?.get("header_filter").toHeaderFilter() + } + assertThat(exception.status.description) + .isEqualTo("Invalid access log header filter. Expected HEADER_NAME:REGEX") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @Test + fun `should set header filter for accessLogFilter`() { + // given + val proto = accessLogFilterProto(value = "test:^((.+):(.+))$", fieldName = "header_filter") + + // when + val headerFilterSettings = proto.structValue?.fieldsMap?.get("header_filter").toHeaderFilter() + + // expects + assertThat(headerFilterSettings?.headerName).isEqualTo("test") + assertThat(headerFilterSettings?.regex).isEqualTo("^((.+):(.+))\$") + } + + @Test + fun `should throw exception for invalid header filter data`() { + // given + val proto = accessLogFilterProto(value = "test;test", fieldName = "header_filter") + + // expects + val exception = assertThrows { + proto.structValue?.fieldsMap?.get("header_filter").toHeaderFilter() + } + assertThat(exception.status.description) + .isEqualTo("Invalid access log header filter. Expected HEADER_NAME:REGEX") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @Test + fun `should set response flag filter for accessLogFilter`() { + // given + val availableFlags = listOf( + "UH", "UF", "UO", "NR", "URX", "NC", "DT", "DC", "LH", "UT", "LR", "UR", + "UC", "DI", "FI", "RL", "UAEX", "RLSE", "IH", "SI", "DPE", "UPE", "UMSDR", + "OM", "DF" ) - val incoming = proto.structValue?.fieldsMap?.get("incoming").toIncoming(snapshotProperties()) + val proto = accessLogFilterProto(value = availableFlags.joinToString(","), fieldName = "response_flag_filter") + + // when + val responseFlags = proto.structValue?.fieldsMap?.get("response_flag_filter").toResponseFlagFilter() // expects - assertThat(incoming.healthCheck.clusterName).isEqualTo("local_service_health_check") - assertThat(incoming.healthCheck.path).isEqualTo("/status/ping") - assertThat(incoming.healthCheck.hasCustomHealthCheck()).isTrue() + assertThat(responseFlags).isEqualTo(availableFlags) } @Test - fun `should parse allServiceDependency with timeouts configuration`() { + fun `should throw exception for invalid response flag filter data`() { + // given + val availableFlagsAndInvalid = listOf( + "UH", "UF", "UO", "NR", "URX", "NC", "DT", "DC", "LH", "UT", "LR", "UR", + "UC", "DI", "FI", "RL", "UAEX", "RLSE", "IH", "SI", "DPE", "UPE", "UMSDR", + "OM", "DF", "invalid" + ) + val proto = + accessLogFilterProto(value = availableFlagsAndInvalid.joinToString(","), fieldName = "response_flag_filter") + + // expects + val exception = assertThrows { + proto.structValue?.fieldsMap?.get("response_flag_filter").toResponseFlagFilter() + } + assertThat(exception.status.description) + .isEqualTo("Invalid access log response flag filter. Expected valid values separated by comma") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @Test + fun `should set not health check filter for accessLogFilter`() { + // given + val proto = accessLogBooleanFilterProto(value = true, fieldName = "not_health_check_filter") + + // when + val value = proto.structValue?.fieldsMap?.get("not_health_check_filter")?.boolValue + + // expects + assertThat(value).isEqualTo(true) + } + + @Test + fun `should throw exception for null value response flag filter data`() { + // given + val proto = accessLogFilterProto(value = null, fieldName = "response_flag_filter") + + // expects + val exception = assertThrows { + proto.structValue?.fieldsMap?.get("response_flag_filter").toResponseFlagFilter() + } + assertThat(exception.status.description) + .isEqualTo("Invalid access log response flag filter. Expected valid values separated by comma") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + private fun createJwtSnapshotProperties(): SnapshotProperties { + val snapshotProperties = SnapshotProperties() + val jwtFilterProperties = JwtFilterProperties() + val oauthProviders = mapOf( + "oauth2-mock" to + OAuthProvider( + jwksUri = URI.create("http://localhost:8080/jwks-address/"), + clusterName = "oauth" + ) + ) + jwtFilterProperties.providers = oauthProviders + snapshotProperties.jwt = jwtFilterProperties + + return snapshotProperties + } +} + +class DomainDependencyTest { + + @Test + fun `should accept domain dependency`() { // given val proto = outgoingDependenciesProto { - withService(serviceName = "*", idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "10s") + withDomain(url = "http://domain") } // when - val outgoing = proto.toOutgoing(snapshotProperties(allServicesDependenciesIdentifier = "*")) + val outgoing = proto.toOutgoing(snapshotProperties()) // expects - assertThat(outgoing.allServicesDependencies).isTrue() - assertThat(outgoing.defaultServiceSettings).hasTimeouts( + val dependency = outgoing.getDomainDependencies().single() + assertThat(dependency.domain).isEqualTo("http://domain") + } + + @Test + fun `should deduplicate domains dependencies based on url`() { + // given + val proto = outgoingDependenciesProto { + withDomain(url = "http://domain", requestTimeout = "8s", idleTimeout = "8s") + withDomain(url = "http://domain", requestTimeout = "10s", idleTimeout = "10s", connectionIdleTimeout = "5s") + withDomain(url = "http://domain2") + } + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) + + // expects + val dependencies = outgoing.getDomainDependencies() + assertThat(dependencies).hasSize(2) + assertThat(dependencies[0].getHost()).isEqualTo("domain") + assertThat(dependencies[0].getPort()).isEqualTo(80) + assertThat(dependencies[0].settings).hasTimeouts( idleTimeout = "10s", requestTimeout = "10s", - connectionIdleTimeout = "10s" + connectionIdleTimeout = "5s" ) - assertThat(outgoing.getServiceDependencies()).isEmpty() + assertThat(dependencies[1].getHost()).isEqualTo("domain2") + assertThat(dependencies[1].getPort()).isEqualTo(80) } @Test - fun `should parse allServiceDependency and use requestTimeout from properties`() { + fun `should return custom port for domain dependency if it was defined`() { // given val proto = outgoingDependenciesProto { - withService(serviceName = "*", idleTimeout = "10s", requestTimeout = null, connectionIdleTimeout = "10s") + withDomain(url = "http://domain:1234") } - val properties = snapshotProperties(allServicesDependenciesIdentifier = "*", requestTimeout = "5s") + // when + val outgoing = proto.toOutgoing(snapshotProperties()) - val outgoing = proto.toOutgoing(properties) + // expects + val dependency = outgoing.getDomainDependencies().single() + assertThat(dependency.getPort()).isEqualTo(1234) + } + + @Test + fun `should return correct names for domain dependency without port specified`() { + // given + val proto = outgoingDependenciesProto { + withDomain(url = "http://domain.pl") + } + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) // expects - assertThat(outgoing.allServicesDependencies).isTrue() - assertThat(outgoing.defaultServiceSettings).hasTimeouts( - idleTimeout = "10s", - requestTimeout = "5s", - connectionIdleTimeout = "10s" - ) - assertThat(outgoing.getServiceDependencies()).isEmpty() + val dependency = outgoing.getDomainDependencies().single() + assertThat(dependency.getClusterName()).isEqualTo("domain_pl_80") + assertThat(dependency.getRouteDomain()).isEqualTo("domain.pl") } @Test - fun `should parse allServiceDependency and use idleTimeout from properties`() { + fun `should return correct names for domain dependency with port specified`() { // given val proto = outgoingDependenciesProto { - withService(serviceName = "*", idleTimeout = null, requestTimeout = "10s", connectionIdleTimeout = "10s") + withDomain(url = "http://domain.pl:80") } - val properties = snapshotProperties(allServicesDependenciesIdentifier = "*", idleTimeout = "5s") + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) + + // expects + val dependency = outgoing.getDomainDependencies().single() + assertThat(dependency.getClusterName()).isEqualTo("domain_pl_80") + assertThat(dependency.getRouteDomain()).isEqualTo("domain.pl:80") + } + + @Test + fun `should parse domain dependencies and for missing config use config defined in properties even if allServiceDependency is defined`() { + // given + val proto = outgoingDependenciesProto { + withService(serviceName = "*", idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "10s") + withDomain( + url = "http://domain-name-1", + idleTimeout = "5s", + requestTimeout = null, + connectionIdleTimeout = null + ) + withDomain( + url = "http://domain-name-2", + idleTimeout = null, + requestTimeout = "4s", + connectionIdleTimeout = null + ) + withDomain( + url = "http://domain-name-3", + idleTimeout = null, + requestTimeout = null, + connectionIdleTimeout = "3s" + ) + withDomain( + url = "http://domain-name-4", + idleTimeout = null, + requestTimeout = null, + connectionIdleTimeout = null + ) + } + val properties = snapshotProperties(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") // when val outgoing = proto.toOutgoing(properties) @@ -656,59 +773,119 @@ class NodeMetadataTest { // expects assertThat(outgoing.allServicesDependencies).isTrue() assertThat(outgoing.defaultServiceSettings).hasTimeouts( - idleTimeout = "5s", + idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "10s" ) - assertThat(outgoing.getServiceDependencies()).isEmpty() + + outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-1") + .hasTimeouts(idleTimeout = "5s", requestTimeout = "12s", connectionIdleTimeout = "12s") + outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-2") + .hasTimeouts(idleTimeout = "12s", requestTimeout = "4s", connectionIdleTimeout = "12s") + outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-3") + .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "3s") + outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-4") + .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") + } + + @Test + fun `should reject dependency with unsupported protocol in domain field `() { + // given + val proto = outgoingDependenciesProto { + withDomain(url = "ftp://domain") + } + + // expects + val exception = assertThrows { proto.toOutgoing(snapshotProperties()) } + assertThat(exception.status.description) + .isEqualTo("Unsupported protocol for domain dependency for domain ftp://domain") + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @Test + fun `should return correct host and default port for domain dependency`() { + // given + val proto = outgoingDependenciesProto { + withDomain(url = "http://domain") + } + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) + + // expects + val dependency = outgoing.getDomainDependencies().single() + assertThat(dependency.getHost()).isEqualTo("domain") + assertThat(dependency.getPort()).isEqualTo(80) + } + + @Test + fun `should return correct host and default port for https domain dependency`() { + // given + val proto = outgoingDependenciesProto { + withDomain(url = "https://domain") + } + + // when + val outgoing = proto.toOutgoing(snapshotProperties()) + + // expects + val dependency = outgoing.getDomainDependencies().single() + assertThat(dependency.getHost()).isEqualTo("domain") + assertThat(dependency.getPort()).isEqualTo(443) + } + + fun List.assertDomainDependency(name: String): ObjectAssert { + val list = this.filter { it.domain == name } + assertThat(list).hasSize(1) + val single = list.single().settings + return assertThat(single) } +} +class ServiceDependencyTest { @Test - fun `should parse allServiceDependency and use connectionIdleTimeout from properties`() { + fun `should deduplicate services dependencies based on serviceName`() { // given val proto = outgoingDependenciesProto { - withService(serviceName = "*", idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = null) + withService(serviceName = "service-1", requestTimeout = "8s", idleTimeout = "8s") + withService( + serviceName = "service-1", + requestTimeout = "10s", + idleTimeout = "10s", + connectionIdleTimeout = "10s" + ) + withService(serviceName = "service-2") } - val properties = snapshotProperties(allServicesDependenciesIdentifier = "*", connectionIdleTimeout = "5s") - // when - val outgoing = proto.toOutgoing(properties) + // when + val outgoing = proto.toOutgoing(snapshotProperties()) - // expects - assertThat(outgoing.allServicesDependencies).isTrue() - assertThat(outgoing.defaultServiceSettings).hasTimeouts( + // expect + val dependencies = outgoing.getServiceDependencies() + assertThat(dependencies).hasSize(2) + assertThat(dependencies[0].service).isEqualTo("service-1") + assertThat(dependencies[0].settings).hasTimeouts( idleTimeout = "10s", requestTimeout = "10s", - connectionIdleTimeout = "5s" + connectionIdleTimeout = "10s" ) - assertThat(outgoing.getServiceDependencies()).isEmpty() + assertThat(dependencies[1].service).isEqualTo("service-2") } @Test - fun `should parse allServiceDependency and use timeouts from properties`() { + fun `should accept service dependency with redirect policy defined`() { // given val proto = outgoingDependenciesProto { - withService(serviceName = "*", idleTimeout = null, requestTimeout = null) + withService(serviceName = "service-1", handleInternalRedirect = true) } - val properties = - snapshotProperties( - allServicesDependenciesIdentifier = "*", - idleTimeout = "5s", - requestTimeout = "5s", - connectionIdleTimeout = "5s" - ) - // when - val outgoing = proto.toOutgoing(properties) + // when + val outgoing = proto.toOutgoing(snapshotProperties()) - // expects - assertThat(outgoing.allServicesDependencies).isTrue() - assertThat(outgoing.defaultServiceSettings).hasTimeouts( - idleTimeout = "5s", - requestTimeout = "5s", - connectionIdleTimeout = "5s" - ) - assertThat(outgoing.getServiceDependencies()).isEmpty() + // expect + val dependency = outgoing.getServiceDependencies().single() + assertThat(dependency.service).isEqualTo("service-1") + assertThat(dependency.settings.handleInternalRedirect).isEqualTo(true) } @Test @@ -816,40 +993,24 @@ class NodeMetadataTest { .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") } + fun List.assertServiceDependency(name: String): ObjectAssert { + val list = this.filter { it.service == name } + assertThat(list).hasSize(1) + val single = list.single().settings + return assertThat(single) + } +} + +class AllServiceDependencyTest { @Test - fun `should parse domain dependencies and for missing config use config defined in properties even if allServiceDependency is defined`() { + fun `should parse allServiceDependency with timeouts configuration`() { // given val proto = outgoingDependenciesProto { withService(serviceName = "*", idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "10s") - withDomain( - url = "http://domain-name-1", - idleTimeout = "5s", - requestTimeout = null, - connectionIdleTimeout = null - ) - withDomain( - url = "http://domain-name-2", - idleTimeout = null, - requestTimeout = "4s", - connectionIdleTimeout = null - ) - withDomain( - url = "http://domain-name-3", - idleTimeout = null, - requestTimeout = null, - connectionIdleTimeout = "3s" - ) - withDomain( - url = "http://domain-name-4", - idleTimeout = null, - requestTimeout = null, - connectionIdleTimeout = null - ) } - val properties = snapshotProperties(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") - // when - val outgoing = proto.toOutgoing(properties) + // when + val outgoing = proto.toOutgoing(snapshotProperties(allServicesDependenciesIdentifier = "*")) // expects assertThat(outgoing.allServicesDependencies).isTrue() @@ -858,341 +1019,347 @@ class NodeMetadataTest { requestTimeout = "10s", connectionIdleTimeout = "10s" ) - - outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-1") - .hasTimeouts(idleTimeout = "5s", requestTimeout = "12s", connectionIdleTimeout = "12s") - outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-2") - .hasTimeouts(idleTimeout = "12s", requestTimeout = "4s", connectionIdleTimeout = "12s") - outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-3") - .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "3s") - outgoing.getDomainDependencies().assertDomainDependency("http://domain-name-4") - .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") + assertThat(outgoing.getServiceDependencies()).isEmpty() } @Test - fun `should throw exception when there are multiple allServiceDependency`() { + fun `should parse allServiceDependency and use requestTimeout from properties`() { // given val proto = outgoingDependenciesProto { - withServices(serviceDependencies = listOf("*", "*", "a")) - } - - // expects - val exception = assertThrows { - proto.toOutgoing(snapshotProperties(allServicesDependenciesIdentifier = "*")) + withService(serviceName = "*", idleTimeout = "10s", requestTimeout = null, connectionIdleTimeout = "10s") } - assertThat(exception.status.description) - .isEqualTo("Define at most one 'all serviceDependencies identifier' as an service dependency") - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } - - @Test - fun `should set empty healthCheckPath for incoming settings when healthCheckPath is empty`() { - // given - val proto = proxySettingsProto( - incomingSettings = true, - path = "/path" - ) - val incoming = proto.structValue?.fieldsMap?.get("incoming").toIncoming(snapshotProperties()) - - // expects - assertThat(incoming.healthCheck.clusterName).isEqualTo("local_service_health_check") - assertThat(incoming.healthCheck.path).isEqualTo("") - assertThat(incoming.healthCheck.hasCustomHealthCheck()).isFalse() - } - - @Test - fun `should set healthCheckPath and healthCheckClusterName for incoming settings`() { - // given - val proto = proxySettingsProto( - incomingSettings = true, - path = "/path", - healthCheckPath = "/status/ping", - healthCheckClusterName = "local_service_health_check" - ) - val incoming = proto.structValue?.fieldsMap?.get("incoming").toIncoming(snapshotProperties()) - - // expects - assertThat(incoming.healthCheck.clusterName).isEqualTo("local_service_health_check") - assertThat(incoming.healthCheck.path).isEqualTo("/status/ping") - assertThat(incoming.healthCheck.hasCustomHealthCheck()).isTrue() - } - - @Test - fun `should reject configuration with number timeout format`() { - // given - val proto = Value.newBuilder().setNumberValue(10.0).build() - + val properties = snapshotProperties(allServicesDependenciesIdentifier = "*", requestTimeout = "5s") // when - val exception = assertThrows { proto.toDuration() } - - // then - assertThat(exception.status.description).isEqualTo( - "Timeout definition has number format" + - " but should be in string format and ends with 's'" - ) - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } - - @Test - fun `should reject configuration with incorrect string timeout format`() { - // given - val proto = Value.newBuilder().setStringValue("20").build() - // when - val exception = assertThrows { proto.toDuration() } + val outgoing = proto.toOutgoing(properties) - // then - assertThat(exception.status.description).isEqualTo( - "Timeout definition has incorrect format: " + - "Invalid duration string: 20" + // expects + assertThat(outgoing.allServicesDependencies).isTrue() + assertThat(outgoing.defaultServiceSettings).hasTimeouts( + idleTimeout = "10s", + requestTimeout = "5s", + connectionIdleTimeout = "10s" ) - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } - - @Test - fun `should return duration when correct configuration provided`() { - // given - val proto = Value.newBuilder().setStringValue("20s").build() - - // when - val duration = proto.toDuration() - - // then - assertThat(duration).isNotNull - assertThat(duration!!.seconds).isEqualTo(20L) + assertThat(outgoing.getServiceDependencies()).isEmpty() } @Test - fun `should return null when empty value provided`() { - // given - val proto = Value.newBuilder().build() - - // when - val duration = proto.toDuration() - - // then - assertThat(duration).isNull() - } - - @ParameterizedTest - @MethodSource("validComparisonFilterData") - fun `should set statusCodeFilter for accessLogFilter`(input: String, op: ComparisonFilter.Op, code: Int) { + fun `should parse allServiceDependency and use idleTimeout from properties`() { // given - val proto = accessLogFilterProto(value = input, fieldName = "status_code_filter") - + val proto = outgoingDependenciesProto { + withService(serviceName = "*", idleTimeout = null, requestTimeout = "10s", connectionIdleTimeout = "10s") + } + val properties = snapshotProperties(allServicesDependenciesIdentifier = "*", idleTimeout = "5s") // when - val statusCodeFilterSettings = proto.structValue?.fieldsMap?.get("status_code_filter").toComparisonFilter() - // expects - assertThat(statusCodeFilterSettings?.comparisonCode).isEqualTo(code) - assertThat(statusCodeFilterSettings?.comparisonOperator).isEqualTo(op) - } - - @ParameterizedTest - @MethodSource("invalidComparisonFilterData") - fun `should throw exception for invalid status code filter data`(input: String) { - // given - val proto = accessLogFilterProto(value = input, fieldName = "status_code_filter") + val outgoing = proto.toOutgoing(properties) // expects - val exception = assertThrows { - proto.structValue?.fieldsMap?.get("status_code_filter").toComparisonFilter() - } - assertThat(exception.status.description) - .isEqualTo("Invalid access log comparison filter. Expected OPERATOR:VALUE") - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(outgoing.allServicesDependencies).isTrue() + assertThat(outgoing.defaultServiceSettings).hasTimeouts( + idleTimeout = "5s", + requestTimeout = "10s", + connectionIdleTimeout = "10s" + ) + assertThat(outgoing.getServiceDependencies()).isEmpty() } - @ParameterizedTest - @MethodSource("errorMessages") - fun `should throw exception for null value comparison filter data`(filter: String, errorMessage: String) { + @Test + fun `should parse allServiceDependency and use connectionIdleTimeout from properties`() { // given - val proto = accessLogFilterProto(value = null, fieldName = filter) + val proto = outgoingDependenciesProto { + withService(serviceName = "*", idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = null) + } + val properties = snapshotProperties(allServicesDependenciesIdentifier = "*", connectionIdleTimeout = "5s") + // when + + val outgoing = proto.toOutgoing(properties) // expects - val exception = assertThrows { - proto.structValue?.fieldsMap?.get(filter).toComparisonFilter() - } - assertThat(exception.status.description) - .isEqualTo(errorMessage) - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(outgoing.allServicesDependencies).isTrue() + assertThat(outgoing.defaultServiceSettings).hasTimeouts( + idleTimeout = "10s", + requestTimeout = "10s", + connectionIdleTimeout = "5s" + ) + assertThat(outgoing.getServiceDependencies()).isEmpty() } - @ParameterizedTest - @MethodSource("validComparisonFilterData") - fun `should set duration filter for accessLogFilter`(input: String, op: ComparisonFilter.Op, code: Int) { + @Test + fun `should parse allServiceDependency and use timeouts from properties`() { // given - val proto = accessLogFilterProto(value = input, fieldName = "duration_filter") - + val proto = outgoingDependenciesProto { + withService(serviceName = "*", idleTimeout = null, requestTimeout = null) + } + val properties = + snapshotProperties( + allServicesDependenciesIdentifier = "*", + idleTimeout = "5s", + requestTimeout = "5s", + connectionIdleTimeout = "5s" + ) // when - val durationFilterSettings = proto.structValue?.fieldsMap?.get("duration_filter").toComparisonFilter() + + val outgoing = proto.toOutgoing(properties) // expects - assertThat(durationFilterSettings?.comparisonCode).isEqualTo(code) - assertThat(durationFilterSettings?.comparisonOperator).isEqualTo(op) + assertThat(outgoing.allServicesDependencies).isTrue() + assertThat(outgoing.defaultServiceSettings).hasTimeouts( + idleTimeout = "5s", + requestTimeout = "5s", + connectionIdleTimeout = "5s" + ) + assertThat(outgoing.getServiceDependencies()).isEmpty() } - @ParameterizedTest - @MethodSource("invalidComparisonFilterData") - fun `should throw exception for invalid duration filter data`(input: String) { + @Test + fun `should throw exception when there are multiple allServiceDependency`() { // given - val proto = accessLogFilterProto(value = input, fieldName = "duration_filter") + val proto = outgoingDependenciesProto { + withServices(serviceDependencies = listOf("*", "*", "a")) + } // expects val exception = assertThrows { - proto.structValue?.fieldsMap?.get("duration_filter").toComparisonFilter() + proto.toOutgoing(snapshotProperties(allServicesDependenciesIdentifier = "*")) } assertThat(exception.status.description) - .isEqualTo("Invalid access log comparison filter. Expected OPERATOR:VALUE") + .isEqualTo("Define at most one 'all serviceDependencies identifier' as an service dependency") assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } @Test - fun `should throw exception for null value header filter data`() { + fun `should reject domain dependency with unsupported all services dependencies identifier`() { // given - val proto = accessLogFilterProto(value = null, fieldName = "header_filter") + val proto = outgoingDependenciesProto { + withDomain(url = "*") + } + val properties = snapshotProperties(allServicesDependenciesIdentifier = "*") // expects - val exception = assertThrows { - proto.structValue?.fieldsMap?.get("header_filter").toHeaderFilter() - } + val exception = assertThrows { proto.toOutgoing(properties) } assertThat(exception.status.description) - .isEqualTo("Invalid access log header filter. Expected HEADER_NAME:REGEX") + .isEqualTo("Unsupported 'all serviceDependencies identifier' for domain dependency: *") assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } +} + +class DomainPatternDependencyTest { @Test - fun `should set header filter for accessLogFilter`() { + fun `should accept domain pattern dependency`() { // given - val proto = accessLogFilterProto(value = "test:^((.+):(.+))$", fieldName = "header_filter") + val proto = outgoingDependenciesProto { + withDomainPattern(pattern = "*.example.com") + } // when - val headerFilterSettings = proto.structValue?.fieldsMap?.get("header_filter").toHeaderFilter() + val outgoing = proto.toOutgoing(snapshotProperties()) // expects - assertThat(headerFilterSettings?.headerName).isEqualTo("test") - assertThat(headerFilterSettings?.regex).isEqualTo("^((.+):(.+))\$") + val dependency = outgoing.getDomainPatternDependencies().single() + assertThat(dependency.domainPattern).isEqualTo("*.example.com") } @Test - fun `should throw exception for invalid header filter data`() { + fun `should reject domain pattern dependency with schema`() { // given - val proto = accessLogFilterProto(value = "test;test", fieldName = "header_filter") + val proto = outgoingDependenciesProto { + withDomainPattern(pattern = "http://example.com") + } // expects - val exception = assertThrows { - proto.structValue?.fieldsMap?.get("header_filter").toHeaderFilter() - } - assertThat(exception.status.description) - .isEqualTo("Invalid access log header filter. Expected HEADER_NAME:REGEX") + val exception = assertThrows { proto.toOutgoing(snapshotProperties()) } + assertThat(exception.status.description).isEqualTo( + "Unsupported format for domainPattern: domainPattern cannot contain a schema like http:// or https://" + ) assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } +} +class TagDependencyTest { @Test - fun `should set response flag filter for accessLogFilter`() { + fun `should deduplicate tag dependencies based on tag`() { // given - val availableFlags = listOf( - "UH", "UF", "UO", "NR", "URX", "NC", "DT", "DC", "LH", "UT", "LR", "UR", - "UC", "DI", "FI", "RL", "UAEX", "RLSE", "IH", "SI", "DPE", "UPE", "UMSDR", - "OM", "DF" - ) - val proto = accessLogFilterProto(value = availableFlags.joinToString(","), fieldName = "response_flag_filter") + val proto = outgoingDependenciesProto { + withTag(tag = "tag-1", requestTimeout = "8s", idleTimeout = "8s") + withTag( + tag = "tag-1", + requestTimeout = "10s", + idleTimeout = "10s", + connectionIdleTimeout = "10s" + ) + withTag(tag = "tag-2") + } // when - val responseFlags = proto.structValue?.fieldsMap?.get("response_flag_filter").toResponseFlagFilter() + val outgoing = proto.toOutgoing(snapshotProperties()) - // expects - assertThat(responseFlags).isEqualTo(availableFlags) - } + // expect + val dependencies = outgoing.getTagDependencies() + assertThat(dependencies) + .hasSize(2) - @Test - fun `should throw exception for invalid response flag filter data`() { - // given - val availableFlagsAndInvalid = listOf( - "UH", "UF", "UO", "NR", "URX", "NC", "DT", "DC", "LH", "UT", "LR", "UR", - "UC", "DI", "FI", "RL", "UAEX", "RLSE", "IH", "SI", "DPE", "UPE", "UMSDR", - "OM", "DF", "invalid" + assertThat(dependencies[0].tag).isEqualTo("tag-1") + assertThat(dependencies[0].settings).hasTimeouts( + idleTimeout = "10s", + requestTimeout = "10s", + connectionIdleTimeout = "10s" ) - val proto = - accessLogFilterProto(value = availableFlagsAndInvalid.joinToString(","), fieldName = "response_flag_filter") - - // expects - val exception = assertThrows { - proto.structValue?.fieldsMap?.get("response_flag_filter").toResponseFlagFilter() - } - assertThat(exception.status.description) - .isEqualTo("Invalid access log response flag filter. Expected valid values separated by comma") - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(dependencies[1].tag).isEqualTo("tag-2") } @Test - fun `should set not health check filter for accessLogFilter`() { + fun `should parse tag dependencies and for missing config use config defined in allServiceDependency`() { // given - val proto = accessLogBooleanFilterProto(value = true, fieldName = "not_health_check_filter") - + val proto = outgoingDependenciesProto { + withService(serviceName = "*", idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "10s") + withTag( + tag = "tag-1", + idleTimeout = "5s", + requestTimeout = null, + connectionIdleTimeout = null + ) + withTag( + tag = "tag-2", + idleTimeout = null, + requestTimeout = "4s", + connectionIdleTimeout = null + ) + withTag( + tag = "tag-3", + idleTimeout = null, + requestTimeout = null, + connectionIdleTimeout = "3s" + ) + withTag( + tag = "tag-4", + idleTimeout = null, + requestTimeout = null, + connectionIdleTimeout = null + ) + } + val properties = snapshotProperties(allServicesDependenciesIdentifier = "*") // when - val value = proto.structValue?.fieldsMap?.get("not_health_check_filter")?.boolValue + + val outgoing = proto.toOutgoing(properties) // expects - assertThat(value).isEqualTo(true) + assertThat(outgoing.allServicesDependencies).isTrue() + assertThat(outgoing.defaultServiceSettings).hasTimeouts( + idleTimeout = "10s", + requestTimeout = "10s", + connectionIdleTimeout = "10s" + ) + + outgoing.getTagDependencies().assertTagDependency("tag-1") + .hasTimeouts(idleTimeout = "5s", requestTimeout = "10s", connectionIdleTimeout = "10s") + outgoing.getTagDependencies().assertTagDependency("tag-2") + .hasTimeouts(idleTimeout = "10s", requestTimeout = "4s", connectionIdleTimeout = "10s") + outgoing.getTagDependencies().assertTagDependency("tag-3") + .hasTimeouts(idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "3s") + outgoing.getTagDependencies().assertTagDependency("tag-4") + .hasTimeouts(idleTimeout = "10s", requestTimeout = "10s", connectionIdleTimeout = "10s") } @Test - fun `should throw exception for null value response flag filter data`() { + fun `should parse tag dependencies and for missing configs use config defined in properties when allServiceDependency isn't defined`() { // given - val proto = accessLogFilterProto(value = null, fieldName = "response_flag_filter") - - // expects - val exception = assertThrows { - proto.structValue?.fieldsMap?.get("response_flag_filter").toResponseFlagFilter() + val proto = outgoingDependenciesProto { + withTag( + tag = "tag-1", + idleTimeout = "5s", + requestTimeout = null, + connectionIdleTimeout = null + ) + withTag( + tag = "tag-2", + idleTimeout = null, + requestTimeout = "4s", + connectionIdleTimeout = null + ) + withTag( + tag = "tag-3", + idleTimeout = null, + requestTimeout = null, + connectionIdleTimeout = "3s" + ) + withTag( + tag = "tag-4", + idleTimeout = null, + requestTimeout = null, + connectionIdleTimeout = null + ) } - assertThat(exception.status.description) - .isEqualTo("Invalid access log response flag filter. Expected valid values separated by comma") - assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } + val properties = snapshotProperties(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") + // when - fun ObjectAssert.hasTimeouts( - idleTimeout: String, - connectionIdleTimeout: String, - requestTimeout: String - ): ObjectAssert { - this.extracting { it.timeoutPolicy }.isEqualTo( - Outgoing.TimeoutPolicy( - idleTimeout = Durations.parse(idleTimeout), - connectionIdleTimeout = Durations.parse(connectionIdleTimeout), - requestTimeout = Durations.parse(requestTimeout) - ) + val outgoing = proto.toOutgoing(properties) + + // expects + assertThat(outgoing.allServicesDependencies).isFalse() + assertThat(outgoing.defaultServiceSettings).hasTimeouts( + idleTimeout = "12s", + requestTimeout = "12s", + connectionIdleTimeout = "12s" ) - return this - } - fun List.assertServiceDependency(name: String): ObjectAssert { - val list = this.filter { it.service == name } - assertThat(list).hasSize(1) - val single = list.single().settings - return assertThat(single) + outgoing.getTagDependencies().assertTagDependency("tag-1") + .hasTimeouts(idleTimeout = "5s", requestTimeout = "12s", connectionIdleTimeout = "12s") + outgoing.getTagDependencies().assertTagDependency("tag-2") + .hasTimeouts(idleTimeout = "12s", requestTimeout = "4s", connectionIdleTimeout = "12s") + outgoing.getTagDependencies().assertTagDependency("tag-3") + .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "3s") + outgoing.getTagDependencies().assertTagDependency("tag-4") + .hasTimeouts(idleTimeout = "12s", requestTimeout = "12s", connectionIdleTimeout = "12s") } - fun List.assertDomainDependency(name: String): ObjectAssert { - val list = this.filter { it.domain == name } + fun List.assertTagDependency(name: String): ObjectAssert { + val list = this.filter { it.tag == name } assertThat(list).hasSize(1) val single = list.single().settings return assertThat(single) } +} - private fun createJwtSnapshotProperties(): SnapshotProperties { - val snapshotProperties = SnapshotProperties() - val jwtFilterProperties = JwtFilterProperties() - val oauthProviders = mapOf( - "oauth2-mock" to - OAuthProvider( - jwksUri = URI.create("http://localhost:8080/jwks-address/"), - clusterName = "oauth" - ) - ) - jwtFilterProperties.providers = oauthProviders - snapshotProperties.jwt = jwtFilterProperties +private fun snapshotProperties( + allServicesDependenciesIdentifier: String = "*", + handleInternalRedirect: Boolean = false, + idleTimeout: String = "120s", + connectionIdleTimeout: String = "120s", + requestTimeout: String = "120s" +) = SnapshotProperties().apply { + outgoingPermissions.allServicesDependencies.identifier = allServicesDependenciesIdentifier + egress.handleInternalRedirect = handleInternalRedirect + egress.commonHttp.idleTimeout = Duration.ofNanos(Durations.toNanos(Durations.parse(idleTimeout))) + egress.commonHttp.connectionIdleTimeout = + Duration.ofNanos(Durations.toNanos(Durations.parse(connectionIdleTimeout))) + egress.commonHttp.requestTimeout = Duration.ofNanos(Durations.toNanos(Durations.parse(requestTimeout))) +} - return snapshotProperties - } +fun timeouts( + idleTimeout: String, + connectionIdleTimeout: String, + requestTimeout: String +) = DependencySettings( + timeoutPolicy = Outgoing.TimeoutPolicy( + idleTimeout = Durations.parse(idleTimeout), + connectionIdleTimeout = Durations.parse(connectionIdleTimeout), + requestTimeout = Durations.parse(requestTimeout) + ) +) + +fun ObjectAssert.hasTimeouts( + idleTimeout: String, + connectionIdleTimeout: String, + requestTimeout: String +): ObjectAssert { + this.extracting { it.timeoutPolicy }.isEqualTo( + Outgoing.TimeoutPolicy( + idleTimeout = Durations.parse(idleTimeout), + connectionIdleTimeout = Durations.parse(connectionIdleTimeout), + requestTimeout = Durations.parse(requestTimeout) + ) + ) + return this } diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidatorTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidatorTest.kt index 76451d45c..cfce239f0 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidatorTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidatorTest.kt @@ -1,9 +1,8 @@ package pl.allegro.tech.servicemesh.envoycontrol.groups +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest import io.grpc.Status -import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.assertj.core.api.Assertions.catchThrowable import org.junit.jupiter.api.Assertions.assertDoesNotThrow import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -16,10 +15,11 @@ import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest as DiscoveryReq class NodeMetadataValidatorTest { - val validator = NodeMetadataValidator(SnapshotProperties().apply { + private val validator = NodeMetadataValidator(SnapshotProperties().apply { outgoingPermissions = createOutgoingPermissions( enabled = true, - servicesAllowedToUseWildcard = mutableSetOf("vis-1", "vis-2") + servicesAllowedToUseWildcard = mutableSetOf("vis-1", "vis-2"), + tagPrefix = "tag:" ) incomingPermissions = createIncomingPermissions( enabled = true, @@ -37,16 +37,12 @@ class NodeMetadataValidatorTest { ) val request = DiscoveryRequestV3.newBuilder().setNode(node).build() - // when - val exception = catchThrowable { validator.onV3StreamRequest(streamId = 123, request = request) } - // then - assertThat(exception).isInstanceOf(WildcardPrincipalValidationException::class.java) - val validationException = exception as WildcardPrincipalValidationException - assertThat(validationException.status.description) - .isEqualTo("Blocked service regular-1 from allowing everyone in incoming permissions. Only defined services can use that.") - assertThat(validationException.status.code) - .isEqualTo(Status.Code.INVALID_ARGUMENT) + validator.assertThrow( + request, + WildcardPrincipalValidationException::class.java, + "Blocked service regular-1 from allowing everyone in incoming permissions. Only defined services can use that." + ) } @Test @@ -59,16 +55,12 @@ class NodeMetadataValidatorTest { ) val request = DiscoveryRequestV3.newBuilder().setNode(node).build() - // when - val exception = catchThrowable { validator.onV3StreamRequest(streamId = 123, request = request) } - // expects - assertThat(exception).isInstanceOf(WildcardPrincipalMixedWithOthersValidationException::class.java) - val validationException = exception as WildcardPrincipalMixedWithOthersValidationException - assertThat(validationException.status.description) - .isEqualTo("Blocked service vis-1 from allowing everyone in incoming permissions. Either a wildcard or a list of clients must be provided.") - assertThat(validationException.status.code) - .isEqualTo(Status.Code.INVALID_ARGUMENT) + validator.assertThrow( + request, + WildcardPrincipalMixedWithOthersValidationException::class.java, + "Blocked service vis-1 from allowing everyone in incoming permissions. Either a wildcard or a list of clients must be provided." + ) } @Test @@ -80,16 +72,12 @@ class NodeMetadataValidatorTest { ) val request = DiscoveryRequestV3.newBuilder().setNode(node).build() - // when - val exception = catchThrowable { validator.onV3StreamRequest(streamId = 123, request = request) } - // expects - assertThat(exception).isInstanceOf(AllDependenciesValidationException::class.java) - val validationException = exception as AllDependenciesValidationException - assertThat(validationException.status.description) - .isEqualTo("Blocked service regular-1 from using all dependencies. Only defined services can use all dependencies") - assertThat(validationException.status.code) - .isEqualTo(Status.Code.INVALID_ARGUMENT) + validator.assertThrow( + request, + AllDependenciesValidationException::class.java, + "Blocked service regular-1 from using all dependencies. Only defined services can use all dependencies" + ) } @Test @@ -104,7 +92,7 @@ class NodeMetadataValidatorTest { val request = DiscoveryRequestV3.newBuilder().setNode(node).build() // then - assertDoesNotThrow { validator.onV3StreamRequest(123, request = request) } + validator.notThrow(request) } @Test @@ -118,7 +106,7 @@ class NodeMetadataValidatorTest { val request = DiscoveryRequestV3.newBuilder().setNode(node).build() // then - assertDoesNotThrow { validator.onV3StreamRequest(123, request = request) } + validator.notThrow(request) } @Test @@ -137,7 +125,7 @@ class NodeMetadataValidatorTest { val request = DiscoveryRequestV3.newBuilder().setNode(node).build() // then - assertDoesNotThrow { permissionsDisabledValidator.onV3StreamRequest(123, request = request) } + permissionsDisabledValidator.notThrow(request) } @ParameterizedTest @@ -165,17 +153,12 @@ class NodeMetadataValidatorTest { ) val request = DiscoveryRequestV3.newBuilder().setNode(node).build() - // when - val exception = - catchThrowable { configurationModeValidator.onV3StreamRequest(streamId = 123, request = request) } - // expects - assertThat(exception).isInstanceOf(ConfigurationModeNotSupportedException::class.java) - val validationException = exception as ConfigurationModeNotSupportedException - assertThat(validationException.status.description) - .isEqualTo("Blocked service regular-1 from receiving updates. $modeNotSupportedName is not supported by server.") - assertThat(validationException.status.code) - .isEqualTo(Status.Code.INVALID_ARGUMENT) + configurationModeValidator.assertThrow( + request, + ConfigurationModeNotSupportedException::class.java, + "Blocked service regular-1 from receiving updates. $modeNotSupportedName is not supported by server." + ) } @ParameterizedTest @@ -203,7 +186,7 @@ class NodeMetadataValidatorTest { val request = DiscoveryRequestV3.newBuilder().setNode(node).build() // then - assertDoesNotThrow { configurationModeValidator.onV3StreamRequest(123, request = request) } + validator.notThrow(request) } @Test @@ -219,14 +202,11 @@ class NodeMetadataValidatorTest { val request = DiscoveryRequestV3.newBuilder().setNode(node).build() // expects - assertThatExceptionOfType(ServiceNameNotProvidedException::class.java) - .isThrownBy { requireServiceNameValidator.onV3StreamRequest(streamId = 123, request = request) } - .satisfies { - assertThat(it.status.description).isEqualTo( - "Service name has not been provided." - ) - assertThat(it.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } + requireServiceNameValidator.assertThrow( + request, + ServiceNameNotProvidedException::class.java, + "Service name has not been provided." + ) } @Test @@ -239,7 +219,7 @@ class NodeMetadataValidatorTest { val request = DiscoveryRequestV3.newBuilder().setNode(node).build() // then - assertDoesNotThrow { validator.onV3StreamRequest(123, request = request) } + validator.notThrow(request) } @Test @@ -254,40 +234,83 @@ class NodeMetadataValidatorTest { .build() // then - assertThatExceptionOfType(RateLimitIncorrectValidationException::class.java) - .isThrownBy { validator.onV3StreamRequest(123, request = request) } - .satisfies { - assertThat(it.status.description).isEqualTo( - "Rate limit value: 0/j is incorrect." - ) - assertThat(it.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) - } + validator.assertThrow( + request, + RateLimitIncorrectValidationException::class.java, + "Rate limit value: 0/j is incorrect." + ) + } + + @Test + fun `should throw an exception when service is using tag dependency without prefix`() { + // given + val node = nodeV3( + serviceName = "tag-service", + tagDependencies = setOf("tag:xyz", "abc") + ) + val request = DiscoveryRequestV3.newBuilder() + .setNode(node) + .build() + + // then + validator.assertThrow( + request, + TagDependencyValidationException::class.java, + "Blocked service tag-service from using tag dependencies [abc]. Only allowed tags are supported." + ) + } + + @Test + fun `should not throw an exception when service is using tag dependency with prefix`() { + // given + val node = nodeV3( + serviceName = "tag-service", + tagDependencies = setOf("tag:xyz", "tag:abc") + ) + val request = DiscoveryRequestV3.newBuilder() + .setNode(node) + .build() + + // then + validator.notThrow(request) } private fun createIncomingPermissions( enabled: Boolean = false, servicesAllowedToUseWildcard: MutableSet = mutableSetOf() - ): IncomingPermissionsProperties { - val incomingPermissions = IncomingPermissionsProperties() - incomingPermissions.enabled = enabled - incomingPermissions.tlsAuthentication.servicesAllowedToUseWildcard = servicesAllowedToUseWildcard - return incomingPermissions + ): IncomingPermissionsProperties = IncomingPermissionsProperties().apply { + this.enabled = enabled + this.tlsAuthentication.servicesAllowedToUseWildcard = servicesAllowedToUseWildcard } private fun createOutgoingPermissions( enabled: Boolean = false, - servicesAllowedToUseWildcard: MutableSet = mutableSetOf() - ): OutgoingPermissionsProperties { - val outgoingPermissions = OutgoingPermissionsProperties() - outgoingPermissions.enabled = enabled - outgoingPermissions.servicesAllowedToUseWildcard = servicesAllowedToUseWildcard - return outgoingPermissions + servicesAllowedToUseWildcard: MutableSet = mutableSetOf(), + tagPrefix: String = "" + ): OutgoingPermissionsProperties = OutgoingPermissionsProperties().apply { + this.enabled = enabled + this.servicesAllowedToUseWildcard = servicesAllowedToUseWildcard + this.tagPrefix = tagPrefix + } + + private fun createCommunicationMode( + ads: Boolean = true, + xds: Boolean = true + ): EnabledCommunicationModes = EnabledCommunicationModes().apply { + this.ads = ads + this.xds = xds } - private fun createCommunicationMode(ads: Boolean = true, xds: Boolean = true): EnabledCommunicationModes { - val enabledCommunicationModes = EnabledCommunicationModes() - enabledCommunicationModes.ads = ads - enabledCommunicationModes.xds = xds - return enabledCommunicationModes + private fun NodeMetadataValidator.assertThrow( + request: DiscoveryRequest, + exceptionClass: Class, + description: String, + ) = assertThatExceptionOfType(exceptionClass) + .isThrownBy { this.onV3StreamRequest(123, request) } + .matches { it.status.description == description } + .matches { it.status.code == Status.Code.INVALID_ARGUMENT } + + private fun NodeMetadataValidator.notThrow(request: DiscoveryRequest) = assertDoesNotThrow { + this.onV3StreamRequest(123, request) } } 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 59757cb6d..a205957dd 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 @@ -21,7 +21,8 @@ fun nodeV3( connectionIdleTimeout: String? = null, healthCheckPath: String? = null, healthCheckClusterName: String? = null, - rateLimit: String? = null + rateLimit: String? = null, + tagDependencies: Set = emptySet() ): NodeV3 { val meta = NodeV3.newBuilder().metadataBuilder @@ -37,13 +38,14 @@ fun nodeV3( meta.putFields("ads", Value.newBuilder().setBoolValue(ads).build()) } - if (incomingSettings || serviceDependencies.isNotEmpty()) { + if (incomingSettings || serviceDependencies.isNotEmpty() || tagDependencies.isNotEmpty()) { meta.putFields( "proxy_settings", proxySettingsProto( path = "/endpoint", clients = clients, serviceDependencies = serviceDependencies, + tagDependencies = tagDependencies, incomingSettings = incomingSettings, idleTimeout = idleTimeout, responseTimeout = responseTimeout, @@ -129,7 +131,8 @@ fun proxySettingsProto( healthCheckPath: String? = null, healthCheckClusterName: String? = null, clients: List = listOf("client1"), - rateLimit: String? = null + rateLimit: String? = null, + tagDependencies: Set = emptySet() ): Value = struct { if (incomingSettings) { putFields("incoming", struct { @@ -162,9 +165,14 @@ fun proxySettingsProto( }) }) } - if (serviceDependencies.isNotEmpty()) { + if (serviceDependencies.isNotEmpty() || tagDependencies.isNotEmpty()) { putFields("outgoing", outgoingDependenciesProto { - withServices(serviceDependencies.toList(), idleTimeout, responseTimeout) + serviceDependencies.forEach { + withService(it, idleTimeout, responseTimeout) + } + tagDependencies.forEach { + withTag(it, idleTimeout, responseTimeout) + } }) } } @@ -195,6 +203,7 @@ class OutgoingDependenciesProtoScope { val service: String? = null, val domain: String? = null, val domainPattern: String? = null, + val tag: String? = null, val idleTimeout: String? = null, val connectionIdleTimeout: String? = null, val requestTimeout: String? = null, @@ -256,6 +265,20 @@ class OutgoingDependenciesProtoScope { ) ) + fun withTag( + tag: String, + idleTimeout: String? = null, + connectionIdleTimeout: String? = null, + requestTimeout: String? = null + ) = dependencies.add( + Dependency( + tag = tag, + idleTimeout = idleTimeout, + connectionIdleTimeout = connectionIdleTimeout, + requestTimeout = requestTimeout + ) + ) + fun withInvalid(service: String? = null, domain: String? = null) = dependencies.add( Dependency( service = service, @@ -276,6 +299,7 @@ fun outgoingDependenciesProto( service = it.service, domain = it.domain, domainPattern = it.domainPattern, + tag = it.tag, idleTimeout = it.idleTimeout, connectionIdleTimeout = it.connectionIdleTimeout, requestTimeout = it.requestTimeout, @@ -292,17 +316,19 @@ fun outgoingDependencyProto( service: String? = null, domain: String? = null, domainPattern: String? = null, + tag: String? = null, handleInternalRedirect: Boolean? = null, idleTimeout: String? = null, connectionIdleTimeout: String? = null, requestTimeout: String? = null, retryPolicy: RetryPolicyInput? = null ) = struct { - service?.also { putFields("service", string(service)) } - domain?.also { putFields("domain", string(domain)) } - retryPolicy?.also { putFields("retryPolicy", retryPolicyProto(retryPolicy)) } - domainPattern?.also { putFields("domainPattern", string(domainPattern)) } - handleInternalRedirect?.also { putFields("handleInternalRedirect", boolean(handleInternalRedirect)) } + service?.also { putFields("service", string(it)) } + domain?.also { putFields("domain", string(it)) } + domainPattern?.also { putFields("domainPattern", string(it)) } + tag?.also { putFields("tag", string(it)) } + retryPolicy?.also { putFields("retryPolicy", retryPolicyProto(it)) } + handleInternalRedirect?.also { putFields("handleInternalRedirect", boolean(it)) } if (idleTimeout != null || requestTimeout != null || connectionIdleTimeout != null) { putFields("timeoutPolicy", outgoingTimeoutPolicy(idleTimeout, connectionIdleTimeout, requestTimeout)) } diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactoryTest.kt new file mode 100644 index 000000000..22cdd9712 --- /dev/null +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/EnvoySnapshotFactoryTest.kt @@ -0,0 +1,96 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import pl.allegro.tech.servicemesh.envoycontrol.services.ClusterState +import pl.allegro.tech.servicemesh.envoycontrol.services.Locality +import pl.allegro.tech.servicemesh.envoycontrol.services.MultiClusterState +import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstance +import pl.allegro.tech.servicemesh.envoycontrol.services.ServiceInstances +import pl.allegro.tech.servicemesh.envoycontrol.services.ServicesState +import java.util.concurrent.ConcurrentHashMap + +class EnvoySnapshotFactoryTest { + + @Test + fun `should return all tags when prefix is empty`() { + // given + val tagPrefix = "" + val serviceTags = mapOf( + serviceWithTags("abc", "uws", "poc"), + serviceWithTags("xyz", "uj"), + serviceWithTags("qwerty") + ) + val state = MultiClusterState(listOf( + ClusterState(serviceState(serviceTags), Locality.LOCAL, "cluster") + )) + + // when + val tags = EnvoySnapshotFactory.extractTags(tagPrefix, state) + + // then + assertThat(tags).isEqualTo(serviceTags) + } + + @Test + fun `should return all tags with prefix`() { + val tagPrefix = "tag:" + val serviceTags = mapOf( + serviceWithTags("abc", "tag:uws", "poc"), + serviceWithTags("xyz", "uj"), + serviceWithTags("qwerty") + ) + val state = MultiClusterState(listOf( + ClusterState(serviceState(serviceTags), Locality.LOCAL, "cluster") + )) + + // when + val tags = EnvoySnapshotFactory.extractTags(tagPrefix, state) + + // then + assertThat(tags).isEqualTo(mapOf( + serviceWithTags("abc", "tag:uws"), + serviceWithTags("xyz"), + serviceWithTags("qwerty") + )) + } + + @Test + fun `should merge multiple Cluster State`() { + // given + val tagPrefix = "" + val serviceTagsCluster1 = mapOf( + serviceWithTags("abc", "uws", "poc"), + serviceWithTags("xyz", "uj"), + serviceWithTags("qwerty")) + val serviceTagsCluster2 = mapOf( + serviceWithTags("abc", "lkj"), + serviceWithTags("xyz"), + serviceWithTags("qwerty", "ban")) + val state = MultiClusterState(listOf( + ClusterState(serviceState(serviceTagsCluster1), Locality.LOCAL, "cluster"), + ClusterState(serviceState(serviceTagsCluster2), Locality.LOCAL, "cluster2") + )) + + // when + val tags = EnvoySnapshotFactory.extractTags(tagPrefix, state) + + // then + assertThat(tags).isEqualTo(mapOf( + serviceWithTags("abc", "uws", "poc", "lkj"), + serviceWithTags("xyz", "uj"), + serviceWithTags("qwerty", "ban") + )) + } + + private fun serviceState(servicesTags: Map>): ServicesState { + val servicesInstances = servicesTags.map { + it.key to setOf(ServiceInstance("${it.key}-1", it.value, null, null)) + }.associateTo(ConcurrentHashMap()) { it.first to ServiceInstances(it.first, it.second) } + return ServicesState(servicesInstances) + } +} + +private fun serviceWithTags(serviceName: String, vararg tags: String): Pair> { + return serviceName to tags.toSet() +} 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 85f5105a2..cfc70af0d 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 @@ -1310,7 +1310,10 @@ class SnapshotUpdaterTest { private fun snapshotFactory(snapshotProperties: SnapshotProperties, meterRegistry: MeterRegistry) = EnvoySnapshotFactory( ingressRoutesFactory = EnvoyIngressRoutesFactory(snapshotProperties), - egressRoutesFactory = EnvoyEgressRoutesFactory(snapshotProperties), + egressRoutesFactory = EnvoyEgressRoutesFactory( + snapshotProperties.egress, + snapshotProperties.incomingPermissions + ), clustersFactory = EnvoyClustersFactory(snapshotProperties), endpointsFactory = EnvoyEndpointsFactory( snapshotProperties, ServiceTagMetadataGenerator(snapshotProperties.routing.serviceTags) @@ -1319,6 +1322,7 @@ class SnapshotUpdaterTest { snapshotProperties, EnvoyHttpFilters.emptyFilters ), + routeSpecificationFactory = RouteSpecificationFactory(snapshotProperties), // Remember when LDS change we have to send RDS again snapshotsVersions = SnapshotsVersions(), properties = snapshotProperties, diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/ResourceUtils.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/ResourceUtils.kt new file mode 100644 index 000000000..ddc8fbe11 --- /dev/null +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/ResourceUtils.kt @@ -0,0 +1,60 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource + +import com.google.protobuf.Duration +import com.google.protobuf.util.Durations +import io.envoyproxy.envoy.config.cluster.v3.Cluster +import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource +import io.envoyproxy.envoy.config.core.v3.ConfigSource +import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions +import pl.allegro.tech.servicemesh.envoycontrol.groups.DependencySettings +import pl.allegro.tech.servicemesh.envoycontrol.groups.Outgoing +import pl.allegro.tech.servicemesh.envoycontrol.groups.ServiceDependency +import pl.allegro.tech.servicemesh.envoycontrol.groups.TagDependency +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties + +fun createClusters( + properties: SnapshotProperties, + serviceNames: List +): Map { + return serviceNames.map { + createCluster(properties, it) + }.associateBy { it.name } +} + +fun createCluster( + defaultProperties: SnapshotProperties, + serviceName: String, +): Cluster { + return Cluster.newBuilder().setName(serviceName) + .setType(Cluster.DiscoveryType.EDS) + .setConnectTimeout(Durations.fromMillis(defaultProperties.edsConnectionTimeout.toMillis())) + .setEdsClusterConfig( + Cluster.EdsClusterConfig.newBuilder().setEdsConfig( + ConfigSource.newBuilder().setAds(AggregatedConfigSource.newBuilder()) + ).setServiceName(serviceName) + ) + .setLbPolicy(defaultProperties.loadBalancing.policy) + .setCommonHttpProtocolOptions( + HttpProtocolOptions.newBuilder() + .setIdleTimeout(Duration.newBuilder().setSeconds(100).build()) + ) + .build() +} + +fun serviceDependency(name: String, idleTimeout: Long) = ServiceDependency( + name, + settings = DependencySettings( + timeoutPolicy = Outgoing.TimeoutPolicy( + connectionIdleTimeout = Duration.newBuilder().setSeconds(idleTimeout).build() + ) + ) +) + +fun tagDependency(name: String, idleTimeout: Long) = TagDependency( + name, + settings = DependencySettings( + timeoutPolicy = Outgoing.TimeoutPolicy( + connectionIdleTimeout = Duration.newBuilder().setSeconds(idleTimeout).build() + ) + ) +) diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClusterFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClusterFactoryTest.kt new file mode 100644 index 000000000..006848c14 --- /dev/null +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/clusters/EnvoyClusterFactoryTest.kt @@ -0,0 +1,321 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.clusters + +import io.envoyproxy.envoy.config.cluster.v3.Cluster +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.ObjectAssert +import org.junit.jupiter.api.Test +import pl.allegro.tech.servicemesh.envoycontrol.groups.AllServicesGroup +import pl.allegro.tech.servicemesh.envoycontrol.groups.CommunicationMode +import pl.allegro.tech.servicemesh.envoycontrol.groups.Outgoing +import pl.allegro.tech.servicemesh.envoycontrol.groups.ProxySettings +import pl.allegro.tech.servicemesh.envoycontrol.groups.ServicesGroup +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.GlobalSnapshot +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.createClusters +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.serviceDependency +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.tagDependency + +class EnvoyClusterFactoryTest { + + companion object { + val allServicesGroup = AllServicesGroup( + communicationMode = CommunicationMode.ADS, + serviceName = "service-name", + discoveryServiceName = "service-name" + ) + + val serviceGroup = ServicesGroup( + communicationMode = CommunicationMode.ADS, + serviceName = "service-name", + discoveryServiceName = "service-name" + ) + } + + @Test + fun `should not return cluster from service dependency when is not present in global snapshot`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + serviceDependencies = listOf(serviceDependency("service-A", 33)) + ) + ) + ) + val services = listOf("service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .isEmpty() + } + + @Test + fun `should return cluster from service dependency`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + serviceDependencies = listOf(serviceDependency("service-A", 33)) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(1) + .first() + .matches { it.name == "service-A" } + .hasIdleTimeout(33) + } + + @Test + fun `should return clusters from tag dependency`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + tagDependencies = listOf(tagDependency("tag", 33)) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties, + tags = mapOf( + serviceWithTags("service-A", "tag"), + serviceWithTags("service-C", "tag") + ) + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(2) + .extracting { it.name } + .containsAll(listOf("service-A", "service-C")) + clustersForGroup.assertServiceCluster("service-A") + .hasIdleTimeout(33) + clustersForGroup.assertServiceCluster("service-C") + .hasIdleTimeout(33) + } + + @Test + fun `should return clusters from tag dependency with keeping order`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + tagDependencies = listOf( + tagDependency("tag-1", 33), + tagDependency("tag-2", 27)) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties, + tags = mapOf( + serviceWithTags("service-A", "tag-1"), + serviceWithTags("service-C", "tag-1", "tag-2"), + serviceWithTags("service-B", "tag-2") + ) + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(3) + .extracting { it.name } + .containsAll(services) + + clustersForGroup.assertServiceCluster("service-A") + .hasIdleTimeout(33) + clustersForGroup.assertServiceCluster("service-B") + .hasIdleTimeout(27) + clustersForGroup.assertServiceCluster("service-C") + .hasIdleTimeout(33) + } + + @Test + fun `should return correct configuration for clusters from service dependency when service has tag`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + serviceDependencies = listOf( + serviceDependency("service-A", 44) + ), + tagDependencies = listOf( + tagDependency("tag", 33) + ) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties, + tags = mapOf( + serviceWithTags("service-A", "tag"), + serviceWithTags("service-C", "tag") + ) + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(2) + .extracting { it.name } + .containsAll(listOf("service-A", "service-C")) + clustersForGroup.assertServiceCluster("service-A") + .hasIdleTimeout(44) + clustersForGroup.assertServiceCluster("service-C") + .hasIdleTimeout(33) + } + + @Test + fun `should return all clusters when is AllServiceGroup`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = allServicesGroup + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(3) + .extracting { it.name } + .containsAll(services) + } + + @Test + fun `should return all clusters when is AllServiceGroup and has service dependency`() { + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = allServicesGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + serviceDependencies = listOf( + serviceDependency("service-A", 34) + ) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties + ) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(3) + .extracting { it.name } + .containsAll(services) + clustersForGroup.assertServiceCluster("service-A") + .hasIdleTimeout(34) + } + + @Test + fun `should return all clusters when is AllServiceGroup and has tag dependency`() { + val properties = SnapshotProperties() + val factory = EnvoyClustersFactory(properties) + val group = allServicesGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + tagDependencies = listOf( + tagDependency("tag", 27) + ) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = buildGlobalSnapshot( + services = services, + properties = properties, + tags = mapOf(serviceWithTags("service-A", "tag"))) + + // when + val clustersForGroup = factory.getClustersForGroup(group, globalSnapshot) + + // then + assertThat(clustersForGroup) + .hasSize(3) + .extracting { it.name } + .containsAll(services) + clustersForGroup.assertServiceCluster("service-A") + .hasIdleTimeout(27) + } +} + +private fun buildGlobalSnapshot( + services: Collection = emptyList(), + properties: SnapshotProperties = SnapshotProperties(), + tags: Map> = emptyMap() +) = GlobalSnapshot( + clusters = createClusters(properties, services.toList()), + allServicesNames = services.toSet(), + endpoints = emptyMap(), + clusterConfigurations = emptyMap(), + securedClusters = emptyMap(), + tags = tags +) + +private fun List.assertServiceCluster(name: String): ObjectAssert { + return assertThat(this) + .filteredOn { it.name == name } + .hasSize(1) + .first() +} + +private fun ObjectAssert.hasIdleTimeout(idleTimeout: Long): ObjectAssert { + this.extracting { it.commonHttpProtocolOptions.idleTimeout.seconds } + .isEqualTo(idleTimeout) + return this +} + +private fun serviceWithTags(serviceName: String, vararg tags: String): Pair> { + return serviceName to tags.toSet() +} diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactoryTest.kt new file mode 100644 index 000000000..5835c6618 --- /dev/null +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/EnvoyListenersFactoryTest.kt @@ -0,0 +1,102 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import pl.allegro.tech.servicemesh.envoycontrol.groups.AccessLogFilterSettings +import pl.allegro.tech.servicemesh.envoycontrol.groups.CommunicationMode +import pl.allegro.tech.servicemesh.envoycontrol.groups.ListenersConfig +import pl.allegro.tech.servicemesh.envoycontrol.groups.Outgoing +import pl.allegro.tech.servicemesh.envoycontrol.groups.ProxySettings +import pl.allegro.tech.servicemesh.envoycontrol.groups.ServicesGroup +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.AccessLogFiltersProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.GlobalSnapshot +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.createClusters +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters.EnvoyHttpFilters +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.serviceDependency +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.tagDependency + +class EnvoyListenersFactoryTest { + + companion object { + val serviceGroup = ServicesGroup( + communicationMode = CommunicationMode.ADS, + serviceName = "service-name", + discoveryServiceName = "service-name", + listenersConfig = ListenersConfig( + ingressHost = "10.10.10.10", + ingressPort = 8888, + egressHost = "11.11.11.11", + egressPort = 9999, + accessLogFilterSettings = AccessLogFilterSettings(null, AccessLogFiltersProperties()), + useTransparentProxy = true + ) + ) + } + + @Test + fun `should return egress http proxy virtual listener with service dependency`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyListenersFactory(properties, EnvoyHttpFilters(emptyList(), emptyList())) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + serviceDependencies = listOf(serviceDependency("service-A", 33)) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = GlobalSnapshot( + clusters = createClusters(properties, services), + allServicesNames = services.toSet(), + endpoints = emptyMap(), + clusterConfigurations = emptyMap(), + securedClusters = emptyMap(), + tags = emptyMap() + ) + + // when + val listeners = factory.createListeners(group, globalSnapshot) + + // then + Assertions.assertThat(listeners) + .extracting { it.name } + .contains("0.0.0.0:80") + } + + @Test + fun `should return egress http proxy virtual listener with tag dependency`() { + // given + val properties = SnapshotProperties() + val factory = EnvoyListenersFactory(properties, EnvoyHttpFilters(emptyList(), emptyList())) + val group = serviceGroup.copy( + proxySettings = ProxySettings( + outgoing = Outgoing( + tagDependencies = listOf(tagDependency("tag", 33)) + ) + ) + ) + val services = listOf("service-A", "service-B", "service-C") + val globalSnapshot = GlobalSnapshot( + clusters = createClusters(properties, services), + allServicesNames = services.toSet(), + endpoints = emptyMap(), + clusterConfigurations = emptyMap(), + securedClusters = emptyMap(), + tags = mapOf(serviceWithTags("service-B", "tag")) + ) + + // when + val listeners = factory.createListeners(group, globalSnapshot) + + // then + Assertions.assertThat(listeners) + .extracting { it.name } + .contains("0.0.0.0:80") + } +} + +private fun serviceWithTags(serviceName: String, vararg tags: String): Pair> { + return serviceName to tags.toSet() +} 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 aebc9238c..fc85ec50c 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 @@ -45,7 +45,8 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { setOf(), SnapshotResources.create(listOf(), "").resources(), mapOf(), - SnapshotResources.create(listOf(), "").resources() + SnapshotResources.create(listOf(), "").resources(), + emptyMap() ) @ParameterizedTest(name = "should generate RBAC rules for {arguments} OAuth Policy") 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 e8ab1d68b..bb9988eb5 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 @@ -106,7 +106,8 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { setOf(), SnapshotResources.create(listOf(), "").resources(), mapOf(), - SnapshotResources.create(listOf(), "").resources() + SnapshotResources.create(listOf(), "").resources(), + emptyMap() ) val clusterLoadAssignment = ClusterLoadAssignment.newBuilder() @@ -128,7 +129,8 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { setOf(), SnapshotResources.create(listOf(clusterLoadAssignment), "").resources(), mapOf(), - SnapshotResources.create(listOf(), "").resources() + SnapshotResources.create(listOf(), "").resources(), + emptyMap() ) @Test 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 eebb7f283..dc3d2b8d8 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 @@ -18,6 +18,8 @@ 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.snapshot.EgressProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.IncomingPermissionsProperties import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingOnAnyMethod import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingOnMethod import pl.allegro.tech.servicemesh.envoycontrol.groups.matchingOnPrefix @@ -45,9 +47,12 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should add client identity header if incoming permissions are enabled`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties().apply { - incomingPermissions.enabled = true - }) + val routesFactory = EnvoyEgressRoutesFactory( + EgressProperties(), + IncomingPermissionsProperties().apply { + enabled = true + } + ) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, false) @@ -67,9 +72,12 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should not add client identity header if incoming permissions are disabled`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties().apply { - incomingPermissions.enabled = false - }) + val routesFactory = EnvoyEgressRoutesFactory( + EgressProperties(), + IncomingPermissionsProperties().apply { + enabled = false + } + ) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, false) @@ -89,7 +97,7 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should add upstream remote address header if addUpstreamAddress is enabled`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties()) + val routesFactory = EnvoyEgressRoutesFactory(EgressProperties(), IncomingPermissionsProperties()) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, true) @@ -102,7 +110,7 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should not add upstream remote address header if addUpstreamAddress is disabled`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties()) + val routesFactory = EnvoyEgressRoutesFactory(EgressProperties(), IncomingPermissionsProperties()) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, false) @@ -115,9 +123,12 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should not add auto rewrite host header when feature is disabled in configuration`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties().apply { - egress.hostHeaderRewriting.enabled = false - }) + val routesFactory = EnvoyEgressRoutesFactory( + EgressProperties().apply { + hostHeaderRewriting.enabled = false + }, + IncomingPermissionsProperties() + ) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, false) @@ -137,7 +148,7 @@ internal class EnvoyEgressRoutesFactoryTest { egress.hostHeaderRewriting.enabled = true egress.hostHeaderRewriting.customHostHeader = "test_header" } - val routesFactory = EnvoyEgressRoutesFactory(snapshotProperties) + val routesFactory = EnvoyEgressRoutesFactory(snapshotProperties.egress, snapshotProperties.incomingPermissions) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, false) @@ -153,9 +164,12 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should create route config with headers to remove`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties().apply { - egress.headersToRemove = mutableListOf("x-special-case-header", "x-custom") - }) + val routesFactory = EnvoyEgressRoutesFactory( + EgressProperties().apply { + headersToRemove = mutableListOf("x-special-case-header", "x-custom") + }, + IncomingPermissionsProperties() + ) // when val routeConfig = routesFactory.createEgressRouteConfig("client1", clusters, false) @@ -167,9 +181,12 @@ internal class EnvoyEgressRoutesFactoryTest { @Test fun `should create route config for domains`() { // given - val routesFactory = EnvoyEgressRoutesFactory(SnapshotProperties().apply { - egress.headersToRemove = mutableListOf("x-special-case-header", "x-custom") - }) + val routesFactory = EnvoyEgressRoutesFactory( + EgressProperties().apply { + headersToRemove = mutableListOf("x-special-case-header", "x-custom") + }, + IncomingPermissionsProperties() + ) val routesSpecifications = listOf( RouteSpecification("example_pl_1553", listOf("example.pl:1553"), DependencySettings()), RouteSpecification("example_com_1553", listOf("example.com:1553"), DependencySettings()) diff --git a/envoy-control-runner/src/main/resources/application-docker.yaml b/envoy-control-runner/src/main/resources/application-docker.yaml index 4b6460432..9a720d978 100644 --- a/envoy-control-runner/src/main/resources/application-docker.yaml +++ b/envoy-control-runner/src/main/resources/application-docker.yaml @@ -6,4 +6,4 @@ envoy-control: chaos: username: "user" - password: "pass" \ No newline at end of file + password: "pass" 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 98eb5b8cc..02004e2c3 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 @@ -9,6 +9,7 @@ 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 open class EndpointMetadataMergingTests { @@ -35,6 +36,7 @@ open class EndpointMetadataMergingTests { } @Test + @Suppress("MagicNumber") fun `should merge all service tags of endpoints with the same ip and port`() { // given consul.server.operations.registerService(name = "echo", extension = service, tags = listOf("ipsum")) @@ -52,9 +54,11 @@ open class EndpointMetadataMergingTests { val dolomStats = callEchoServiceRepeatedly(service, repeat = 1, tag = "dolom") // then - assertThat(ipsumStats.hits(service)).isEqualTo(1) - assertThat(loremStats.hits(service)).isEqualTo(1) - assertThat(dolomStats.hits(service)).isEqualTo(1) + untilAsserted(wait = Duration.ofSeconds(30)) { + assertThat(ipsumStats.hits(service)).isEqualTo(1) + assertThat(loremStats.hits(service)).isEqualTo(1) + assertThat(dolomStats.hits(service)).isEqualTo(1) + } } protected open fun callEchoServiceRepeatedly( diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/OutgoingPermissionsTest.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/OutgoingPermissionsTest.kt index 73f898820..7f71ad253 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/OutgoingPermissionsTest.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/OutgoingPermissionsTest.kt @@ -89,6 +89,8 @@ interface OutgoingPermissionsTest { // given consul().server.operations.registerService(service(), name = "not-accessible") consul().server.operations.registerService(service(), name = "echo") + consul().server.operations.registerService(service(), name = "tag-service-A", tags = listOf("scope")) + consul().server.operations.registerService(service(), name = "tag-service-B", tags = listOf("scope")) untilAsserted { // when @@ -99,6 +101,8 @@ interface OutgoingPermissionsTest { val reachableResponseEchoWithDomain2 = envoy().egressOperations.callService("echo.domain") val reachableDomainResponse = envoy().egressOperations.callDomain("www.example.com") val unreachableDomainResponse = envoy().egressOperations.callDomain("www.another-example.com") + val reachableFirstTagResponse = envoy().egressOperations.callService("tag-service-A") + val reachableSecondTagResponse = envoy().egressOperations.callService("tag-service-B") // then assertThat(reachableResponse).isOk().isFrom(service()) @@ -108,6 +112,8 @@ interface OutgoingPermissionsTest { assertThat(unreachableDomainResponse).isUnreachable() assertThat(unreachableResponse).isUnreachable() assertThat(unregisteredResponse).isUnreachable() + assertThat(reachableFirstTagResponse).isOk().isFrom(service()) + assertThat(reachableSecondTagResponse).isOk().isFrom(service()) } } } diff --git a/envoy-control-tests/src/main/resources/envoy/config_ads.yaml b/envoy-control-tests/src/main/resources/envoy/config_ads.yaml index 5eb15b086..9b6a315a5 100644 --- a/envoy-control-tests/src/main/resources/envoy/config_ads.yaml +++ b/envoy-control-tests/src/main/resources/envoy/config_ads.yaml @@ -63,6 +63,7 @@ node: - domain: "https://www.example.com" - domain: "https://www.example-redirect.com" handleInternalRedirect: true + - tag: scope static_resources: clusters: diff --git a/envoy-control-tests/src/main/resources/envoy/config_xds.yaml b/envoy-control-tests/src/main/resources/envoy/config_xds.yaml index 511b28e73..87588cef8 100644 --- a/envoy-control-tests/src/main/resources/envoy/config_xds.yaml +++ b/envoy-control-tests/src/main/resources/envoy/config_xds.yaml @@ -63,6 +63,7 @@ node: - domain: "https://www.example.com" - domain: "https://www.example-redirect.com" handleInternalRedirect: true + - tag: scope static_resources: clusters: diff --git a/tools/docker-compose.yaml b/tools/docker-compose.yaml index 63606a60e..05bbcde69 100644 --- a/tools/docker-compose.yaml +++ b/tools/docker-compose.yaml @@ -3,7 +3,7 @@ version: '3' services: consul: container_name: consul - image: consul:1.5.3 + image: consul:1.10.12 ports: - "18500:8500" - "18300:8300"