xds: add more route matching types in converted Route data structure (#7031)

Parse other matcher types (e.g., header matchers, runtime fraction matchers, etc) that xDS Route supports.
This commit is contained in:
Chengyuan Zhang 2020-05-18 18:47:37 +00:00 committed by GitHub
parent efa9cf6798
commit 02e3c00c39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1139 additions and 408 deletions

View File

@ -24,7 +24,8 @@ dependencies {
project(':grpc-core'),
project(':grpc-services'),
project(path: ':grpc-alts', configuration: 'shadow'),
libraries.gson
libraries.gson,
libraries.re2j
def nettyDependency = implementation project(':grpc-netty')
implementation (libraries.opencensus_proto) {

View File

@ -16,9 +16,14 @@
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
import io.envoyproxy.envoy.type.FractionalPercent;
import io.envoyproxy.envoy.type.FractionalPercent.DenominatorType;
import io.grpc.EquivalentAddressGroup;
@ -38,6 +43,10 @@ import javax.annotation.Nullable;
*
* <p>For data types that need to be sent as protobuf messages, a {@code toEnvoyProtoXXX} instance
* method is defined to convert an instance to Envoy proto message.
*
* <p>Data conversion should follow the invariant: converted data is guaranteed to be valid for
* gRPC. If the protobuf message contains invalid data, the conversion should fail and no object
* should be instantiated.
*/
final class EnvoyProtoData {
@ -45,6 +54,83 @@ final class EnvoyProtoData {
private EnvoyProtoData() {
}
static final class StructOrError<T> {
/**
* Returns a {@link StructOrError} for the successfully converted data object.
*/
static <T> StructOrError<T> fromStruct(T struct) {
return new StructOrError<>(struct);
}
/**
* Returns a {@link StructOrError} for the failure to convert the data object.
*/
static <T> StructOrError<T> fromError(String errorDetail) {
return new StructOrError<>(errorDetail);
}
private final String errorDetail;
private final T struct;
private StructOrError(T struct) {
this.struct = checkNotNull(struct, "struct");
this.errorDetail = null;
}
private StructOrError(String errorDetail) {
this.struct = null;
this.errorDetail = checkNotNull(errorDetail, "errorDetail");
}
/**
* Returns struct if exists, otherwise null.
*/
@Nullable
public T getStruct() {
return struct;
}
/**
* Returns error detail if exists, otherwise null.
*/
@Nullable
String getErrorDetail() {
return errorDetail;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
StructOrError<?> that = (StructOrError<?>) o;
return Objects.equals(errorDetail, that.errorDetail) && Objects.equals(struct, that.struct);
}
@Override
public int hashCode() {
return Objects.hash(errorDetail, struct);
}
@Override
public String toString() {
if (struct != null) {
return MoreObjects.toStringHelper(this)
.add("struct", struct)
.toString();
} else {
assert errorDetail != null;
return MoreObjects.toStringHelper(this)
.add("error", errorDetail)
.toString();
}
}
}
/**
* See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.core.Locality}.
*/
@ -342,7 +428,6 @@ final class EnvoyProtoData {
/** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.Route}. */
static final class Route {
private final RouteMatch routeMatch;
@Nullable
private final RouteAction routeAction;
@VisibleForTesting
@ -355,11 +440,14 @@ final class EnvoyProtoData {
return routeMatch;
}
@Nullable
RouteAction getRouteAction() {
return routeAction;
}
boolean isDefaultRoute() {
return routeMatch.isMatchAll();
}
@Override
public boolean equals(Object o) {
if (this == o) {
@ -386,55 +474,96 @@ final class EnvoyProtoData {
.toString();
}
static Route fromEnvoyProtoRoute(io.envoyproxy.envoy.api.v2.route.Route proto) {
RouteMatch routeMatch = RouteMatch.fromEnvoyProtoRouteMatch(proto.getMatch());
RouteAction routeAction = null;
if (proto.hasRoute()) {
routeAction = RouteAction.fromEnvoyProtoRouteAction(proto.getRoute());
@Nullable
static StructOrError<Route> fromEnvoyProtoRoute(io.envoyproxy.envoy.api.v2.route.Route proto) {
StructOrError<RouteMatch> routeMatch = RouteMatch.fromEnvoyProtoRouteMatch(proto.getMatch());
if (routeMatch == null) {
return null;
}
return new Route(routeMatch, routeAction);
if (routeMatch.getErrorDetail() != null) {
return StructOrError.fromError(
"Invalid route [" + proto.getName() + "]: " + routeMatch.getErrorDetail());
}
StructOrError<RouteAction> routeAction;
switch (proto.getActionCase()) {
case ROUTE:
routeAction = RouteAction.fromEnvoyProtoRouteAction(proto.getRoute());
break;
case REDIRECT:
return StructOrError.fromError("Unsupported action type: redirect");
case DIRECT_RESPONSE:
return StructOrError.fromError("Unsupported action type: direct_response");
case FILTER_ACTION:
return StructOrError.fromError("Unsupported action type: filter_action");
case ACTION_NOT_SET:
default:
return StructOrError.fromError("Unknown action type: " + proto.getActionCase());
}
if (routeAction.getErrorDetail() != null) {
return StructOrError.fromError(
"Invalid route [" + proto.getName() + "]: " + routeAction.getErrorDetail());
}
return StructOrError.fromStruct(new Route(routeMatch.getStruct(), routeAction.getStruct()));
}
}
/** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteMatch}. */
static final class RouteMatch {
private final String prefix;
private final String path;
private final boolean hasRegex;
private final boolean caseSensitive;
// Exactly one of the following fields is non-null.
@Nullable
private final String pathPrefixMatch;
@Nullable
private final String pathExactMatch;
@Nullable
private final Pattern pathSafeRegExMatch;
private final List<HeaderMatcher> headerMatchers;
@Nullable
private final Fraction fractionMatch;
@VisibleForTesting
RouteMatch(String prefix, String path, boolean hasRegex, boolean caseSensitive) {
this.prefix = prefix;
this.path = path;
this.hasRegex = hasRegex;
this.caseSensitive = caseSensitive;
RouteMatch(
@Nullable String pathPrefixMatch, @Nullable String pathExactMatch,
@Nullable Pattern pathSafeRegExMatch, @Nullable Fraction fractionMatch,
List<HeaderMatcher> headerMatchers) {
this.pathPrefixMatch = pathPrefixMatch;
this.pathExactMatch = pathExactMatch;
this.pathSafeRegExMatch = pathSafeRegExMatch;
this.fractionMatch = fractionMatch;
this.headerMatchers = headerMatchers;
}
String getPrefix() {
return prefix;
RouteMatch(@Nullable String pathPrefixMatch, @Nullable String pathExactMatch) {
this(
pathPrefixMatch, pathExactMatch, null, null,
Collections.<HeaderMatcher>emptyList());
}
String getPath() {
return path;
@Nullable
String getPathPrefixMatch() {
return pathPrefixMatch;
}
boolean hasRegex() {
return hasRegex;
@Nullable
String getPathExactMatch() {
return pathExactMatch;
}
boolean isCaseSensitive() {
return caseSensitive;
}
boolean isDefaultMatcher() {
if (hasRegex) {
boolean isMatchAll() {
if (pathSafeRegExMatch != null || fractionMatch != null) {
return false;
}
if (!path.isEmpty()) {
if (!headerMatchers.isEmpty()) {
return false;
}
return prefix.isEmpty() || prefix.equals("/");
if (pathExactMatch != null) {
return false;
}
if (pathPrefixMatch != null) {
return pathPrefixMatch.isEmpty() || pathPrefixMatch.equals("/");
}
return false;
}
@Override
@ -446,62 +575,388 @@ final class EnvoyProtoData {
return false;
}
RouteMatch that = (RouteMatch) o;
return hasRegex == that.hasRegex
&& caseSensitive == that.caseSensitive
&& Objects.equals(prefix, that.prefix)
&& Objects.equals(path, that.path);
return Objects.equals(pathPrefixMatch, that.pathPrefixMatch)
&& Objects.equals(pathExactMatch, that.pathExactMatch)
&& Objects.equals(
pathSafeRegExMatch == null ? null : pathSafeRegExMatch.pattern(),
that.pathSafeRegExMatch == null ? null : that.pathSafeRegExMatch.pattern())
&& Objects.equals(fractionMatch, that.fractionMatch)
&& Objects.equals(headerMatchers, that.headerMatchers);
}
@Override
public int hashCode() {
return Objects.hash(prefix, path, caseSensitive, hasRegex);
return Objects.hash(
pathPrefixMatch, pathExactMatch,
pathSafeRegExMatch == null ? null : pathSafeRegExMatch.pattern(), headerMatchers,
fractionMatch);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("prefix", prefix)
.add("path", path)
.add("hasRegex", hasRegex)
.add("caseSensitive", caseSensitive)
.toString();
ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
if (pathPrefixMatch != null) {
toStringHelper.add("pathPrefixMatch", pathPrefixMatch);
}
if (pathExactMatch != null) {
toStringHelper.add("pathExactMatch", pathExactMatch);
}
if (pathSafeRegExMatch != null) {
toStringHelper.add("pathSafeRegExMatch",pathSafeRegExMatch.pattern());
}
if (fractionMatch != null) {
toStringHelper.add("fractionMatch", fractionMatch);
}
return toStringHelper.add("headerMatchers", headerMatchers).toString();
}
@VisibleForTesting
static RouteMatch fromEnvoyProtoRouteMatch(
@SuppressWarnings("deprecation")
@Nullable
static StructOrError<RouteMatch> fromEnvoyProtoRouteMatch(
io.envoyproxy.envoy.api.v2.route.RouteMatch proto) {
return new RouteMatch(
/* prefix= */ proto.getPrefix(),
/* path= */ proto.getPath(),
/* hasRegex= */ !proto.getRegex().isEmpty() || proto.hasSafeRegex(),
// case_sensitive defaults to true if the field is not set
/*caseSensitive= */ !proto.hasCaseSensitive() || proto.getCaseSensitive().getValue());
if (proto.getQueryParametersCount() != 0) {
return null;
}
if (proto.hasCaseSensitive() && !proto.getCaseSensitive().getValue()) {
return StructOrError.fromError("Unsupported match option: case insensitive");
}
Fraction fraction = null;
if (proto.hasRuntimeFraction()) {
io.envoyproxy.envoy.type.FractionalPercent percent =
proto.getRuntimeFraction().getDefaultValue();
int numerator = percent.getNumerator();
int denominator = 0;
switch (percent.getDenominator()) {
case HUNDRED:
denominator = 100;
break;
case TEN_THOUSAND:
denominator = 10_000;
break;
case MILLION:
denominator = 1_000_000;
break;
case UNRECOGNIZED:
default:
return StructOrError.fromError(
"Unrecognized fractional percent denominator: " + percent.getDenominator());
}
fraction = new Fraction(numerator, denominator);
}
String prefixPathMatch = null;
String exactPathMatch = null;
Pattern safeRegExPathMatch = null;
switch (proto.getPathSpecifierCase()) {
case PREFIX:
prefixPathMatch = proto.getPrefix();
// Supported prefix match format:
// "", "/" (default)
// "/service/"
if (!prefixPathMatch.isEmpty() && !prefixPathMatch.equals("/")) {
if (!prefixPathMatch.startsWith("/") || !prefixPathMatch.endsWith("/")
|| prefixPathMatch.length() < 3) {
return StructOrError.fromError(
"Invalid format of prefix path match: " + prefixPathMatch);
}
}
break;
case PATH:
exactPathMatch = proto.getPath();
int lastSlash = exactPathMatch.lastIndexOf('/');
// Supported exact match format:
// "/service/method"
if (!exactPathMatch.startsWith("/") || lastSlash == 0
|| lastSlash == exactPathMatch.length() - 1) {
return StructOrError.fromError(
"Invalid format of exact path match: " + exactPathMatch);
}
break;
case REGEX:
return StructOrError.fromError("Unsupported path match type: regex");
case SAFE_REGEX:
String rawPattern = proto.getSafeRegex().getRegex();
try {
safeRegExPathMatch = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
}
break;
case PATHSPECIFIER_NOT_SET:
default:
return StructOrError.fromError("Unknown path match type");
}
List<HeaderMatcher> headerMatchers = new ArrayList<>();
for (io.envoyproxy.envoy.api.v2.route.HeaderMatcher hmProto : proto.getHeadersList()) {
StructOrError<HeaderMatcher> headerMatcher =
HeaderMatcher.fromEnvoyProtoHeaderMatcher(hmProto);
if (headerMatcher.getErrorDetail() != null) {
return StructOrError.fromError(headerMatcher.getErrorDetail());
}
headerMatchers.add(headerMatcher.getStruct());
}
return StructOrError.fromStruct(
new RouteMatch(
prefixPathMatch, exactPathMatch, safeRegExPathMatch, fraction,
Collections.unmodifiableList(headerMatchers)));
}
static final class Fraction {
private final int numerator;
private final int denominator;
@VisibleForTesting
Fraction(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Fraction that = (Fraction) o;
return Objects.equals(numerator, that.numerator)
&& Objects.equals(denominator, that.denominator);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("numerator", numerator)
.add("denominator", denominator)
.toString();
}
}
}
/**
* See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.HeaderMatcher}.
*/
@SuppressWarnings("unused")
static final class HeaderMatcher {
private final String name;
// Exactly one of the following fields is non-null.
@Nullable
private final String exactMatch;
@Nullable
private final Pattern safeRegExMatch;
@Nullable
private final Range rangeMatch;
@Nullable
private final Boolean presentMatch;
@Nullable
private final String prefixMatch;
@Nullable
private final String suffixMatch;
private final boolean isInvertedMatch;
@VisibleForTesting
HeaderMatcher(
String name,
@Nullable String exactMatch, @Nullable Pattern safeRegExMatch, @Nullable Range rangeMatch,
@Nullable Boolean presentMatch, @Nullable String prefixMatch, @Nullable String suffixMatch,
boolean isInvertedMatch) {
this.name = name;
this.exactMatch = exactMatch;
this.safeRegExMatch = safeRegExMatch;
this.rangeMatch = rangeMatch;
this.presentMatch = presentMatch;
this.prefixMatch = prefixMatch;
this.suffixMatch = suffixMatch;
this.isInvertedMatch = isInvertedMatch;
}
// TODO (chengyuanzhang): add getters when needed.
@VisibleForTesting
@SuppressWarnings("deprecation")
static StructOrError<HeaderMatcher> fromEnvoyProtoHeaderMatcher(
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto) {
String exactMatch = null;
Pattern safeRegExMatch = null;
Range rangeMatch = null;
Boolean presentMatch = null;
String prefixMatch = null;
String suffixMatch = null;
switch (proto.getHeaderMatchSpecifierCase()) {
case EXACT_MATCH:
exactMatch = proto.getExactMatch();
break;
case REGEX_MATCH:
return StructOrError.fromError(
"HeaderMatcher [" + proto.getName() + "] has unsupported match type: regex");
case SAFE_REGEX_MATCH:
String rawPattern = proto.getSafeRegexMatch().getRegex();
try {
safeRegExMatch = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
return StructOrError.fromError(
"HeaderMatcher [" + proto.getName() + "] contains malformed safe regex pattern: "
+ e.getMessage());
}
break;
case RANGE_MATCH:
rangeMatch = new Range(proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd());
break;
case PRESENT_MATCH:
presentMatch = proto.getPresentMatch();
break;
case PREFIX_MATCH:
prefixMatch = proto.getPrefixMatch();
break;
case SUFFIX_MATCH:
suffixMatch = proto.getSuffixMatch();
break;
case HEADERMATCHSPECIFIER_NOT_SET:
default:
return StructOrError.fromError("Unknown header matcher type");
}
return StructOrError.fromStruct(
new HeaderMatcher(
proto.getName(), exactMatch, safeRegExMatch, rangeMatch, presentMatch,
prefixMatch, suffixMatch, proto.getInvertMatch()));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
HeaderMatcher that = (HeaderMatcher) o;
return Objects.equals(name, that.name)
&& Objects.equals(exactMatch, that.exactMatch)
&& Objects.equals(
safeRegExMatch == null ? null : safeRegExMatch.pattern(),
that.safeRegExMatch == null ? null : that.safeRegExMatch.pattern())
&& Objects.equals(rangeMatch, that.rangeMatch)
&& Objects.equals(presentMatch, that.presentMatch)
&& Objects.equals(prefixMatch, that.prefixMatch)
&& Objects.equals(suffixMatch, that.suffixMatch)
&& Objects.equals(isInvertedMatch, that.isInvertedMatch);
}
@Override
public int hashCode() {
return Objects.hash(
name, exactMatch, safeRegExMatch == null ? null : safeRegExMatch.pattern(),
rangeMatch, presentMatch, prefixMatch, suffixMatch, isInvertedMatch);
}
@Override
public String toString() {
ToStringHelper toStringHelper =
MoreObjects.toStringHelper(this).add("name", name);
if (exactMatch != null) {
toStringHelper.add("exactMatch", exactMatch);
}
if (safeRegExMatch != null) {
toStringHelper.add("safeRegExMatch", safeRegExMatch.pattern());
}
if (rangeMatch != null) {
toStringHelper.add("rangeMatch", rangeMatch);
}
if (presentMatch != null) {
toStringHelper.add("presentMatch", presentMatch);
}
if (prefixMatch != null) {
toStringHelper.add("prefixMatch", prefixMatch);
}
if (suffixMatch != null) {
toStringHelper.add("suffixMatch", suffixMatch);
}
return toStringHelper.add("isInvertedMatch", isInvertedMatch).toString();
}
static final class Range {
private final long start;
private final long end;
@VisibleForTesting
Range(long start, long end) {
this.start = start;
this.end = end;
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Range that = (Range) o;
return Objects.equals(start, that.start)
&& Objects.equals(end, that.end);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("start", start)
.add("end", end)
.toString();
}
}
}
/** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteAction}. */
static final class RouteAction {
// Exactly one of the following fields is non-null.
@Nullable
private final String cluster;
@Nullable
private final String clusterHeader;
private final List<ClusterWeight> weightedCluster;
@Nullable
private final List<ClusterWeight> weightedClusters;
@VisibleForTesting
RouteAction(String cluster, String clusterHeader, List<ClusterWeight> weightedCluster) {
RouteAction(
@Nullable String cluster, @Nullable String clusterHeader,
@Nullable List<ClusterWeight> weightedClusters) {
this.cluster = cluster;
this.clusterHeader = clusterHeader;
this.weightedCluster = Collections.unmodifiableList(weightedCluster);
this.weightedClusters = weightedClusters;
}
@Nullable
String getCluster() {
return cluster;
}
@Nullable
String getClusterHeader() {
return clusterHeader;
}
@Nullable
List<ClusterWeight> getWeightedCluster() {
return weightedCluster;
return weightedClusters;
}
@Override
@ -515,33 +970,57 @@ final class EnvoyProtoData {
RouteAction that = (RouteAction) o;
return Objects.equals(cluster, that.cluster)
&& Objects.equals(clusterHeader, that.clusterHeader)
&& Objects.equals(weightedCluster, that.weightedCluster);
&& Objects.equals(weightedClusters, that.weightedClusters);
}
@Override
public int hashCode() {
return Objects.hash(cluster, clusterHeader, weightedCluster);
return Objects.hash(cluster, clusterHeader, weightedClusters);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("cluster", cluster)
.add("clusterHeader", clusterHeader)
.add("weightedCluster", weightedCluster)
.toString();
ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
if (cluster != null) {
toStringHelper.add("cluster", cluster);
}
if (clusterHeader != null) {
toStringHelper.add("clusterHeader", clusterHeader);
}
if (weightedClusters != null) {
toStringHelper.add("weightedClusters", weightedClusters);
}
return toStringHelper.toString();
}
private static RouteAction fromEnvoyProtoRouteAction(
@VisibleForTesting
static StructOrError<RouteAction> fromEnvoyProtoRouteAction(
io.envoyproxy.envoy.api.v2.route.RouteAction proto) {
List<ClusterWeight> weightedCluster = new ArrayList<>();
List<io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight> clusterWeights
= proto.getWeightedClusters().getClustersList();
for (io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight clusterWeight
: clusterWeights) {
weightedCluster.add(ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight));
String cluster = null;
String clusterHeader = null;
List<ClusterWeight> weightedClusters = null;
switch (proto.getClusterSpecifierCase()) {
case CLUSTER:
cluster = proto.getCluster();
break;
case CLUSTER_HEADER:
clusterHeader = proto.getClusterHeader();
break;
case WEIGHTED_CLUSTERS:
List<io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight> clusterWeights
= proto.getWeightedClusters().getClustersList();
weightedClusters = new ArrayList<>();
for (io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight clusterWeight
: clusterWeights) {
weightedClusters.add(ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight));
}
break;
case CLUSTERSPECIFIER_NOT_SET:
default:
return StructOrError.fromError(
"Unknown cluster specifier: " + proto.getClusterSpecifierCase());
}
return new RouteAction(proto.getCluster(), proto.getClusterHeader(), weightedCluster);
return StructOrError.fromStruct(new RouteAction(cluster, clusterHeader, weightedClusters));
}
}
@ -592,7 +1071,8 @@ final class EnvoyProtoData {
.toString();
}
private static ClusterWeight fromEnvoyProtoClusterWeight(
@VisibleForTesting
static ClusterWeight fromEnvoyProtoClusterWeight(
io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight proto) {
return new ClusterWeight(proto.getName(), proto.getWeight().getValue());
}

View File

@ -63,12 +63,12 @@ import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.EnvoyProtoData.DropOverload;
import io.grpc.xds.EnvoyProtoData.Locality;
import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints;
import io.grpc.xds.EnvoyProtoData.RouteAction;
import io.grpc.xds.EnvoyProtoData.RouteMatch;
import io.grpc.xds.EnvoyProtoData.StructOrError;
import io.grpc.xds.LoadReportClient.LoadReportCallback;
import io.grpc.xds.XdsLogger.XdsLogLevel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -601,14 +601,13 @@ final class XdsClientImpl extends XdsClient {
// data or one supersedes the other. TBD.
if (requestedHttpConnManager.hasRouteConfig()) {
RouteConfiguration rc = requestedHttpConnManager.getRouteConfig();
routes = findRoutesInRouteConfig(rc, ldsResourceName);
String errorDetail = validateRoutes(routes);
if (errorDetail != null) {
try {
routes = findRoutesInRouteConfig(rc, ldsResourceName);
} catch (InvalidProtoDataException e) {
errorMessage =
"Listener " + ldsResourceName + " : cannot find a valid cluster name in any "
+ "virtual hosts inside RouteConfiguration with domains matching: "
+ ldsResourceName
+ " with the reason : " + errorDetail;
+ "virtual hosts domains matching: " + ldsResourceName
+ " with the reason : " + e.getMessage();
}
} else if (requestedHttpConnManager.hasRds()) {
Rds rds = requestedHttpConnManager.getRds();
@ -643,23 +642,10 @@ final class XdsClientImpl extends XdsClient {
}
if (routes != null) {
// Found routes in the in-lined RouteConfiguration.
ConfigUpdate configUpdate;
if (!enableExperimentalRouting) {
EnvoyProtoData.Route defaultRoute = Iterables.getLast(routes);
configUpdate =
ConfigUpdate.newBuilder()
.addRoutes(ImmutableList.of(defaultRoute))
.build();
logger.log(
XdsLogLevel.INFO,
"Found cluster name (inlined in route config): {0}",
defaultRoute.getRouteAction().getCluster());
} else {
configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build();
logger.log(
XdsLogLevel.INFO,
"Found routes (inlined in route config): {0}", routes);
}
logger.log(
XdsLogLevel.DEBUG,
"Found routes (inlined in route config): {0}", routes);
ConfigUpdate configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build();
configWatcher.onConfigChanged(configUpdate);
} else if (rdsRouteConfigName != null) {
// Send an RDS request if the resource to request has changed.
@ -800,9 +786,10 @@ final class XdsClientImpl extends XdsClient {
// Resolved cluster name for the requested resource, if exists.
List<EnvoyProtoData.Route> routes = null;
if (requestedRouteConfig != null) {
routes = findRoutesInRouteConfig(requestedRouteConfig, ldsResourceName);
String errorDetail = validateRoutes(routes);
if (errorDetail != null) {
try {
routes = findRoutesInRouteConfig(requestedRouteConfig, ldsResourceName);
} catch (InvalidProtoDataException e) {
String errorDetail = e.getMessage();
adsStream.sendNackRequest(
ADS_TYPE_URL_RDS, ImmutableList.of(adsStream.rdsResourceName),
rdsResponse.getVersionInfo(),
@ -824,24 +811,9 @@ final class XdsClientImpl extends XdsClient {
rdsRespTimer.cancel();
rdsRespTimer = null;
}
// Found routes in the in-lined RouteConfiguration.
ConfigUpdate configUpdate;
if (!enableExperimentalRouting) {
EnvoyProtoData.Route defaultRoute = Iterables.getLast(routes);
configUpdate =
ConfigUpdate.newBuilder()
.addRoutes(ImmutableList.of(defaultRoute))
.build();
logger.log(
XdsLogLevel.INFO,
"Found cluster name: {0}",
defaultRoute.getRouteAction().getCluster());
} else {
configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build();
logger.log(XdsLogLevel.INFO, "Found {0} routes", routes.size());
logger.log(XdsLogLevel.DEBUG, "Found routes: {0}", routes);
}
logger.log(XdsLogLevel.DEBUG, "Found routes: {0}", routes);
ConfigUpdate configUpdate =
ConfigUpdate.newBuilder().addRoutes(routes).build();
configWatcher.onConfigChanged(configUpdate);
}
}
@ -849,9 +821,79 @@ final class XdsClientImpl extends XdsClient {
/**
* Processes a RouteConfiguration message to find the routes that requests for the given host will
* be routed to.
*
* @throws InvalidProtoDataException if the message contains invalid data.
*/
private static List<EnvoyProtoData.Route> findRoutesInRouteConfig(
RouteConfiguration config, String hostName) throws InvalidProtoDataException {
VirtualHost targetVirtualHost = findVirtualHostForHostName(config, hostName);
if (targetVirtualHost == null) {
throw new InvalidProtoDataException("Unable to find virtual host for " + hostName);
}
// 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.
return populateRoutesInVirtualHost(targetVirtualHost);
}
@VisibleForTesting
static List<EnvoyProtoData.Route> findRoutesInRouteConfig(
static List<EnvoyProtoData.Route> populateRoutesInVirtualHost(VirtualHost virtualHost)
throws InvalidProtoDataException {
List<EnvoyProtoData.Route> routes = new ArrayList<>();
List<Route> routesProto = virtualHost.getRoutesList();
for (Route routeProto : routesProto) {
StructOrError<EnvoyProtoData.Route> route =
EnvoyProtoData.Route.fromEnvoyProtoRoute(routeProto);
if (route == null) {
continue;
} else if (route.getErrorDetail() != null) {
throw new InvalidProtoDataException(
"Virtual host [" + virtualHost.getName() + "] contains invalid route : "
+ route.getErrorDetail());
}
routes.add(route.getStruct());
}
if (routes.isEmpty()) {
throw new InvalidProtoDataException(
"Virtual host [" + virtualHost.getName() + "] contains no usable route");
}
// The last route must be a default route.
if (!Iterables.getLast(routes).isDefaultRoute()) {
throw new InvalidProtoDataException(
"Virtual host [" + virtualHost.getName()
+ "] contains non-default route as the last route");
}
// We only validate the default route unless path matching is enabled.
if (!enableExperimentalRouting) {
EnvoyProtoData.Route defaultRoute = Iterables.getLast(routes);
if (defaultRoute.getRouteAction().getCluster() == null) {
throw new InvalidProtoDataException(
"Virtual host [" + virtualHost.getName()
+ "] default route contains no cluster name");
}
return Collections.singletonList(defaultRoute);
}
// We do more validation if path matching is enabled, but whether every single route is
// required to be valid for grpc is TBD.
// For now we consider the whole list invalid if anything invalid for grpc is found.
// TODO(zdapeng): Fix it if the decision is different from current implementation.
// TODO(zdapeng): Add test for validation.
for (EnvoyProtoData.Route route : routes) {
if (route.getRouteAction().getCluster() == null
&& route.getRouteAction().getWeightedCluster() == null) {
throw new InvalidProtoDataException(
"Virtual host [" + virtualHost.getName()
+ "] contains route without cluster or weighted cluster");
}
}
return Collections.unmodifiableList(routes);
}
@VisibleForTesting
@Nullable
static VirtualHost findVirtualHostForHostName(
RouteConfiguration config, String hostName) {
List<VirtualHost> virtualHosts = config.getVirtualHostsList();
// Domain search order:
@ -889,91 +931,7 @@ final class XdsClientImpl extends XdsClient {
break;
}
}
List<EnvoyProtoData.Route> routes = new ArrayList<>();
// 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) {
List<Route> routesProto = targetVirtualHost.getRoutesList();
for (Route route : routesProto) {
routes.add(EnvoyProtoData.Route.fromEnvoyProtoRoute(route));
}
}
return routes;
}
/**
* Validates the given list of routes and returns error details if there's any error.
*/
@Nullable
private static String validateRoutes(List<EnvoyProtoData.Route> routes) {
if (routes.isEmpty()) {
return "No routes found";
}
// We only validate the default route unless path matching is enabled.
if (!enableExperimentalRouting) {
EnvoyProtoData.Route route = routes.get(routes.size() - 1);
RouteMatch routeMatch = route.getRouteMatch();
if (!routeMatch.isDefaultMatcher()) {
return "The last route must be the default route";
}
if (!routeMatch.isCaseSensitive()) {
return "Case-insensitive route match not supported";
}
if (route.getRouteAction() == null) {
return "Route action is not specified for the default route";
}
if (route.getRouteAction().getCluster().isEmpty()) {
return "Cluster is not specified for the default route";
}
return null;
}
// We do more validation if path matching is enabled, but whether every single route is required
// to be valid for grpc is TBD.
// For now we consider the whole list invalid if anything invalid for grpc is found.
// TODO(zdapeng): Fix it if the decision is different from current implementation.
// TODO(zdapeng): Add test for validation.
for (int i = 0; i < routes.size(); i++) {
EnvoyProtoData.Route route = routes.get(i);
RouteAction routeAction = route.getRouteAction();
if (routeAction == null) {
return "Route action is not specified for one of the routes";
}
RouteMatch routeMatch = route.getRouteMatch();
if (!routeMatch.isCaseSensitive()) {
return "Case-insensitive route match not supported";
}
if (!routeMatch.isDefaultMatcher()) {
String prefix = routeMatch.getPrefix();
String path = routeMatch.getPath();
if (!prefix.isEmpty()) {
if (!prefix.startsWith("/") || !prefix.endsWith("/") || prefix.length() < 3) {
return "Prefix route match must be in the format of '/service/'";
}
} else if (!path.isEmpty()) {
int lastSlash = path.lastIndexOf('/');
if (!path.startsWith("/") || lastSlash == 0 || lastSlash == path.length() - 1) {
return "Path route match must be in the format of '/service/method'";
}
} else if (routeMatch.hasRegex()) {
return "Regex route match not supported";
}
}
if (i == routes.size() - 1) {
if (!routeMatch.isDefaultMatcher()) {
return "The last route must be the default route";
}
}
if (routeAction.getCluster().isEmpty() && routeAction.getWeightedCluster().isEmpty()) {
return "Either cluster or weighted cluster route action must be provided";
}
}
return null;
return targetVirtualHost;
}
/**
@ -1792,4 +1750,13 @@ final class XdsClientImpl extends XdsClient {
return res;
}
}
@VisibleForTesting
static final class InvalidProtoDataException extends RuntimeException {
private static final long serialVersionUID = 1L;
private InvalidProtoDataException(String message) {
super(message, null, false, false);
}
}
}

View File

@ -159,21 +159,27 @@ final class XdsNameResolver extends NameResolver {
rawLbConfig = generateXdsRoutingRawConfig(update.getRoutes());
} else {
Route defaultRoute = Iterables.getOnlyElement(update.getRoutes());
RouteAction action = defaultRoute.getRouteAction();
String clusterName = defaultRoute.getRouteAction().getCluster();
if (!clusterName.isEmpty()) {
if (action.getCluster() != null) {
logger.log(
XdsLogLevel.INFO,
"Received config update from xDS client {0}: cluster_name={1}",
xdsClient,
clusterName);
rawLbConfig = generateCdsRawConfig(clusterName);
} else {
} else if (action.getWeightedCluster() != null) {
logger.log(
XdsLogLevel.INFO,
"Received config update with one weighted cluster route from xDS client {0}",
xdsClient);
List<ClusterWeight> clusterWeights = defaultRoute.getRouteAction().getWeightedCluster();
rawLbConfig = generateWeightedTargetRawConfig(clusterWeights);
} else {
// TODO (chengyuanzhang): route with cluster_header
logger.log(
XdsLogLevel.WARNING, "Route action with cluster_header is not implemented");
return;
}
}
@ -229,15 +235,18 @@ final class XdsNameResolver extends NameResolver {
for (Route route : routesUpdate) {
String service = "";
String method = "";
if (!route.getRouteMatch().isDefaultMatcher()) {
String prefix = route.getRouteMatch().getPrefix();
String path = route.getRouteMatch().getPath();
if (!prefix.isEmpty()) {
if (!route.isDefaultRoute()) {
String prefix = route.getRouteMatch().getPathPrefixMatch();
String path = route.getRouteMatch().getPathExactMatch();
if (prefix != null) {
service = prefix.substring(1, prefix.length() - 1);
} else if (!path.isEmpty()) {
} else if (path != null) {
int splitIndex = path.lastIndexOf('/');
service = path.substring(1, splitIndex);
method = path.substring(splitIndex + 1);
} else {
// TODO (chengyuanzhang): match with regex.
continue;
}
}
Map<String, String> methodName = ImmutableMap.of("service", service, "method", method);
@ -247,10 +256,10 @@ final class XdsNameResolver extends NameResolver {
if (exitingActions.containsKey(routeAction)) {
actionName = exitingActions.get(routeAction);
} else {
if (!routeAction.getCluster().isEmpty()) {
if (routeAction.getCluster() != null) {
actionName = "cds:" + routeAction.getCluster();
actionPolicy = generateCdsRawConfig(routeAction.getCluster());
} else {
} else if (routeAction.getWeightedCluster() != null) {
StringBuilder sb = new StringBuilder("weighted:");
List<ClusterWeight> clusterWeights = routeAction.getWeightedCluster();
for (ClusterWeight clusterWeight : clusterWeights) {
@ -266,6 +275,9 @@ final class XdsNameResolver extends NameResolver {
actionName = actionName + "_" + exitingActions.size();
}
actionPolicy = generateWeightedTargetRawConfig(clusterWeights);
} else {
// TODO (chengyuanzhang): route with cluster_header.
continue;
}
exitingActions.put(routeAction, actionName);
List<?> childPolicies = ImmutableList.of(actionPolicy);

View File

@ -20,8 +20,20 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.testing.EqualsTester;
import com.google.protobuf.BoolValue;
import io.envoyproxy.envoy.api.v2.route.RouteMatch;
import com.google.protobuf.UInt32Value;
import com.google.re2j.Pattern;
import io.envoyproxy.envoy.api.v2.route.QueryParameterMatcher;
import io.envoyproxy.envoy.api.v2.route.RedirectAction;
import io.grpc.xds.EnvoyProtoData.ClusterWeight;
import io.grpc.xds.EnvoyProtoData.HeaderMatcher;
import io.grpc.xds.EnvoyProtoData.Locality;
import io.grpc.xds.EnvoyProtoData.Route;
import io.grpc.xds.EnvoyProtoData.RouteAction;
import io.grpc.xds.EnvoyProtoData.RouteMatch;
import io.grpc.xds.EnvoyProtoData.StructOrError;
import java.util.Arrays;
import java.util.Collections;
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -75,24 +87,386 @@ public class EnvoyProtoDataTest {
// TODO(chengyuanzhang): add test for other data types.
@Test
public void routeMatchCaseSensitive() {
assertThat(
EnvoyProtoData.RouteMatch.fromEnvoyProtoRouteMatch(RouteMatch.newBuilder().build())
.isCaseSensitive())
.isTrue();
assertThat(
EnvoyProtoData.RouteMatch.fromEnvoyProtoRouteMatch(
RouteMatch.newBuilder()
.setCaseSensitive(BoolValue.newBuilder().setValue(true))
.build())
.isCaseSensitive())
.isTrue();
assertThat(
EnvoyProtoData.RouteMatch.fromEnvoyProtoRouteMatch(
RouteMatch.newBuilder()
.setCaseSensitive(BoolValue.newBuilder().setValue(false))
.build())
.isCaseSensitive())
.isFalse();
public void convertRoute() {
io.envoyproxy.envoy.api.v2.route.Route proto1 =
io.envoyproxy.envoy.api.v2.route.Route.newBuilder()
.setName("route-blade")
.setMatch(
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setPath("/service/method"))
.setRoute(
io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder()
.setCluster("cluster-foo"))
.build();
StructOrError<Route> struct1 = Route.fromEnvoyProtoRoute(proto1);
assertThat(struct1.getErrorDetail()).isNull();
assertThat(struct1.getStruct())
.isEqualTo(
new Route(
new RouteMatch(
null, "/service/method", null, null, Collections.<HeaderMatcher>emptyList()),
new RouteAction("cluster-foo", null, null)));
io.envoyproxy.envoy.api.v2.route.Route unsupportedProto =
io.envoyproxy.envoy.api.v2.route.Route.newBuilder()
.setName("route-blade")
.setMatch(io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setPath(""))
.setRedirect(RedirectAction.getDefaultInstance())
.build();
StructOrError<Route> unsupportedStruct = Route.fromEnvoyProtoRoute(unsupportedProto);
assertThat(unsupportedStruct.getErrorDetail()).isNotNull();
assertThat(unsupportedStruct.getStruct()).isNull();
}
@Test
public void isDefaultRoute() {
StructOrError<Route> struct1 = Route.fromEnvoyProtoRoute(buildSimpleRouteProto("", null));
StructOrError<Route> struct2 = Route.fromEnvoyProtoRoute(buildSimpleRouteProto("/", null));
StructOrError<Route> struct3 =
Route.fromEnvoyProtoRoute(buildSimpleRouteProto("/service/", null));
StructOrError<Route> struct4 =
Route.fromEnvoyProtoRoute(buildSimpleRouteProto(null, "/service/method"));
assertThat(struct1.getStruct().isDefaultRoute()).isTrue();
assertThat(struct2.getStruct().isDefaultRoute()).isTrue();
assertThat(struct3.getStruct().isDefaultRoute()).isFalse();
assertThat(struct4.getStruct().isDefaultRoute()).isFalse();
}
private static io.envoyproxy.envoy.api.v2.route.Route buildSimpleRouteProto(
@Nullable String pathPrefix, @Nullable String path) {
io.envoyproxy.envoy.api.v2.route.Route.Builder routeBuilder =
io.envoyproxy.envoy.api.v2.route.Route.newBuilder()
.setName("simple-route")
.setRoute(io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder()
.setCluster("simple-cluster"));
if (pathPrefix != null) {
routeBuilder.setMatch(io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setPrefix(pathPrefix));
} else if (path != null) {
routeBuilder.setMatch(io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setPath(path));
}
return routeBuilder.build();
}
@Test
public void convertRouteMatch_pathMatching() {
// path_specifier = prefix
io.envoyproxy.envoy.api.v2.route.RouteMatch proto1 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setPrefix("/").build();
StructOrError<RouteMatch> struct1 = RouteMatch.fromEnvoyProtoRouteMatch(proto1);
assertThat(struct1.getErrorDetail()).isNull();
assertThat(struct1.getStruct()).isEqualTo(
new RouteMatch("/", null, null, null, Collections.<HeaderMatcher>emptyList()));
// path_specifier = path
io.envoyproxy.envoy.api.v2.route.RouteMatch proto2 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setPath("/service/method").build();
StructOrError<RouteMatch> struct2 = RouteMatch.fromEnvoyProtoRouteMatch(proto2);
assertThat(struct2.getErrorDetail()).isNull();
assertThat(struct2.getStruct()).isEqualTo(
new RouteMatch(
null, "/service/method", null, null, Collections.<HeaderMatcher>emptyList()));
// path_specifier = regex
@SuppressWarnings("deprecation")
io.envoyproxy.envoy.api.v2.route.RouteMatch proto3 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setRegex("*").build();
StructOrError<RouteMatch> struct3 = RouteMatch.fromEnvoyProtoRouteMatch(proto3);
assertThat(struct3.getErrorDetail()).isNotNull();
assertThat(struct3.getStruct()).isNull();
// path_specifier = safe_regex
io.envoyproxy.envoy.api.v2.route.RouteMatch proto4 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setSafeRegex(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex(".")).build();
StructOrError<RouteMatch> struct4 = RouteMatch.fromEnvoyProtoRouteMatch(proto4);
assertThat(struct4.getErrorDetail()).isNull();
assertThat(struct4.getStruct()).isEqualTo(
new RouteMatch(
null, null, Pattern.compile("."), null, Collections.<HeaderMatcher>emptyList()));
// case_sensitive = false
io.envoyproxy.envoy.api.v2.route.RouteMatch proto5 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setCaseSensitive(BoolValue.newBuilder().setValue(false))
.build();
StructOrError<RouteMatch> struct5 = RouteMatch.fromEnvoyProtoRouteMatch(proto5);
assertThat(struct5.getErrorDetail()).isNotNull();
assertThat(struct5.getStruct()).isNull();
// query_parameters is set
io.envoyproxy.envoy.api.v2.route.RouteMatch proto6 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.addQueryParameters(QueryParameterMatcher.getDefaultInstance())
.build();
StructOrError<RouteMatch> struct6 = RouteMatch.fromEnvoyProtoRouteMatch(proto6);
assertThat(struct6).isNull();
// path_specifier unset
io.envoyproxy.envoy.api.v2.route.RouteMatch unsetProto =
io.envoyproxy.envoy.api.v2.route.RouteMatch.getDefaultInstance();
StructOrError<RouteMatch> unsetStruct = RouteMatch.fromEnvoyProtoRouteMatch(unsetProto);
assertThat(unsetStruct.getErrorDetail()).isNotNull();
assertThat(unsetStruct.getStruct()).isNull();
}
@Test
public void convertRouteMatch_pathMatchFormat() {
StructOrError<RouteMatch> struct1 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto("", null));
StructOrError<RouteMatch> struct2 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto("/", null));
StructOrError<RouteMatch> struct3 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto("/service", null));
StructOrError<RouteMatch> struct4 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto("/service/", null));
StructOrError<RouteMatch> struct5 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto(null, ""));
StructOrError<RouteMatch> struct6 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto(null, "/service/method"));
StructOrError<RouteMatch> struct7 =
RouteMatch.fromEnvoyProtoRouteMatch(buildSimpleRouteMatchProto(null, "/service/method/"));
StructOrError<RouteMatch> struct8 =
RouteMatch.fromEnvoyProtoRouteMatch(
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setSafeRegex(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex("["))
.build());
assertThat(struct1.getStruct()).isNotNull();
assertThat(struct2.getStruct()).isNotNull();
assertThat(struct3.getStruct()).isNull();
assertThat(struct4.getStruct()).isNotNull();
assertThat(struct5.getStruct()).isNull();
assertThat(struct6.getStruct()).isNotNull();
assertThat(struct7.getStruct()).isNull();
assertThat(struct8.getStruct()).isNull();
}
private static io.envoyproxy.envoy.api.v2.route.RouteMatch buildSimpleRouteMatchProto(
@Nullable String pathPrefix, @Nullable String path) {
io.envoyproxy.envoy.api.v2.route.RouteMatch.Builder builder =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder();
if (pathPrefix != null) {
builder.setPrefix(pathPrefix);
} else if (path != null) {
builder.setPath(path);
}
return builder.build();
}
@Test
public void convertRouteMatch_withHeaderMatching() {
io.envoyproxy.envoy.api.v2.route.RouteMatch proto =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setPrefix("")
.addHeaders(
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName(":scheme")
.setPrefixMatch("http"))
.addHeaders(
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName(":method")
.setExactMatch("PUT"))
.build();
StructOrError<RouteMatch> struct = RouteMatch.fromEnvoyProtoRouteMatch(proto);
assertThat(struct.getErrorDetail()).isNull();
assertThat(struct.getStruct())
.isEqualTo(
new RouteMatch("", null, null, null,
Arrays.asList(
new HeaderMatcher(":scheme", null, null, null, null, "http", null, false),
new HeaderMatcher(":method", "PUT", null, null, null, null, null, false))));
}
@Test
public void convertRouteMatch_withRuntimeFraction() {
io.envoyproxy.envoy.api.v2.route.RouteMatch proto =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setPrefix("")
.setRuntimeFraction(
io.envoyproxy.envoy.api.v2.core.RuntimeFractionalPercent.newBuilder()
.setDefaultValue(
io.envoyproxy.envoy.type.FractionalPercent.newBuilder()
.setNumerator(30)
.setDenominator(
io.envoyproxy.envoy.type.FractionalPercent.DenominatorType
.HUNDRED)))
.build();
StructOrError<RouteMatch> struct = RouteMatch.fromEnvoyProtoRouteMatch(proto);
assertThat(struct.getErrorDetail()).isNull();
assertThat(struct.getStruct())
.isEqualTo(
new RouteMatch(
"", null, null, new RouteMatch.Fraction(30, 100),
Collections.<HeaderMatcher>emptyList()));
}
@Test
public void convertRouteAction() {
// cluster_specifier = cluster
io.envoyproxy.envoy.api.v2.route.RouteAction proto1 =
io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder()
.setCluster("cluster-foo")
.build();
StructOrError<RouteAction> struct1 = RouteAction.fromEnvoyProtoRouteAction(proto1);
assertThat(struct1.getErrorDetail()).isNull();
assertThat(struct1.getStruct().getCluster()).isEqualTo("cluster-foo");
assertThat(struct1.getStruct().getClusterHeader()).isNull();
assertThat(struct1.getStruct().getWeightedCluster()).isNull();
// cluster_specifier = cluster_header
io.envoyproxy.envoy.api.v2.route.RouteAction proto2 =
io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder()
.setClusterHeader("cluster-bar")
.build();
StructOrError<RouteAction> struct2 = RouteAction.fromEnvoyProtoRouteAction(proto2);
assertThat(struct2.getErrorDetail()).isNull();
assertThat(struct2.getStruct().getCluster()).isNull();
assertThat(struct2.getStruct().getClusterHeader()).isEqualTo("cluster-bar");
assertThat(struct2.getStruct().getWeightedCluster()).isNull();
// cluster_specifier = weighted_cluster
io.envoyproxy.envoy.api.v2.route.RouteAction proto3 =
io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder()
.setWeightedClusters(
io.envoyproxy.envoy.api.v2.route.WeightedCluster.newBuilder()
.addClusters(
io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight.newBuilder()
.setName("cluster-baz")
.setWeight(UInt32Value.newBuilder().setValue(100))))
.build();
StructOrError<RouteAction> struct3 = RouteAction.fromEnvoyProtoRouteAction(proto3);
assertThat(struct3.getErrorDetail()).isNull();
assertThat(struct3.getStruct().getCluster()).isNull();
assertThat(struct3.getStruct().getClusterHeader()).isNull();
assertThat(struct3.getStruct().getWeightedCluster())
.containsExactly(new ClusterWeight("cluster-baz", 100));
// cluster_specifier unset
io.envoyproxy.envoy.api.v2.route.RouteAction unsetProto =
io.envoyproxy.envoy.api.v2.route.RouteAction.getDefaultInstance();
StructOrError<RouteAction> unsetStruct = RouteAction.fromEnvoyProtoRouteAction(unsetProto);
assertThat(unsetStruct.getErrorDetail()).isNotNull();
assertThat(unsetStruct.getStruct()).isNull();
}
@Test
public void convertHeaderMatcher() {
// header_match_specifier = exact_match
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto1 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName(":method")
.setExactMatch("PUT")
.build();
StructOrError<HeaderMatcher> struct1 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto1);
assertThat(struct1.getErrorDetail()).isNull();
assertThat(struct1.getStruct()).isEqualTo(
new HeaderMatcher(":method", "PUT", null, null, null, null, null, false));
// header_match_specifier = regex_match
@SuppressWarnings("deprecation")
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto2 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName(":method")
.setRegexMatch("*")
.build();
StructOrError<HeaderMatcher> struct2 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto2);
assertThat(struct2.getErrorDetail()).isNotNull();
assertThat(struct2.getStruct()).isNull();
// header_match_specifier = safe_regex_match
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto3 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName(":method")
.setSafeRegexMatch(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex("P*"))
.build();
StructOrError<HeaderMatcher> struct3 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto3);
assertThat(struct3.getErrorDetail()).isNull();
assertThat(struct3.getStruct()).isEqualTo(
new HeaderMatcher(":method", null, Pattern.compile("P*"), null, null, null, null, false));
// header_match_specifier = range_match
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto4 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName("timeout")
.setRangeMatch(
io.envoyproxy.envoy.type.Int64Range.newBuilder().setStart(10L).setEnd(20L))
.build();
StructOrError<HeaderMatcher> struct4 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto4);
assertThat(struct4.getErrorDetail()).isNull();
assertThat(struct4.getStruct()).isEqualTo(
new HeaderMatcher(
"timeout", null, null, new HeaderMatcher.Range(10L, 20L), null, null, null, false));
// header_match_specifier = present_match
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto5 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName("user-agent")
.setPresentMatch(true)
.build();
StructOrError<HeaderMatcher> struct5 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto5);
assertThat(struct5.getErrorDetail()).isNull();
assertThat(struct5.getStruct()).isEqualTo(
new HeaderMatcher("user-agent", null, null, null, true, null, null, false));
// header_match_specifier = prefix_match
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto6 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName("authority")
.setPrefixMatch("service-foo")
.build();
StructOrError<HeaderMatcher> struct6 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto6);
assertThat(struct6.getErrorDetail()).isNull();
assertThat(struct6.getStruct()).isEqualTo(
new HeaderMatcher("authority", null, null, null, null, "service-foo", null, false));
// header_match_specifier = suffix_match
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto7 =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName("authority")
.setSuffixMatch("googleapis.com")
.build();
StructOrError<HeaderMatcher> struct7 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto7);
assertThat(struct7.getErrorDetail()).isNull();
assertThat(struct7.getStruct()).isEqualTo(
new HeaderMatcher(
"authority", null, null, null, null, null, "googleapis.com", false));
// header_match_specifier unset
io.envoyproxy.envoy.api.v2.route.HeaderMatcher unsetProto =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.getDefaultInstance();
StructOrError<HeaderMatcher> unsetStruct =
HeaderMatcher.fromEnvoyProtoHeaderMatcher(unsetProto);
assertThat(unsetStruct.getErrorDetail()).isNotNull();
assertThat(unsetStruct.getStruct()).isNull();
}
@Test
public void convertHeaderMatcher_malformedRegExPattern() {
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.newBuilder()
.setName(":method")
.setSafeRegexMatch(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex("["))
.build();
StructOrError<HeaderMatcher> struct = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto);
assertThat(struct.getErrorDetail()).isNotNull();
assertThat(struct.getStruct()).isNull();
}
@Test
public void convertClusterWeight() {
io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight proto =
io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight.newBuilder()
.setName("cluster-foo")
.setWeight(UInt32Value.newBuilder().setValue(30)).build();
ClusterWeight struct = ClusterWeight.fromEnvoyProtoClusterWeight(proto);
assertThat(struct.getName()).isEqualTo("cluster-foo");
assertThat(struct.getWeight()).isEqualTo(30);
}
}

View File

@ -113,6 +113,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
@ -177,6 +178,8 @@ public class XdsClientImplTest {
@Rule
public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule();
@Rule
public ExpectedException thrown = ExpectedException.none();
private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() {
@ -722,25 +725,21 @@ public class XdsClientImplTest {
new EnvoyProtoData.Route(
// path match with cluster route
new EnvoyProtoData.RouteMatch(
/* prefix= */ "",
/* path= */ "/service1/method1",
/* hasRegex= */ false,
/* caseSensitive= */ true),
/* prefix= */ null,
/* path= */ "/service1/method1"),
new EnvoyProtoData.RouteAction(
"cl1.googleapis.com",
"",
ImmutableList.<EnvoyProtoData.ClusterWeight>of())));
null,
null)));
assertThat(routes.get(1)).isEqualTo(
new EnvoyProtoData.Route(
// path match with weighted cluster route
new EnvoyProtoData.RouteMatch(
/* prefix= */ "",
/* path= */ "/service2/method2",
/* hasRegex= */ false,
/* caseSensitive= */ true),
/* prefix= */ null,
/* path= */ "/service2/method2"),
new EnvoyProtoData.RouteAction(
"",
"",
null,
null,
ImmutableList.of(
new EnvoyProtoData.ClusterWeight("cl21.googleapis.com", 30),
new EnvoyProtoData.ClusterWeight("cl22.googleapis.com", 70)
@ -750,25 +749,21 @@ public class XdsClientImplTest {
// prefix match with cluster route
new EnvoyProtoData.RouteMatch(
/* prefix= */ "/service1/",
/* path= */ "",
/* hasRegex= */ false,
/* caseSensitive= */ true),
/* path= */ null),
new EnvoyProtoData.RouteAction(
"cl1.googleapis.com",
"",
ImmutableList.<EnvoyProtoData.ClusterWeight>of())));
null,
null)));
assertThat(routes.get(3)).isEqualTo(
new EnvoyProtoData.Route(
// default match with cluster route
new EnvoyProtoData.RouteMatch(
/* prefix= */ "",
/* path= */ "",
/* hasRegex= */ false,
/* caseSensitive= */ true),
/* path= */ null),
new EnvoyProtoData.RouteAction(
"cluster.googleapis.com",
"",
ImmutableList.<EnvoyProtoData.ClusterWeight>of())));
null,
null)));
}
/**
@ -904,97 +899,6 @@ public class XdsClientImplTest {
assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty();
}
/**
* Client receives an RDS response (after a previous LDS request-response) containing a
* RouteConfiguration message for the requested resource. But the RouteConfiguration message
* is invalid as the VirtualHost with domains matching the requested hostname contains a route
* with a case-insensitive matcher.
* The RDS response is NACKed, as if the XdsClient has not received this response.
* The config watcher is NOT notified with an error.
*/
@Test
public void matchingVirtualHostWithCaseInsensitiveAndSensitiveRouteMatch() {
xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
StreamObserver<DiscoveryRequest> requestObserver = requestObservers.poll();
Rds rdsConfig =
Rds.newBuilder()
// Must set to use ADS.
.setConfigSource(
ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance()))
.setRouteConfigName("route-foo.googleapis.com")
.build();
List<Any> listeners = ImmutableList.of(
Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */
Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build())))
);
DiscoveryResponse response =
buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000");
responseObserver.onNext(response);
// Client sends an ACK LDS request and an RDS request for "route-foo.googleapis.com". (Omitted)
assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1);
// A VirtualHost with a Route with a case-insensitive matcher.
VirtualHost virtualHost =
VirtualHost.newBuilder()
.setName("virtualhost00.googleapis.com") // don't care
.addDomains(TARGET_AUTHORITY)
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("").setCaseSensitive(
BoolValue.newBuilder().setValue(false))))
.build();
List<Any> routeConfigs = ImmutableList.of(
Any.pack(
buildRouteConfiguration("route-foo.googleapis.com",
ImmutableList.of(virtualHost))));
response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000");
responseObserver.onNext(response);
// Client sent an NACK RDS request.
verify(requestObserver)
.onNext(
argThat(new DiscoveryRequestMatcher("", "route-foo.googleapis.com",
XdsClientImpl.ADS_TYPE_URL_RDS, "0000")));
// A VirtualHost with a Route with a case-sensitive matcher.
virtualHost =
VirtualHost.newBuilder()
.setName("virtualhost00.googleapis.com") // don't care
.addDomains(TARGET_AUTHORITY)
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("").setCaseSensitive(
BoolValue.newBuilder().setValue(true))))
.build();
routeConfigs = ImmutableList.of(
Any.pack(
buildRouteConfiguration("route-foo.googleapis.com",
ImmutableList.of(virtualHost))));
response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000");
responseObserver.onNext(response);
// Client sent an ACK RDS request.
verify(requestObserver)
.onNext(
argThat(new DiscoveryRequestMatcher(
"0",
ImmutableList.of("route-foo.googleapis.com"),
XdsClientImpl.ADS_TYPE_URL_RDS,
"0000")));
verify(configWatcher).onConfigChanged(any(ConfigUpdate.class));
verifyNoMoreInteractions(configWatcher);
}
/**
* Client receives LDS/RDS responses for updating resources previously received.
*
@ -3464,117 +3368,108 @@ public class XdsClientImplTest {
}
@Test
public void findClusterNameInRouteConfig_exactMatchFirst() {
public void findVirtualHostForHostName_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));
List<EnvoyProtoData.Route> routes =
XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname);
assertThat(routes).hasSize(1);
assertThat(routes.get(0).getRouteAction().getCluster())
.isEqualTo(targetClusterName);
assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1);
}
@Test
public void findClusterNameInRouteConfig_preferSuffixDomainOverPrefixDomain() {
public void findVirtualHostForHostName_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));
List<EnvoyProtoData.Route> routes =
XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname);
assertThat(routes).hasSize(1);
assertThat(routes.get(0).getRouteAction().getCluster())
.isEqualTo(targetClusterName);
assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1);
}
@Test
public void findClusterNameInRouteConfig_asteriskMatchAnyDomain() {
public void findVirtualHostForHostName_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));
List<EnvoyProtoData.Route> routes =
XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname);
assertThat(routes).hasSize(1);
assertThat(routes.get(0).getRouteAction().getCluster())
.isEqualTo(targetClusterName);
assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1);
}
@Test
public void populateRoutesInVirtualHost_routeWithCaseInsensitiveMatch() {
VirtualHost virtualHost =
VirtualHost.newBuilder()
.setName("virtualhost00.googleapis.com") // don't care
.addDomains(TARGET_AUTHORITY)
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com"))
.setMatch(
RouteMatch.newBuilder()
.setPrefix("")
.setCaseSensitive(BoolValue.newBuilder().setValue(false))))
.build();
thrown.expect(XdsClientImpl.InvalidProtoDataException.class);
XdsClientImpl.populateRoutesInVirtualHost(virtualHost);
}
@Test
public void populateRoutesInVirtualHost_lastRouteIsNotDefaultRoute() {
VirtualHost virtualHost =
VirtualHost.newBuilder()
.setName("virtualhost00.googleapis.com") // don't care
.addDomains(TARGET_AUTHORITY)
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com"))
.setMatch(
RouteMatch.newBuilder()
.setPrefix("/service/method")
.setCaseSensitive(BoolValue.newBuilder().setValue(true))))
.build();
thrown.expect(XdsClientImpl.InvalidProtoDataException.class);
XdsClientImpl.populateRoutesInVirtualHost(virtualHost);
}
@Test

View File

@ -355,23 +355,23 @@ public class XdsNameResolverTest {
ImmutableList.of(
// path match, routed to cluster
Route.newBuilder()
.setMatch(buildPathMatch("fooSvc", "hello"))
.setMatch(buildPathExactMatch("fooSvc", "hello"))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build(),
// prefix match, routed to cluster
Route.newBuilder()
.setMatch(buildPrefixMatch("fooSvc"))
.setMatch(buildPathPrefixMatch("fooSvc"))
.setRoute(buildClusterRoute("cluster-foo.googleapis.com"))
.build(),
// path match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPathMatch("barSvc", "hello"))
.setMatch(buildPathExactMatch("barSvc", "hello"))
.setRoute(buildWeightedClusterRoute(ImmutableMap.of(
"cluster-hello.googleapis.com", 40, "cluster-hello2.googleapis.com", 60)))
.build(),
// prefix match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPrefixMatch("barSvc"))
.setMatch(buildPathPrefixMatch("barSvc"))
.setRoute(
buildWeightedClusterRoute(
ImmutableMap.of(
@ -451,6 +451,7 @@ public class XdsNameResolverTest {
// with a route resolution for a single weighted cluster route.
Route weightedClustersDefaultRoute =
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPrefix(""))
.setRoute(buildWeightedClusterRoute(
ImmutableMap.of(
"cluster-foo.googleapis.com", 20, "cluster-bar.googleapis.com", 80)))
@ -496,23 +497,23 @@ public class XdsNameResolverTest {
ImmutableList.of(
// path match, routed to cluster
Route.newBuilder()
.setMatch(buildPathMatch("fooSvc", "hello"))
.setMatch(buildPathExactMatch("fooSvc", "hello"))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build(),
// prefix match, routed to cluster
Route.newBuilder()
.setMatch(buildPrefixMatch("fooSvc"))
.setMatch(buildPathPrefixMatch("fooSvc"))
.setRoute(buildClusterRoute("cluster-foo.googleapis.com"))
.build(),
// duplicate path match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPathMatch("fooSvc", "hello"))
.setMatch(buildPathExactMatch("fooSvc", "hello"))
.setRoute(buildWeightedClusterRoute(ImmutableMap.of(
"cluster-hello.googleapis.com", 40, "cluster-hello2.googleapis.com", 60)))
.build(),
// duplicage prefix match, routed to weighted clusters
// duplicate prefix match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPrefixMatch("fooSvc"))
.setMatch(buildPathPrefixMatch("fooSvc"))
.setRoute(
buildWeightedClusterRoute(
ImmutableMap.of(
@ -520,6 +521,7 @@ public class XdsNameResolverTest {
.build(),
// default, routed to cluster
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPrefix(""))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build());
List<Any> routeConfigs = ImmutableList.of(
@ -722,11 +724,11 @@ public class XdsNameResolverTest {
return buildDiscoveryResponse(versionInfo, routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, nonce);
}
private static RouteMatch buildPrefixMatch(String service) {
private static RouteMatch buildPathPrefixMatch(String service) {
return RouteMatch.newBuilder().setPrefix("/" + service + "/").build();
}
private static RouteMatch buildPathMatch(String service, String method) {
private static RouteMatch buildPathExactMatch(String service, String method) {
return RouteMatch.newBuilder().setPath("/" + service + "/" + method).build();
}