mirror of https://github.com/grpc/grpc-java.git
xds: precise logic for selecting the virtual host in RDS responses (#6661)
Implements the precise logic for choosing the virtual host in RouteConfiguration of RDS responses. Specifically, fixes logic for domain search order. Minor fix for checking match field in RouteConfiguration. See RouteConfiguration Proto section in gRPC Client xDS API Flow design doc for specification.
This commit is contained in:
parent
06b983567e
commit
3e6a77a7ef
|
|
@ -508,7 +508,7 @@ final class XdsClientImpl extends XdsClient {
|
|||
// data or one supersedes the other. TBD.
|
||||
if (requestedHttpConnManager.hasRouteConfig()) {
|
||||
RouteConfiguration rc = requestedHttpConnManager.getRouteConfig();
|
||||
clusterName = processRouteConfig(rc);
|
||||
clusterName = findClusterNameInRouteConfig(rc, hostName);
|
||||
if (clusterName == null) {
|
||||
errorMessage = "Cannot find a valid cluster name in VirtualHost inside "
|
||||
+ "RouteConfiguration with domains matching: " + hostName + ".";
|
||||
|
|
@ -599,7 +599,7 @@ final class XdsClientImpl extends XdsClient {
|
|||
// Resolved cluster name for the requested resource, if exists.
|
||||
String clusterName = null;
|
||||
if (requestedRouteConfig != null) {
|
||||
clusterName = processRouteConfig(requestedRouteConfig);
|
||||
clusterName = findClusterNameInRouteConfig(requestedRouteConfig, hostName);
|
||||
if (clusterName == null) {
|
||||
adsStream.sendNackRequest(ADS_TYPE_URL_RDS, ImmutableList.of(adsStream.rdsResourceName),
|
||||
"Cannot find a valid cluster name in VirtualHost inside "
|
||||
|
|
@ -624,38 +624,66 @@ final class XdsClientImpl extends XdsClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Processes RouteConfiguration message (from an resource information in an LDS or RDS
|
||||
* response), which may contain a VirtualHost with domains matching the "xds:"
|
||||
* URI hostname directly in-line. Returns the clusterName found in that VirtualHost
|
||||
* message. Returns {@code null} if such a clusterName cannot be resolved.
|
||||
*
|
||||
* <p>Note we only validate VirtualHosts with domains matching the "xds:" URI hostname.
|
||||
* Processes a RouteConfiguration message to find the name of upstream cluster that requests
|
||||
* for the given host will be routed to. Returns the clusterName if found.
|
||||
* Otherwise, returns {@code null}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Nullable
|
||||
private String processRouteConfig(RouteConfiguration config) {
|
||||
static String findClusterNameInRouteConfig(RouteConfiguration config, String hostName) {
|
||||
List<VirtualHost> virtualHosts = config.getVirtualHostsList();
|
||||
int matchingLen = -1; // longest length of wildcard pattern that matches host name
|
||||
// Domain search order:
|
||||
// 1. Exact domain names: ``www.foo.com``.
|
||||
// 2. Suffix domain wildcards: ``*.foo.com`` or ``*-bar.foo.com``.
|
||||
// 3. Prefix domain wildcards: ``foo.*`` or ``foo-*``.
|
||||
// 4. Special wildcard ``*`` matching any domain.
|
||||
//
|
||||
// The longest wildcards match first.
|
||||
// Assuming only a single virtual host in the entire route configuration can match
|
||||
// on ``*`` and a domain must be unique across all virtual hosts.
|
||||
int matchingLen = -1; // longest length of wildcard pattern that matches host name
|
||||
boolean exactMatchFound = false; // true if a virtual host with exactly matched domain found
|
||||
VirtualHost targetVirtualHost = null; // target VirtualHost with longest matched domain
|
||||
for (VirtualHost vHost : virtualHosts) {
|
||||
for (String domain : vHost.getDomainsList()) {
|
||||
if (matchHostName(hostName, domain) && domain.length() > matchingLen) {
|
||||
boolean selected = false;
|
||||
if (matchHostName(hostName, domain)) { // matching
|
||||
if (!domain.contains("*")) { // exact matching
|
||||
exactMatchFound = true;
|
||||
targetVirtualHost = vHost;
|
||||
break;
|
||||
} else if (domain.length() > matchingLen) { // longer matching pattern
|
||||
selected = true;
|
||||
} else if (domain.length() == matchingLen && domain.startsWith("*")) { // suffix matching
|
||||
selected = true;
|
||||
}
|
||||
}
|
||||
if (selected) {
|
||||
matchingLen = domain.length();
|
||||
targetVirtualHost = vHost;
|
||||
}
|
||||
}
|
||||
if (exactMatchFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with the virtual host that has longest wildcard matched domain name with the
|
||||
// hostname in original "xds:" URI.
|
||||
// Note we would consider upstream cluster not found if the virtual host is not configured
|
||||
// correctly for gRPC, even if there exist other virtual hosts with (lower priority)
|
||||
// matching domains.
|
||||
if (targetVirtualHost != null) {
|
||||
// The client will look only at the last route in the list (the default route),
|
||||
// whose match field must be empty and whose route field must be set.
|
||||
// whose match field must contain a prefix field whose value is empty string
|
||||
// and whose route field must be set.
|
||||
List<Route> routes = targetVirtualHost.getRoutesList();
|
||||
if (!routes.isEmpty()) {
|
||||
Route route = routes.get(routes.size() - 1);
|
||||
// TODO(chengyuanzhang): check the match field must be empty.
|
||||
if (route.hasRoute()) {
|
||||
return route.getRoute().getCluster();
|
||||
if (route.getMatch().getPrefix().equals("")) {
|
||||
if (route.hasRoute()) {
|
||||
return route.getRoute().getCluster();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ import io.envoyproxy.envoy.api.v2.core.Node;
|
|||
import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats;
|
||||
import io.envoyproxy.envoy.api.v2.route.RedirectAction;
|
||||
import io.envoyproxy.envoy.api.v2.route.Route;
|
||||
import io.envoyproxy.envoy.api.v2.route.RouteAction;
|
||||
import io.envoyproxy.envoy.api.v2.route.RouteMatch;
|
||||
import io.envoyproxy.envoy.api.v2.route.VirtualHost;
|
||||
import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager;
|
||||
import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.Rds;
|
||||
|
|
@ -3196,6 +3198,111 @@ public class XdsClientImplTest {
|
|||
assertThat(XdsClientImpl.matchHostName("foo-bar", pattern)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findClusterNameInRouteConfig_exactMatchFirst() {
|
||||
String hostname = "a.googleapis.com";
|
||||
String targetClusterName = "cluster-hello.googleapis.com";
|
||||
VirtualHost vHost1 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost01.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("a.googleapis.com", "b.googleapis.com"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster(targetClusterName))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
VirtualHost vHost2 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost02.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("*.googleapis.com"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("cluster-hi.googleapis.com"))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
VirtualHost vHost3 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost03.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("*"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("cluster-hey.googleapis.com"))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
RouteConfiguration routeConfig =
|
||||
buildRouteConfiguration(
|
||||
"route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2, vHost3));
|
||||
String result = XdsClientImpl.findClusterNameInRouteConfig(routeConfig, hostname);
|
||||
assertThat(result).isEqualTo(targetClusterName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findClusterNameInRouteConfig_preferSuffixDomainOverPrefixDomain() {
|
||||
String hostname = "a.googleapis.com";
|
||||
String targetClusterName = "cluster-hello.googleapis.com";
|
||||
VirtualHost vHost1 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost01.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("*.googleapis.com", "b.googleapis.com"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster(targetClusterName))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
VirtualHost vHost2 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost02.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("a.googleapis.*"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("cluster-hi.googleapis.com"))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
VirtualHost vHost3 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost03.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("*"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("cluster-hey.googleapis.com"))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
RouteConfiguration routeConfig =
|
||||
buildRouteConfiguration(
|
||||
"route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2, vHost3));
|
||||
String result = XdsClientImpl.findClusterNameInRouteConfig(routeConfig, hostname);
|
||||
assertThat(result).isEqualTo(targetClusterName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findClusterNameInRouteConfig_asteriskMatchAnyDomain() {
|
||||
String hostname = "a.googleapis.com";
|
||||
String targetClusterName = "cluster-hello.googleapis.com";
|
||||
VirtualHost vHost1 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost01.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("*"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster(targetClusterName))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
VirtualHost vHost2 =
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost02.googleapis.com") // don't care
|
||||
.addAllDomains(ImmutableList.of("b.googleapis.com"))
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("cluster-hi.googleapis.com"))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
RouteConfiguration routeConfig =
|
||||
buildRouteConfiguration(
|
||||
"route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2));
|
||||
String result = XdsClientImpl.findClusterNameInRouteConfig(routeConfig, hostname);
|
||||
assertThat(result).isEqualTo(targetClusterName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void messagePrinter_printLdsResponse() {
|
||||
MessagePrinter printer = new MessagePrinter();
|
||||
|
|
@ -3232,10 +3339,16 @@ public class XdsClientImplTest {
|
|||
+ " \"name\": \"virtualhost00.googleapis.com\",\n"
|
||||
+ " \"domains\": [\"foo.googleapis.com\", \"bar.googleapis.com\"],\n"
|
||||
+ " \"routes\": [{\n"
|
||||
+ " \"match\": {\n"
|
||||
+ " \"prefix\": \"\"\n"
|
||||
+ " },\n"
|
||||
+ " \"route\": {\n"
|
||||
+ " \"cluster\": \"whatever cluster\"\n"
|
||||
+ " }\n"
|
||||
+ " }, {\n"
|
||||
+ " \"match\": {\n"
|
||||
+ " \"prefix\": \"\"\n"
|
||||
+ " },\n"
|
||||
+ " \"route\": {\n"
|
||||
+ " \"cluster\": \"cluster.googleapis.com\"\n"
|
||||
+ " }\n"
|
||||
|
|
@ -3276,10 +3389,16 @@ public class XdsClientImplTest {
|
|||
+ " \"name\": \"virtualhost00.googleapis.com\",\n"
|
||||
+ " \"domains\": [\"foo.googleapis.com\", \"bar.googleapis.com\"],\n"
|
||||
+ " \"routes\": [{\n"
|
||||
+ " \"match\": {\n"
|
||||
+ " \"prefix\": \"\"\n"
|
||||
+ " },\n"
|
||||
+ " \"route\": {\n"
|
||||
+ " \"cluster\": \"whatever cluster\"\n"
|
||||
+ " }\n"
|
||||
+ " }, {\n"
|
||||
+ " \"match\": {\n"
|
||||
+ " \"prefix\": \"\"\n"
|
||||
+ " },\n"
|
||||
+ " \"route\": {\n"
|
||||
+ " \"cluster\": \"cluster.googleapis.com\"\n"
|
||||
+ " }\n"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import io.envoyproxy.envoy.api.v2.core.SocketAddress;
|
|||
import io.envoyproxy.envoy.api.v2.listener.FilterChain;
|
||||
import io.envoyproxy.envoy.api.v2.route.Route;
|
||||
import io.envoyproxy.envoy.api.v2.route.RouteAction;
|
||||
import io.envoyproxy.envoy.api.v2.route.RouteMatch;
|
||||
import io.envoyproxy.envoy.api.v2.route.VirtualHost;
|
||||
import io.envoyproxy.envoy.config.listener.v2.ApiListener;
|
||||
import io.envoyproxy.envoy.type.FractionalPercent;
|
||||
|
|
@ -104,17 +105,19 @@ class XdsClientTestHelper {
|
|||
}
|
||||
|
||||
static VirtualHost buildVirtualHost(List<String> domains, String clusterName) {
|
||||
return
|
||||
VirtualHost.newBuilder()
|
||||
.setName("virtualhost00.googleapis.com") // don't care
|
||||
.addAllDomains(domains)
|
||||
.addRoutes(Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("whatever cluster")))
|
||||
.addRoutes(
|
||||
// Only the last (default) route matters.
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster(clusterName)))
|
||||
.build();
|
||||
return VirtualHost.newBuilder()
|
||||
.setName("virtualhost00.googleapis.com") // don't care
|
||||
.addAllDomains(domains)
|
||||
.addRoutes(
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster("whatever cluster"))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.addRoutes(
|
||||
// Only the last (default) route matters.
|
||||
Route.newBuilder()
|
||||
.setRoute(RouteAction.newBuilder().setCluster(clusterName))
|
||||
.setMatch(RouteMatch.newBuilder().setPrefix("")))
|
||||
.build();
|
||||
}
|
||||
|
||||
static Cluster buildCluster(String clusterName, @Nullable String edsServiceName,
|
||||
|
|
|
|||
Loading…
Reference in New Issue