Skip to content

Commit 4654e30

Browse files
normalize path (#407)
* normalize path * unescape action * change pathWithEscapedSlashesAction to unchanged * configurable path normalization * fix linter * #292 Traffic splitting - Updated param name (#399) * #292 Traffic splitting - Updated param name (cherry picked from commit 1abb570) * fix linter * fix validator * add default value for path normalization in tests --------- Co-authored-by: Nastassia Dailidava <[email protected]>
1 parent d6481de commit 4654e30

File tree

13 files changed

+199
-6
lines changed

13 files changed

+199
-6
lines changed

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
Lists all changes with user impact.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
5-
## [0.20.4]
5+
## [0.20.9]
6+
### Changed
7+
- Configurable path normalization
8+
9+
## [0.20.6 - 0.20.8]
10+
11+
### Changed
12+
- Merge slashes in http request
13+
- Feature flag for auditing global snapshot
14+
15+
## [0.20.5]
616

717
### Changed
818
- Added possibility to add response header for weighted secondary cluster
@@ -23,6 +33,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
2333
- Updated property names: secondaryClusterPostfix is changed to secondaryClusterSuffix,
2434
- aggregateClusterPostfix is changed to aggregateClusterSuffix
2535

36+
## [0.20.2]
37+
38+
### Changed
39+
- Updated property names: secondaryClusterPostfix is changed to secondaryClusterSuffix,
40+
- aggregateClusterPostfix is changed to aggregateClusterSuffix
41+
2642
## [0.20.1]
2743

2844
### Changed

envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/Groups.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ sealed class Group {
77
abstract val serviceName: String
88
abstract val discoveryServiceName: String?
99
abstract val proxySettings: ProxySettings
10+
abstract val pathNormalizationConfig: PathNormalizationConfig
1011
abstract val listenersConfig: ListenersConfig?
1112
}
1213

@@ -15,17 +16,25 @@ data class ServicesGroup(
1516
override val serviceName: String = "",
1617
override val discoveryServiceName: String? = null,
1718
override val proxySettings: ProxySettings = ProxySettings(),
18-
override val listenersConfig: ListenersConfig? = null
19+
override val pathNormalizationConfig: PathNormalizationConfig = PathNormalizationConfig(),
20+
override val listenersConfig: ListenersConfig? = null,
1921
) : Group()
2022

2123
data class AllServicesGroup(
2224
override val communicationMode: CommunicationMode,
2325
override val serviceName: String = "",
2426
override val discoveryServiceName: String? = null,
2527
override val proxySettings: ProxySettings = ProxySettings(),
28+
override val pathNormalizationConfig: PathNormalizationConfig = PathNormalizationConfig(),
2629
override val listenersConfig: ListenersConfig? = null
2730
) : Group()
2831

32+
data class PathNormalizationConfig(
33+
val normalizationEnabled: Boolean? = null,
34+
val mergeSlashes: Boolean? = null,
35+
val pathWithEscapedSlashesAction: String? = null
36+
)
37+
2938
data class ListenersConfig(
3039
val ingressHost: String,
3140
val ingressPort: Int,

envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/MetadataNodeGroup.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,14 @@ class MetadataNodeGroup(
172172
val discoveryServiceName = nodeMetadata.discoveryServiceName
173173
val proxySettings = proxySettings(nodeMetadata)
174174
val listenersConfig = createListenersConfig(node.id, node.metadata, node.userAgentBuildVersion)
175-
176175
return when {
177176
hasAllServicesDependencies(nodeMetadata) ->
178177
AllServicesGroup(
179178
nodeMetadata.communicationMode,
180179
serviceName,
181180
discoveryServiceName,
182181
proxySettings,
182+
nodeMetadata.pathNormalizationConfig,
183183
listenersConfig
184184
)
185185
else ->
@@ -188,6 +188,7 @@ class MetadataNodeGroup(
188188
serviceName,
189189
discoveryServiceName,
190190
proxySettings,
191+
nodeMetadata.pathNormalizationConfig,
191192
listenersConfig
192193
)
193194
}

envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class NodeMetadata(metadata: Struct, properties: SnapshotProperties) {
3333

3434
val communicationMode = getCommunicationMode(metadata.fieldsMap["ads"])
3535

36+
val pathNormalizationConfig = getPathNormalization(metadata.fieldsMap["path_normalization"], properties)
3637
val proxySettings: ProxySettings = ProxySettings(metadata.fieldsMap["proxy_settings"], properties)
3738
}
3839

@@ -68,6 +69,23 @@ data class ProxySettings(
6869
)
6970
}
7071

72+
fun getPathNormalization(proto: Value?, snapshotProperties: SnapshotProperties): PathNormalizationConfig {
73+
val defaultNormalizationConfig = PathNormalizationConfig(
74+
snapshotProperties.pathNormalization.enabled,
75+
snapshotProperties.pathNormalization.mergeSlashes,
76+
snapshotProperties.pathNormalization.pathWithEscapedSlashesAction
77+
)
78+
if (proto == null) {
79+
return defaultNormalizationConfig
80+
}
81+
return PathNormalizationConfig(
82+
normalizationEnabled = proto.field("enabled")?.boolValue ?: defaultNormalizationConfig.normalizationEnabled,
83+
mergeSlashes = proto.field("merge_slashes")?.boolValue ?: defaultNormalizationConfig.mergeSlashes,
84+
pathWithEscapedSlashesAction = proto.field("path_with_escaped_slashes_action")?.stringValue
85+
?: defaultNormalizationConfig.pathWithEscapedSlashesAction
86+
)
87+
}
88+
7189
private fun getCommunicationMode(proto: Value?): CommunicationMode {
7290
val ads = proto
7391
?.boolValue
@@ -366,9 +384,11 @@ fun Value.toIncomingEndpoint(properties: SnapshotProperties): IncomingEndpoint {
366384
pathPrefix != null -> IncomingEndpoint(
367385
pathPrefix, PathMatchingType.PATH_PREFIX, methods, clients, unlistedClientsPolicy, oauth
368386
)
387+
369388
pathRegex != null -> IncomingEndpoint(
370389
pathRegex, PathMatchingType.PATH_REGEX, methods, clients, unlistedClientsPolicy, oauth
371390
)
391+
372392
else -> throw NodeMetadataValidationException("One of 'path', 'pathPrefix' or 'pathRegex' field is required")
373393
}
374394
}
@@ -391,9 +411,11 @@ fun Value.toIncomingRateLimitEndpoint(): IncomingRateLimitEndpoint {
391411
pathPrefix != null -> IncomingRateLimitEndpoint(
392412
pathPrefix, PathMatchingType.PATH_PREFIX, methods, clients, rateLimit
393413
)
414+
394415
pathRegex != null -> IncomingRateLimitEndpoint(
395416
pathRegex, PathMatchingType.PATH_REGEX, methods, clients, rateLimit
396417
)
418+
397419
else -> throw NodeMetadataValidationException("One of 'path', 'pathPrefix' or 'pathRegex' field is required")
398420
}
399421
}
@@ -473,13 +495,15 @@ fun Value.toDuration(): Duration? {
473495
"Timeout definition has number format" +
474496
" but should be in string format and ends with 's'"
475497
)
498+
476499
Value.KindCase.STRING_VALUE -> {
477500
try {
478501
this.stringValue?.takeIf { it.isNotBlank() }?.let { Durations.parse(it) }
479502
} catch (ex: ParseException) {
480503
throw NodeMetadataValidationException("Timeout definition has incorrect format: ${ex.message}")
481504
}
482505
}
506+
483507
else -> null
484508
}
485509
}

envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataValidator.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package pl.allegro.tech.servicemesh.envoycontrol.groups
22

33
import io.envoyproxy.controlplane.server.DiscoveryServerCallbacks
4+
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
45
import pl.allegro.tech.servicemesh.envoycontrol.groups.CommunicationMode.ADS
56
import pl.allegro.tech.servicemesh.envoycontrol.groups.CommunicationMode.XDS
67
import pl.allegro.tech.servicemesh.envoycontrol.logger
@@ -40,6 +41,9 @@ class ServiceNameNotProvidedException : NodeMetadataValidationException(
4041
"Service name has not been provided."
4142
)
4243

44+
class InvalidPathWithEscapedSlashesAction(action: String) : NodeMetadataValidationException(
45+
"$action is invalid value for pathWithEscapedSlashesAction."
46+
)
4347
class RateLimitIncorrectValidationException(rateLimit: String?) : NodeMetadataValidationException(
4448
"Rate limit value: $rateLimit is incorrect."
4549
)
@@ -82,11 +86,24 @@ class NodeMetadataValidator(
8286
private fun validateMetadata(metadata: NodeMetadata) {
8387
validateServiceName(metadata)
8488
validateDependencies(metadata)
89+
validatePathNormalization(metadata)
8590
validateIncomingEndpoints(metadata)
8691
validateIncomingRateLimitEndpoints(metadata)
8792
validateConfigurationMode(metadata)
8893
}
8994

95+
private fun validatePathNormalization(metadata: NodeMetadata) {
96+
val action = metadata.pathNormalizationConfig.pathWithEscapedSlashesAction
97+
98+
if (action != null) {
99+
val actionIsValidEnumValue = HttpConnectionManager.PathWithEscapedSlashesAction.values()
100+
.any { it.name.uppercase() == action.uppercase() }
101+
if (!actionIsValidEnumValue) {
102+
throw InvalidPathWithEscapedSlashesAction(action)
103+
}
104+
}
105+
}
106+
90107
private fun validateServiceName(metadata: NodeMetadata) {
91108
if (properties.requireServiceName) {
92109
if (metadata.serviceName.isNullOrEmpty()) {

envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/SnapshotProperties.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import java.time.Duration
1313
class SnapshotProperties {
1414
var routes = RoutesProperties()
1515
var localService = LocalServiceProperties()
16+
var pathNormalization = PathNormalizationProperties()
1617
var egress = EgressProperties()
1718
var ingress = IngressProperties()
1819
var incomingPermissions = IncomingPermissionsProperties()
@@ -39,6 +40,11 @@ class SnapshotProperties {
3940
var shouldAuditGlobalSnapshot: Boolean = true
4041
}
4142

43+
class PathNormalizationProperties {
44+
var enabled = true
45+
var mergeSlashes = true
46+
var pathWithEscapedSlashesAction = "KEEP_UNCHANGED"
47+
}
4248
class MetricsProperties {
4349
var cacheSetSnapshot = false
4450
}

envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/HttpConnectionManagerFactory.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class HttpConnectionManagerFactory(
5151
): HttpConnectionManager? {
5252
val listenersConfig = group.listenersConfig!!
5353

54+
val normalizationConfig = group.pathNormalizationConfig
5455
val connectionManagerBuilder = HttpConnectionManager.newBuilder()
5556
.setStatPrefix(statPrefix)
5657
.setRds(setupRds(group.communicationMode, initialFetchTimeout, routeConfigName))
@@ -66,15 +67,22 @@ class HttpConnectionManagerFactory(
6667
.setUseRemoteAddress(BoolValue.newBuilder().setValue(listenersConfig.useRemoteAddress).build())
6768
.setDelayedCloseTimeout(Duration.newBuilder().setSeconds(0).build())
6869
.setCommonHttpProtocolOptions(httpProtocolOptions)
69-
.setMergeSlashes(true)
7070
.setCodecType(HttpConnectionManager.CodecType.AUTO)
7171
.setHttpProtocolOptions(ingressHttp1ProtocolOptions(group.serviceName))
72+
73+
normalizationConfig.normalizationEnabled
74+
?.let { connectionManagerBuilder.setNormalizePath(BoolValue.newBuilder().setValue(it).build()) }
75+
normalizationConfig.mergeSlashes?.let { connectionManagerBuilder.setMergeSlashes(it) }
76+
normalizationConfig.pathWithEscapedSlashesAction?.toPathWithEscapedSlashesActionEnum()?.let {
77+
connectionManagerBuilder.setPathWithEscapedSlashesAction(it)
78+
}
7279
if (listenersConfig.useRemoteAddress) {
7380
connectionManagerBuilder.setXffNumTrustedHops(
7481
snapshotProperties.dynamicListeners.httpFilters.ingressXffNumTrustedHops
7582
)
7683
}
7784
}
85+
7886
Direction.EGRESS -> {
7987
connectionManagerBuilder
8088
.setHttpProtocolOptions(
@@ -106,6 +114,12 @@ class HttpConnectionManagerFactory(
106114
return connectionManagerBuilder.build()
107115
}
108116

117+
private fun String.toPathWithEscapedSlashesActionEnum(): HttpConnectionManager.PathWithEscapedSlashesAction {
118+
return HttpConnectionManager.PathWithEscapedSlashesAction.values()
119+
.find { it.name.uppercase() == this.uppercase() }
120+
?: HttpConnectionManager.PathWithEscapedSlashesAction.UNRECOGNIZED
121+
}
122+
109123
private fun setupRds(
110124
communicationMode: CommunicationMode,
111125
rdsInitialFetchTimeout: Duration,

envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/EnvoySnapshotFactoryTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import pl.allegro.tech.servicemesh.envoycontrol.groups.Group
1717
import pl.allegro.tech.servicemesh.envoycontrol.groups.IncomingRateLimitEndpoint
1818
import pl.allegro.tech.servicemesh.envoycontrol.groups.ListenersConfig
1919
import pl.allegro.tech.servicemesh.envoycontrol.groups.Outgoing
20+
import pl.allegro.tech.servicemesh.envoycontrol.groups.PathNormalizationConfig
2021
import pl.allegro.tech.servicemesh.envoycontrol.groups.ProxySettings
2122
import pl.allegro.tech.servicemesh.envoycontrol.groups.ServicesGroup
2223
import pl.allegro.tech.servicemesh.envoycontrol.groups.with
@@ -405,6 +406,7 @@ class EnvoySnapshotFactoryTest {
405406
serviceDependencies = serviceDependencies(*dependencies),
406407
rateLimitEndpoints = rateLimitEndpoints
407408
),
409+
PathNormalizationConfig(),
408410
listenersConfig
409411
)
410412
}
@@ -430,6 +432,7 @@ class EnvoySnapshotFactoryTest {
430432
serviceDependencies = serviceDependencies(*dependencies),
431433
defaultServiceSettings = defaultServiceSettings
432434
),
435+
PathNormalizationConfig(),
433436
listenersConfig
434437
)
435438
}

envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/MetadataNodeGroupTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import io.envoyproxy.envoy.config.core.v3.Node as NodeV3
2525

2626
class MetadataNodeGroupTest {
2727

28+
private val defaultNormalizationConfig = PathNormalizationConfig(
29+
normalizationEnabled = true,
30+
mergeSlashes = true,
31+
pathWithEscapedSlashesAction = "KEEP_UNCHANGED"
32+
)
2833
@Test
2934
fun `should assign to group with all dependencies`() {
3035
// given
@@ -41,6 +46,7 @@ class MetadataNodeGroupTest {
4146
// because service may define different settings for different dependencies (for example endpoints, which
4247
// will be implemented in https://github.com/allegro/envoy-control/issues/6
4348
communicationMode = XDS,
49+
pathNormalizationConfig = defaultNormalizationConfig,
4450
proxySettings = ProxySettings().with(
4551
serviceDependencies = serviceDependencies("a", "b", "c"),
4652
allServicesDependencies = true
@@ -61,6 +67,7 @@ class MetadataNodeGroupTest {
6167
assertThat(group).isEqualTo(
6268
ServicesGroup(
6369
proxySettings = ProxySettings().with(serviceDependencies = setOf()),
70+
pathNormalizationConfig = defaultNormalizationConfig,
6471
communicationMode = XDS
6572
)
6673
)
@@ -79,6 +86,7 @@ class MetadataNodeGroupTest {
7986
assertThat(group).isEqualTo(
8087
ServicesGroup(
8188
proxySettings = ProxySettings().with(serviceDependencies = serviceDependencies("a", "b", "c")),
89+
pathNormalizationConfig = defaultNormalizationConfig,
8290
communicationMode = XDS
8391
)
8492
)
@@ -97,6 +105,7 @@ class MetadataNodeGroupTest {
97105
assertThat(group).isEqualTo(
98106
AllServicesGroup(
99107
communicationMode = ADS,
108+
pathNormalizationConfig = defaultNormalizationConfig,
100109
proxySettings = ProxySettings().with(allServicesDependencies = true)
101110
)
102111
)
@@ -115,6 +124,7 @@ class MetadataNodeGroupTest {
115124
assertThat(group).isEqualTo(
116125
ServicesGroup(
117126
proxySettings = ProxySettings().with(serviceDependencies = serviceDependencies("a", "b", "c")),
127+
pathNormalizationConfig = defaultNormalizationConfig,
118128
communicationMode = ADS
119129
)
120130
)
@@ -135,6 +145,7 @@ class MetadataNodeGroupTest {
135145
// we have to preserve all services even if outgoingPermissions is disabled,
136146
// because service may define different settings for different dependencies (for example retry config)
137147
communicationMode = ADS,
148+
pathNormalizationConfig = defaultNormalizationConfig,
138149
proxySettings = ProxySettings().with(serviceDependencies = serviceDependencies("a", "b", "c"))
139150
)
140151
)
@@ -157,6 +168,7 @@ class MetadataNodeGroupTest {
157168
assertThat(group).isEqualTo(
158169
ServicesGroup(
159170
proxySettings = ProxySettings().with(serviceDependencies = serviceDependencies("a", "b", "c")),
171+
pathNormalizationConfig = defaultNormalizationConfig,
160172
communicationMode = XDS,
161173
serviceName = "app1"
162174
)
@@ -197,6 +209,7 @@ class MetadataNodeGroupTest {
197209
ServicesGroup(
198210
communicationMode = ADS,
199211
serviceName = "app1",
212+
pathNormalizationConfig = defaultNormalizationConfig,
200213
proxySettings = addedProxySettings.with(serviceDependencies = serviceDependencies("a", "b"))
201214
)
202215
)
@@ -218,6 +231,7 @@ class MetadataNodeGroupTest {
218231
AllServicesGroup(
219232
communicationMode = XDS,
220233
serviceName = "app1",
234+
pathNormalizationConfig = defaultNormalizationConfig,
221235
proxySettings = addedProxySettings.with(allServicesDependencies = true)
222236
)
223237
)

0 commit comments

Comments
 (0)