From 559c706369f0785e13a291441a176561c1433444 Mon Sep 17 00:00:00 2001 From: jrhee17 Date: Wed, 23 Jul 2025 18:10:07 +0900 Subject: [PATCH 1/5] minimal impl --- .../linecorp/armeria/xds/it/RoutingTest.java | 79 ----- .../xds/it/VirtualHostRoutingTest.java | 283 ++++++++++++++++++ .../com/linecorp/armeria/xds/RouteEntry.java | 12 +- .../armeria/xds/VirtualHostSnapshot.java | 12 +- .../xds/client/endpoint/RouteConfig.java | 106 ++----- .../xds/client/endpoint/RouterFilter.java | 4 +- .../client/endpoint/VirtualHostMatcher.java | 142 +++++++++ 7 files changed, 477 insertions(+), 161 deletions(-) delete mode 100644 it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RoutingTest.java create mode 100644 it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java create mode 100644 xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RoutingTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RoutingTest.java deleted file mode 100644 index a4a8b2de563..00000000000 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RoutingTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2025 LY Corporation - * - * LY Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package com.linecorp.armeria.xds.it; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - -import com.linecorp.armeria.client.Endpoint; -import com.linecorp.armeria.client.WebClient; -import com.linecorp.armeria.common.AggregatedHttpResponse; -import com.linecorp.armeria.xds.XdsBootstrap; -import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; - -class RoutingTest { - - @Test - void basicCase() { - final String bootstrap = - """ - static_resources: - listeners: - - name: my-listener - api_listener: - api_listener: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager\ - .v3.HttpConnectionManager - route_config: - name: local_route - virtual_hosts: - - name: local_service1 - domains: [ "*" ] - routes: - - match: - prefix: "/" - route: - cluster: my-cluster1 - http_filters: - - name: envoy.filters.http.router - clusters: - - name: my-cluster1 - type: STATIC - load_assignment: - cluster_name: my-cluster1 - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: 127.0.0.1 - port_value: 8081 - """; - final EndpointCollectingDecorator collector = new EndpointCollectingDecorator(); - try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(XdsResourceReader.fromYaml(bootstrap)); - XdsHttpPreprocessor preprocessor = XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap)) { - final AggregatedHttpResponse res = - WebClient.builder(preprocessor).decorator(collector).build() - .blocking().get("/"); - assertThat(res.status().code()).isEqualTo(200); - assertThat(collector.endpointsQueue()).hasSize(1); - final Endpoint endpoint = collector.endpointsQueue().poll(); - assertThat(endpoint.port()).isEqualTo(8081); - } - } -} diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java new file mode 100644 index 00000000000..60f13e0e6a7 --- /dev/null +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.xds.it; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.client.UnprocessedRequestException; +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpHeaderNames; +import com.linecorp.armeria.common.HttpMethod; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; + +class VirtualHostRoutingTest { + + private static final String virtualHostRoutingBootstrap = + """ + static_resources: + listeners: + - name: my-listener + api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager\ + .v3.HttpConnectionManager + route_config: + name: local_route + virtual_hosts: + - name: local_service1 + domains: %s + routes: + - match: + prefix: "/" + route: + cluster: my-cluster1 + - name: local_service2 + domains: %s + routes: + - match: + prefix: "/" + route: + cluster: my-cluster2 + http_filters: + - name: envoy.filters.http.router + clusters: + - name: my-cluster1 + type: STATIC + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 + - name: my-cluster2 + type: STATIC + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8082 + """; + + static Stream vHostRouting_args() { + return Stream.of( + // exact match + Arguments.of("[ 'foo.com' ]", "[ 'bar.com' ]", "foo.com", 8081), + Arguments.of("[ 'foo.com' ]", "[ 'bar.com' ]", "bar.com", 8082), + Arguments.of("[ '*' ]", "[ 'bar.com' ]", "bar.com", 8082), + Arguments.of("[ 'foo.com', '*' ]", "[ 'bar.com' ]", "bar.com", 8082), + // exact preferred over prefix/suffix match + Arguments.of("[ '*ar.com', '*' ]", "[ 'bar.com' ]", "bar.com", 8082), + Arguments.of("[ 'bar.co*', '*' ]", "[ 'bar.com' ]", "bar.com", 8082), + // suffix match + Arguments.of("[ '*' ]", "[ '*foo.com' ]", "afoo.com", 8082), + Arguments.of("[ '*' ]", "[ '*foo.com' ]", "foo.com", 8081), + Arguments.of("[ '*' ]", "[ '*foo.com' ]", "oo.com", 8081), + // longer suffix is matched + Arguments.of("[ '*o.com' ]", "[ '*oo.com' ]", "foo.com", 8082), + // prefix match + Arguments.of("[ '*' ]", "[ 'foo.com*' ]", "foo.coma", 8082), + Arguments.of("[ '*' ]", "[ 'foo.com*' ]", "foo.com", 8081), + Arguments.of("[ '*' ]", "[ 'foo.com*' ]", "foo.co", 8081), + // longer prefix is matched + Arguments.of("[ 'foo.c*' ]", "[ 'foo.co*' ]", "foo.com", 8082), + // default vhost + Arguments.of("[ 'foo.com' ]", "[ '*' ]", "bar.com", 8082), + Arguments.of("[ 'foo.com' ]", "[ '*' ]", null, 8082), + // port ignore disabled by default + Arguments.of("[ '*' ]", "[ 'foo.com:8082' ]", "foo.com:8082", 8082), + Arguments.of("[ '*' ]", "[ 'foo.com' ]", "foo.com:8082", 8081), + Arguments.of("[ '*' ]", "[ '[::]:8082' ]", "[::]:8082", 8082), + Arguments.of("[ '*' ]", "[ '[::]' ]", "[::]:8082", 8081) + ); + } + + @ParameterizedTest + @MethodSource("vHostRouting_args") + void vHostRouting(String vhostPattern1, String vhostPattern2, + @Nullable String hostHeader, int expectedPort) { + final String bootstrap = virtualHostRoutingBootstrap.formatted(vhostPattern1, vhostPattern2); + final EndpointCollectingDecorator collector = new EndpointCollectingDecorator(); + try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(XdsResourceReader.fromYaml(bootstrap)); + XdsHttpPreprocessor preprocessor = XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap)) { + final RequestHeaders requestHeaders; + if (hostHeader == null) { + requestHeaders = RequestHeaders.of(HttpMethod.GET, "/"); + } else { + requestHeaders = RequestHeaders.of(HttpMethod.GET, "/", HttpHeaderNames.AUTHORITY, hostHeader); + } + final AggregatedHttpResponse res = WebClient.builder(preprocessor).decorator(collector) + .build().blocking() + .execute(HttpRequest.of(requestHeaders)); + assertThat(res.status().code()).isEqualTo(200); + assertThat(collector.endpointsQueue()).hasSize(1); + final Endpoint endpoint = collector.endpointsQueue().poll(); + assertThat(endpoint.port()).isEqualTo(expectedPort); + } + } + + private static final String ignorePortBootstrap = + """ + static_resources: + listeners: + - name: my-listener + api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager\ + .v3.HttpConnectionManager + route_config: + name: local_route + ignore_port_in_host_matching: true + virtual_hosts: + - name: local_service1 + domains: %s + routes: + - match: + prefix: "/" + route: + cluster: my-cluster1 + - name: local_service2 + domains: %s + routes: + - match: + prefix: "/" + route: + cluster: my-cluster2 + http_filters: + - name: envoy.filters.http.router + clusters: + - name: my-cluster1 + type: STATIC + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 + - name: my-cluster2 + type: STATIC + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8082 + """; + + static Stream ignorePortRouting_args() { + return Stream.of( + Arguments.of("[ '*' ]", "[ 'foo.com:8082' ]", "foo.com:8082", 8081), + Arguments.of("[ '*' ]", "[ 'foo.com' ]", "foo.com:8082", 8082), + Arguments.of("[ '*' ]", "[ '[::]:8082' ]", "[::]:8082", 8081), + Arguments.of("[ '*' ]", "[ '[::]' ]", "[::]:8082", 8082) + ); + } + + @ParameterizedTest + @MethodSource("ignorePortRouting_args") + void ignorePortRouting(String vhostPattern1, String vhostPattern2, + @Nullable String hostHeader, int expectedPort) { + final String bootstrap = ignorePortBootstrap.formatted(vhostPattern1, vhostPattern2); + final EndpointCollectingDecorator collector = new EndpointCollectingDecorator(); + try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(XdsResourceReader.fromYaml(bootstrap)); + XdsHttpPreprocessor preprocessor = XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap)) { + final RequestHeaders requestHeaders; + if (hostHeader == null) { + requestHeaders = RequestHeaders.of(HttpMethod.GET, "/"); + } else { + requestHeaders = RequestHeaders.of(HttpMethod.GET, "/", HttpHeaderNames.AUTHORITY, hostHeader); + } + final AggregatedHttpResponse res = WebClient.builder(preprocessor).decorator(collector) + .build().blocking() + .execute(HttpRequest.of(requestHeaders)); + assertThat(res.status().code()).isEqualTo(200); + assertThat(collector.endpointsQueue()).hasSize(1); + final Endpoint endpoint = collector.endpointsQueue().poll(); + assertThat(endpoint.port()).isEqualTo(expectedPort); + } + } + + private static final String noMatchBootstrap = + """ + static_resources: + listeners: + - name: my-listener + api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager\ + .v3.HttpConnectionManager + route_config: + name: local_route + virtual_hosts: + - name: local_service1 + domains: [ "foo.com" ] + routes: + - match: + prefix: "/" + route: + cluster: my-cluster + http_filters: + - name: envoy.filters.http.router + clusters: + - name: my-cluster + type: STATIC + load_assignment: + cluster_name: my-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 + """; + + @Test + void noMatchTest() { + try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(XdsResourceReader.fromYaml(noMatchBootstrap)); + XdsHttpPreprocessor preprocessor = + XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap)) { + assertThatThrownBy(() -> WebClient.of(preprocessor).blocking().get("/")) + .isInstanceOf(UnprocessedRequestException.class) + .cause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("No route has been selected for listener"); + } + } +} diff --git a/xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java b/xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java index 17c6a8669ad..064b0938d8c 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java @@ -27,6 +27,7 @@ import io.envoyproxy.envoy.config.route.v3.Route; import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; /** @@ -38,11 +39,13 @@ public final class RouteEntry { @Nullable private final ClusterSnapshot clusterSnapshot; private final Map filterConfigs; + private final int index; - RouteEntry(Route route, @Nullable ClusterSnapshot clusterSnapshot) { + RouteEntry(Route route, @Nullable ClusterSnapshot clusterSnapshot, int index) { this.route = route; this.clusterSnapshot = clusterSnapshot; filterConfigs = toParsedFilterConfigs(route.getTypedPerFilterConfigMap()); + this.index = index; } /** @@ -71,6 +74,13 @@ public ParsedFilterConfig filterConfig(String filterName) { return filterConfigs.get(filterName); } + /** + * The index of this route within a {@link VirtualHost}. + */ + public int index() { + return index; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java b/xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java index 5df83025f8e..49015e42ef5 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.envoyproxy.envoy.config.route.v3.VirtualHost; /** @@ -41,12 +42,14 @@ public final class VirtualHostSnapshot implements Snapshot routeEntriesBuilder = ImmutableList.builder(); - for (Route route: virtualHost.getRoutesList()) { + final List routes = virtualHost.getRoutesList(); + for (int i = 0; i < routes.size(); i++) { + final Route route = routes.get(i); ClusterSnapshot clusterSnapshot = null; if (route.getRoute().hasCluster()) { clusterSnapshot = clusterSnapshots.get(route.getRoute().getCluster()); } - routeEntriesBuilder.add(new RouteEntry(route, clusterSnapshot)); + routeEntriesBuilder.add(new RouteEntry(route, clusterSnapshot, i)); } routeEntries = routeEntriesBuilder.build(); this.index = index; @@ -64,7 +67,10 @@ public List routeEntries() { return routeEntries; } - int index() { + /** + * The index of this route within a {@link RouteConfiguration}. + */ + public int index() { return index; } diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfig.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfig.java index 11395d07ba1..c0469398244 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfig.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfig.java @@ -18,16 +18,14 @@ import static com.linecorp.armeria.xds.client.endpoint.XdsAttributeKeys.ROUTE_METADATA_MATCH; -import java.util.Map; -import java.util.Objects; +import java.util.List; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableList; import com.linecorp.armeria.client.ClientPreprocessors; import com.linecorp.armeria.client.HttpPreClient; import com.linecorp.armeria.client.PreClientRequestContext; import com.linecorp.armeria.client.RpcPreClient; -import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.RouteEntry; @@ -39,40 +37,43 @@ final class RouteConfig { private final HttpPreClient httpPreClient; private final RpcPreClient rpcPreClient; - private final Map precomputedRoutes; - private static final IndexPair firstPair = new IndexPair(0, 0); + private final List> precomputedRoutes; + private final VirtualHostMatcher virtualHostMatcher; RouteConfig(ListenerSnapshot listenerSnapshot) { this.listenerSnapshot = listenerSnapshot; final ClientPreprocessors preprocessors = FilterUtil.buildDownstreamFilter(listenerSnapshot); httpPreClient = preprocessors.decorate(DelegatingHttpClient.INSTANCE); rpcPreClient = preprocessors.rpcDecorate(DelegatingRpcClient.INSTANCE); - precomputedRoutes = routeEntries(listenerSnapshot); + precomputedRoutes = precomputeRoutes(listenerSnapshot); + virtualHostMatcher = new VirtualHostMatcher(listenerSnapshot); } - private static Map routeEntries(ListenerSnapshot listenerSnapshot) { + private static List> precomputeRoutes(ListenerSnapshot listenerSnapshot) { final RouteSnapshot routeSnapshot = listenerSnapshot.routeSnapshot(); if (routeSnapshot == null) { - return ImmutableMap.of(); + return ImmutableList.of(); } - final ImmutableMap.Builder builder = ImmutableMap.builder(); - for (int i = 0; i < routeSnapshot.virtualHostSnapshots().size(); i++) { + final int vhostsSz = routeSnapshot.virtualHostSnapshots().size(); + final ImmutableList.Builder> vHostsListBuilder = + ImmutableList.builderWithExpectedSize(vhostsSz); + for (int i = 0; i < vhostsSz; i++) { final VirtualHostSnapshot virtualHostSnapshot = routeSnapshot.virtualHostSnapshots().get(i); - for (int j = 0; j < virtualHostSnapshot.routeEntries().size(); j++) { + assert virtualHostSnapshot.index() == i; + final int routesSz = virtualHostSnapshot.routeEntries().size(); + final ImmutableList.Builder routesListBuilder = + ImmutableList.builderWithExpectedSize(routesSz); + for (int j = 0; j < routesSz; j++) { final RouteEntry routeEntry = virtualHostSnapshot.routeEntries().get(j); + assert j == routeEntry.index(); final SelectedRoute selectedRoute = new SelectedRoute(listenerSnapshot, routeSnapshot, virtualHostSnapshot, routeEntry); - final IndexPair pair; - if (i == 0 && j == 0) { - pair = firstPair; - } else { - pair = new IndexPair(i, j); - } - builder.put(pair, selectedRoute); + routesListBuilder.add(selectedRoute); } + vHostsListBuilder.add(routesListBuilder.build()); } - return builder.build(); + return vHostsListBuilder.build(); } ListenerSnapshot listenerSnapshot() { @@ -88,66 +89,21 @@ RpcPreClient rpcPreClient() { } @Nullable - SelectedRoute select(PreClientRequestContext ctx, @Nullable HttpRequest req) { + SelectedRoute select(PreClientRequestContext ctx) { final RouteSnapshot routeSnapshot = listenerSnapshot.routeSnapshot(); if (routeSnapshot == null) { return null; } - - for (int i = 0; i < routeSnapshot.virtualHostSnapshots().size(); i++) { - final VirtualHostSnapshot virtualHostSnapshot = routeSnapshot.virtualHostSnapshots().get(i); - if (!matches(req, virtualHostSnapshot)) { - continue; - } - for (int j = 0; j < virtualHostSnapshot.routeEntries().size(); j++) { - final RouteEntry routeEntry = virtualHostSnapshot.routeEntries().get(j); - if (!matches(req, routeEntry)) { - continue; - } - ctx.setAttr(ROUTE_METADATA_MATCH, routeEntry.route().getRoute().getMetadataMatch()); - if (i == 0 && j == 0) { - return precomputedRoutes.get(firstPair); - } - return precomputedRoutes.get(new IndexPair(i, j)); - } - } - return null; - } - - private static boolean matches(@Nullable HttpRequest req, RouteEntry routeEntry) { - // matches the first entry for now - return true; - } - - private static boolean matches(@Nullable HttpRequest req, VirtualHostSnapshot virtualHostSnapshot) { - // matches the first entry for now - return true; - } - - private static final class IndexPair { - private final int virtualHostIndex; - private final int clusterIndex; - - private IndexPair(int virtualHostIndex, int clusterIndex) { - this.virtualHostIndex = virtualHostIndex; - this.clusterIndex = clusterIndex; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final IndexPair indexPair = (IndexPair) o; - return virtualHostIndex == indexPair.virtualHostIndex && clusterIndex == indexPair.clusterIndex; + final VirtualHostSnapshot virtualHostSnapshot = virtualHostMatcher.find(ctx); + if (virtualHostSnapshot == null) { + return null; } - - @Override - public int hashCode() { - return Objects.hash(virtualHostIndex, clusterIndex); + final List routeEntries = virtualHostSnapshot.routeEntries(); + if (routeEntries.isEmpty()) { + return null; } + final RouteEntry routeEntry = routeEntries.get(0); + ctx.setAttr(ROUTE_METADATA_MATCH, routeEntry.route().getRoute().getMetadataMatch()); + return precomputedRoutes.get(virtualHostSnapshot.index()).get(routeEntry.index()); } } diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java index 861555a989b..4960d532d10 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java @@ -27,7 +27,6 @@ import com.linecorp.armeria.client.PreClientRequestContext; import com.linecorp.armeria.client.Preprocessor; import com.linecorp.armeria.client.UnprocessedRequestException; -import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.Request; import com.linecorp.armeria.common.Response; import com.linecorp.armeria.common.SessionProtocol; @@ -57,8 +56,7 @@ public O execute(PreClient delegate, PreClientRequestContext ctx, I req) t "RouteConfig is not set for the ctx. If a new ctx has been used, " + "please make sure to use ctx.newDerivedContext().")); } - final HttpRequest httpReq = ctx.request(); - final SelectedRoute selectedRoute = routeConfig.select(ctx, httpReq); + final SelectedRoute selectedRoute = routeConfig.select(ctx); if (selectedRoute == null) { throw UnprocessedRequestException.of(new IllegalArgumentException( "No route has been selected for listener '" + routeConfig.listenerSnapshot() + "'.")); diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java new file mode 100644 index 00000000000..ab34de6e057 --- /dev/null +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java @@ -0,0 +1,142 @@ +/* + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.xds.client.endpoint; + +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; + +import com.linecorp.armeria.client.PreClientRequestContext; +import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.xds.ListenerSnapshot; +import com.linecorp.armeria.xds.RouteSnapshot; +import com.linecorp.armeria.xds.VirtualHostSnapshot; + +final class VirtualHostMatcher { + + private static final Comparator KEY_LENGTH_COMPARATOR = Comparator.comparingInt(String::length); + + private final Map exactMatch; + // This can be optimized by keeping an additional map of each length + // To reduce complexity, for now just iterate and match + private final List> prefixMatch; + private final List> suffixMatch; + @Nullable + private final VirtualHostSnapshot defaultVirtualHost; + + private final boolean ignorePortInHostMatching; + + VirtualHostMatcher(ListenerSnapshot listenerSnapshot) { + final RouteSnapshot routeSnapshot = listenerSnapshot.routeSnapshot(); + if (routeSnapshot == null) { + exactMatch = Collections.emptyMap(); + suffixMatch = ImmutableList.of(); + prefixMatch = ImmutableList.of(); + defaultVirtualHost = null; + ignorePortInHostMatching = false; + return; + } + + final Map exactMatch = new HashMap<>(); + final List> prefixMatch = new ArrayList<>(); + final List> suffixMatch = new ArrayList<>(); + VirtualHostSnapshot defaultVirtualHost = null; + + for (VirtualHostSnapshot virtualHostSnapshot: routeSnapshot.virtualHostSnapshots()) { + for (String domain: virtualHostSnapshot.xdsResource().resource().getDomainsList()) { + domain = Ascii.toLowerCase(domain); + if ("*".equals(domain)) { + if (defaultVirtualHost == null) { + defaultVirtualHost = virtualHostSnapshot; + } + continue; + } + if (domain.startsWith("*")) { + suffixMatch.add(new SimpleEntry<>(domain.substring(1), virtualHostSnapshot)); + continue; + } + if (domain.endsWith("*")) { + prefixMatch.add(new SimpleEntry<>(domain.substring(0, domain.length() - 1), + virtualHostSnapshot)); + continue; + } + if (!exactMatch.containsKey(domain)) { + exactMatch.put(domain, virtualHostSnapshot); + } + } + } + // The longest domain should match the host + prefixMatch.sort(Comparator.comparing(e -> e.getKey().length(), Comparator.reverseOrder())); + suffixMatch.sort(Comparator.comparing(e -> e.getKey().length(), Comparator.reverseOrder())); + + this.exactMatch = Collections.unmodifiableMap(exactMatch); + this.suffixMatch = Collections.unmodifiableList(suffixMatch); + this.prefixMatch = Collections.unmodifiableList(prefixMatch); + this.defaultVirtualHost = defaultVirtualHost; + + ignorePortInHostMatching = routeSnapshot.xdsResource().resource().getIgnorePortInHostMatching(); + } + + @Nullable + VirtualHostSnapshot find(PreClientRequestContext ctx) { + if (exactMatch.isEmpty() && prefixMatch.isEmpty() && suffixMatch.isEmpty()) { + return defaultVirtualHost; + } + String authority = ctx.authority(); + if (authority == null) { + return defaultVirtualHost; + } + if (ignorePortInHostMatching) { + final int colonIdx = authority.lastIndexOf(':'); + final int v6EndIdx = authority.lastIndexOf(']'); + if (colonIdx != -1 && colonIdx > v6EndIdx) { + // An ipv6 address in the host header must be enclosed in square brackets + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + authority = authority.substring(0, colonIdx); + } + } + final VirtualHostSnapshot virtualHostSnapshot = exactMatch.get(authority); + if (virtualHostSnapshot != null) { + return virtualHostSnapshot; + } + for (Entry entry: suffixMatch) { + if (authority.length() <= entry.getKey().length()) { + continue; + } + if (authority.endsWith(entry.getKey())) { + return entry.getValue(); + } + } + for (Entry entry: prefixMatch) { + if (authority.length() <= entry.getKey().length()) { + continue; + } + if (authority.startsWith(entry.getKey())) { + return entry.getValue(); + } + } + return defaultVirtualHost; + } +} From 380990992a6fd701091b938f4db983ff42fe925c Mon Sep 17 00:00:00 2001 From: jrhee17 Date: Wed, 23 Jul 2025 18:53:29 +0900 Subject: [PATCH 2/5] minor lint --- .../armeria/xds/client/endpoint/VirtualHostMatcher.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java index ab34de6e057..c062ec16f3f 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java @@ -36,8 +36,6 @@ final class VirtualHostMatcher { - private static final Comparator KEY_LENGTH_COMPARATOR = Comparator.comparingInt(String::length); - private final Map exactMatch; // This can be optimized by keeping an additional map of each length // To reduce complexity, for now just iterate and match From 2ad3b395f9039f33fee3b41a1ab4097c04817509 Mon Sep 17 00:00:00 2001 From: jrhee17 Date: Mon, 28 Jul 2025 10:24:39 +0900 Subject: [PATCH 3/5] address comment by @minwoox --- .../client/endpoint/VirtualHostMatcher.java | 108 +++++------------- 1 file changed, 29 insertions(+), 79 deletions(-) diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java index c062ec16f3f..2ad78d2e98a 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java @@ -16,17 +16,7 @@ package com.linecorp.armeria.xds.client.endpoint; -import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableList; import com.linecorp.armeria.client.PreClientRequestContext; import com.linecorp.armeria.common.annotation.Nullable; @@ -34,79 +24,38 @@ import com.linecorp.armeria.xds.RouteSnapshot; import com.linecorp.armeria.xds.VirtualHostSnapshot; -final class VirtualHostMatcher { +import io.netty.util.DomainWildcardMappingBuilder; +import io.netty.util.Mapping; - private final Map exactMatch; - // This can be optimized by keeping an additional map of each length - // To reduce complexity, for now just iterate and match - private final List> prefixMatch; - private final List> suffixMatch; - @Nullable - private final VirtualHostSnapshot defaultVirtualHost; +final class VirtualHostMatcher { private final boolean ignorePortInHostMatching; + private final Mapping mapping; VirtualHostMatcher(ListenerSnapshot listenerSnapshot) { final RouteSnapshot routeSnapshot = listenerSnapshot.routeSnapshot(); if (routeSnapshot == null) { - exactMatch = Collections.emptyMap(); - suffixMatch = ImmutableList.of(); - prefixMatch = ImmutableList.of(); - defaultVirtualHost = null; ignorePortInHostMatching = false; + mapping = input -> VirtualHostEntry.NOOP; return; } - final Map exactMatch = new HashMap<>(); - final List> prefixMatch = new ArrayList<>(); - final List> suffixMatch = new ArrayList<>(); - VirtualHostSnapshot defaultVirtualHost = null; - + final DomainWildcardMappingBuilder mappingBuilder = + new DomainWildcardMappingBuilder<>(VirtualHostEntry.NOOP); for (VirtualHostSnapshot virtualHostSnapshot: routeSnapshot.virtualHostSnapshots()) { for (String domain: virtualHostSnapshot.xdsResource().resource().getDomainsList()) { domain = Ascii.toLowerCase(domain); - if ("*".equals(domain)) { - if (defaultVirtualHost == null) { - defaultVirtualHost = virtualHostSnapshot; - } - continue; - } - if (domain.startsWith("*")) { - suffixMatch.add(new SimpleEntry<>(domain.substring(1), virtualHostSnapshot)); - continue; - } - if (domain.endsWith("*")) { - prefixMatch.add(new SimpleEntry<>(domain.substring(0, domain.length() - 1), - virtualHostSnapshot)); - continue; - } - if (!exactMatch.containsKey(domain)) { - exactMatch.put(domain, virtualHostSnapshot); - } + mappingBuilder.add(domain, new VirtualHostEntry(virtualHostSnapshot)); } } - // The longest domain should match the host - prefixMatch.sort(Comparator.comparing(e -> e.getKey().length(), Comparator.reverseOrder())); - suffixMatch.sort(Comparator.comparing(e -> e.getKey().length(), Comparator.reverseOrder())); - - this.exactMatch = Collections.unmodifiableMap(exactMatch); - this.suffixMatch = Collections.unmodifiableList(suffixMatch); - this.prefixMatch = Collections.unmodifiableList(prefixMatch); - this.defaultVirtualHost = defaultVirtualHost; - + mapping = mappingBuilder.build(); ignorePortInHostMatching = routeSnapshot.xdsResource().resource().getIgnorePortInHostMatching(); } @Nullable VirtualHostSnapshot find(PreClientRequestContext ctx) { - if (exactMatch.isEmpty() && prefixMatch.isEmpty() && suffixMatch.isEmpty()) { - return defaultVirtualHost; - } String authority = ctx.authority(); - if (authority == null) { - return defaultVirtualHost; - } - if (ignorePortInHostMatching) { + if (authority != null && ignorePortInHostMatching) { final int colonIdx = authority.lastIndexOf(':'); final int v6EndIdx = authority.lastIndexOf(']'); if (colonIdx != -1 && colonIdx > v6EndIdx) { @@ -115,26 +64,27 @@ VirtualHostSnapshot find(PreClientRequestContext ctx) { authority = authority.substring(0, colonIdx); } } - final VirtualHostSnapshot virtualHostSnapshot = exactMatch.get(authority); - if (virtualHostSnapshot != null) { - return virtualHostSnapshot; + return mapping.map(authority).get(); + } + + static class VirtualHostEntry { + + private static final VirtualHostEntry NOOP = new VirtualHostEntry(); + + @Nullable + private final VirtualHostSnapshot virtualHostSnapshot; + + VirtualHostEntry(VirtualHostSnapshot virtualHostSnapshot) { + this.virtualHostSnapshot = virtualHostSnapshot; } - for (Entry entry: suffixMatch) { - if (authority.length() <= entry.getKey().length()) { - continue; - } - if (authority.endsWith(entry.getKey())) { - return entry.getValue(); - } + + private VirtualHostEntry() { + virtualHostSnapshot = null; } - for (Entry entry: prefixMatch) { - if (authority.length() <= entry.getKey().length()) { - continue; - } - if (authority.startsWith(entry.getKey())) { - return entry.getValue(); - } + + @Nullable + VirtualHostSnapshot get() { + return virtualHostSnapshot; } - return defaultVirtualHost; } } From ba584afbd2421ed2398bdf91a99ad13a2019cb39 Mon Sep 17 00:00:00 2001 From: jrhee17 Date: Mon, 28 Jul 2025 10:24:56 +0900 Subject: [PATCH 4/5] Revert "address comment by @minwoox" This reverts commit 2ad3b395f9039f33fee3b41a1ab4097c04817509. --- .../client/endpoint/VirtualHostMatcher.java | 108 +++++++++++++----- 1 file changed, 79 insertions(+), 29 deletions(-) diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java index 2ad78d2e98a..c062ec16f3f 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/VirtualHostMatcher.java @@ -16,7 +16,17 @@ package com.linecorp.armeria.xds.client.endpoint; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; import com.linecorp.armeria.client.PreClientRequestContext; import com.linecorp.armeria.common.annotation.Nullable; @@ -24,38 +34,79 @@ import com.linecorp.armeria.xds.RouteSnapshot; import com.linecorp.armeria.xds.VirtualHostSnapshot; -import io.netty.util.DomainWildcardMappingBuilder; -import io.netty.util.Mapping; - final class VirtualHostMatcher { + private final Map exactMatch; + // This can be optimized by keeping an additional map of each length + // To reduce complexity, for now just iterate and match + private final List> prefixMatch; + private final List> suffixMatch; + @Nullable + private final VirtualHostSnapshot defaultVirtualHost; + private final boolean ignorePortInHostMatching; - private final Mapping mapping; VirtualHostMatcher(ListenerSnapshot listenerSnapshot) { final RouteSnapshot routeSnapshot = listenerSnapshot.routeSnapshot(); if (routeSnapshot == null) { + exactMatch = Collections.emptyMap(); + suffixMatch = ImmutableList.of(); + prefixMatch = ImmutableList.of(); + defaultVirtualHost = null; ignorePortInHostMatching = false; - mapping = input -> VirtualHostEntry.NOOP; return; } - final DomainWildcardMappingBuilder mappingBuilder = - new DomainWildcardMappingBuilder<>(VirtualHostEntry.NOOP); + final Map exactMatch = new HashMap<>(); + final List> prefixMatch = new ArrayList<>(); + final List> suffixMatch = new ArrayList<>(); + VirtualHostSnapshot defaultVirtualHost = null; + for (VirtualHostSnapshot virtualHostSnapshot: routeSnapshot.virtualHostSnapshots()) { for (String domain: virtualHostSnapshot.xdsResource().resource().getDomainsList()) { domain = Ascii.toLowerCase(domain); - mappingBuilder.add(domain, new VirtualHostEntry(virtualHostSnapshot)); + if ("*".equals(domain)) { + if (defaultVirtualHost == null) { + defaultVirtualHost = virtualHostSnapshot; + } + continue; + } + if (domain.startsWith("*")) { + suffixMatch.add(new SimpleEntry<>(domain.substring(1), virtualHostSnapshot)); + continue; + } + if (domain.endsWith("*")) { + prefixMatch.add(new SimpleEntry<>(domain.substring(0, domain.length() - 1), + virtualHostSnapshot)); + continue; + } + if (!exactMatch.containsKey(domain)) { + exactMatch.put(domain, virtualHostSnapshot); + } } } - mapping = mappingBuilder.build(); + // The longest domain should match the host + prefixMatch.sort(Comparator.comparing(e -> e.getKey().length(), Comparator.reverseOrder())); + suffixMatch.sort(Comparator.comparing(e -> e.getKey().length(), Comparator.reverseOrder())); + + this.exactMatch = Collections.unmodifiableMap(exactMatch); + this.suffixMatch = Collections.unmodifiableList(suffixMatch); + this.prefixMatch = Collections.unmodifiableList(prefixMatch); + this.defaultVirtualHost = defaultVirtualHost; + ignorePortInHostMatching = routeSnapshot.xdsResource().resource().getIgnorePortInHostMatching(); } @Nullable VirtualHostSnapshot find(PreClientRequestContext ctx) { + if (exactMatch.isEmpty() && prefixMatch.isEmpty() && suffixMatch.isEmpty()) { + return defaultVirtualHost; + } String authority = ctx.authority(); - if (authority != null && ignorePortInHostMatching) { + if (authority == null) { + return defaultVirtualHost; + } + if (ignorePortInHostMatching) { final int colonIdx = authority.lastIndexOf(':'); final int v6EndIdx = authority.lastIndexOf(']'); if (colonIdx != -1 && colonIdx > v6EndIdx) { @@ -64,27 +115,26 @@ VirtualHostSnapshot find(PreClientRequestContext ctx) { authority = authority.substring(0, colonIdx); } } - return mapping.map(authority).get(); - } - - static class VirtualHostEntry { - - private static final VirtualHostEntry NOOP = new VirtualHostEntry(); - - @Nullable - private final VirtualHostSnapshot virtualHostSnapshot; - - VirtualHostEntry(VirtualHostSnapshot virtualHostSnapshot) { - this.virtualHostSnapshot = virtualHostSnapshot; + final VirtualHostSnapshot virtualHostSnapshot = exactMatch.get(authority); + if (virtualHostSnapshot != null) { + return virtualHostSnapshot; } - - private VirtualHostEntry() { - virtualHostSnapshot = null; + for (Entry entry: suffixMatch) { + if (authority.length() <= entry.getKey().length()) { + continue; + } + if (authority.endsWith(entry.getKey())) { + return entry.getValue(); + } } - - @Nullable - VirtualHostSnapshot get() { - return virtualHostSnapshot; + for (Entry entry: prefixMatch) { + if (authority.length() <= entry.getKey().length()) { + continue; + } + if (authority.startsWith(entry.getKey())) { + return entry.getValue(); + } } + return defaultVirtualHost; } } From e1eefe99798b64662ddf35cab00d553d61fb3ba3 Mon Sep 17 00:00:00 2001 From: jrhee17 Date: Mon, 28 Jul 2025 10:33:03 +0900 Subject: [PATCH 5/5] address comment by @ikhoon --- .../com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java index 60f13e0e6a7..831d1c4eb21 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java @@ -40,6 +40,7 @@ class VirtualHostRoutingTest { + //language=YAML private static final String virtualHostRoutingBootstrap = """ static_resources: @@ -148,6 +149,7 @@ void vHostRouting(String vhostPattern1, String vhostPattern2, } } + //language=YAML private static final String ignorePortBootstrap = """ static_resources: @@ -233,6 +235,7 @@ void ignorePortRouting(String vhostPattern1, String vhostPattern2, } } + //language=YAML private static final String noMatchBootstrap = """ static_resources: