diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index 9b734cfaa11..9344e2ec90e 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkArgument; +import static io.grpc.xds.EnvoyProtoData.HTTP_FAULT_FILTER_NAME; import static io.grpc.xds.EnvoyProtoData.TRANSPORT_SOCKET_NAME_TLS; import com.google.common.annotations.VisibleForTesting; @@ -38,12 +39,14 @@ import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.envoyproxy.envoy.config.route.v3.VirtualHost; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.BackoffPolicy; import io.grpc.xds.EnvoyProtoData.DropOverload; +import io.grpc.xds.EnvoyProtoData.HttpFault; import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; import io.grpc.xds.EnvoyProtoData.Node; @@ -163,6 +166,26 @@ protected void handleLdsResponse(String versionInfo, List resources, String maxStreamDuration = Durations.toNanos(options.getMaxStreamDuration()); } } + boolean hasFaultInjection = false; + HttpFault httpFault = null; + List httpFilters = hcm.getHttpFiltersList(); + for (HttpFilter httpFilter : httpFilters) { + if (HTTP_FAULT_FILTER_NAME.equals(httpFilter.getName())) { + hasFaultInjection = true; + if (httpFilter.hasTypedConfig()) { + StructOrError httpFaultOrError = + HttpFault.decodeFaultFilterConfig(httpFilter.getTypedConfig()); + if (httpFaultOrError.getErrorDetail() != null) { + nackResponse(ResourceType.LDS, nonce, + "Listener " + listenerName + " contains invalid HttpFault filter: " + + httpFaultOrError.getErrorDetail()); + return; + } + httpFault = httpFaultOrError.getStruct(); + } + break; + } + } if (hcm.hasRouteConfig()) { List virtualHosts = new ArrayList<>(); for (VirtualHost virtualHostProto : hcm.getRouteConfig().getVirtualHostsList()) { @@ -176,7 +199,7 @@ protected void handleLdsResponse(String versionInfo, List resources, String } virtualHosts.add(virtualHost.getStruct()); } - update = new LdsUpdate(maxStreamDuration, virtualHosts); + update = new LdsUpdate(maxStreamDuration, virtualHosts, hasFaultInjection, httpFault); } else if (hcm.hasRds()) { Rds rds = hcm.getRds(); if (!rds.getConfigSource().hasAds()) { @@ -184,7 +207,8 @@ protected void handleLdsResponse(String versionInfo, List resources, String "Listener " + listenerName + " with RDS config_source not set to ADS"); return; } - update = new LdsUpdate(maxStreamDuration, rds.getRouteConfigName()); + update = new LdsUpdate( + maxStreamDuration, rds.getRouteConfigName(), hasFaultInjection, httpFault); rdsNames.add(rds.getRouteConfigName()); } else { nackResponse(ResourceType.LDS, nonce, diff --git a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java index 633bd6af91a..b7fe420485e 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java @@ -22,6 +22,8 @@ import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; import com.google.protobuf.Struct; @@ -29,9 +31,11 @@ import com.google.protobuf.util.Durations; import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; +import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault; import io.envoyproxy.envoy.type.v3.FractionalPercent; import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; import io.grpc.EquivalentAddressGroup; +import io.grpc.Status; import io.grpc.xds.RouteMatch.FractionMatcher; import io.grpc.xds.RouteMatch.HeaderMatcher; import io.grpc.xds.RouteMatch.PathMatcher; @@ -61,6 +65,7 @@ // TODO(chengyuanzhang): put data types into smaller categories. final class EnvoyProtoData { static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls"; + static final String HTTP_FAULT_FILTER_NAME = "envoy.fault"; // Prevent instantiation. private EnvoyProtoData() { @@ -756,54 +761,7 @@ static final class DropOverload { static DropOverload fromEnvoyProtoDropOverload( io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment.Policy.DropOverload proto) { FractionalPercent percent = proto.getDropPercentage(); - int numerator = percent.getNumerator(); - DenominatorType type = percent.getDenominator(); - switch (type) { - case TEN_THOUSAND: - numerator *= 100; - break; - case HUNDRED: - numerator *= 10_000; - break; - case MILLION: - break; - case UNRECOGNIZED: - default: - throw new IllegalArgumentException("Unknown denominator type of " + percent); - } - - if (numerator > 1_000_000) { - numerator = 1_000_000; - } - - return new DropOverload(proto.getCategory(), numerator); - } - - @VisibleForTesting - static DropOverload fromEnvoyProtoDropOverloadV2( - io.envoyproxy.envoy.api.v2.ClusterLoadAssignment.Policy.DropOverload proto) { - io.envoyproxy.envoy.type.FractionalPercent percent = proto.getDropPercentage(); - int numerator = percent.getNumerator(); - io.envoyproxy.envoy.type.FractionalPercent.DenominatorType type = percent.getDenominator(); - switch (type) { - case TEN_THOUSAND: - numerator *= 100; - break; - case HUNDRED: - numerator *= 10_000; - break; - case MILLION: - break; - case UNRECOGNIZED: - default: - throw new IllegalArgumentException("Unknown denominator type of " + percent); - } - - if (numerator > 1_000_000) { - numerator = 1_000_000; - } - - return new DropOverload(proto.getCategory(), numerator); + return new DropOverload(proto.getCategory(), getRatePerMillion(percent)); } String getCategory() { @@ -849,12 +807,19 @@ static final class VirtualHost { private final List domains; // The list of routes that will be matched, in order, for incoming requests. private final List routes; + @Nullable + private final HttpFault httpFault; @VisibleForTesting VirtualHost(String name, List domains, List routes) { + this(name, domains, routes, null); + } + + VirtualHost(String name, List domains, List routes, HttpFault httpFault) { this.name = name; this.domains = domains; this.routes = routes; + this.httpFault = httpFault; } String getName() { @@ -869,13 +834,21 @@ List getRoutes() { return routes; } + @Nullable + HttpFault getHttpFault() { + return httpFault; + } + @Override public String toString() { - return MoreObjects.toStringHelper(this) + ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) .add("name", name) .add("domains", domains) - .add("routes", routes) - .toString(); + .add("routes", routes); + if (httpFault != null) { + toStringHelper.add("httpFault", httpFault); + } + return toStringHelper.toString(); } static StructOrError fromEnvoyProtoVirtualHost( @@ -893,9 +866,24 @@ static StructOrError fromEnvoyProtoVirtualHost( } routes.add(route.getStruct()); } + HttpFault httpFault = null; + Map filterConfigMap = proto.getTypedPerFilterConfigMap(); + if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { + Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); + StructOrError httpFaultOrError = + HttpFault.decodeFaultFilterConfig(rawFaultFilterConfig); + if (httpFaultOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "Virtual host [" + name + "] contains invalid HttpFault filter : " + + httpFaultOrError.getErrorDetail()); + } + httpFault = httpFaultOrError.getStruct(); + } return StructOrError.fromStruct( - new VirtualHost(name, Collections.unmodifiableList(proto.getDomainsList()), - Collections.unmodifiableList(routes))); + new VirtualHost( + name, Collections.unmodifiableList(proto.getDomainsList()), + Collections.unmodifiableList(routes), + httpFault)); } } @@ -903,11 +891,18 @@ static StructOrError fromEnvoyProtoVirtualHost( static final class Route { private final RouteMatch routeMatch; private final RouteAction routeAction; + @Nullable + private final HttpFault httpFault; @VisibleForTesting - Route(RouteMatch routeMatch, @Nullable RouteAction routeAction) { + Route(RouteMatch routeMatch, RouteAction routeAction) { + this(routeMatch, routeAction, null); + } + + Route(RouteMatch routeMatch, RouteAction routeAction, @Nullable HttpFault httpFault) { this.routeMatch = routeMatch; this.routeAction = routeAction; + this.httpFault = httpFault; } RouteMatch getRouteMatch() { @@ -918,6 +913,11 @@ RouteAction getRouteAction() { return routeAction; } + @Nullable + HttpFault getHttpFault() { + return httpFault; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -928,20 +928,24 @@ public boolean equals(Object o) { } Route route = (Route) o; return Objects.equals(routeMatch, route.routeMatch) - && Objects.equals(routeAction, route.routeAction); + && Objects.equals(routeAction, route.routeAction) + && Objects.equals(httpFault, route.httpFault); } @Override public int hashCode() { - return Objects.hash(routeMatch, routeAction); + return Objects.hash(routeMatch, routeAction, httpFault); } @Override public String toString() { - return MoreObjects.toStringHelper(this) + ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) .add("routeMatch", routeMatch) - .add("routeAction", routeAction) - .toString(); + .add("routeAction", routeAction); + if (httpFault != null) { + toStringHelper.add("httpFault", httpFault); + } + return toStringHelper.toString(); } @Nullable @@ -978,7 +982,22 @@ static StructOrError fromEnvoyProtoRoute( return StructOrError.fromError( "Invalid route [" + proto.getName() + "]: " + routeAction.getErrorDetail()); } - return StructOrError.fromStruct(new Route(routeMatch.getStruct(), routeAction.getStruct())); + + HttpFault httpFault = null; + Map filterConfigMap = proto.getTypedPerFilterConfigMap(); + if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { + Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); + StructOrError httpFaultOrError = + HttpFault.decodeFaultFilterConfig(rawFaultFilterConfig); + if (httpFaultOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "Route [" + proto.getName() + "] contains invalid HttpFault filter: " + + httpFaultOrError.getErrorDetail()); + } + httpFault = httpFaultOrError.getStruct(); + } + return StructOrError.fromStruct( + new Route(routeMatch.getStruct(), routeAction.getStruct(), httpFault)); } @VisibleForTesting @@ -1113,6 +1132,308 @@ static StructOrError convertEnvoyProtoHeaderMatcher( } } + /** + * See corresponding Envoy proto message {@link + * io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault}. + */ + static final class HttpFault { + @Nullable + final FaultDelay faultDelay; + @Nullable + final FaultAbort faultAbort; + String upstreamCluster; + List downstreamNodes; + List headers; + @Nullable + Integer maxActiveFaults; + + private HttpFault( + @Nullable FaultDelay faultDelay, @Nullable FaultAbort faultAbort, String upstreamCluster, + List downstreamNodes, List headers, + @Nullable Integer maxActiveFaults) { + this.faultDelay = faultDelay; + this.faultAbort = faultAbort; + this.upstreamCluster = upstreamCluster; + this.downstreamNodes = downstreamNodes; + this.headers = headers; + this.maxActiveFaults = maxActiveFaults; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HttpFault httpFault = (HttpFault) o; + return Objects.equals(faultDelay, httpFault.faultDelay) + && Objects.equals(faultAbort, httpFault.faultAbort) + && Objects.equals(upstreamCluster, httpFault.upstreamCluster) + && Objects.equals(downstreamNodes, httpFault.downstreamNodes) + && Objects.equals(headers, httpFault.headers) + && Objects.equals(maxActiveFaults, httpFault.maxActiveFaults); + } + + @Override + public int hashCode() { + return Objects.hash( + faultDelay, faultAbort, upstreamCluster, downstreamNodes, headers, maxActiveFaults); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("faultDelay", faultDelay) + .add("faultAbort", faultAbort) + .add("upstreamCluster", upstreamCluster) + .add("downstreamNodes", downstreamNodes) + .add("headers", headers) + .add("maxActiveFaults", maxActiveFaults) + .toString(); + } + + static StructOrError decodeFaultFilterConfig(Any rawFaultFilterConfig) { + if (rawFaultFilterConfig.getTypeUrl().equals( + "type.googleapis.com/envoy.config.filter.http.fault.v2.HTTPFault")) { + rawFaultFilterConfig = rawFaultFilterConfig.toBuilder().setTypeUrl( + "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault").build(); + } + HTTPFault httpFaultProto; + try { + httpFaultProto = rawFaultFilterConfig.unpack(HTTPFault.class); + } catch (InvalidProtocolBufferException e) { + return StructOrError.fromError("Invalid proto: " + e); + } + return fromEnvoyProtoHttpFault(httpFaultProto); + } + + private static StructOrError fromEnvoyProtoHttpFault(HTTPFault httpFault) { + FaultDelay faultDelay = null; + FaultAbort faultAbort = null; + if (httpFault.hasDelay()) { + faultDelay = FaultDelay.fromEnvoyProtoFaultDelay(httpFault.getDelay()); + } + if (httpFault.hasAbort()) { + StructOrError faultAbortOrError = + FaultAbort.fromEnvoyProtoFaultAbort(httpFault.getAbort()); + if (faultAbortOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "HttpFault contains invalid FaultAbort: " + faultAbortOrError.getErrorDetail()); + } + faultAbort = faultAbortOrError.getStruct(); + } + if (faultDelay == null && faultAbort == null) { + return StructOrError.fromError( + "Invalid HttpFault: neither fault_delay nor fault_abort is specified"); + } + String upstreamCluster = httpFault.getUpstreamCluster(); + List downstreamNodes = httpFault.getDownstreamNodesList(); + List headers = new ArrayList<>(); + for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto : httpFault.getHeadersList()) { + StructOrError headerMatcherOrError = + Route.convertEnvoyProtoHeaderMatcher(proto); + if (headerMatcherOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "HttpFault contains invalid header matcher: " + + headerMatcherOrError.getErrorDetail()); + } + headers.add(headerMatcherOrError.getStruct()); + } + Integer maxActiveFaults = null; + if (httpFault.hasMaxActiveFaults()) { + maxActiveFaults = httpFault.getMaxActiveFaults().getValue(); + if (maxActiveFaults < 0) { + maxActiveFaults = Integer.MAX_VALUE; + } + } + return StructOrError.fromStruct(new HttpFault( + faultDelay, faultAbort, upstreamCluster, downstreamNodes, headers, maxActiveFaults)); + } + } + + /** + * See corresponding Envoy proto message {@link + * io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay}. + */ + static final class FaultDelay { + @Nullable + final Long delayNanos; + final boolean headerDelay; + final int ratePerMillion; + + private FaultDelay(@Nullable Long delayNanos, boolean headerDelay, int ratePerMillion) { + this.delayNanos = delayNanos; + this.headerDelay = headerDelay; + this.ratePerMillion = ratePerMillion; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FaultDelay that = (FaultDelay) o; + return ratePerMillion == that.ratePerMillion + && headerDelay == that.headerDelay + && Objects.equals(delayNanos, that.delayNanos); + } + + @Override + public int hashCode() { + return Objects.hash(delayNanos, headerDelay, ratePerMillion); + } + + @Override + public String toString() { + ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) + .add("ratePerMillion", ratePerMillion); + if (headerDelay) { + toStringHelper.add("type", "header delay"); + } else { + toStringHelper.add("type", "fixed delay"); + toStringHelper.add("delayNanos", delayNanos); + } + return toStringHelper.toString(); + } + + private static FaultDelay fromEnvoyProtoFaultDelay( + io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay faultDelay) { + int rate = getRatePerMillion(faultDelay.getPercentage()); + if (faultDelay.hasHeaderDelay()) { + return new FaultDelay(null, true, rate); + } + long delay = Durations.toNanos(faultDelay.getFixedDelay()); + return new FaultDelay(delay, false, rate); + } + } + + /** + * See corresponding Envoy proto message {@link + * io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort}. + */ + static final class FaultAbort { + @Nullable + final Status status; + final boolean headerAbort; + final int ratePerMillion; + + private FaultAbort(@Nullable Status status, boolean headerAbort, int ratePerMillion) { + this.status = status; + this.headerAbort = headerAbort; + this.ratePerMillion = ratePerMillion; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FaultAbort that = (FaultAbort) o; + return ratePerMillion == that.ratePerMillion + && headerAbort == that.headerAbort + && Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(status, headerAbort, ratePerMillion); + } + + @Override + public String toString() { + ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) + .add("ratePerMillion", ratePerMillion); + if (headerAbort) { + toStringHelper.add("type", "header abort"); + } else { + toStringHelper.add("type", "fixed status"); + toStringHelper.add("status", status); + } + return toStringHelper.toString(); + } + + private static StructOrError fromEnvoyProtoFaultAbort( + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort faultAbort) { + int rate = getRatePerMillion(faultAbort.getPercentage()); + boolean headerAbort = false; + Status status = null; + switch (faultAbort.getErrorTypeCase()) { + case HEADER_ABORT: + headerAbort = true; + break; + case HTTP_STATUS: + status = convertHttpStatus(faultAbort.getHttpStatus()); + break; + case GRPC_STATUS: + status = Status.fromCodeValue(faultAbort.getGrpcStatus()); + break; + case ERRORTYPE_NOT_SET: + default: + return StructOrError.fromError( + "Unknown error type case: " + faultAbort.getErrorTypeCase()); + } + return StructOrError.fromStruct(new FaultAbort(status, headerAbort, rate)); + } + + private static Status convertHttpStatus(int httpCode) { + Status status; + switch (httpCode) { + case 400: + status = Status.INTERNAL; + break; + case 401: + status = Status.UNAUTHENTICATED; + break; + case 403: + status = Status.PERMISSION_DENIED; + break; + case 404: + status = Status.UNIMPLEMENTED; + break; + case 429: + case 502: + case 503: + case 504: + status = Status.UNAVAILABLE; + break; + default: + status = Status.UNKNOWN; + } + return status.withDescription("HTTP code: " + httpCode); + } + } + + private static int getRatePerMillion(FractionalPercent percent) { + int numerator = percent.getNumerator(); + DenominatorType type = percent.getDenominator(); + switch (type) { + case TEN_THOUSAND: + numerator *= 100; + break; + case HUNDRED: + numerator *= 10_000; + break; + case MILLION: + break; + case UNRECOGNIZED: + default: + throw new IllegalArgumentException("Unknown denominator type of " + percent); + } + + if (numerator > 1_000_000 || numerator < 0) { + numerator = 1_000_000; + } + return numerator; + } + /** * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.route.v3.RouteAction}. */ @@ -1203,7 +1524,13 @@ static StructOrError fromEnvoyProtoRouteAction( weightedClusters = new ArrayList<>(); for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight : clusterWeights) { - weightedClusters.add(ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight)); + StructOrError clusterWeightOrError = + ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight); + if (clusterWeightOrError.getErrorDetail() != null) { + return StructOrError.fromError("RouteAction contains invalid ClusterWeight: " + + clusterWeightOrError.getErrorDetail()); + } + weightedClusters.add(clusterWeightOrError.getStruct()); } // TODO(chengyuanzhang): validate if the sum of weights equals to total weight. break; @@ -1233,11 +1560,14 @@ static StructOrError fromEnvoyProtoRouteAction( static final class ClusterWeight { private final String name; private final int weight; + @Nullable + private final HttpFault httpFault; @VisibleForTesting - ClusterWeight(String name, int weight) { + ClusterWeight(String name, int weight, @Nullable HttpFault httpFault) { this.name = name; this.weight = weight; + this.httpFault = httpFault; } String getName() { @@ -1248,6 +1578,11 @@ int getWeight() { return weight; } + @Nullable + HttpFault getHttpFault() { + return httpFault; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -1257,26 +1592,44 @@ public boolean equals(Object o) { return false; } ClusterWeight that = (ClusterWeight) o; - return weight == that.weight && Objects.equals(name, that.name); + return weight == that.weight && Objects.equals(name, that.name) + && Objects.equals(httpFault, that.httpFault); } @Override public int hashCode() { - return Objects.hash(name, weight); + return Objects.hash(name, weight, httpFault); } @Override public String toString() { - return MoreObjects.toStringHelper(this) + ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) .add("name", name) - .add("weight", weight) - .toString(); + .add("weight", weight); + if (httpFault != null) { + toStringHelper.add("httpFault", httpFault); + } + return toStringHelper.toString(); } @VisibleForTesting - static ClusterWeight fromEnvoyProtoClusterWeight( + static StructOrError fromEnvoyProtoClusterWeight( io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto) { - return new ClusterWeight(proto.getName(), proto.getWeight().getValue()); + HttpFault httpFault = null; + Map filterConfigMap = proto.getTypedPerFilterConfigMap(); + if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { + Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); + StructOrError httpFaultOrError = + HttpFault.decodeFaultFilterConfig(rawFaultFilterConfig); + if (httpFaultOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "ClusterWeight [" + proto.getName() + "] contains invalid HttpFault filter: " + + httpFaultOrError.getErrorDetail()); + } + httpFault = httpFaultOrError.getStruct(); + } + return StructOrError.fromStruct( + new ClusterWeight(proto.getName(), proto.getWeight().getValue(), httpFault)); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsClient.java b/xds/src/main/java/io/grpc/xds/XdsClient.java index 8da3601fd11..809558b088b 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/XdsClient.java @@ -23,6 +23,7 @@ import com.google.common.base.MoreObjects.ToStringHelper; import io.grpc.Status; import io.grpc.xds.EnvoyProtoData.DropOverload; +import io.grpc.xds.EnvoyProtoData.HttpFault; import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; import io.grpc.xds.EnvoyProtoData.VirtualHost; @@ -54,26 +55,39 @@ static final class LdsUpdate implements ResourceUpdate { // The list virtual hosts that make up the route table. @Nullable final List virtualHosts; - - LdsUpdate(long httpMaxStreamDurationNano, String rdsName) { - this(httpMaxStreamDurationNano, rdsName, null); + // Listener contains the HttpFault filter. + final boolean hasFaultInjection; + @Nullable // Can be null even if hasFaultInjection is true. + final HttpFault httpFault; + + LdsUpdate( + long httpMaxStreamDurationNano, String rdsName, boolean hasFaultInjection, + @Nullable HttpFault httpFault) { + this(httpMaxStreamDurationNano, rdsName, null, hasFaultInjection, httpFault); } - LdsUpdate(long httpMaxStreamDurationNano, List virtualHosts) { - this(httpMaxStreamDurationNano, null, virtualHosts); + LdsUpdate( + long httpMaxStreamDurationNano, List virtualHosts, + boolean hasFaultInjection, @Nullable HttpFault httpFault) { + this(httpMaxStreamDurationNano, null, virtualHosts, hasFaultInjection, httpFault); } - private LdsUpdate(long httpMaxStreamDurationNano, @Nullable String rdsName, - @Nullable List virtualHosts) { + private LdsUpdate( + long httpMaxStreamDurationNano, @Nullable String rdsName, + @Nullable List virtualHosts, boolean hasFaultInjection, + @Nullable HttpFault httpFault) { this.httpMaxStreamDurationNano = httpMaxStreamDurationNano; this.rdsName = rdsName; this.virtualHosts = virtualHosts == null ? null : Collections.unmodifiableList(new ArrayList<>(virtualHosts)); + this.hasFaultInjection = hasFaultInjection; + this.httpFault = httpFault; } @Override public int hashCode() { - return Objects.hash(httpMaxStreamDurationNano, rdsName, virtualHosts); + return Objects.hash( + httpMaxStreamDurationNano, rdsName, virtualHosts, hasFaultInjection, httpFault); } @Override @@ -87,7 +101,9 @@ public boolean equals(Object o) { LdsUpdate that = (LdsUpdate) o; return httpMaxStreamDurationNano == that.httpMaxStreamDurationNano && Objects.equals(rdsName, that.rdsName) - && Objects.equals(virtualHosts, that.virtualHosts); + && Objects.equals(virtualHosts, that.virtualHosts) + && hasFaultInjection == that.hasFaultInjection + && Objects.equals(httpFault, that.httpFault); } @Override @@ -99,6 +115,10 @@ public String toString() { } else { toStringHelper.add("virtualHosts", virtualHosts); } + if (hasFaultInjection) { + toStringHelper.add("faultInjectionEnabled", true) + .add("httpFault", httpFault); + } return toStringHelper.toString(); } } diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java index 5395e778517..c47494f73e5 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java @@ -25,9 +25,11 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.protobuf.Any; import com.google.protobuf.Message; +import com.google.protobuf.StringValue; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig; import io.grpc.BindableService; import io.grpc.ManagedChannel; @@ -42,6 +44,7 @@ import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.AbstractXdsClient.ResourceType; import io.grpc.xds.EnvoyProtoData.DropOverload; +import io.grpc.xds.EnvoyProtoData.HttpFault; import io.grpc.xds.EnvoyProtoData.LbEndpoint; import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; @@ -65,6 +68,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -328,6 +332,61 @@ public void ldsResourceUpdated() { assertThat(ldsUpdateCaptor.getValue().rdsName).isEqualTo(RDS_RESOURCE); } + @Test + public void ldsResourceUpdate_withFaultInjection() { + DiscoveryRpcCall call = + startResourceWatcher(ResourceType.LDS, LDS_RESOURCE, ldsResourceWatcher); + List listeners = ImmutableList.of( + Any.pack(mf.buildListener( + LDS_RESOURCE, + mf.buildRouteConfiguration( + "do not care", + ImmutableList.of( + mf.buildVirtualHost( + mf.buildOpaqueRoutes(1), + ImmutableMap.of( + "irrelevant", + Any.pack(StringValue.of("irrelevant")), + "envoy.fault", + mf.buildHttpFaultTypedConfig( + 300L, 1000, "cluster1", ImmutableList.of(), 100, null, null, + null))), + mf.buildVirtualHost( + mf.buildOpaqueRoutes(2), + ImmutableMap.of( + "envoy.fault", + mf.buildHttpFaultTypedConfig( + null, null, "cluster2", ImmutableList.of(), 101, null, 503, + 2000))) + )), + ImmutableList.of( + mf.buildHttpFilter("irrelevant", null), + mf.buildHttpFilter("envoy.fault", null) + )))); + call.sendResponse("0", listeners, ResourceType.LDS, "0000"); + + // Client sends an ACK LDS request. + call.verifyRequest(NODE, "0", Collections.singletonList(LDS_RESOURCE), ResourceType.LDS, + "0000"); + verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); + LdsUpdate ldsUpdate = ldsUpdateCaptor.getValue(); + assertThat(ldsUpdate.virtualHosts).hasSize(2); + assertThat(ldsUpdate.hasFaultInjection).isTrue(); + assertThat(ldsUpdate.httpFault).isNull(); + HttpFault httpFault = ldsUpdate.virtualHosts.get(0).getHttpFault(); + assertThat(httpFault.faultDelay.delayNanos).isEqualTo(300); + assertThat(httpFault.faultDelay.ratePerMillion).isEqualTo(1000); + assertThat(httpFault.faultAbort).isNull(); + assertThat(httpFault.upstreamCluster).isEqualTo("cluster1"); + assertThat(httpFault.maxActiveFaults).isEqualTo(100); + httpFault = ldsUpdate.virtualHosts.get(1).getHttpFault(); + assertThat(httpFault.faultDelay).isNull(); + assertThat(httpFault.faultAbort.status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(httpFault.faultAbort.ratePerMillion).isEqualTo(2000); + assertThat(httpFault.upstreamCluster).isEqualTo("cluster2"); + assertThat(httpFault.maxActiveFaults).isEqualTo(101); + } + @Test public void ldsResourceDeleted() { DiscoveryRpcCall call = @@ -1445,15 +1504,33 @@ protected abstract static class LrsRpcCall { protected abstract static class MessageFactory { - protected abstract Message buildListener(String name, Message routeConfiguration); + protected final Message buildListener(String name, Message routeConfiguration) { + return buildListener(name, routeConfiguration, Collections.emptyList()); + } + + @SuppressWarnings("unchecked") + protected abstract Message buildListener( + String name, Message routeConfiguration, List httpFilters); protected abstract Message buildListenerForRds(String name, String rdsResourceName); + protected abstract Message buildHttpFilter(String name, @Nullable Any typedConfig); + + protected abstract Any buildHttpFaultTypedConfig( + @Nullable Long delayNanos, @Nullable Integer delayRate, String upstreamCluster, + List downstreamNodes, @Nullable Integer maxActiveFaults, @Nullable Status status, + @Nullable Integer httpCode, @Nullable Integer abortRate); + protected abstract Message buildRouteConfiguration(String name, List virtualHostList); protected abstract List buildOpaqueVirtualHosts(int num); + protected abstract Message buildVirtualHost( + List routes, Map typedConfigMap); + + protected abstract List buildOpaqueRoutes(int num); + protected abstract Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, boolean enableLrs, @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers); diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java index a2bfb8ee964..341bac7a720 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java @@ -65,9 +65,16 @@ import io.envoyproxy.envoy.api.v2.listener.FilterChain; import io.envoyproxy.envoy.api.v2.route.Route; import io.envoyproxy.envoy.api.v2.route.RouteAction; +import io.envoyproxy.envoy.api.v2.route.RouteMatch; import io.envoyproxy.envoy.api.v2.route.VirtualHost; import io.envoyproxy.envoy.config.cluster.aggregate.v2alpha.ClusterConfig; +import io.envoyproxy.envoy.config.filter.fault.v2.FaultDelay; +import io.envoyproxy.envoy.config.filter.fault.v2.FaultDelay.HeaderDelay; +import io.envoyproxy.envoy.config.filter.http.fault.v2.FaultAbort; +import io.envoyproxy.envoy.config.filter.http.fault.v2.FaultAbort.HeaderAbort; +import io.envoyproxy.envoy.config.filter.http.fault.v2.HTTPFault; import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager; +import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.HttpFilter; import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.Rds; import io.envoyproxy.envoy.config.listener.v2.ApiListener; import io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase; @@ -79,12 +86,14 @@ import io.grpc.BindableService; import io.grpc.Context; import io.grpc.Context.CancellationListener; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import io.grpc.xds.AbstractXdsClient.ResourceType; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.junit.runner.RunWith; @@ -232,8 +241,10 @@ protected void sendResponse(List clusters, long loadReportIntervalNano) private static class MessageFactoryV2 extends MessageFactory { + @SuppressWarnings("unchecked") @Override - protected Message buildListener(String name, Message routeConfiguration) { + protected Message buildListener( + String name, Message routeConfiguration, List httpFilters) { return Listener.newBuilder() .setName(name) .setAddress(Address.getDefaultInstance()) @@ -241,7 +252,9 @@ protected Message buildListener(String name, Message routeConfiguration) { .setApiListener( ApiListener.newBuilder().setApiListener(Any.pack( HttpConnectionManager.newBuilder() - .setRouteConfig((RouteConfiguration) routeConfiguration).build()))) + .setRouteConfig((RouteConfiguration) routeConfiguration) + .addAllHttpFilters((List) httpFilters) + .build()))) .build(); } @@ -264,6 +277,56 @@ protected Message buildListenerForRds(String name, String rdsResourceName) { .build(); } + @Override + protected Message buildHttpFilter(String name, @Nullable Any typedConfig) { + HttpFilter.Builder builder = HttpFilter.newBuilder().setName(name); + if (typedConfig != null) { + builder.setTypedConfig(typedConfig); + } + return builder.build(); + } + + @Override + protected Any buildHttpFaultTypedConfig( + @Nullable Long delayNanos, @Nullable Integer delayRate, String upstreamCluster, + List downstreamNodes, @Nullable Integer maxActiveFaults, @Nullable Status status, + @Nullable Integer httpCode, @Nullable Integer abortRate) { + HTTPFault.Builder builder = HTTPFault.newBuilder(); + if (delayRate != null) { + FaultDelay.Builder delayBuilder + = FaultDelay.newBuilder(); + delayBuilder.setPercentage( + FractionalPercent.newBuilder() + .setNumerator(delayRate).setDenominator(DenominatorType.MILLION)); + if (delayNanos != null) { + delayBuilder.setFixedDelay(Durations.fromNanos(delayNanos)); + } else { + delayBuilder.setHeaderDelay(HeaderDelay.newBuilder()); + } + builder.setDelay(delayBuilder); + } + if (abortRate != null) { + FaultAbort.Builder abortBuilder = FaultAbort.newBuilder(); + abortBuilder.setPercentage( + FractionalPercent.newBuilder() + .setNumerator(abortRate).setDenominator(DenominatorType.MILLION)); + if (status != null) { + throw new UnsupportedOperationException(); + } else if (httpCode != null) { + abortBuilder.setHttpStatus(httpCode); + } else { + abortBuilder.setHeaderAbort(HeaderAbort.newBuilder()); + } + builder.setAbort(abortBuilder); + } + builder.setUpstreamCluster(upstreamCluster); + builder.addAllDownstreamNodes(downstreamNodes); + if (maxActiveFaults != null) { + builder.setMaxActiveFaults(UInt32Value.of(maxActiveFaults)); + } + return Any.pack(builder.build()); + } + @Override protected Message buildRouteConfiguration(String name, List virtualHostList) { RouteConfiguration.Builder builder = RouteConfiguration.newBuilder(); @@ -285,7 +348,7 @@ protected List buildOpaqueVirtualHosts(int num) { .addRoutes( Route.newBuilder() .setRoute(RouteAction.newBuilder().setCluster("do not care")) - .setMatch(io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder() + .setMatch(RouteMatch.newBuilder() .setPrefix("do not care"))) .build(); virtualHosts.add(virtualHost); @@ -293,6 +356,32 @@ protected List buildOpaqueVirtualHosts(int num) { return virtualHosts; } + @SuppressWarnings("unchecked") + @Override + protected Message buildVirtualHost( + List routes, Map typedConfigMap) { + return VirtualHost.newBuilder() + .setName("do not care") + .addDomains("do not care") + .addAllRoutes((List) routes) + .putAllTypedPerFilterConfig(typedConfigMap) + .build(); + } + + @Override + protected List buildOpaqueRoutes(int num) { + List routes = new ArrayList<>(num); + for (int i = 0; i < num; i++) { + Route route = + Route.newBuilder() + .setRoute(RouteAction.newBuilder().setCluster("do not care")) + .setMatch(RouteMatch.newBuilder().setPrefix("do not care")) + .build(); + routes.add(route); + } + return routes; + } + @Override protected Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, boolean enableLrs, @Nullable Message upstreamTlsContext, diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java index b9de6bd97d1..dca11b39846 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java @@ -64,7 +64,13 @@ import io.envoyproxy.envoy.config.route.v3.RouteMatch; import io.envoyproxy.envoy.config.route.v3.VirtualHost; import io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig; +import io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay; +import io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay.HeaderDelay; +import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort; +import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.HeaderAbort; +import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig; @@ -80,12 +86,14 @@ import io.grpc.BindableService; import io.grpc.Context; import io.grpc.Context.CancellationListener; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import io.grpc.xds.AbstractXdsClient.ResourceType; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.junit.runner.RunWith; @@ -233,8 +241,10 @@ protected void sendResponse(List clusters, long loadReportIntervalNano) private static class MessageFactoryV3 extends MessageFactory { + @SuppressWarnings("unchecked") @Override - protected Message buildListener(String name, Message routeConfiguration) { + protected Message buildListener( + String name, Message routeConfiguration, List httpFilters) { return Listener.newBuilder() .setName(name) .setAddress(Address.getDefaultInstance()) @@ -242,7 +252,9 @@ protected Message buildListener(String name, Message routeConfiguration) { .setApiListener( ApiListener.newBuilder().setApiListener(Any.pack( HttpConnectionManager.newBuilder() - .setRouteConfig((RouteConfiguration) routeConfiguration).build()))) + .setRouteConfig((RouteConfiguration) routeConfiguration) + .addAllHttpFilters((List) httpFilters) + .build()))) .build(); } @@ -265,6 +277,55 @@ protected Message buildListenerForRds(String name, String rdsResourceName) { .build(); } + @Override + protected Message buildHttpFilter(String name, @Nullable Any typedConfig) { + HttpFilter.Builder builder = HttpFilter.newBuilder().setName(name); + if (typedConfig != null) { + builder.setTypedConfig(typedConfig); + } + return builder.build(); + } + + @Override + protected Any buildHttpFaultTypedConfig( + @Nullable Long delayNanos, @Nullable Integer delayRate, String upstreamCluster, + List downstreamNodes, @Nullable Integer maxActiveFaults, @Nullable Status status, + @Nullable Integer httpCode, @Nullable Integer abortRate) { + HTTPFault.Builder builder = HTTPFault.newBuilder(); + if (delayRate != null) { + FaultDelay.Builder delayBuilder = FaultDelay.newBuilder(); + delayBuilder.setPercentage( + FractionalPercent.newBuilder() + .setNumerator(delayRate).setDenominator(DenominatorType.MILLION)); + if (delayNanos != null) { + delayBuilder.setFixedDelay(Durations.fromNanos(delayNanos)); + } else { + delayBuilder.setHeaderDelay(HeaderDelay.newBuilder()); + } + builder.setDelay(delayBuilder); + } + if (abortRate != null) { + FaultAbort.Builder abortBuilder = FaultAbort.newBuilder(); + abortBuilder.setPercentage( + FractionalPercent.newBuilder() + .setNumerator(abortRate).setDenominator(DenominatorType.MILLION)); + if (status != null) { + abortBuilder.setGrpcStatus(status.getCode().value()); + } else if (httpCode != null) { + abortBuilder.setHttpStatus(httpCode); + } else { + abortBuilder.setHeaderAbort(HeaderAbort.newBuilder()); + } + builder.setAbort(abortBuilder); + } + builder.setUpstreamCluster(upstreamCluster); + builder.addAllDownstreamNodes(downstreamNodes); + if (maxActiveFaults != null) { + builder.setMaxActiveFaults(UInt32Value.of(maxActiveFaults)); + } + return Any.pack(builder.build()); + } + @Override protected Message buildRouteConfiguration(String name, List virtualHostList) { RouteConfiguration.Builder builder = RouteConfiguration.newBuilder(); @@ -293,6 +354,32 @@ protected List buildOpaqueVirtualHosts(int num) { return virtualHosts; } + @SuppressWarnings("unchecked") + @Override + protected Message buildVirtualHost( + List routes, Map typedConfigMap) { + return VirtualHost.newBuilder() + .setName("do not care") + .addDomains("do not care") + .addAllRoutes((List) routes) + .putAllTypedPerFilterConfig(typedConfigMap) + .build(); + } + + @Override + protected List buildOpaqueRoutes(int num) { + List routes = new ArrayList<>(num); + for (int i = 0; i < num; i++) { + Route route = + Route.newBuilder() + .setRoute(RouteAction.newBuilder().setCluster("do not care")) + .setMatch(RouteMatch.newBuilder().setPrefix("do not care")) + .build(); + routes.add(route); + } + return routes; + } + @Override protected Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, boolean enableLrs, @Nullable Message upstreamTlsContext, diff --git a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java index 600075fc41c..ae10beb66f2 100644 --- a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java +++ b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java @@ -402,7 +402,7 @@ public void convertRouteAction_weightedCluster() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct().getCluster()).isNull(); assertThat(struct.getStruct().getWeightedCluster()).containsExactly( - new ClusterWeight("cluster-foo", 30), new ClusterWeight("cluster-bar", 70)); + new ClusterWeight("cluster-foo", 30, null), new ClusterWeight("cluster-bar", 70, null)); } @Test @@ -547,7 +547,7 @@ public void convertClusterWeight() { io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight.newBuilder() .setName("cluster-foo") .setWeight(UInt32Value.newBuilder().setValue(30)).build(); - ClusterWeight struct = ClusterWeight.fromEnvoyProtoClusterWeight(proto); + ClusterWeight struct = ClusterWeight.fromEnvoyProtoClusterWeight(proto).getStruct(); assertThat(struct.getName()).isEqualTo("cluster-foo"); assertThat(struct.getWeight()).isEqualTo(30); } diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 7df19c0be1e..219a4aee5f4 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -437,7 +437,8 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { new RouteAction( TimeUnit.SECONDS.toNanos(20L), null, Arrays.asList( - new ClusterWeight(cluster1, 20), new ClusterWeight(cluster2, 80)))))); + new ClusterWeight(cluster1, 20, null), + new ClusterWeight(cluster2, 80, null)))))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddresses()).isEmpty(); @@ -744,7 +745,7 @@ public void run() { if (!resourceName.equals(ldsResource)) { return; } - ldsWatcher.onChanged(new LdsUpdate(httpMaxStreamDurationNano, virtualHosts)); + ldsWatcher.onChanged(new LdsUpdate(httpMaxStreamDurationNano, virtualHosts, false, null)); } }); } @@ -758,7 +759,8 @@ public void run() { } VirtualHost virtualHost = new VirtualHost("virtual-host", Collections.singletonList(AUTHORITY), routes); - ldsWatcher.onChanged(new LdsUpdate(0, Collections.singletonList(virtualHost))); + ldsWatcher.onChanged( + new LdsUpdate(0, Collections.singletonList(virtualHost), false, null)); } }); } @@ -770,7 +772,7 @@ public void run() { if (!resourceName.equals(ldsResource)) { return; } - ldsWatcher.onChanged(new LdsUpdate(0, rdsName)); + ldsWatcher.onChanged(new LdsUpdate(0, rdsName, false, null)); } }); }