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-core'),
project(':grpc-services'), project(':grpc-services'),
project(path: ':grpc-alts', configuration: 'shadow'), project(path: ':grpc-alts', configuration: 'shadow'),
libraries.gson libraries.gson,
libraries.re2j
def nettyDependency = implementation project(':grpc-netty') def nettyDependency = implementation project(':grpc-netty')
implementation (libraries.opencensus_proto) { implementation (libraries.opencensus_proto) {

View File

@ -16,9 +16,14 @@
package io.grpc.xds; package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList; 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;
import io.envoyproxy.envoy.type.FractionalPercent.DenominatorType; import io.envoyproxy.envoy.type.FractionalPercent.DenominatorType;
import io.grpc.EquivalentAddressGroup; 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 * <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. * 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 { final class EnvoyProtoData {
@ -45,6 +54,83 @@ final class EnvoyProtoData {
private 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}. * 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}. */ /** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.Route}. */
static final class Route { static final class Route {
private final RouteMatch routeMatch; private final RouteMatch routeMatch;
@Nullable
private final RouteAction routeAction; private final RouteAction routeAction;
@VisibleForTesting @VisibleForTesting
@ -355,11 +440,14 @@ final class EnvoyProtoData {
return routeMatch; return routeMatch;
} }
@Nullable
RouteAction getRouteAction() { RouteAction getRouteAction() {
return routeAction; return routeAction;
} }
boolean isDefaultRoute() {
return routeMatch.isMatchAll();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {
@ -386,55 +474,96 @@ final class EnvoyProtoData {
.toString(); .toString();
} }
static Route fromEnvoyProtoRoute(io.envoyproxy.envoy.api.v2.route.Route proto) { @Nullable
RouteMatch routeMatch = RouteMatch.fromEnvoyProtoRouteMatch(proto.getMatch()); static StructOrError<Route> fromEnvoyProtoRoute(io.envoyproxy.envoy.api.v2.route.Route proto) {
RouteAction routeAction = null; StructOrError<RouteMatch> routeMatch = RouteMatch.fromEnvoyProtoRouteMatch(proto.getMatch());
if (proto.hasRoute()) { if (routeMatch == null) {
routeAction = RouteAction.fromEnvoyProtoRouteAction(proto.getRoute()); 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}. */ /** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteMatch}. */
static final class RouteMatch { static final class RouteMatch {
private final String prefix; // Exactly one of the following fields is non-null.
private final String path; @Nullable
private final boolean hasRegex; private final String pathPrefixMatch;
private final boolean caseSensitive; @Nullable
private final String pathExactMatch;
@Nullable
private final Pattern pathSafeRegExMatch;
private final List<HeaderMatcher> headerMatchers;
@Nullable
private final Fraction fractionMatch;
@VisibleForTesting @VisibleForTesting
RouteMatch(String prefix, String path, boolean hasRegex, boolean caseSensitive) { RouteMatch(
this.prefix = prefix; @Nullable String pathPrefixMatch, @Nullable String pathExactMatch,
this.path = path; @Nullable Pattern pathSafeRegExMatch, @Nullable Fraction fractionMatch,
this.hasRegex = hasRegex; List<HeaderMatcher> headerMatchers) {
this.caseSensitive = caseSensitive; this.pathPrefixMatch = pathPrefixMatch;
this.pathExactMatch = pathExactMatch;
this.pathSafeRegExMatch = pathSafeRegExMatch;
this.fractionMatch = fractionMatch;
this.headerMatchers = headerMatchers;
} }
String getPrefix() { RouteMatch(@Nullable String pathPrefixMatch, @Nullable String pathExactMatch) {
return prefix; this(
pathPrefixMatch, pathExactMatch, null, null,
Collections.<HeaderMatcher>emptyList());
} }
String getPath() { @Nullable
return path; String getPathPrefixMatch() {
return pathPrefixMatch;
} }
boolean hasRegex() { @Nullable
return hasRegex; String getPathExactMatch() {
return pathExactMatch;
} }
boolean isCaseSensitive() { boolean isMatchAll() {
return caseSensitive; if (pathSafeRegExMatch != null || fractionMatch != null) {
}
boolean isDefaultMatcher() {
if (hasRegex) {
return false; return false;
} }
if (!path.isEmpty()) { if (!headerMatchers.isEmpty()) {
return false; return false;
} }
return prefix.isEmpty() || prefix.equals("/"); if (pathExactMatch != null) {
return false;
}
if (pathPrefixMatch != null) {
return pathPrefixMatch.isEmpty() || pathPrefixMatch.equals("/");
}
return false;
} }
@Override @Override
@ -446,62 +575,388 @@ final class EnvoyProtoData {
return false; return false;
} }
RouteMatch that = (RouteMatch) o; RouteMatch that = (RouteMatch) o;
return hasRegex == that.hasRegex return Objects.equals(pathPrefixMatch, that.pathPrefixMatch)
&& caseSensitive == that.caseSensitive && Objects.equals(pathExactMatch, that.pathExactMatch)
&& Objects.equals(prefix, that.prefix) && Objects.equals(
&& Objects.equals(path, that.path); pathSafeRegExMatch == null ? null : pathSafeRegExMatch.pattern(),
that.pathSafeRegExMatch == null ? null : that.pathSafeRegExMatch.pattern())
&& Objects.equals(fractionMatch, that.fractionMatch)
&& Objects.equals(headerMatchers, that.headerMatchers);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(prefix, path, caseSensitive, hasRegex); return Objects.hash(
pathPrefixMatch, pathExactMatch,
pathSafeRegExMatch == null ? null : pathSafeRegExMatch.pattern(), headerMatchers,
fractionMatch);
} }
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
.add("prefix", prefix) if (pathPrefixMatch != null) {
.add("path", path) toStringHelper.add("pathPrefixMatch", pathPrefixMatch);
.add("hasRegex", hasRegex) }
.add("caseSensitive", caseSensitive) if (pathExactMatch != null) {
.toString(); 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 @VisibleForTesting
static RouteMatch fromEnvoyProtoRouteMatch( @SuppressWarnings("deprecation")
@Nullable
static StructOrError<RouteMatch> fromEnvoyProtoRouteMatch(
io.envoyproxy.envoy.api.v2.route.RouteMatch proto) { io.envoyproxy.envoy.api.v2.route.RouteMatch proto) {
return new RouteMatch( if (proto.getQueryParametersCount() != 0) {
/* prefix= */ proto.getPrefix(), return null;
/* path= */ proto.getPath(), }
/* hasRegex= */ !proto.getRegex().isEmpty() || proto.hasSafeRegex(), if (proto.hasCaseSensitive() && !proto.getCaseSensitive().getValue()) {
// case_sensitive defaults to true if the field is not set return StructOrError.fromError("Unsupported match option: case insensitive");
/*caseSensitive= */ !proto.hasCaseSensitive() || proto.getCaseSensitive().getValue()); }
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}. */ /** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteAction}. */
static final class RouteAction { static final class RouteAction {
// Exactly one of the following fields is non-null.
@Nullable
private final String cluster; private final String cluster;
@Nullable
private final String clusterHeader; private final String clusterHeader;
private final List<ClusterWeight> weightedCluster; @Nullable
private final List<ClusterWeight> weightedClusters;
@VisibleForTesting @VisibleForTesting
RouteAction(String cluster, String clusterHeader, List<ClusterWeight> weightedCluster) { RouteAction(
@Nullable String cluster, @Nullable String clusterHeader,
@Nullable List<ClusterWeight> weightedClusters) {
this.cluster = cluster; this.cluster = cluster;
this.clusterHeader = clusterHeader; this.clusterHeader = clusterHeader;
this.weightedCluster = Collections.unmodifiableList(weightedCluster); this.weightedClusters = weightedClusters;
} }
@Nullable
String getCluster() { String getCluster() {
return cluster; return cluster;
} }
@Nullable
String getClusterHeader() { String getClusterHeader() {
return clusterHeader; return clusterHeader;
} }
@Nullable
List<ClusterWeight> getWeightedCluster() { List<ClusterWeight> getWeightedCluster() {
return weightedCluster; return weightedClusters;
} }
@Override @Override
@ -515,33 +970,57 @@ final class EnvoyProtoData {
RouteAction that = (RouteAction) o; RouteAction that = (RouteAction) o;
return Objects.equals(cluster, that.cluster) return Objects.equals(cluster, that.cluster)
&& Objects.equals(clusterHeader, that.clusterHeader) && Objects.equals(clusterHeader, that.clusterHeader)
&& Objects.equals(weightedCluster, that.weightedCluster); && Objects.equals(weightedClusters, that.weightedClusters);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(cluster, clusterHeader, weightedCluster); return Objects.hash(cluster, clusterHeader, weightedClusters);
} }
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
.add("cluster", cluster) if (cluster != null) {
.add("clusterHeader", clusterHeader) toStringHelper.add("cluster", cluster);
.add("weightedCluster", weightedCluster) }
.toString(); 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) { io.envoyproxy.envoy.api.v2.route.RouteAction proto) {
List<ClusterWeight> weightedCluster = new ArrayList<>(); String cluster = null;
List<io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight> clusterWeights String clusterHeader = null;
= proto.getWeightedClusters().getClustersList(); List<ClusterWeight> weightedClusters = null;
for (io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight clusterWeight switch (proto.getClusterSpecifierCase()) {
: clusterWeights) { case CLUSTER:
weightedCluster.add(ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight)); 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(); .toString();
} }
private static ClusterWeight fromEnvoyProtoClusterWeight( @VisibleForTesting
static ClusterWeight fromEnvoyProtoClusterWeight(
io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight proto) { io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight proto) {
return new ClusterWeight(proto.getName(), proto.getWeight().getValue()); 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.DropOverload;
import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.Locality;
import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints;
import io.grpc.xds.EnvoyProtoData.RouteAction; import io.grpc.xds.EnvoyProtoData.StructOrError;
import io.grpc.xds.EnvoyProtoData.RouteMatch;
import io.grpc.xds.LoadReportClient.LoadReportCallback; import io.grpc.xds.LoadReportClient.LoadReportCallback;
import io.grpc.xds.XdsLogger.XdsLogLevel; import io.grpc.xds.XdsLogger.XdsLogLevel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -601,14 +601,13 @@ final class XdsClientImpl extends XdsClient {
// data or one supersedes the other. TBD. // data or one supersedes the other. TBD.
if (requestedHttpConnManager.hasRouteConfig()) { if (requestedHttpConnManager.hasRouteConfig()) {
RouteConfiguration rc = requestedHttpConnManager.getRouteConfig(); RouteConfiguration rc = requestedHttpConnManager.getRouteConfig();
routes = findRoutesInRouteConfig(rc, ldsResourceName); try {
String errorDetail = validateRoutes(routes); routes = findRoutesInRouteConfig(rc, ldsResourceName);
if (errorDetail != null) { } catch (InvalidProtoDataException e) {
errorMessage = errorMessage =
"Listener " + ldsResourceName + " : cannot find a valid cluster name in any " "Listener " + ldsResourceName + " : cannot find a valid cluster name in any "
+ "virtual hosts inside RouteConfiguration with domains matching: " + "virtual hosts domains matching: " + ldsResourceName
+ ldsResourceName + " with the reason : " + e.getMessage();
+ " with the reason : " + errorDetail;
} }
} else if (requestedHttpConnManager.hasRds()) { } else if (requestedHttpConnManager.hasRds()) {
Rds rds = requestedHttpConnManager.getRds(); Rds rds = requestedHttpConnManager.getRds();
@ -643,23 +642,10 @@ final class XdsClientImpl extends XdsClient {
} }
if (routes != null) { if (routes != null) {
// Found routes in the in-lined RouteConfiguration. // Found routes in the in-lined RouteConfiguration.
ConfigUpdate configUpdate; logger.log(
if (!enableExperimentalRouting) { XdsLogLevel.DEBUG,
EnvoyProtoData.Route defaultRoute = Iterables.getLast(routes); "Found routes (inlined in route config): {0}", routes);
configUpdate = ConfigUpdate configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build();
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);
}
configWatcher.onConfigChanged(configUpdate); configWatcher.onConfigChanged(configUpdate);
} else if (rdsRouteConfigName != null) { } else if (rdsRouteConfigName != null) {
// Send an RDS request if the resource to request has changed. // 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. // Resolved cluster name for the requested resource, if exists.
List<EnvoyProtoData.Route> routes = null; List<EnvoyProtoData.Route> routes = null;
if (requestedRouteConfig != null) { if (requestedRouteConfig != null) {
routes = findRoutesInRouteConfig(requestedRouteConfig, ldsResourceName); try {
String errorDetail = validateRoutes(routes); routes = findRoutesInRouteConfig(requestedRouteConfig, ldsResourceName);
if (errorDetail != null) { } catch (InvalidProtoDataException e) {
String errorDetail = e.getMessage();
adsStream.sendNackRequest( adsStream.sendNackRequest(
ADS_TYPE_URL_RDS, ImmutableList.of(adsStream.rdsResourceName), ADS_TYPE_URL_RDS, ImmutableList.of(adsStream.rdsResourceName),
rdsResponse.getVersionInfo(), rdsResponse.getVersionInfo(),
@ -824,24 +811,9 @@ final class XdsClientImpl extends XdsClient {
rdsRespTimer.cancel(); rdsRespTimer.cancel();
rdsRespTimer = null; rdsRespTimer = null;
} }
logger.log(XdsLogLevel.DEBUG, "Found routes: {0}", routes);
// Found routes in the in-lined RouteConfiguration. ConfigUpdate configUpdate =
ConfigUpdate configUpdate; ConfigUpdate.newBuilder().addRoutes(routes).build();
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);
}
configWatcher.onConfigChanged(configUpdate); 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 * Processes a RouteConfiguration message to find the routes that requests for the given host will
* be routed to. * 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 @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) { RouteConfiguration config, String hostName) {
List<VirtualHost> virtualHosts = config.getVirtualHostsList(); List<VirtualHost> virtualHosts = config.getVirtualHostsList();
// Domain search order: // Domain search order:
@ -889,91 +931,7 @@ final class XdsClientImpl extends XdsClient {
break; break;
} }
} }
return targetVirtualHost;
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;
} }
/** /**
@ -1792,4 +1750,13 @@ final class XdsClientImpl extends XdsClient {
return res; 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()); rawLbConfig = generateXdsRoutingRawConfig(update.getRoutes());
} else { } else {
Route defaultRoute = Iterables.getOnlyElement(update.getRoutes()); Route defaultRoute = Iterables.getOnlyElement(update.getRoutes());
RouteAction action = defaultRoute.getRouteAction();
String clusterName = defaultRoute.getRouteAction().getCluster(); String clusterName = defaultRoute.getRouteAction().getCluster();
if (!clusterName.isEmpty()) { if (action.getCluster() != null) {
logger.log( logger.log(
XdsLogLevel.INFO, XdsLogLevel.INFO,
"Received config update from xDS client {0}: cluster_name={1}", "Received config update from xDS client {0}: cluster_name={1}",
xdsClient, xdsClient,
clusterName); clusterName);
rawLbConfig = generateCdsRawConfig(clusterName); rawLbConfig = generateCdsRawConfig(clusterName);
} else { } else if (action.getWeightedCluster() != null) {
logger.log( logger.log(
XdsLogLevel.INFO, XdsLogLevel.INFO,
"Received config update with one weighted cluster route from xDS client {0}", "Received config update with one weighted cluster route from xDS client {0}",
xdsClient); xdsClient);
List<ClusterWeight> clusterWeights = defaultRoute.getRouteAction().getWeightedCluster(); List<ClusterWeight> clusterWeights = defaultRoute.getRouteAction().getWeightedCluster();
rawLbConfig = generateWeightedTargetRawConfig(clusterWeights); 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) { for (Route route : routesUpdate) {
String service = ""; String service = "";
String method = ""; String method = "";
if (!route.getRouteMatch().isDefaultMatcher()) { if (!route.isDefaultRoute()) {
String prefix = route.getRouteMatch().getPrefix(); String prefix = route.getRouteMatch().getPathPrefixMatch();
String path = route.getRouteMatch().getPath(); String path = route.getRouteMatch().getPathExactMatch();
if (!prefix.isEmpty()) { if (prefix != null) {
service = prefix.substring(1, prefix.length() - 1); service = prefix.substring(1, prefix.length() - 1);
} else if (!path.isEmpty()) { } else if (path != null) {
int splitIndex = path.lastIndexOf('/'); int splitIndex = path.lastIndexOf('/');
service = path.substring(1, splitIndex); service = path.substring(1, splitIndex);
method = path.substring(splitIndex + 1); method = path.substring(splitIndex + 1);
} else {
// TODO (chengyuanzhang): match with regex.
continue;
} }
} }
Map<String, String> methodName = ImmutableMap.of("service", service, "method", method); Map<String, String> methodName = ImmutableMap.of("service", service, "method", method);
@ -247,10 +256,10 @@ final class XdsNameResolver extends NameResolver {
if (exitingActions.containsKey(routeAction)) { if (exitingActions.containsKey(routeAction)) {
actionName = exitingActions.get(routeAction); actionName = exitingActions.get(routeAction);
} else { } else {
if (!routeAction.getCluster().isEmpty()) { if (routeAction.getCluster() != null) {
actionName = "cds:" + routeAction.getCluster(); actionName = "cds:" + routeAction.getCluster();
actionPolicy = generateCdsRawConfig(routeAction.getCluster()); actionPolicy = generateCdsRawConfig(routeAction.getCluster());
} else { } else if (routeAction.getWeightedCluster() != null) {
StringBuilder sb = new StringBuilder("weighted:"); StringBuilder sb = new StringBuilder("weighted:");
List<ClusterWeight> clusterWeights = routeAction.getWeightedCluster(); List<ClusterWeight> clusterWeights = routeAction.getWeightedCluster();
for (ClusterWeight clusterWeight : clusterWeights) { for (ClusterWeight clusterWeight : clusterWeights) {
@ -266,6 +275,9 @@ final class XdsNameResolver extends NameResolver {
actionName = actionName + "_" + exitingActions.size(); actionName = actionName + "_" + exitingActions.size();
} }
actionPolicy = generateWeightedTargetRawConfig(clusterWeights); actionPolicy = generateWeightedTargetRawConfig(clusterWeights);
} else {
// TODO (chengyuanzhang): route with cluster_header.
continue;
} }
exitingActions.put(routeAction, actionName); exitingActions.put(routeAction, actionName);
List<?> childPolicies = ImmutableList.of(actionPolicy); 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.common.testing.EqualsTester;
import com.google.protobuf.BoolValue; 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.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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
@ -75,24 +87,386 @@ public class EnvoyProtoDataTest {
// TODO(chengyuanzhang): add test for other data types. // TODO(chengyuanzhang): add test for other data types.
@Test @Test
public void routeMatchCaseSensitive() { public void convertRoute() {
assertThat( io.envoyproxy.envoy.api.v2.route.Route proto1 =
EnvoyProtoData.RouteMatch.fromEnvoyProtoRouteMatch(RouteMatch.newBuilder().build()) io.envoyproxy.envoy.api.v2.route.Route.newBuilder()
.isCaseSensitive()) .setName("route-blade")
.isTrue(); .setMatch(
assertThat( io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
EnvoyProtoData.RouteMatch.fromEnvoyProtoRouteMatch( .setPath("/service/method"))
RouteMatch.newBuilder() .setRoute(
.setCaseSensitive(BoolValue.newBuilder().setValue(true)) io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder()
.build()) .setCluster("cluster-foo"))
.isCaseSensitive()) .build();
.isTrue(); StructOrError<Route> struct1 = Route.fromEnvoyProtoRoute(proto1);
assertThat( assertThat(struct1.getErrorDetail()).isNull();
EnvoyProtoData.RouteMatch.fromEnvoyProtoRouteMatch( assertThat(struct1.getStruct())
RouteMatch.newBuilder() .isEqualTo(
.setCaseSensitive(BoolValue.newBuilder().setValue(false)) new Route(
.build()) new RouteMatch(
.isCaseSensitive()) null, "/service/method", null, null, Collections.<HeaderMatcher>emptyList()),
.isFalse(); 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.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@ -177,6 +178,8 @@ public class XdsClientImplTest {
@Rule @Rule
public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule();
@Rule
public ExpectedException thrown = ExpectedException.none();
private final SynchronizationContext syncContext = new SynchronizationContext( private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() { new Thread.UncaughtExceptionHandler() {
@ -722,25 +725,21 @@ public class XdsClientImplTest {
new EnvoyProtoData.Route( new EnvoyProtoData.Route(
// path match with cluster route // path match with cluster route
new EnvoyProtoData.RouteMatch( new EnvoyProtoData.RouteMatch(
/* prefix= */ "", /* prefix= */ null,
/* path= */ "/service1/method1", /* path= */ "/service1/method1"),
/* hasRegex= */ false,
/* caseSensitive= */ true),
new EnvoyProtoData.RouteAction( new EnvoyProtoData.RouteAction(
"cl1.googleapis.com", "cl1.googleapis.com",
"", null,
ImmutableList.<EnvoyProtoData.ClusterWeight>of()))); null)));
assertThat(routes.get(1)).isEqualTo( assertThat(routes.get(1)).isEqualTo(
new EnvoyProtoData.Route( new EnvoyProtoData.Route(
// path match with weighted cluster route // path match with weighted cluster route
new EnvoyProtoData.RouteMatch( new EnvoyProtoData.RouteMatch(
/* prefix= */ "", /* prefix= */ null,
/* path= */ "/service2/method2", /* path= */ "/service2/method2"),
/* hasRegex= */ false,
/* caseSensitive= */ true),
new EnvoyProtoData.RouteAction( new EnvoyProtoData.RouteAction(
"", null,
"", null,
ImmutableList.of( ImmutableList.of(
new EnvoyProtoData.ClusterWeight("cl21.googleapis.com", 30), new EnvoyProtoData.ClusterWeight("cl21.googleapis.com", 30),
new EnvoyProtoData.ClusterWeight("cl22.googleapis.com", 70) new EnvoyProtoData.ClusterWeight("cl22.googleapis.com", 70)
@ -750,25 +749,21 @@ public class XdsClientImplTest {
// prefix match with cluster route // prefix match with cluster route
new EnvoyProtoData.RouteMatch( new EnvoyProtoData.RouteMatch(
/* prefix= */ "/service1/", /* prefix= */ "/service1/",
/* path= */ "", /* path= */ null),
/* hasRegex= */ false,
/* caseSensitive= */ true),
new EnvoyProtoData.RouteAction( new EnvoyProtoData.RouteAction(
"cl1.googleapis.com", "cl1.googleapis.com",
"", null,
ImmutableList.<EnvoyProtoData.ClusterWeight>of()))); null)));
assertThat(routes.get(3)).isEqualTo( assertThat(routes.get(3)).isEqualTo(
new EnvoyProtoData.Route( new EnvoyProtoData.Route(
// default match with cluster route // default match with cluster route
new EnvoyProtoData.RouteMatch( new EnvoyProtoData.RouteMatch(
/* prefix= */ "", /* prefix= */ "",
/* path= */ "", /* path= */ null),
/* hasRegex= */ false,
/* caseSensitive= */ true),
new EnvoyProtoData.RouteAction( new EnvoyProtoData.RouteAction(
"cluster.googleapis.com", "cluster.googleapis.com",
"", null,
ImmutableList.<EnvoyProtoData.ClusterWeight>of()))); null)));
} }
/** /**
@ -904,97 +899,6 @@ public class XdsClientImplTest {
assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); 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. * Client receives LDS/RDS responses for updating resources previously received.
* *
@ -3464,117 +3368,108 @@ public class XdsClientImplTest {
} }
@Test @Test
public void findClusterNameInRouteConfig_exactMatchFirst() { public void findVirtualHostForHostName_exactMatchFirst() {
String hostname = "a.googleapis.com"; String hostname = "a.googleapis.com";
String targetClusterName = "cluster-hello.googleapis.com";
VirtualHost vHost1 = VirtualHost vHost1 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost01.googleapis.com") // don't care .setName("virtualhost01.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("a.googleapis.com", "b.googleapis.com")) .addAllDomains(ImmutableList.of("a.googleapis.com", "b.googleapis.com"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster(targetClusterName))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
VirtualHost vHost2 = VirtualHost vHost2 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost02.googleapis.com") // don't care .setName("virtualhost02.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("*.googleapis.com")) .addAllDomains(ImmutableList.of("*.googleapis.com"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster-hi.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
VirtualHost vHost3 = VirtualHost vHost3 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost03.googleapis.com") // don't care .setName("virtualhost03.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("*")) .addAllDomains(ImmutableList.of("*"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster-hey.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
RouteConfiguration routeConfig = RouteConfiguration routeConfig =
buildRouteConfiguration( buildRouteConfiguration(
"route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2, vHost3)); "route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2, vHost3));
List<EnvoyProtoData.Route> routes = assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1);
XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname);
assertThat(routes).hasSize(1);
assertThat(routes.get(0).getRouteAction().getCluster())
.isEqualTo(targetClusterName);
} }
@Test @Test
public void findClusterNameInRouteConfig_preferSuffixDomainOverPrefixDomain() { public void findVirtualHostForHostName_preferSuffixDomainOverPrefixDomain() {
String hostname = "a.googleapis.com"; String hostname = "a.googleapis.com";
String targetClusterName = "cluster-hello.googleapis.com";
VirtualHost vHost1 = VirtualHost vHost1 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost01.googleapis.com") // don't care .setName("virtualhost01.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("*.googleapis.com", "b.googleapis.com")) .addAllDomains(ImmutableList.of("*.googleapis.com", "b.googleapis.com"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster(targetClusterName))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
VirtualHost vHost2 = VirtualHost vHost2 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost02.googleapis.com") // don't care .setName("virtualhost02.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("a.googleapis.*")) .addAllDomains(ImmutableList.of("a.googleapis.*"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster-hi.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
VirtualHost vHost3 = VirtualHost vHost3 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost03.googleapis.com") // don't care .setName("virtualhost03.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("*")) .addAllDomains(ImmutableList.of("*"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster-hey.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
RouteConfiguration routeConfig = RouteConfiguration routeConfig =
buildRouteConfiguration( buildRouteConfiguration(
"route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2, vHost3)); "route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2, vHost3));
List<EnvoyProtoData.Route> routes = assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1);
XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname);
assertThat(routes).hasSize(1);
assertThat(routes.get(0).getRouteAction().getCluster())
.isEqualTo(targetClusterName);
} }
@Test @Test
public void findClusterNameInRouteConfig_asteriskMatchAnyDomain() { public void findVirtualHostForHostName_asteriskMatchAnyDomain() {
String hostname = "a.googleapis.com"; String hostname = "a.googleapis.com";
String targetClusterName = "cluster-hello.googleapis.com";
VirtualHost vHost1 = VirtualHost vHost1 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost01.googleapis.com") // don't care .setName("virtualhost01.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("*")) .addAllDomains(ImmutableList.of("*"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster(targetClusterName))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
VirtualHost vHost2 = VirtualHost vHost2 =
VirtualHost.newBuilder() VirtualHost.newBuilder()
.setName("virtualhost02.googleapis.com") // don't care .setName("virtualhost02.googleapis.com") // don't care
.addAllDomains(ImmutableList.of("b.googleapis.com")) .addAllDomains(ImmutableList.of("b.googleapis.com"))
.addRoutes(
Route.newBuilder()
.setRoute(RouteAction.newBuilder().setCluster("cluster-hi.googleapis.com"))
.setMatch(RouteMatch.newBuilder().setPrefix("")))
.build(); .build();
RouteConfiguration routeConfig = RouteConfiguration routeConfig =
buildRouteConfiguration( buildRouteConfiguration(
"route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2)); "route-foo.googleapis.com", ImmutableList.of(vHost1, vHost2));
List<EnvoyProtoData.Route> routes = assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1);
XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname); }
assertThat(routes).hasSize(1);
assertThat(routes.get(0).getRouteAction().getCluster()) @Test
.isEqualTo(targetClusterName); 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 @Test

View File

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