Skip to content

Commit 9ddbeeb

Browse files
authored
Allow XdsFilter to handle unknown Message config (#6710)
Subset of #6700 Motivation `HttpFilterFactory<T extends Message>` forced each factory implementation to declare a specific protobuf `Message` type for its config, and required the framework to own config parsing and `FilterConfig` envelope unwrapping. This made it difficult to handle unknown or dynamically-typed xDS filter configs (e.g. Istio-specific filters), and leaked framework concerns into the factory API. Additionally, `ParsedFilterConfig` wrapped per-route filter configs and required the framework to merge and propagate them through the snapshot hierarchy (`RouteSnapshot` → `VirtualHostSnapshot` → `RouteEntry`), coupling snapshot construction to filter logic. Modifications - Replace `HttpFilterFactory<T extends Message>` with an untyped `HttpFilterFactory` interface; `create(HttpFilter, Any)` now receives the raw `Any` config directly, and factories are responsible for all parsing including `FilterConfig` envelope unwrapping. Returning `null` skips the filter. - Introduce `XdsHttpFilter` as the return type of `create()`, with default no-op `httpPreprocessor()`, `rpcPreprocessor()`, `httpDecorator()`, and `rpcDecorator()` methods. - Delete `ParsedFilterConfig`; per-route filter configs are now stored and merged as `Map<String, Any>` directly. - Move 3-level `typed_per_filter_config` merging (route-config → vhost → route) into `RouteEntry`'s constructor, eliminating `withFilterConfigs()` from `VirtualHostSnapshot` and `withRouter()` from `RouteSnapshot`. - Thread `@Nullable ListenerXdsResource` through `RouteStream` inner classes so `RouteEntry` can access upstream HTTP filters from the Router directly. - Update `RouterFilterFactory` to implement the new untyped interface. - Simplify `FilterUtil`: remove `toParsedFilterConfigs()`, update `mergeFilterConfigs` to operate on `Map<String, Any>`, `buildUpstreamFilter` takes `@Nullable RetryPolicy` directly. Result - `HttpFilterFactory` implementations no longer need to declare a protobuf type parameter, and can handle any xDS filter config including unknown or Istio-specific ones. - Per-route filter config merging is self-contained in `RouteEntry` rather than spread across the snapshot hierarchy. - `RouteEntry.filterConfig(String)` now returns `@Nullable Any` instead of `@Nullable ParsedFilterConfig`. Breaking - `HttpFilterFactory` implementations must now implement `XdsHttpFilter create(HttpFilter httpFilter, Any config)` for custom filter implementations
1 parent f21a684 commit 9ddbeeb

File tree

12 files changed

+239
-423
lines changed

12 files changed

+239
-423
lines changed

xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java

Lines changed: 34 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -20,48 +20,33 @@
2020

2121
import java.util.List;
2222
import java.util.Map;
23-
import java.util.Map.Entry;
24-
import java.util.function.Function;
2523

2624
import com.google.common.collect.ImmutableMap;
2725
import com.google.protobuf.Any;
28-
import com.google.protobuf.Message;
2926

3027
import com.linecorp.armeria.client.ClientDecoration;
3128
import com.linecorp.armeria.client.ClientDecorationBuilder;
3229
import com.linecorp.armeria.client.ClientPreprocessors;
3330
import com.linecorp.armeria.client.ClientPreprocessorsBuilder;
34-
import com.linecorp.armeria.client.DecoratingHttpClientFunction;
35-
import com.linecorp.armeria.client.DecoratingRpcClientFunction;
36-
import com.linecorp.armeria.client.HttpClient;
37-
import com.linecorp.armeria.client.HttpPreprocessor;
38-
import com.linecorp.armeria.client.RpcPreprocessor;
39-
import com.linecorp.armeria.client.retry.RetryingClient;
4031
import com.linecorp.armeria.common.annotation.Nullable;
4132
import com.linecorp.armeria.xds.filter.HttpFilterFactory;
4233
import com.linecorp.armeria.xds.filter.HttpFilterFactoryRegistry;
34+
import com.linecorp.armeria.xds.filter.XdsHttpFilter;
4335

36+
import io.envoyproxy.envoy.config.route.v3.RetryPolicy;
4437
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
4538
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter;
4639
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter.ConfigTypeCase;
4740

4841
final class FilterUtil {
4942

50-
static Map<String, ParsedFilterConfig> mergeFilterConfigs(
51-
Map<String, ParsedFilterConfig> filterConfigs1,
52-
Map<String, ParsedFilterConfig> filterConfigs2) {
53-
final ImmutableMap.Builder<String, ParsedFilterConfig> builder = ImmutableMap.builder();
54-
builder.putAll(filterConfigs1);
55-
builder.putAll(filterConfigs2);
56-
return builder.buildKeepingLast();
57-
}
58-
59-
static Map<String, ParsedFilterConfig> toParsedFilterConfigs(Map<String, Any> filterConfigMap) {
60-
final ImmutableMap.Builder<String, ParsedFilterConfig> filterConfigsBuilder = ImmutableMap.builder();
61-
for (Entry<String, Any> e: filterConfigMap.entrySet()) {
62-
filterConfigsBuilder.put(e.getKey(), ParsedFilterConfig.of(e.getKey(), e.getValue()));
63-
}
64-
return filterConfigsBuilder.buildKeepingLast();
43+
static Map<String, Any> mergeFilterConfigs(
44+
Map<String, Any> filterConfigs1,
45+
Map<String, Any> filterConfigs2) {
46+
return ImmutableMap.<String, Any>builder()
47+
.putAll(filterConfigs1)
48+
.putAll(filterConfigs2)
49+
.buildKeepingLast();
6550
}
6651

6752
static ClientPreprocessors buildDownstreamFilter(
@@ -73,98 +58,58 @@ static ClientPreprocessors buildDownstreamFilter(
7358
final ClientPreprocessorsBuilder builder = ClientPreprocessors.builder();
7459
for (int i = httpFilters.size() - 1; i >= 0; i--) {
7560
final HttpFilter httpFilter = httpFilters.get(i);
76-
final XdsFilter<?> xdsFilter = xdsHttpFilter(httpFilter, null);
77-
if (xdsFilter == null) {
61+
final XdsHttpFilter instance = resolveInstance(httpFilter, null);
62+
if (instance == null) {
7863
continue;
7964
}
80-
if (xdsFilter.filterConfig().disabled()) {
81-
continue;
82-
}
83-
builder.add(xdsFilter.httpPreprocessor());
84-
builder.addRpc(xdsFilter.rpcPreprocessor());
65+
builder.add(instance.httpPreprocessor());
66+
builder.addRpc(instance.rpcPreprocessor());
8567
}
8668
return builder.build();
8769
}
8870

8971
static ClientDecoration buildUpstreamFilter(
90-
List<HttpFilter> httpFilters, Map<String, ParsedFilterConfig> filterConfigs,
91-
@Nullable Function<? super HttpClient, RetryingClient> retryingDecorator) {
72+
List<HttpFilter> httpFilters, Map<String, Any> filterConfigs,
73+
@Nullable RetryPolicy retryPolicy) {
9274
final ClientDecorationBuilder builder = ClientDecoration.builder();
9375
for (int i = httpFilters.size() - 1; i >= 0; i--) {
9476
final HttpFilter httpFilter = httpFilters.get(i);
95-
final ParsedFilterConfig parsedFilterConfig = filterConfigs.get(httpFilter.getName());
96-
final XdsFilter<?> xdsFilter = xdsHttpFilter(httpFilter, parsedFilterConfig);
97-
if (xdsFilter == null) {
98-
continue;
99-
}
100-
if (xdsFilter.filterConfig().disabled()) {
77+
final Any perRouteConfig = filterConfigs.get(httpFilter.getName());
78+
final XdsHttpFilter instance = resolveInstance(httpFilter, perRouteConfig);
79+
if (instance == null) {
10180
continue;
10281
}
103-
builder.add(xdsFilter.httpDecorator());
104-
builder.addRpc(xdsFilter.rpcDecorator());
82+
builder.add(instance.httpDecorator());
83+
builder.addRpc(instance.rpcDecorator());
10584
}
106-
if (retryingDecorator != null) {
85+
if (retryPolicy != null) {
10786
// add the retrying decorator as the first (outermost) decorator if exists
108-
builder.add(retryingDecorator);
87+
builder.add(new RetryStateFactory(retryPolicy).retryingDecorator());
10988
}
11089
return builder.build();
11190
}
11291

11392
@Nullable
114-
private static XdsFilter<?> xdsHttpFilter(HttpFilter httpFilter,
115-
@Nullable ParsedFilterConfig parsedFilterConfig) {
116-
final HttpFilterFactory<?> filterFactory =
93+
private static XdsHttpFilter resolveInstance(
94+
HttpFilter httpFilter, @Nullable Any perRouteConfig) {
95+
final HttpFilterFactory filterFactory =
11796
HttpFilterFactoryRegistry.filterFactory(httpFilter.getName());
11897
if (filterFactory == null) {
119-
if (httpFilter.getIsOptional()) {
120-
return null;
98+
if (!httpFilter.getIsOptional()) {
99+
throw new IllegalArgumentException(
100+
"Unknown HTTP filter '" + httpFilter.getName() +
101+
"': no HttpFilterFactory registered. Register an SPI " +
102+
"HttpFilterFactory implementation to handle this filter.");
121103
}
122-
throw new IllegalArgumentException("Couldn't find filter factory: " + httpFilter.getName());
104+
return null;
123105
}
124106
checkArgument(httpFilter.getConfigTypeCase() == ConfigTypeCase.TYPED_CONFIG ||
125107
httpFilter.getConfigTypeCase() == ConfigTypeCase.CONFIGTYPE_NOT_SET,
126108
"Only 'typed_config' is supported, but '%s' was supplied",
127109
httpFilter.getConfigTypeCase());
128-
return new XdsFilter<>(filterFactory, httpFilter, parsedFilterConfig);
129-
}
130-
131-
private static class XdsFilter<T extends Message> {
132-
133-
private final HttpFilterFactory<T> filterFactory;
134-
private final T config;
135-
private final ParsedFilterConfig filterConfig;
136-
137-
XdsFilter(HttpFilterFactory<T> filterFactory, HttpFilter httpFilter,
138-
@Nullable ParsedFilterConfig filterConfig) {
139-
this.filterFactory = filterFactory;
140-
if (filterConfig != null) {
141-
this.filterConfig = filterConfig;
142-
} else {
143-
this.filterConfig = ParsedFilterConfig.of(httpFilter.getName(), httpFilter.getTypedConfig(),
144-
httpFilter.getIsOptional(), httpFilter.getDisabled());
145-
}
146-
config = this.filterConfig.parsedConfig(filterFactory.defaultConfig());
147-
}
148-
149-
public ParsedFilterConfig filterConfig() {
150-
return filterConfig;
151-
}
152-
153-
public HttpPreprocessor httpPreprocessor() {
154-
return filterFactory.httpPreprocessor(config);
155-
}
156-
157-
public RpcPreprocessor rpcPreprocessor() {
158-
return filterFactory.rpcPreprocessor(config);
159-
}
160-
161-
public DecoratingHttpClientFunction httpDecorator() {
162-
return filterFactory.httpDecorator(config);
163-
}
164-
165-
public DecoratingRpcClientFunction rpcDecorator() {
166-
return filterFactory.rpcDecorator(config);
167-
}
110+
final Any effectiveConfig =
111+
perRouteConfig != null ? perRouteConfig : httpFilter.getTypedConfig();
112+
return filterFactory.create(httpFilter, effectiveConfig);
168113
}
169114

170115
private FilterUtil() {}

xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ public final class ListenerSnapshot implements Snapshot<ListenerXdsResource> {
4242

4343
ListenerSnapshot(ListenerXdsResource listenerXdsResource, @Nullable RouteSnapshot routeSnapshot) {
4444
this.listenerXdsResource = listenerXdsResource;
45-
if (listenerXdsResource.router() != null && routeSnapshot != null) {
46-
this.routeSnapshot = routeSnapshot.withRouter(listenerXdsResource.router());
47-
} else {
48-
this.routeSnapshot = routeSnapshot;
49-
}
45+
this.routeSnapshot = routeSnapshot;
5046
downstreamFilter = FilterUtil.buildDownstreamFilter(listenerXdsResource.connectionManager());
5147
}
5248

xds/src/main/java/com/linecorp/armeria/xds/ListenerStream.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ protected Subscription onStart(SnapshotWatcher<ListenerSnapshot> watcher) {
6262
.subscribe(watcher);
6363
}
6464

65-
private SnapshotStream<ListenerSnapshot> resource2snapshot(ListenerXdsResource resource,
66-
@Nullable ConfigSource parentConfigSource) {
65+
private SnapshotStream<ListenerSnapshot> resource2snapshot(
66+
ListenerXdsResource resource, @Nullable ConfigSource parentConfigSource) {
6767
SnapshotStream<ListenerSnapshot> node = null;
6868
final HttpConnectionManager connectionManager = resource.connectionManager();
6969
if (connectionManager != null) {
7070
if (connectionManager.hasRouteConfig()) {
7171
final RouteConfiguration routeConfig = connectionManager.getRouteConfig();
72-
node = new RouteStream(context, routeConfig)
72+
node = new RouteStream(context, routeConfig, resource)
7373
.map(routeSnapshot -> new ListenerSnapshot(resource, routeSnapshot));
7474
} else if (connectionManager.hasRds()) {
7575
final Rds rds = connectionManager.getRds();
@@ -81,7 +81,7 @@ private SnapshotStream<ListenerSnapshot> resource2snapshot(ListenerXdsResource r
8181
return SnapshotStream.error(new XdsResourceException(LISTENER, resourceName,
8282
"config source not found"));
8383
}
84-
node = new RouteStream(configSource, routeName, context)
84+
node = new RouteStream(configSource, routeName, context, resource)
8585
.map(routeSnapshot -> new ListenerSnapshot(resource, routeSnapshot));
8686
}
8787
}

xds/src/main/java/com/linecorp/armeria/xds/ParsedFilterConfig.java

Lines changed: 0 additions & 119 deletions
This file was deleted.

0 commit comments

Comments
 (0)