xds: refactor xds client to make it resource agnostic (#9444)

Mainly refactor work to make type specific xds resources generic, e.g.
1. Define abstract class XdsResourceType to be extended by pluggable new resources. It mainly contains abstract method doParse() to parse unpacked proto messges and produce a ResourceUpdate. The common unpacking proto logic is in XdsResourceType default method parse()
2. Move the parsing/processing logics to specific XdsResourceType. Implementing:
XdsListenerResource for LDS
XdsRouteConfigureResource for RDS
XdsClusterResource for CDS
XdsEndpointResource for EDS
3. The XdsResourceTypes are singleton. To process for each XdsClient, context is passed in parameters, defined by XdsResourceType.Args.
4. Watchers will use generic APIs to subscribe to resource watchXdsResource(XdsResourceType, resourceName, watcher). Watcher and ResourceSubscribers becomes java generic class.
This commit is contained in:
yifeizhuang 2022-09-16 10:08:16 -07:00 committed by GitHub
parent a3c1d7711f
commit e1ad984db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3392 additions and 3173 deletions

View File

@ -19,6 +19,14 @@ package io.grpc.xds;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static io.grpc.xds.XdsClusterResource.ADS_TYPE_URL_CDS;
import static io.grpc.xds.XdsClusterResource.ADS_TYPE_URL_CDS_V2;
import static io.grpc.xds.XdsEndpointResource.ADS_TYPE_URL_EDS;
import static io.grpc.xds.XdsEndpointResource.ADS_TYPE_URL_EDS_V2;
import static io.grpc.xds.XdsListenerResource.ADS_TYPE_URL_LDS;
import static io.grpc.xds.XdsListenerResource.ADS_TYPE_URL_LDS_V2;
import static io.grpc.xds.XdsRouteConfigureResource.ADS_TYPE_URL_RDS;
import static io.grpc.xds.XdsRouteConfigureResource.ADS_TYPE_URL_RDS_V2;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
@ -41,11 +49,14 @@ import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.ClientXdsClient.XdsChannelFactory; import io.grpc.xds.ClientXdsClient.XdsChannelFactory;
import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.EnvoyProtoData.Node;
import io.grpc.xds.XdsClient.ResourceStore; import io.grpc.xds.XdsClient.ResourceStore;
import io.grpc.xds.XdsClient.ResourceUpdate;
import io.grpc.xds.XdsClient.XdsResponseHandler; import io.grpc.xds.XdsClient.XdsResponseHandler;
import io.grpc.xds.XdsLogger.XdsLogLevel; import io.grpc.xds.XdsLogger.XdsLogLevel;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -55,23 +66,6 @@ import javax.annotation.Nullable;
* the xDS RPC stream. * the xDS RPC stream.
*/ */
final class AbstractXdsClient { final class AbstractXdsClient {
private static final String ADS_TYPE_URL_LDS_V2 = "type.googleapis.com/envoy.api.v2.Listener";
private static final String ADS_TYPE_URL_LDS =
"type.googleapis.com/envoy.config.listener.v3.Listener";
private static final String ADS_TYPE_URL_RDS_V2 =
"type.googleapis.com/envoy.api.v2.RouteConfiguration";
private static final String ADS_TYPE_URL_RDS =
"type.googleapis.com/envoy.config.route.v3.RouteConfiguration";
@VisibleForTesting
static final String ADS_TYPE_URL_CDS_V2 = "type.googleapis.com/envoy.api.v2.Cluster";
private static final String ADS_TYPE_URL_CDS =
"type.googleapis.com/envoy.config.cluster.v3.Cluster";
private static final String ADS_TYPE_URL_EDS_V2 =
"type.googleapis.com/envoy.api.v2.ClusterLoadAssignment";
private static final String ADS_TYPE_URL_EDS =
"type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment";
private final SynchronizationContext syncContext; private final SynchronizationContext syncContext;
private final InternalLogId logId; private final InternalLogId logId;
private final XdsLogger logger; private final XdsLogger logger;
@ -88,10 +82,7 @@ final class AbstractXdsClient {
// Last successfully applied version_info for each resource type. Starts with empty string. // Last successfully applied version_info for each resource type. Starts with empty string.
// A version_info is used to update management server with client's most recent knowledge of // A version_info is used to update management server with client's most recent knowledge of
// resources. // resources.
private String ldsVersion = ""; private final Map<ResourceType, String> versions = new HashMap<>();
private String rdsVersion = "";
private String cdsVersion = "";
private String edsVersion = "";
private boolean shutdown; private boolean shutdown;
@Nullable @Nullable
@ -162,16 +153,17 @@ final class AbstractXdsClient {
* Updates the resource subscription for the given resource type. * Updates the resource subscription for the given resource type.
*/ */
// Must be synchronized. // Must be synchronized.
void adjustResourceSubscription(ResourceType type) { void adjustResourceSubscription(XdsResourceType<?> resourceType) {
if (isInBackoff()) { if (isInBackoff()) {
return; return;
} }
if (adsStream == null) { if (adsStream == null) {
startRpcStream(); startRpcStream();
} }
Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type); Collection<String> resources = resourceStore.getSubscribedResources(serverInfo,
resourceType.typeName());
if (resources != null) { if (resources != null) {
adsStream.sendDiscoveryRequest(type, resources); adsStream.sendDiscoveryRequest(resourceType, resources);
} }
} }
@ -180,24 +172,9 @@ final class AbstractXdsClient {
* and sends an ACK request to the management server. * and sends an ACK request to the management server.
*/ */
// Must be synchronized. // Must be synchronized.
void ackResponse(ResourceType type, String versionInfo, String nonce) { void ackResponse(XdsResourceType<?> xdsResourceType, String versionInfo, String nonce) {
switch (type) { ResourceType type = xdsResourceType.typeName();
case LDS: versions.put(type, versionInfo);
ldsVersion = versionInfo;
break;
case RDS:
rdsVersion = versionInfo;
break;
case CDS:
cdsVersion = versionInfo;
break;
case EDS:
edsVersion = versionInfo;
break;
case UNKNOWN:
default:
throw new AssertionError("Unknown resource type: " + type);
}
logger.log(XdsLogLevel.INFO, "Sending ACK for {0} update, nonce: {1}, current version: {2}", logger.log(XdsLogLevel.INFO, "Sending ACK for {0} update, nonce: {1}, current version: {2}",
type, nonce, versionInfo); type, nonce, versionInfo);
Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type); Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type);
@ -212,8 +189,9 @@ final class AbstractXdsClient {
* accepted version) to the management server. * accepted version) to the management server.
*/ */
// Must be synchronized. // Must be synchronized.
void nackResponse(ResourceType type, String nonce, String errorDetail) { void nackResponse(XdsResourceType<?> xdsResourceType, String nonce, String errorDetail) {
String versionInfo = getCurrentVersion(type); ResourceType type = xdsResourceType.typeName();
String versionInfo = versions.getOrDefault(type, "");
logger.log(XdsLogLevel.INFO, "Sending NACK for {0} update, nonce: {1}, current version: {2}", logger.log(XdsLogLevel.INFO, "Sending NACK for {0} update, nonce: {1}, current version: {2}",
type, nonce, versionInfo); type, nonce, versionInfo);
Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type); Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type);
@ -253,30 +231,6 @@ final class AbstractXdsClient {
stopwatch.reset().start(); stopwatch.reset().start();
} }
/** Returns the latest accepted version of the given resource type. */
// Must be synchronized.
String getCurrentVersion(ResourceType type) {
String version;
switch (type) {
case LDS:
version = ldsVersion;
break;
case RDS:
version = rdsVersion;
break;
case CDS:
version = cdsVersion;
break;
case EDS:
version = edsVersion;
break;
case UNKNOWN:
default:
throw new AssertionError("Unknown resource type: " + type);
}
return version;
}
@VisibleForTesting @VisibleForTesting
final class RpcRetryTask implements Runnable { final class RpcRetryTask implements Runnable {
@Override @Override
@ -291,13 +245,14 @@ final class AbstractXdsClient {
} }
Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type); Collection<String> resources = resourceStore.getSubscribedResources(serverInfo, type);
if (resources != null) { if (resources != null) {
adsStream.sendDiscoveryRequest(type, resources); adsStream.sendDiscoveryRequest(resourceStore.getXdsResourceType(type), resources);
} }
} }
xdsResponseHandler.handleStreamRestarted(serverInfo); xdsResponseHandler.handleStreamRestarted(serverInfo);
} }
} }
// TODO(zivy) : remove and replace with XdsResourceType
enum ResourceType { enum ResourceType {
UNKNOWN, LDS, RDS, CDS, EDS; UNKNOWN, LDS, RDS, CDS, EDS;
@ -361,17 +316,13 @@ final class AbstractXdsClient {
private abstract class AbstractAdsStream { private abstract class AbstractAdsStream {
private boolean responseReceived; private boolean responseReceived;
private boolean closed; private boolean closed;
// Response nonce for the most recently received discovery responses of each resource type. // Response nonce for the most recently received discovery responses of each resource type.
// Client initiated requests start response nonce with empty string. // Client initiated requests start response nonce with empty string.
// A nonce is used to indicate the specific DiscoveryResponse each DiscoveryRequest // Nonce in each response is echoed back in the following ACK/NACK request. It is
// corresponds to. // used for management server to identify which response the client is ACKing/NACking.
// A nonce becomes stale following a newer nonce being presented to the client in a // To avoid confusion, client-initiated requests will always use the nonce in
// DiscoveryResponse. // most recently received responses of each resource type.
private String ldsRespNonce = ""; private final Map<ResourceType, String> respNonces = new HashMap<>();
private String rdsRespNonce = "";
private String cdsRespNonce = "";
private String edsRespNonce = "";
abstract void start(); abstract void start();
@ -381,35 +332,20 @@ final class AbstractXdsClient {
* Sends a discovery request with the given {@code versionInfo}, {@code nonce} and * Sends a discovery request with the given {@code versionInfo}, {@code nonce} and
* {@code errorDetail}. Used for reacting to a specific discovery response. For * {@code errorDetail}. Used for reacting to a specific discovery response. For
* client-initiated discovery requests, use {@link * client-initiated discovery requests, use {@link
* #sendDiscoveryRequest(ResourceType, Collection)}. * #sendDiscoveryRequest(XdsResourceType, Collection)}.
*/ */
abstract void sendDiscoveryRequest(ResourceType type, String versionInfo, abstract void sendDiscoveryRequest(ResourceType type, String version,
Collection<String> resources, String nonce, @Nullable String errorDetail); Collection<String> resources, String nonce, @Nullable String errorDetail);
/** /**
* Sends a client-initiated discovery request. * Sends a client-initiated discovery request.
*/ */
final void sendDiscoveryRequest(ResourceType type, Collection<String> resources) { final void sendDiscoveryRequest(XdsResourceType<? extends ResourceUpdate> xdsResourceType,
String nonce; Collection<String> resources) {
switch (type) { ResourceType type = xdsResourceType.typeName();
case LDS:
nonce = ldsRespNonce;
break;
case RDS:
nonce = rdsRespNonce;
break;
case CDS:
nonce = cdsRespNonce;
break;
case EDS:
nonce = edsRespNonce;
break;
case UNKNOWN:
default:
throw new AssertionError("Unknown resource type: " + type);
}
logger.log(XdsLogLevel.INFO, "Sending {0} request for resources: {1}", type, resources); logger.log(XdsLogLevel.INFO, "Sending {0} request for resources: {1}", type, resources);
sendDiscoveryRequest(type, getCurrentVersion(type), resources, nonce, null); sendDiscoveryRequest(type, versions.getOrDefault(type, ""), resources,
respNonces.getOrDefault(type, ""), null);
} }
final void handleRpcResponse( final void handleRpcResponse(
@ -418,31 +354,8 @@ final class AbstractXdsClient {
return; return;
} }
responseReceived = true; responseReceived = true;
// Nonce in each response is echoed back in the following ACK/NACK request. It is respNonces.put(type, nonce);
// used for management server to identify which response the client is ACKing/NACking. xdsResponseHandler.handleResourceResponse(type, serverInfo, versionInfo, resources, nonce);
// To avoid confusion, client-initiated requests will always use the nonce in
// most recently received responses of each resource type.
switch (type) {
case LDS:
ldsRespNonce = nonce;
xdsResponseHandler.handleLdsResponse(serverInfo, versionInfo, resources, nonce);
break;
case RDS:
rdsRespNonce = nonce;
xdsResponseHandler.handleRdsResponse(serverInfo, versionInfo, resources, nonce);
break;
case CDS:
cdsRespNonce = nonce;
xdsResponseHandler.handleCdsResponse(serverInfo, versionInfo, resources, nonce);
break;
case EDS:
edsRespNonce = nonce;
xdsResponseHandler.handleEdsResponse(serverInfo, versionInfo, resources, nonce);
break;
case UNKNOWN:
default:
logger.log(XdsLogLevel.WARNING, "Ignore an unknown type of DiscoveryResponse");
}
} }
final void handleRpcError(Throwable t) { final void handleRpcError(Throwable t) {

View File

@ -35,9 +35,9 @@ import io.grpc.internal.ServiceConfigUtil.PolicySelection;
import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig;
import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig;
import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism;
import io.grpc.xds.XdsClient.CdsResourceWatcher; import io.grpc.xds.XdsClient.ResourceWatcher;
import io.grpc.xds.XdsClient.CdsUpdate; import io.grpc.xds.XdsClusterResource.CdsUpdate;
import io.grpc.xds.XdsClient.CdsUpdate.ClusterType; import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType;
import io.grpc.xds.XdsLogger.XdsLogLevel; import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
import java.util.ArrayDeque; import java.util.ArrayDeque;
@ -221,7 +221,7 @@ final class CdsLoadBalancer2 extends LoadBalancer {
} }
} }
private final class ClusterState implements CdsResourceWatcher { private final class ClusterState implements ResourceWatcher<CdsUpdate> {
private final String name; private final String name;
@Nullable @Nullable
private Map<String, ClusterState> childClusterStates; private Map<String, ClusterState> childClusterStates;
@ -237,12 +237,12 @@ final class CdsLoadBalancer2 extends LoadBalancer {
} }
private void start() { private void start() {
xdsClient.watchCdsResource(name, this); xdsClient.watchXdsResource(XdsClusterResource.getInstance(), name, this);
} }
void shutdown() { void shutdown() {
shutdown = true; shutdown = true;
xdsClient.cancelCdsResourceWatch(name, this); xdsClient.cancelXdsResourceWatch(XdsClusterResource.getInstance(), name, this);
if (childClusterStates != null) { // recursively shut down all descendants if (childClusterStates != null) { // recursively shut down all descendants
for (ClusterState state : childClusterStates.values()) { for (ClusterState state : childClusterStates.values()) {
state.shutdown(); state.shutdown();
@ -300,6 +300,7 @@ final class CdsLoadBalancer2 extends LoadBalancer {
if (shutdown) { if (shutdown) {
return; return;
} }
logger.log(XdsLogLevel.DEBUG, "Received cluster update {0}", update); logger.log(XdsLogLevel.DEBUG, "Received cluster update {0}", update);
discovered = true; discovered = true;
result = update; result = update;

File diff suppressed because it is too large Load Diff

View File

@ -52,8 +52,8 @@ import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig;
import io.grpc.xds.XdsClient.EdsResourceWatcher; import io.grpc.xds.XdsClient.ResourceWatcher;
import io.grpc.xds.XdsClient.EdsUpdate; import io.grpc.xds.XdsEndpointResource.EdsUpdate;
import io.grpc.xds.XdsLogger.XdsLogLevel; import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
import java.net.URI; import java.net.URI;
@ -350,7 +350,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer {
} }
} }
private final class EdsClusterState extends ClusterState implements EdsResourceWatcher { private final class EdsClusterState extends ClusterState implements ResourceWatcher<EdsUpdate> {
@Nullable @Nullable
private final String edsServiceName; private final String edsServiceName;
private Map<Locality, String> localityPriorityNames = Collections.emptyMap(); private Map<Locality, String> localityPriorityNames = Collections.emptyMap();
@ -367,7 +367,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer {
void start() { void start() {
String resourceName = edsServiceName != null ? edsServiceName : name; String resourceName = edsServiceName != null ? edsServiceName : name;
logger.log(XdsLogLevel.INFO, "Start watching EDS resource {0}", resourceName); logger.log(XdsLogLevel.INFO, "Start watching EDS resource {0}", resourceName);
xdsClient.watchEdsResource(resourceName, this); xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), resourceName, this);
} }
@Override @Override
@ -375,7 +375,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer {
super.shutdown(); super.shutdown();
String resourceName = edsServiceName != null ? edsServiceName : name; String resourceName = edsServiceName != null ? edsServiceName : name;
logger.log(XdsLogLevel.INFO, "Stop watching EDS resource {0}", resourceName); logger.log(XdsLogLevel.INFO, "Stop watching EDS resource {0}", resourceName);
xdsClient.cancelEdsResourceWatch(resourceName, this); xdsClient.cancelXdsResourceWatch(XdsEndpointResource.getInstance(), resourceName, this);
} }
@Override @Override

View File

@ -19,23 +19,14 @@ package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME; import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.UrlEscapers; import com.google.common.net.UrlEscapers;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.Any; import com.google.protobuf.Any;
import io.grpc.Status; import io.grpc.Status;
import io.grpc.xds.AbstractXdsClient.ResourceType; import io.grpc.xds.AbstractXdsClient.ResourceType;
import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.Endpoints.DropOverload;
import io.grpc.xds.Endpoints.LocalityLbEndpoints;
import io.grpc.xds.EnvoyServerProtoData.Listener;
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterDropStats;
import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats;
import java.net.URI; import java.net.URI;
@ -43,10 +34,8 @@ import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
@ -118,295 +107,13 @@ abstract class XdsClient {
return Joiner.on('/').join(encodedSegs); return Joiner.on('/').join(encodedSegs);
} }
@AutoValue
abstract static class LdsUpdate implements ResourceUpdate {
// Http level api listener configuration.
@Nullable
abstract HttpConnectionManager httpConnectionManager();
// Tcp level listener configuration.
@Nullable
abstract Listener listener();
static LdsUpdate forApiListener(HttpConnectionManager httpConnectionManager) {
checkNotNull(httpConnectionManager, "httpConnectionManager");
return new AutoValue_XdsClient_LdsUpdate(httpConnectionManager, null);
}
static LdsUpdate forTcpListener(Listener listener) {
checkNotNull(listener, "listener");
return new AutoValue_XdsClient_LdsUpdate(null, listener);
}
}
static final class RdsUpdate implements ResourceUpdate {
// The list virtual hosts that make up the route table.
final List<VirtualHost> virtualHosts;
RdsUpdate(List<VirtualHost> virtualHosts) {
this.virtualHosts = Collections.unmodifiableList(
new ArrayList<>(checkNotNull(virtualHosts, "virtualHosts")));
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("virtualHosts", virtualHosts)
.toString();
}
@Override
public int hashCode() {
return Objects.hash(virtualHosts);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RdsUpdate that = (RdsUpdate) o;
return Objects.equals(virtualHosts, that.virtualHosts);
}
}
/** xDS resource update for cluster-level configuration. */
@AutoValue
abstract static class CdsUpdate implements ResourceUpdate {
abstract String clusterName();
abstract ClusterType clusterType();
abstract ImmutableMap<String, ?> lbPolicyConfig();
// Only valid if lbPolicy is "ring_hash_experimental".
abstract long minRingSize();
// Only valid if lbPolicy is "ring_hash_experimental".
abstract long maxRingSize();
// Only valid if lbPolicy is "least_request_experimental".
abstract int choiceCount();
// Alternative resource name to be used in EDS requests.
/// Only valid for EDS cluster.
@Nullable
abstract String edsServiceName();
// Corresponding DNS name to be used if upstream endpoints of the cluster is resolvable
// via DNS.
// Only valid for LOGICAL_DNS cluster.
@Nullable
abstract String dnsHostName();
// Load report server info for reporting loads via LRS.
// Only valid for EDS or LOGICAL_DNS cluster.
@Nullable
abstract ServerInfo lrsServerInfo();
// Max number of concurrent requests can be sent to this cluster.
// Only valid for EDS or LOGICAL_DNS cluster.
@Nullable
abstract Long maxConcurrentRequests();
// TLS context used to connect to connect to this cluster.
// Only valid for EDS or LOGICAL_DNS cluster.
@Nullable
abstract UpstreamTlsContext upstreamTlsContext();
// List of underlying clusters making of this aggregate cluster.
// Only valid for AGGREGATE cluster.
@Nullable
abstract ImmutableList<String> prioritizedClusterNames();
// Outlier detection configuration.
@Nullable
abstract OutlierDetection outlierDetection();
static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
checkNotNull(prioritizedClusterNames, "prioritizedClusterNames");
return new AutoValue_XdsClient_CdsUpdate.Builder()
.clusterName(clusterName)
.clusterType(ClusterType.AGGREGATE)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.prioritizedClusterNames(ImmutableList.copyOf(prioritizedClusterNames));
}
static Builder forEds(String clusterName, @Nullable String edsServiceName,
@Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests,
@Nullable UpstreamTlsContext upstreamTlsContext,
@Nullable OutlierDetection outlierDetection) {
return new AutoValue_XdsClient_CdsUpdate.Builder()
.clusterName(clusterName)
.clusterType(ClusterType.EDS)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.edsServiceName(edsServiceName)
.lrsServerInfo(lrsServerInfo)
.maxConcurrentRequests(maxConcurrentRequests)
.upstreamTlsContext(upstreamTlsContext)
.outlierDetection(outlierDetection);
}
static Builder forLogicalDns(String clusterName, String dnsHostName,
@Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests,
@Nullable UpstreamTlsContext upstreamTlsContext) {
return new AutoValue_XdsClient_CdsUpdate.Builder()
.clusterName(clusterName)
.clusterType(ClusterType.LOGICAL_DNS)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.dnsHostName(dnsHostName)
.lrsServerInfo(lrsServerInfo)
.maxConcurrentRequests(maxConcurrentRequests)
.upstreamTlsContext(upstreamTlsContext);
}
enum ClusterType {
EDS, LOGICAL_DNS, AGGREGATE
}
enum LbPolicy {
ROUND_ROBIN, RING_HASH, LEAST_REQUEST
}
// FIXME(chengyuanzhang): delete this after UpstreamTlsContext's toString() is fixed.
@Override
public final String toString() {
return MoreObjects.toStringHelper(this)
.add("clusterName", clusterName())
.add("clusterType", clusterType())
.add("lbPolicyConfig", lbPolicyConfig())
.add("minRingSize", minRingSize())
.add("maxRingSize", maxRingSize())
.add("choiceCount", choiceCount())
.add("edsServiceName", edsServiceName())
.add("dnsHostName", dnsHostName())
.add("lrsServerInfo", lrsServerInfo())
.add("maxConcurrentRequests", maxConcurrentRequests())
.add("prioritizedClusterNames", prioritizedClusterNames())
// Exclude upstreamTlsContext and outlierDetection as their string representations are
// cumbersome.
.toString();
}
@AutoValue.Builder
abstract static class Builder {
// Private, use one of the static factory methods instead.
protected abstract Builder clusterName(String clusterName);
// Private, use one of the static factory methods instead.
protected abstract Builder clusterType(ClusterType clusterType);
protected abstract Builder lbPolicyConfig(ImmutableMap<String, ?> lbPolicyConfig);
Builder roundRobinLbPolicy() {
return this.lbPolicyConfig(ImmutableMap.of("round_robin", ImmutableMap.of()));
}
Builder ringHashLbPolicy(Long minRingSize, Long maxRingSize) {
return this.lbPolicyConfig(ImmutableMap.of("ring_hash_experimental",
ImmutableMap.of("minRingSize", minRingSize.doubleValue(), "maxRingSize",
maxRingSize.doubleValue())));
}
Builder leastRequestLbPolicy(Integer choiceCount) {
return this.lbPolicyConfig(ImmutableMap.of("least_request_experimental",
ImmutableMap.of("choiceCount", choiceCount.doubleValue())));
}
// Private, use leastRequestLbPolicy(int).
protected abstract Builder choiceCount(int choiceCount);
// Private, use ringHashLbPolicy(long, long).
protected abstract Builder minRingSize(long minRingSize);
// Private, use ringHashLbPolicy(long, long).
protected abstract Builder maxRingSize(long maxRingSize);
// Private, use CdsUpdate.forEds() instead.
protected abstract Builder edsServiceName(String edsServiceName);
// Private, use CdsUpdate.forLogicalDns() instead.
protected abstract Builder dnsHostName(String dnsHostName);
// Private, use one of the static factory methods instead.
protected abstract Builder lrsServerInfo(ServerInfo lrsServerInfo);
// Private, use one of the static factory methods instead.
protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests);
// Private, use one of the static factory methods instead.
protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext);
// Private, use CdsUpdate.forAggregate() instead.
protected abstract Builder prioritizedClusterNames(List<String> prioritizedClusterNames);
protected abstract Builder outlierDetection(OutlierDetection outlierDetection);
abstract CdsUpdate build();
}
}
static final class EdsUpdate implements ResourceUpdate {
final String clusterName;
final Map<Locality, LocalityLbEndpoints> localityLbEndpointsMap;
final List<DropOverload> dropPolicies;
EdsUpdate(String clusterName, Map<Locality, LocalityLbEndpoints> localityLbEndpoints,
List<DropOverload> dropPolicies) {
this.clusterName = checkNotNull(clusterName, "clusterName");
this.localityLbEndpointsMap = Collections.unmodifiableMap(
new LinkedHashMap<>(checkNotNull(localityLbEndpoints, "localityLbEndpoints")));
this.dropPolicies = Collections.unmodifiableList(
new ArrayList<>(checkNotNull(dropPolicies, "dropPolicies")));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EdsUpdate that = (EdsUpdate) o;
return Objects.equals(clusterName, that.clusterName)
&& Objects.equals(localityLbEndpointsMap, that.localityLbEndpointsMap)
&& Objects.equals(dropPolicies, that.dropPolicies);
}
@Override
public int hashCode() {
return Objects.hash(clusterName, localityLbEndpointsMap, dropPolicies);
}
@Override
public String toString() {
return
MoreObjects
.toStringHelper(this)
.add("clusterName", clusterName)
.add("localityLbEndpointsMap", localityLbEndpointsMap)
.add("dropPolicies", dropPolicies)
.toString();
}
}
interface ResourceUpdate { interface ResourceUpdate {
} }
/** /**
* Watcher interface for a single requested xDS resource. * Watcher interface for a single requested xDS resource.
*/ */
interface ResourceWatcher { interface ResourceWatcher<T extends ResourceUpdate> {
/** /**
* Called when the resource discovery RPC encounters some transient error. * Called when the resource discovery RPC encounters some transient error.
@ -426,22 +133,8 @@ abstract class XdsClient {
* @param resourceName name of the resource requested in discovery request. * @param resourceName name of the resource requested in discovery request.
*/ */
void onResourceDoesNotExist(String resourceName); void onResourceDoesNotExist(String resourceName);
}
interface LdsResourceWatcher extends ResourceWatcher { void onChanged(T update);
void onChanged(LdsUpdate update);
}
interface RdsResourceWatcher extends ResourceWatcher {
void onChanged(RdsUpdate update);
}
interface CdsResourceWatcher extends ResourceWatcher {
void onChanged(CdsUpdate update);
}
interface EdsResourceWatcher extends ResourceWatcher {
void onChanged(EdsUpdate update);
} }
/** /**
@ -609,58 +302,19 @@ abstract class XdsClient {
} }
/** /**
* Registers a data watcher for the given LDS resource. * Registers a data watcher for the given Xds resource.
*/ */
void watchLdsResource(String resourceName, LdsResourceWatcher watcher) { <T extends ResourceUpdate> void watchXdsResource(XdsResourceType<T> type, String resourceName,
ResourceWatcher<T> watcher) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/** /**
* Unregisters the given LDS resource watcher. * Unregisters the given resource watcher.
*/ */
void cancelLdsResourceWatch(String resourceName, LdsResourceWatcher watcher) { <T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
throw new UnsupportedOperationException(); String resourceName,
} ResourceWatcher<T> watcher) {
/**
* Registers a data watcher for the given RDS resource.
*/
void watchRdsResource(String resourceName, RdsResourceWatcher watcher) {
throw new UnsupportedOperationException();
}
/**
* Unregisters the given RDS resource watcher.
*/
void cancelRdsResourceWatch(String resourceName, RdsResourceWatcher watcher) {
throw new UnsupportedOperationException();
}
/**
* Registers a data watcher for the given CDS resource.
*/
void watchCdsResource(String resourceName, CdsResourceWatcher watcher) {
throw new UnsupportedOperationException();
}
/**
* Unregisters the given CDS resource watcher.
*/
void cancelCdsResourceWatch(String resourceName, CdsResourceWatcher watcher) {
throw new UnsupportedOperationException();
}
/**
* Registers a data watcher for the given EDS resource.
*/
void watchEdsResource(String resourceName, EdsResourceWatcher watcher) {
throw new UnsupportedOperationException();
}
/**
* Unregisters the given EDS resource watcher.
*/
void cancelEdsResourceWatch(String resourceName, EdsResourceWatcher watcher) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -691,21 +345,10 @@ abstract class XdsClient {
} }
interface XdsResponseHandler { interface XdsResponseHandler {
/** Called when an LDS response is received. */ /** Called when a xds response is received. */
void handleLdsResponse( void handleResourceResponse(
ServerInfo serverInfo, String versionInfo, List<Any> resources, String nonce); ResourceType resourceType, ServerInfo serverInfo, String versionInfo, List<Any> resources,
String nonce);
/** Called when an RDS response is received. */
void handleRdsResponse(
ServerInfo serverInfo, String versionInfo, List<Any> resources, String nonce);
/** Called when an CDS response is received. */
void handleCdsResponse(
ServerInfo serverInfo, String versionInfo, List<Any> resources, String nonce);
/** Called when an EDS response is received. */
void handleEdsResponse(
ServerInfo serverInfo, String versionInfo, List<Any> resources, String nonce);
/** Called when the ADS stream is closed passively. */ /** Called when the ADS stream is closed passively. */
// Must be synchronized. // Must be synchronized.
@ -727,5 +370,8 @@ abstract class XdsClient {
// Must be synchronized. // Must be synchronized.
@Nullable @Nullable
Collection<String> getSubscribedResources(ServerInfo serverInfo, ResourceType type); Collection<String> getSubscribedResources(ServerInfo serverInfo, ResourceType type);
@Nullable
XdsResourceType<? extends ResourceUpdate> getXdsResourceType(ResourceType type);
} }
} }

View File

@ -0,0 +1,686 @@
/*
* Copyright 2022 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.AbstractXdsClient.ResourceType;
import static io.grpc.xds.AbstractXdsClient.ResourceType.CDS;
import static io.grpc.xds.AbstractXdsClient.ResourceType.EDS;
import static io.grpc.xds.Bootstrapper.ServerInfo;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.Duration;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.util.Durations;
import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds;
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
import io.envoyproxy.envoy.config.core.v3.RoutingPriority;
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext;
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
import io.grpc.LoadBalancerRegistry;
import io.grpc.NameResolver;
import io.grpc.internal.ServiceConfigUtil;
import io.grpc.internal.ServiceConfigUtil.LbConfig;
import io.grpc.xds.ClientXdsClient.ResourceInvalidException;
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
import io.grpc.xds.XdsClient.ResourceUpdate;
import io.grpc.xds.XdsClusterResource.CdsUpdate;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.annotation.Nullable;
class XdsClusterResource extends XdsResourceType<CdsUpdate> {
static final String ADS_TYPE_URL_CDS_V2 = "type.googleapis.com/envoy.api.v2.Cluster";
static final String ADS_TYPE_URL_CDS =
"type.googleapis.com/envoy.config.cluster.v3.Cluster";
private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT =
"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext";
private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT_V2 =
"type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext";
private static final XdsClusterResource instance = new XdsClusterResource();
public static XdsClusterResource getInstance() {
return instance;
}
@Override
@Nullable
String extractResourceName(Message unpackedResource) {
if (!(unpackedResource instanceof Cluster)) {
return null;
}
return ((Cluster) unpackedResource).getName();
}
@Override
ResourceType typeName() {
return CDS;
}
@Override
String typeUrl() {
return ADS_TYPE_URL_CDS;
}
@Override
String typeUrlV2() {
return ADS_TYPE_URL_CDS_V2;
}
@Nullable
@Override
ResourceType dependentResource() {
return EDS;
}
@Override
@SuppressWarnings("unchecked")
Class<Cluster> unpackedClassName() {
return Cluster.class;
}
@Override
CdsUpdate doParse(Args args, Message unpackedMessage,
Set<String> retainedResources, boolean isResourceV3)
throws ResourceInvalidException {
if (!(unpackedMessage instanceof Cluster)) {
throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
}
Set<String> certProviderInstances = null;
if (args.bootstrapInfo != null && args.bootstrapInfo.certProviders() != null) {
certProviderInstances = args.bootstrapInfo.certProviders().keySet();
}
return processCluster((Cluster) unpackedMessage, retainedResources, certProviderInstances,
args.serverInfo, args.loadBalancerRegistry);
}
@VisibleForTesting
static CdsUpdate processCluster(Cluster cluster, Set<String> retainedEdsResources,
Set<String> certProviderInstances,
Bootstrapper.ServerInfo serverInfo,
LoadBalancerRegistry loadBalancerRegistry)
throws ResourceInvalidException {
StructOrError<CdsUpdate.Builder> structOrError;
switch (cluster.getClusterDiscoveryTypeCase()) {
case TYPE:
structOrError = parseNonAggregateCluster(cluster, retainedEdsResources,
certProviderInstances, serverInfo);
break;
case CLUSTER_TYPE:
structOrError = parseAggregateCluster(cluster);
break;
case CLUSTERDISCOVERYTYPE_NOT_SET:
default:
throw new ResourceInvalidException(
"Cluster " + cluster.getName() + ": unspecified cluster discovery type");
}
if (structOrError.getErrorDetail() != null) {
throw new ResourceInvalidException(structOrError.getErrorDetail());
}
CdsUpdate.Builder updateBuilder = structOrError.getStruct();
ImmutableMap<String, ?> lbPolicyConfig = LoadBalancerConfigFactory.newConfig(cluster,
enableLeastRequest, enableCustomLbConfig);
// Validate the LB config by trying to parse it with the corresponding LB provider.
LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig);
NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider(
lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig(
lbConfig.getRawConfigValue());
if (configOrError.getError() != null) {
throw new ResourceInvalidException(structOrError.getErrorDetail());
}
updateBuilder.lbPolicyConfig(lbPolicyConfig);
return updateBuilder.build();
}
private static StructOrError<CdsUpdate.Builder> parseAggregateCluster(Cluster cluster) {
String clusterName = cluster.getName();
Cluster.CustomClusterType customType = cluster.getClusterType();
String typeName = customType.getName();
if (!typeName.equals(AGGREGATE_CLUSTER_TYPE_NAME)) {
return StructOrError.fromError(
"Cluster " + clusterName + ": unsupported custom cluster type: " + typeName);
}
io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig clusterConfig;
try {
clusterConfig = unpackCompatibleType(customType.getTypedConfig(),
io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig.class,
TYPE_URL_CLUSTER_CONFIG, TYPE_URL_CLUSTER_CONFIG_V2);
} catch (InvalidProtocolBufferException e) {
return StructOrError.fromError("Cluster " + clusterName + ": malformed ClusterConfig: " + e);
}
return StructOrError.fromStruct(CdsUpdate.forAggregate(
clusterName, clusterConfig.getClustersList()));
}
private static StructOrError<CdsUpdate.Builder> parseNonAggregateCluster(
Cluster cluster, Set<String> edsResources, Set<String> certProviderInstances,
Bootstrapper.ServerInfo serverInfo) {
String clusterName = cluster.getName();
Bootstrapper.ServerInfo lrsServerInfo = null;
Long maxConcurrentRequests = null;
EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = null;
OutlierDetection outlierDetection = null;
if (cluster.hasLrsServer()) {
if (!cluster.getLrsServer().hasSelf()) {
return StructOrError.fromError(
"Cluster " + clusterName + ": only support LRS for the same management server");
}
lrsServerInfo = serverInfo;
}
if (cluster.hasCircuitBreakers()) {
List<Thresholds> thresholds = cluster.getCircuitBreakers().getThresholdsList();
for (Thresholds threshold : thresholds) {
if (threshold.getPriority() != RoutingPriority.DEFAULT) {
continue;
}
if (threshold.hasMaxRequests()) {
maxConcurrentRequests = (long) threshold.getMaxRequests().getValue();
}
}
}
if (cluster.getTransportSocketMatchesCount() > 0) {
return StructOrError.fromError("Cluster " + clusterName
+ ": transport-socket-matches not supported.");
}
if (cluster.hasTransportSocket()) {
if (!TRANSPORT_SOCKET_NAME_TLS.equals(cluster.getTransportSocket().getName())) {
return StructOrError.fromError("transport-socket with name "
+ cluster.getTransportSocket().getName() + " not supported.");
}
try {
upstreamTlsContext = UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext(
validateUpstreamTlsContext(
unpackCompatibleType(cluster.getTransportSocket().getTypedConfig(),
io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext.class,
TYPE_URL_UPSTREAM_TLS_CONTEXT, TYPE_URL_UPSTREAM_TLS_CONTEXT_V2),
certProviderInstances));
} catch (InvalidProtocolBufferException | ResourceInvalidException e) {
return StructOrError.fromError(
"Cluster " + clusterName + ": malformed UpstreamTlsContext: " + e);
}
}
if (cluster.hasOutlierDetection() && enableOutlierDetection) {
try {
outlierDetection = OutlierDetection.fromEnvoyOutlierDetection(
validateOutlierDetection(cluster.getOutlierDetection()));
} catch (ResourceInvalidException e) {
return StructOrError.fromError(
"Cluster " + clusterName + ": malformed outlier_detection: " + e);
}
}
Cluster.DiscoveryType type = cluster.getType();
if (type == Cluster.DiscoveryType.EDS) {
String edsServiceName = null;
io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig edsClusterConfig =
cluster.getEdsClusterConfig();
if (!edsClusterConfig.getEdsConfig().hasAds()
&& ! edsClusterConfig.getEdsConfig().hasSelf()) {
return StructOrError.fromError(
"Cluster " + clusterName + ": field eds_cluster_config must be set to indicate to use"
+ " EDS over ADS or self ConfigSource");
}
// If the service_name field is set, that value will be used for the EDS request.
if (!edsClusterConfig.getServiceName().isEmpty()) {
edsServiceName = edsClusterConfig.getServiceName();
edsResources.add(edsServiceName);
} else {
edsResources.add(clusterName);
}
return StructOrError.fromStruct(CdsUpdate.forEds(
clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext,
outlierDetection));
} else if (type.equals(Cluster.DiscoveryType.LOGICAL_DNS)) {
if (!cluster.hasLoadAssignment()) {
return StructOrError.fromError(
"Cluster " + clusterName + ": LOGICAL_DNS clusters must have a single host");
}
ClusterLoadAssignment assignment = cluster.getLoadAssignment();
if (assignment.getEndpointsCount() != 1
|| assignment.getEndpoints(0).getLbEndpointsCount() != 1) {
return StructOrError.fromError(
"Cluster " + clusterName + ": LOGICAL_DNS clusters must have a single "
+ "locality_lb_endpoint and a single lb_endpoint");
}
io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint lbEndpoint =
assignment.getEndpoints(0).getLbEndpoints(0);
if (!lbEndpoint.hasEndpoint() || !lbEndpoint.getEndpoint().hasAddress()
|| !lbEndpoint.getEndpoint().getAddress().hasSocketAddress()) {
return StructOrError.fromError(
"Cluster " + clusterName
+ ": LOGICAL_DNS clusters must have an endpoint with address and socket_address");
}
SocketAddress socketAddress = lbEndpoint.getEndpoint().getAddress().getSocketAddress();
if (!socketAddress.getResolverName().isEmpty()) {
return StructOrError.fromError(
"Cluster " + clusterName
+ ": LOGICAL DNS clusters must NOT have a custom resolver name set");
}
if (socketAddress.getPortSpecifierCase() != SocketAddress.PortSpecifierCase.PORT_VALUE) {
return StructOrError.fromError(
"Cluster " + clusterName
+ ": LOGICAL DNS clusters socket_address must have port_value");
}
String dnsHostName = String.format(
Locale.US, "%s:%d", socketAddress.getAddress(), socketAddress.getPortValue());
return StructOrError.fromStruct(CdsUpdate.forLogicalDns(
clusterName, dnsHostName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext));
}
return StructOrError.fromError(
"Cluster " + clusterName + ": unsupported built-in discovery type: " + type);
}
static io.envoyproxy.envoy.config.cluster.v3.OutlierDetection validateOutlierDetection(
io.envoyproxy.envoy.config.cluster.v3.OutlierDetection outlierDetection)
throws ResourceInvalidException {
if (outlierDetection.hasInterval()) {
if (!Durations.isValid(outlierDetection.getInterval())) {
throw new ResourceInvalidException("outlier_detection interval is not a valid Duration");
}
if (hasNegativeValues(outlierDetection.getInterval())) {
throw new ResourceInvalidException("outlier_detection interval has a negative value");
}
}
if (outlierDetection.hasBaseEjectionTime()) {
if (!Durations.isValid(outlierDetection.getBaseEjectionTime())) {
throw new ResourceInvalidException(
"outlier_detection base_ejection_time is not a valid Duration");
}
if (hasNegativeValues(outlierDetection.getBaseEjectionTime())) {
throw new ResourceInvalidException(
"outlier_detection base_ejection_time has a negative value");
}
}
if (outlierDetection.hasMaxEjectionTime()) {
if (!Durations.isValid(outlierDetection.getMaxEjectionTime())) {
throw new ResourceInvalidException(
"outlier_detection max_ejection_time is not a valid Duration");
}
if (hasNegativeValues(outlierDetection.getMaxEjectionTime())) {
throw new ResourceInvalidException(
"outlier_detection max_ejection_time has a negative value");
}
}
if (outlierDetection.hasMaxEjectionPercent()
&& outlierDetection.getMaxEjectionPercent().getValue() > 100) {
throw new ResourceInvalidException(
"outlier_detection max_ejection_percent is > 100");
}
if (outlierDetection.hasEnforcingSuccessRate()
&& outlierDetection.getEnforcingSuccessRate().getValue() > 100) {
throw new ResourceInvalidException(
"outlier_detection enforcing_success_rate is > 100");
}
if (outlierDetection.hasFailurePercentageThreshold()
&& outlierDetection.getFailurePercentageThreshold().getValue() > 100) {
throw new ResourceInvalidException(
"outlier_detection failure_percentage_threshold is > 100");
}
if (outlierDetection.hasEnforcingFailurePercentage()
&& outlierDetection.getEnforcingFailurePercentage().getValue() > 100) {
throw new ResourceInvalidException(
"outlier_detection enforcing_failure_percentage is > 100");
}
return outlierDetection;
}
static boolean hasNegativeValues(Duration duration) {
return duration.getSeconds() < 0 || duration.getNanos() < 0;
}
@VisibleForTesting
static io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
validateUpstreamTlsContext(
io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext upstreamTlsContext,
Set<String> certProviderInstances)
throws ResourceInvalidException {
if (upstreamTlsContext.hasCommonTlsContext()) {
validateCommonTlsContext(upstreamTlsContext.getCommonTlsContext(), certProviderInstances,
false);
} else {
throw new ResourceInvalidException("common-tls-context is required in upstream-tls-context");
}
return upstreamTlsContext;
}
@VisibleForTesting
static void validateCommonTlsContext(
CommonTlsContext commonTlsContext, Set<String> certProviderInstances, boolean server)
throws ResourceInvalidException {
if (commonTlsContext.hasCustomHandshaker()) {
throw new ResourceInvalidException(
"common-tls-context with custom_handshaker is not supported");
}
if (commonTlsContext.hasTlsParams()) {
throw new ResourceInvalidException("common-tls-context with tls_params is not supported");
}
if (commonTlsContext.hasValidationContextSdsSecretConfig()) {
throw new ResourceInvalidException(
"common-tls-context with validation_context_sds_secret_config is not supported");
}
if (commonTlsContext.hasValidationContextCertificateProvider()) {
throw new ResourceInvalidException(
"common-tls-context with validation_context_certificate_provider is not supported");
}
if (commonTlsContext.hasValidationContextCertificateProviderInstance()) {
throw new ResourceInvalidException(
"common-tls-context with validation_context_certificate_provider_instance is not"
+ " supported");
}
String certInstanceName = getIdentityCertInstanceName(commonTlsContext);
if (certInstanceName == null) {
if (server) {
throw new ResourceInvalidException(
"tls_certificate_provider_instance is required in downstream-tls-context");
}
if (commonTlsContext.getTlsCertificatesCount() > 0) {
throw new ResourceInvalidException(
"tls_certificate_provider_instance is unset");
}
if (commonTlsContext.getTlsCertificateSdsSecretConfigsCount() > 0) {
throw new ResourceInvalidException(
"tls_certificate_provider_instance is unset");
}
if (commonTlsContext.hasTlsCertificateCertificateProvider()) {
throw new ResourceInvalidException(
"tls_certificate_provider_instance is unset");
}
} else if (certProviderInstances == null || !certProviderInstances.contains(certInstanceName)) {
throw new ResourceInvalidException(
"CertificateProvider instance name '" + certInstanceName
+ "' not defined in the bootstrap file.");
}
String rootCaInstanceName = getRootCertInstanceName(commonTlsContext);
if (rootCaInstanceName == null) {
if (!server) {
throw new ResourceInvalidException(
"ca_certificate_provider_instance is required in upstream-tls-context");
}
} else {
if (certProviderInstances == null || !certProviderInstances.contains(rootCaInstanceName)) {
throw new ResourceInvalidException(
"ca_certificate_provider_instance name '" + rootCaInstanceName
+ "' not defined in the bootstrap file.");
}
CertificateValidationContext certificateValidationContext = null;
if (commonTlsContext.hasValidationContext()) {
certificateValidationContext = commonTlsContext.getValidationContext();
} else if (commonTlsContext.hasCombinedValidationContext() && commonTlsContext
.getCombinedValidationContext().hasDefaultValidationContext()) {
certificateValidationContext = commonTlsContext.getCombinedValidationContext()
.getDefaultValidationContext();
}
if (certificateValidationContext != null) {
if (certificateValidationContext.getMatchSubjectAltNamesCount() > 0 && server) {
throw new ResourceInvalidException(
"match_subject_alt_names only allowed in upstream_tls_context");
}
if (certificateValidationContext.getVerifyCertificateSpkiCount() > 0) {
throw new ResourceInvalidException(
"verify_certificate_spki in default_validation_context is not supported");
}
if (certificateValidationContext.getVerifyCertificateHashCount() > 0) {
throw new ResourceInvalidException(
"verify_certificate_hash in default_validation_context is not supported");
}
if (certificateValidationContext.hasRequireSignedCertificateTimestamp()) {
throw new ResourceInvalidException(
"require_signed_certificate_timestamp in default_validation_context is not "
+ "supported");
}
if (certificateValidationContext.hasCrl()) {
throw new ResourceInvalidException("crl in default_validation_context is not supported");
}
if (certificateValidationContext.hasCustomValidatorConfig()) {
throw new ResourceInvalidException(
"custom_validator_config in default_validation_context is not supported");
}
}
}
}
private static String getIdentityCertInstanceName(CommonTlsContext commonTlsContext) {
if (commonTlsContext.hasTlsCertificateProviderInstance()) {
return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName();
} else if (commonTlsContext.hasTlsCertificateCertificateProviderInstance()) {
return commonTlsContext.getTlsCertificateCertificateProviderInstance().getInstanceName();
}
return null;
}
private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) {
if (commonTlsContext.hasValidationContext()) {
if (commonTlsContext.getValidationContext().hasCaCertificateProviderInstance()) {
return commonTlsContext.getValidationContext().getCaCertificateProviderInstance()
.getInstanceName();
}
} else if (commonTlsContext.hasCombinedValidationContext()) {
CommonTlsContext.CombinedCertificateValidationContext combinedCertificateValidationContext
= commonTlsContext.getCombinedValidationContext();
if (combinedCertificateValidationContext.hasDefaultValidationContext()
&& combinedCertificateValidationContext.getDefaultValidationContext()
.hasCaCertificateProviderInstance()) {
return combinedCertificateValidationContext.getDefaultValidationContext()
.getCaCertificateProviderInstance().getInstanceName();
} else if (combinedCertificateValidationContext
.hasValidationContextCertificateProviderInstance()) {
return combinedCertificateValidationContext
.getValidationContextCertificateProviderInstance().getInstanceName();
}
}
return null;
}
/** xDS resource update for cluster-level configuration. */
@AutoValue
abstract static class CdsUpdate implements ResourceUpdate {
abstract String clusterName();
abstract ClusterType clusterType();
abstract ImmutableMap<String, ?> lbPolicyConfig();
// Only valid if lbPolicy is "ring_hash_experimental".
abstract long minRingSize();
// Only valid if lbPolicy is "ring_hash_experimental".
abstract long maxRingSize();
// Only valid if lbPolicy is "least_request_experimental".
abstract int choiceCount();
// Alternative resource name to be used in EDS requests.
/// Only valid for EDS cluster.
@Nullable
abstract String edsServiceName();
// Corresponding DNS name to be used if upstream endpoints of the cluster is resolvable
// via DNS.
// Only valid for LOGICAL_DNS cluster.
@Nullable
abstract String dnsHostName();
// Load report server info for reporting loads via LRS.
// Only valid for EDS or LOGICAL_DNS cluster.
@Nullable
abstract ServerInfo lrsServerInfo();
// Max number of concurrent requests can be sent to this cluster.
// Only valid for EDS or LOGICAL_DNS cluster.
@Nullable
abstract Long maxConcurrentRequests();
// TLS context used to connect to connect to this cluster.
// Only valid for EDS or LOGICAL_DNS cluster.
@Nullable
abstract UpstreamTlsContext upstreamTlsContext();
// List of underlying clusters making of this aggregate cluster.
// Only valid for AGGREGATE cluster.
@Nullable
abstract ImmutableList<String> prioritizedClusterNames();
// Outlier detection configuration.
@Nullable
abstract OutlierDetection outlierDetection();
static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
checkNotNull(prioritizedClusterNames, "prioritizedClusterNames");
return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
.clusterName(clusterName)
.clusterType(ClusterType.AGGREGATE)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.prioritizedClusterNames(ImmutableList.copyOf(prioritizedClusterNames));
}
static Builder forEds(String clusterName, @Nullable String edsServiceName,
@Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests,
@Nullable UpstreamTlsContext upstreamTlsContext,
@Nullable OutlierDetection outlierDetection) {
return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
.clusterName(clusterName)
.clusterType(ClusterType.EDS)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.edsServiceName(edsServiceName)
.lrsServerInfo(lrsServerInfo)
.maxConcurrentRequests(maxConcurrentRequests)
.upstreamTlsContext(upstreamTlsContext)
.outlierDetection(outlierDetection);
}
static Builder forLogicalDns(String clusterName, String dnsHostName,
@Nullable ServerInfo lrsServerInfo,
@Nullable Long maxConcurrentRequests,
@Nullable UpstreamTlsContext upstreamTlsContext) {
return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
.clusterName(clusterName)
.clusterType(ClusterType.LOGICAL_DNS)
.minRingSize(0)
.maxRingSize(0)
.choiceCount(0)
.dnsHostName(dnsHostName)
.lrsServerInfo(lrsServerInfo)
.maxConcurrentRequests(maxConcurrentRequests)
.upstreamTlsContext(upstreamTlsContext);
}
enum ClusterType {
EDS, LOGICAL_DNS, AGGREGATE
}
enum LbPolicy {
ROUND_ROBIN, RING_HASH, LEAST_REQUEST
}
// FIXME(chengyuanzhang): delete this after UpstreamTlsContext's toString() is fixed.
@Override
public final String toString() {
return MoreObjects.toStringHelper(this)
.add("clusterName", clusterName())
.add("clusterType", clusterType())
.add("lbPolicyConfig", lbPolicyConfig())
.add("minRingSize", minRingSize())
.add("maxRingSize", maxRingSize())
.add("choiceCount", choiceCount())
.add("edsServiceName", edsServiceName())
.add("dnsHostName", dnsHostName())
.add("lrsServerInfo", lrsServerInfo())
.add("maxConcurrentRequests", maxConcurrentRequests())
// Exclude upstreamTlsContext and outlierDetection as their string representations are
// cumbersome.
.add("prioritizedClusterNames", prioritizedClusterNames())
.toString();
}
@AutoValue.Builder
abstract static class Builder {
// Private, use one of the static factory methods instead.
protected abstract Builder clusterName(String clusterName);
// Private, use one of the static factory methods instead.
protected abstract Builder clusterType(ClusterType clusterType);
protected abstract Builder lbPolicyConfig(ImmutableMap<String, ?> lbPolicyConfig);
Builder roundRobinLbPolicy() {
return this.lbPolicyConfig(ImmutableMap.of("round_robin", ImmutableMap.of()));
}
Builder ringHashLbPolicy(Long minRingSize, Long maxRingSize) {
return this.lbPolicyConfig(ImmutableMap.of("ring_hash_experimental",
ImmutableMap.of("minRingSize", minRingSize.doubleValue(), "maxRingSize",
maxRingSize.doubleValue())));
}
Builder leastRequestLbPolicy(Integer choiceCount) {
return this.lbPolicyConfig(ImmutableMap.of("least_request_experimental",
ImmutableMap.of("choiceCount", choiceCount.doubleValue())));
}
// Private, use leastRequestLbPolicy(int).
protected abstract Builder choiceCount(int choiceCount);
// Private, use ringHashLbPolicy(long, long).
protected abstract Builder minRingSize(long minRingSize);
// Private, use ringHashLbPolicy(long, long).
protected abstract Builder maxRingSize(long maxRingSize);
// Private, use CdsUpdate.forEds() instead.
protected abstract Builder edsServiceName(String edsServiceName);
// Private, use CdsUpdate.forLogicalDns() instead.
protected abstract Builder dnsHostName(String dnsHostName);
// Private, use one of the static factory methods instead.
protected abstract Builder lrsServerInfo(ServerInfo lrsServerInfo);
// Private, use one of the static factory methods instead.
protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests);
// Private, use one of the static factory methods instead.
protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext);
// Private, use CdsUpdate.forAggregate() instead.
protected abstract Builder prioritizedClusterNames(List<String> prioritizedClusterNames);
protected abstract Builder outlierDetection(OutlierDetection outlierDetection);
abstract CdsUpdate build();
}
}
}

View File

@ -0,0 +1,259 @@
/*
* Copyright 2022 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.AbstractXdsClient.ResourceType;
import static io.grpc.xds.AbstractXdsClient.ResourceType.EDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.Message;
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
import io.envoyproxy.envoy.type.v3.FractionalPercent;
import io.grpc.EquivalentAddressGroup;
import io.grpc.xds.ClientXdsClient.ResourceInvalidException;
import io.grpc.xds.Endpoints.DropOverload;
import io.grpc.xds.Endpoints.LocalityLbEndpoints;
import io.grpc.xds.XdsClient.ResourceUpdate;
import io.grpc.xds.XdsEndpointResource.EdsUpdate;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
class XdsEndpointResource extends XdsResourceType<EdsUpdate> {
static final String ADS_TYPE_URL_EDS_V2 =
"type.googleapis.com/envoy.api.v2.ClusterLoadAssignment";
static final String ADS_TYPE_URL_EDS =
"type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment";
private static final XdsEndpointResource instance = new XdsEndpointResource();
public static XdsEndpointResource getInstance() {
return instance;
}
@Override
@Nullable
String extractResourceName(Message unpackedResource) {
if (!(unpackedResource instanceof ClusterLoadAssignment)) {
return null;
}
return ((ClusterLoadAssignment) unpackedResource).getClusterName();
}
@Override
ResourceType typeName() {
return EDS;
}
@Override
String typeUrl() {
return ADS_TYPE_URL_EDS;
}
@Override
String typeUrlV2() {
return ADS_TYPE_URL_EDS_V2;
}
@Nullable
@Override
ResourceType dependentResource() {
return null;
}
@Override
Class<ClusterLoadAssignment> unpackedClassName() {
return ClusterLoadAssignment.class;
}
@Override
EdsUpdate doParse(Args args, Message unpackedMessage,
Set<String> retainedResources, boolean isResourceV3)
throws ResourceInvalidException {
if (!(unpackedMessage instanceof ClusterLoadAssignment)) {
throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
}
return processClusterLoadAssignment((ClusterLoadAssignment) unpackedMessage);
}
private static EdsUpdate processClusterLoadAssignment(ClusterLoadAssignment assignment)
throws ResourceInvalidException {
Map<Integer, Set<Locality>> priorities = new HashMap<>();
Map<Locality, LocalityLbEndpoints> localityLbEndpointsMap = new LinkedHashMap<>();
List<Endpoints.DropOverload> dropOverloads = new ArrayList<>();
int maxPriority = -1;
for (io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints localityLbEndpointsProto
: assignment.getEndpointsList()) {
StructOrError<LocalityLbEndpoints> structOrError =
parseLocalityLbEndpoints(localityLbEndpointsProto);
if (structOrError == null) {
continue;
}
if (structOrError.getErrorDetail() != null) {
throw new ResourceInvalidException(structOrError.getErrorDetail());
}
LocalityLbEndpoints localityLbEndpoints = structOrError.getStruct();
int priority = localityLbEndpoints.priority();
maxPriority = Math.max(maxPriority, priority);
// Note endpoints with health status other than HEALTHY and UNKNOWN are still
// handed over to watching parties. It is watching parties' responsibility to
// filter out unhealthy endpoints. See EnvoyProtoData.LbEndpoint#isHealthy().
Locality locality = parseLocality(localityLbEndpointsProto.getLocality());
localityLbEndpointsMap.put(locality, localityLbEndpoints);
if (!priorities.containsKey(priority)) {
priorities.put(priority, new HashSet<>());
}
if (!priorities.get(priority).add(locality)) {
throw new ResourceInvalidException("ClusterLoadAssignment has duplicate locality:"
+ locality + " for priority:" + priority);
}
}
if (priorities.size() != maxPriority + 1) {
throw new ResourceInvalidException("ClusterLoadAssignment has sparse priorities");
}
for (ClusterLoadAssignment.Policy.DropOverload dropOverloadProto
: assignment.getPolicy().getDropOverloadsList()) {
dropOverloads.add(parseDropOverload(dropOverloadProto));
}
return new EdsUpdate(assignment.getClusterName(), localityLbEndpointsMap, dropOverloads);
}
private static Locality parseLocality(io.envoyproxy.envoy.config.core.v3.Locality proto) {
return Locality.create(proto.getRegion(), proto.getZone(), proto.getSubZone());
}
private static DropOverload parseDropOverload(
io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment.Policy.DropOverload proto) {
return DropOverload.create(proto.getCategory(), getRatePerMillion(proto.getDropPercentage()));
}
private static int getRatePerMillion(FractionalPercent percent) {
int numerator = percent.getNumerator();
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 < 0) {
numerator = 1_000_000;
}
return numerator;
}
@VisibleForTesting
@Nullable
static StructOrError<LocalityLbEndpoints> parseLocalityLbEndpoints(
io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) {
// Filter out localities without or with 0 weight.
if (!proto.hasLoadBalancingWeight() || proto.getLoadBalancingWeight().getValue() < 1) {
return null;
}
if (proto.getPriority() < 0) {
return StructOrError.fromError("negative priority");
}
List<Endpoints.LbEndpoint> endpoints = new ArrayList<>(proto.getLbEndpointsCount());
for (io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint endpoint : proto.getLbEndpointsList()) {
// The endpoint field of each lb_endpoints must be set.
// Inside of it: the address field must be set.
if (!endpoint.hasEndpoint() || !endpoint.getEndpoint().hasAddress()) {
return StructOrError.fromError("LbEndpoint with no endpoint/address");
}
io.envoyproxy.envoy.config.core.v3.SocketAddress socketAddress =
endpoint.getEndpoint().getAddress().getSocketAddress();
InetSocketAddress addr =
new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue());
boolean isHealthy =
endpoint.getHealthStatus() == io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY
|| endpoint.getHealthStatus()
== io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN;
endpoints.add(Endpoints.LbEndpoint.create(
new EquivalentAddressGroup(ImmutableList.<java.net.SocketAddress>of(addr)),
endpoint.getLoadBalancingWeight().getValue(), isHealthy));
}
return StructOrError.fromStruct(Endpoints.LocalityLbEndpoints.create(
endpoints, proto.getLoadBalancingWeight().getValue(), proto.getPriority()));
}
static final class EdsUpdate implements ResourceUpdate {
final String clusterName;
final Map<Locality, LocalityLbEndpoints> localityLbEndpointsMap;
final List<DropOverload> dropPolicies;
EdsUpdate(String clusterName, Map<Locality, LocalityLbEndpoints> localityLbEndpoints,
List<DropOverload> dropPolicies) {
this.clusterName = checkNotNull(clusterName, "clusterName");
this.localityLbEndpointsMap = Collections.unmodifiableMap(
new LinkedHashMap<>(checkNotNull(localityLbEndpoints, "localityLbEndpoints")));
this.dropPolicies = Collections.unmodifiableList(
new ArrayList<>(checkNotNull(dropPolicies, "dropPolicies")));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EdsUpdate that = (EdsUpdate) o;
return Objects.equals(clusterName, that.clusterName)
&& Objects.equals(localityLbEndpointsMap, that.localityLbEndpointsMap)
&& Objects.equals(dropPolicies, that.dropPolicies);
}
@Override
public int hashCode() {
return Objects.hash(clusterName, localityLbEndpointsMap, dropPolicies);
}
@Override
public String toString() {
return
MoreObjects
.toStringHelper(this)
.add("clusterName", clusterName)
.add("localityLbEndpointsMap", localityLbEndpointsMap)
.add("dropPolicies", dropPolicies)
.toString();
}
}
}

View File

@ -0,0 +1,633 @@
/*
* Copyright 2022 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.AbstractXdsClient.ResourceType;
import static io.grpc.xds.AbstractXdsClient.ResourceType.LDS;
import static io.grpc.xds.AbstractXdsClient.ResourceType.RDS;
import static io.grpc.xds.ClientXdsClient.ResourceInvalidException;
import static io.grpc.xds.XdsClient.ResourceUpdate;
import static io.grpc.xds.XdsClusterResource.validateCommonTlsContext;
import static io.grpc.xds.XdsRouteConfigureResource.extractVirtualHosts;
import com.github.udpa.udpa.type.v1.TypedStruct;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.util.Durations;
import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions;
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
import io.envoyproxy.envoy.config.core.v3.TrafficDirection;
import io.envoyproxy.envoy.config.listener.v3.Listener;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds;
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext;
import io.grpc.xds.EnvoyServerProtoData.CidrRange;
import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType;
import io.grpc.xds.EnvoyServerProtoData.FilterChain;
import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.XdsListenerResource.LdsUpdate;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
class XdsListenerResource extends XdsResourceType<LdsUpdate> {
static final String ADS_TYPE_URL_LDS_V2 = "type.googleapis.com/envoy.api.v2.Listener";
static final String ADS_TYPE_URL_LDS =
"type.googleapis.com/envoy.config.listener.v3.Listener";
private static final String TYPE_URL_HTTP_CONNECTION_MANAGER_V2 =
"type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2"
+ ".HttpConnectionManager";
static final String TYPE_URL_HTTP_CONNECTION_MANAGER =
"type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3"
+ ".HttpConnectionManager";
private static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls";
private static final XdsListenerResource instance = new XdsListenerResource();
public static XdsListenerResource getInstance() {
return instance;
}
@Override
@Nullable
String extractResourceName(Message unpackedResource) {
if (!(unpackedResource instanceof Listener)) {
return null;
}
return ((Listener) unpackedResource).getName();
}
@Override
ResourceType typeName() {
return LDS;
}
@Override
Class<Listener> unpackedClassName() {
return Listener.class;
}
@Override
String typeUrl() {
return ADS_TYPE_URL_LDS;
}
@Override
String typeUrlV2() {
return ADS_TYPE_URL_LDS_V2;
}
@Nullable
@Override
ResourceType dependentResource() {
return RDS;
}
@Override
LdsUpdate doParse(Args args, Message unpackedMessage, Set<String> retainedResources,
boolean isResourceV3)
throws ResourceInvalidException {
if (!(unpackedMessage instanceof Listener)) {
throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
}
Listener listener = (Listener) unpackedMessage;
if (listener.hasApiListener()) {
return processClientSideListener(
listener, retainedResources, args, enableFaultInjection && isResourceV3);
} else {
return processServerSideListener(
listener, retainedResources, args, enableRbac && isResourceV3);
}
}
private LdsUpdate processClientSideListener(
Listener listener, Set<String> rdsResources, Args args, boolean parseHttpFilter)
throws ResourceInvalidException {
// Unpack HttpConnectionManager from the Listener.
HttpConnectionManager hcm;
try {
hcm = unpackCompatibleType(
listener.getApiListener().getApiListener(), HttpConnectionManager.class,
TYPE_URL_HTTP_CONNECTION_MANAGER, TYPE_URL_HTTP_CONNECTION_MANAGER_V2);
} catch (InvalidProtocolBufferException e) {
throw new ResourceInvalidException(
"Could not parse HttpConnectionManager config from ApiListener", e);
}
return LdsUpdate.forApiListener(parseHttpConnectionManager(
hcm, rdsResources, args.filterRegistry, parseHttpFilter, true /* isForClient */));
}
private LdsUpdate processServerSideListener(
Listener proto, Set<String> rdsResources, Args args, boolean parseHttpFilter)
throws ResourceInvalidException {
Set<String> certProviderInstances = null;
if (args.bootstrapInfo != null && args.bootstrapInfo.certProviders() != null) {
certProviderInstances = args.bootstrapInfo.certProviders().keySet();
}
return LdsUpdate.forTcpListener(parseServerSideListener(
proto, rdsResources, args.tlsContextManager, args.filterRegistry, certProviderInstances,
parseHttpFilter));
}
@VisibleForTesting
static EnvoyServerProtoData.Listener parseServerSideListener(
Listener proto, Set<String> rdsResources, TlsContextManager tlsContextManager,
FilterRegistry filterRegistry, Set<String> certProviderInstances, boolean parseHttpFilter)
throws ResourceInvalidException {
if (!proto.getTrafficDirection().equals(TrafficDirection.INBOUND)
&& !proto.getTrafficDirection().equals(TrafficDirection.UNSPECIFIED)) {
throw new ResourceInvalidException(
"Listener " + proto.getName() + " with invalid traffic direction: "
+ proto.getTrafficDirection());
}
if (!proto.getListenerFiltersList().isEmpty()) {
throw new ResourceInvalidException(
"Listener " + proto.getName() + " cannot have listener_filters");
}
if (proto.hasUseOriginalDst()) {
throw new ResourceInvalidException(
"Listener " + proto.getName() + " cannot have use_original_dst set to true");
}
String address = null;
if (proto.getAddress().hasSocketAddress()) {
SocketAddress socketAddress = proto.getAddress().getSocketAddress();
address = socketAddress.getAddress();
switch (socketAddress.getPortSpecifierCase()) {
case NAMED_PORT:
address = address + ":" + socketAddress.getNamedPort();
break;
case PORT_VALUE:
address = address + ":" + socketAddress.getPortValue();
break;
default:
// noop
}
}
ImmutableList.Builder<FilterChain> filterChains = ImmutableList.builder();
Set<FilterChainMatch> uniqueSet = new HashSet<>();
for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) {
filterChains.add(
parseFilterChain(fc, rdsResources, tlsContextManager, filterRegistry, uniqueSet,
certProviderInstances, parseHttpFilter));
}
FilterChain defaultFilterChain = null;
if (proto.hasDefaultFilterChain()) {
defaultFilterChain = parseFilterChain(
proto.getDefaultFilterChain(), rdsResources, tlsContextManager, filterRegistry,
null, certProviderInstances, parseHttpFilter);
}
return EnvoyServerProtoData.Listener.create(
proto.getName(), address, filterChains.build(), defaultFilterChain);
}
@VisibleForTesting
static FilterChain parseFilterChain(
io.envoyproxy.envoy.config.listener.v3.FilterChain proto, Set<String> rdsResources,
TlsContextManager tlsContextManager, FilterRegistry filterRegistry,
Set<FilterChainMatch> uniqueSet, Set<String> certProviderInstances, boolean parseHttpFilters)
throws ResourceInvalidException {
if (proto.getFiltersCount() != 1) {
throw new ResourceInvalidException("FilterChain " + proto.getName()
+ " should contain exact one HttpConnectionManager filter");
}
io.envoyproxy.envoy.config.listener.v3.Filter filter = proto.getFiltersList().get(0);
if (!filter.hasTypedConfig()) {
throw new ResourceInvalidException(
"FilterChain " + proto.getName() + " contains filter " + filter.getName()
+ " without typed_config");
}
Any any = filter.getTypedConfig();
// HttpConnectionManager is the only supported network filter at the moment.
if (!any.getTypeUrl().equals(TYPE_URL_HTTP_CONNECTION_MANAGER)) {
throw new ResourceInvalidException(
"FilterChain " + proto.getName() + " contains filter " + filter.getName()
+ " with unsupported typed_config type " + any.getTypeUrl());
}
HttpConnectionManager hcmProto;
try {
hcmProto = any.unpack(HttpConnectionManager.class);
} catch (InvalidProtocolBufferException e) {
throw new ResourceInvalidException("FilterChain " + proto.getName() + " with filter "
+ filter.getName() + " failed to unpack message", e);
}
io.grpc.xds.HttpConnectionManager httpConnectionManager = parseHttpConnectionManager(
hcmProto, rdsResources, filterRegistry, parseHttpFilters, false /* isForClient */);
EnvoyServerProtoData.DownstreamTlsContext downstreamTlsContext = null;
if (proto.hasTransportSocket()) {
if (!TRANSPORT_SOCKET_NAME_TLS.equals(proto.getTransportSocket().getName())) {
throw new ResourceInvalidException("transport-socket with name "
+ proto.getTransportSocket().getName() + " not supported.");
}
DownstreamTlsContext downstreamTlsContextProto;
try {
downstreamTlsContextProto =
proto.getTransportSocket().getTypedConfig().unpack(DownstreamTlsContext.class);
} catch (InvalidProtocolBufferException e) {
throw new ResourceInvalidException("FilterChain " + proto.getName()
+ " failed to unpack message", e);
}
downstreamTlsContext =
EnvoyServerProtoData.DownstreamTlsContext.fromEnvoyProtoDownstreamTlsContext(
validateDownstreamTlsContext(downstreamTlsContextProto, certProviderInstances));
}
FilterChainMatch filterChainMatch = parseFilterChainMatch(proto.getFilterChainMatch());
checkForUniqueness(uniqueSet, filterChainMatch);
return FilterChain.create(
proto.getName(),
filterChainMatch,
httpConnectionManager,
downstreamTlsContext,
tlsContextManager
);
}
@VisibleForTesting
static DownstreamTlsContext validateDownstreamTlsContext(
DownstreamTlsContext downstreamTlsContext, Set<String> certProviderInstances)
throws ResourceInvalidException {
if (downstreamTlsContext.hasCommonTlsContext()) {
validateCommonTlsContext(downstreamTlsContext.getCommonTlsContext(), certProviderInstances,
true);
} else {
throw new ResourceInvalidException(
"common-tls-context is required in downstream-tls-context");
}
if (downstreamTlsContext.hasRequireSni()) {
throw new ResourceInvalidException(
"downstream-tls-context with require-sni is not supported");
}
DownstreamTlsContext.OcspStaplePolicy ocspStaplePolicy = downstreamTlsContext
.getOcspStaplePolicy();
if (ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.UNRECOGNIZED
&& ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.LENIENT_STAPLING) {
throw new ResourceInvalidException(
"downstream-tls-context with ocsp_staple_policy value " + ocspStaplePolicy.name()
+ " is not supported");
}
return downstreamTlsContext;
}
private static void checkForUniqueness(Set<FilterChainMatch> uniqueSet,
FilterChainMatch filterChainMatch) throws ResourceInvalidException {
if (uniqueSet != null) {
List<FilterChainMatch> crossProduct = getCrossProduct(filterChainMatch);
for (FilterChainMatch cur : crossProduct) {
if (!uniqueSet.add(cur)) {
throw new ResourceInvalidException("FilterChainMatch must be unique. "
+ "Found duplicate: " + cur);
}
}
}
}
private static List<FilterChainMatch> getCrossProduct(FilterChainMatch filterChainMatch) {
// repeating fields to process:
// prefixRanges, applicationProtocols, sourcePrefixRanges, sourcePorts, serverNames
List<FilterChainMatch> expandedList = expandOnPrefixRange(filterChainMatch);
expandedList = expandOnApplicationProtocols(expandedList);
expandedList = expandOnSourcePrefixRange(expandedList);
expandedList = expandOnSourcePorts(expandedList);
return expandOnServerNames(expandedList);
}
private static List<FilterChainMatch> expandOnPrefixRange(FilterChainMatch filterChainMatch) {
ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
if (filterChainMatch.prefixRanges().isEmpty()) {
expandedList.add(filterChainMatch);
} else {
for (CidrRange cidrRange : filterChainMatch.prefixRanges()) {
expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
ImmutableList.of(cidrRange),
filterChainMatch.applicationProtocols(),
filterChainMatch.sourcePrefixRanges(),
filterChainMatch.connectionSourceType(),
filterChainMatch.sourcePorts(),
filterChainMatch.serverNames(),
filterChainMatch.transportProtocol()));
}
}
return expandedList;
}
private static List<FilterChainMatch> expandOnApplicationProtocols(
Collection<FilterChainMatch> set) {
ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
for (FilterChainMatch filterChainMatch : set) {
if (filterChainMatch.applicationProtocols().isEmpty()) {
expandedList.add(filterChainMatch);
} else {
for (String applicationProtocol : filterChainMatch.applicationProtocols()) {
expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
filterChainMatch.prefixRanges(),
ImmutableList.of(applicationProtocol),
filterChainMatch.sourcePrefixRanges(),
filterChainMatch.connectionSourceType(),
filterChainMatch.sourcePorts(),
filterChainMatch.serverNames(),
filterChainMatch.transportProtocol()));
}
}
}
return expandedList;
}
private static List<FilterChainMatch> expandOnSourcePrefixRange(
Collection<FilterChainMatch> set) {
ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
for (FilterChainMatch filterChainMatch : set) {
if (filterChainMatch.sourcePrefixRanges().isEmpty()) {
expandedList.add(filterChainMatch);
} else {
for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.sourcePrefixRanges()) {
expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
filterChainMatch.prefixRanges(),
filterChainMatch.applicationProtocols(),
ImmutableList.of(cidrRange),
filterChainMatch.connectionSourceType(),
filterChainMatch.sourcePorts(),
filterChainMatch.serverNames(),
filterChainMatch.transportProtocol()));
}
}
}
return expandedList;
}
private static List<FilterChainMatch> expandOnSourcePorts(Collection<FilterChainMatch> set) {
ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
for (FilterChainMatch filterChainMatch : set) {
if (filterChainMatch.sourcePorts().isEmpty()) {
expandedList.add(filterChainMatch);
} else {
for (Integer sourcePort : filterChainMatch.sourcePorts()) {
expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
filterChainMatch.prefixRanges(),
filterChainMatch.applicationProtocols(),
filterChainMatch.sourcePrefixRanges(),
filterChainMatch.connectionSourceType(),
ImmutableList.of(sourcePort),
filterChainMatch.serverNames(),
filterChainMatch.transportProtocol()));
}
}
}
return expandedList;
}
private static List<FilterChainMatch> expandOnServerNames(Collection<FilterChainMatch> set) {
ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
for (FilterChainMatch filterChainMatch : set) {
if (filterChainMatch.serverNames().isEmpty()) {
expandedList.add(filterChainMatch);
} else {
for (String serverName : filterChainMatch.serverNames()) {
expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
filterChainMatch.prefixRanges(),
filterChainMatch.applicationProtocols(),
filterChainMatch.sourcePrefixRanges(),
filterChainMatch.connectionSourceType(),
filterChainMatch.sourcePorts(),
ImmutableList.of(serverName),
filterChainMatch.transportProtocol()));
}
}
}
return expandedList;
}
private static FilterChainMatch parseFilterChainMatch(
io.envoyproxy.envoy.config.listener.v3.FilterChainMatch proto)
throws ResourceInvalidException {
ImmutableList.Builder<CidrRange> prefixRanges = ImmutableList.builder();
ImmutableList.Builder<CidrRange> sourcePrefixRanges = ImmutableList.builder();
try {
for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getPrefixRangesList()) {
prefixRanges.add(
CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue()));
}
for (io.envoyproxy.envoy.config.core.v3.CidrRange range
: proto.getSourcePrefixRangesList()) {
sourcePrefixRanges.add(
CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue()));
}
} catch (UnknownHostException e) {
throw new ResourceInvalidException("Failed to create CidrRange", e);
}
ConnectionSourceType sourceType;
switch (proto.getSourceType()) {
case ANY:
sourceType = ConnectionSourceType.ANY;
break;
case EXTERNAL:
sourceType = ConnectionSourceType.EXTERNAL;
break;
case SAME_IP_OR_LOOPBACK:
sourceType = ConnectionSourceType.SAME_IP_OR_LOOPBACK;
break;
default:
throw new ResourceInvalidException("Unknown source-type: " + proto.getSourceType());
}
return FilterChainMatch.create(
proto.getDestinationPort().getValue(),
prefixRanges.build(),
ImmutableList.copyOf(proto.getApplicationProtocolsList()),
sourcePrefixRanges.build(),
sourceType,
ImmutableList.copyOf(proto.getSourcePortsList()),
ImmutableList.copyOf(proto.getServerNamesList()),
proto.getTransportProtocol());
}
@VisibleForTesting
static io.grpc.xds.HttpConnectionManager parseHttpConnectionManager(
HttpConnectionManager proto, Set<String> rdsResources, FilterRegistry filterRegistry,
boolean parseHttpFilter, boolean isForClient) throws ResourceInvalidException {
if (enableRbac && proto.getXffNumTrustedHops() != 0) {
throw new ResourceInvalidException(
"HttpConnectionManager with xff_num_trusted_hops unsupported");
}
if (enableRbac && !proto.getOriginalIpDetectionExtensionsList().isEmpty()) {
throw new ResourceInvalidException("HttpConnectionManager with "
+ "original_ip_detection_extensions unsupported");
}
// Obtain max_stream_duration from Http Protocol Options.
long maxStreamDuration = 0;
if (proto.hasCommonHttpProtocolOptions()) {
HttpProtocolOptions options = proto.getCommonHttpProtocolOptions();
if (options.hasMaxStreamDuration()) {
maxStreamDuration = Durations.toNanos(options.getMaxStreamDuration());
}
}
// Parse http filters.
List<Filter.NamedFilterConfig> filterConfigs = null;
if (parseHttpFilter) {
if (proto.getHttpFiltersList().isEmpty()) {
throw new ResourceInvalidException("Missing HttpFilter in HttpConnectionManager.");
}
filterConfigs = new ArrayList<>();
Set<String> names = new HashSet<>();
for (int i = 0; i < proto.getHttpFiltersCount(); i++) {
io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter
httpFilter = proto.getHttpFiltersList().get(i);
String filterName = httpFilter.getName();
if (!names.add(filterName)) {
throw new ResourceInvalidException(
"HttpConnectionManager contains duplicate HttpFilter: " + filterName);
}
StructOrError<Filter.FilterConfig> filterConfig =
parseHttpFilter(httpFilter, filterRegistry, isForClient);
if ((i == proto.getHttpFiltersCount() - 1)
&& (filterConfig == null || !isTerminalFilter(filterConfig.getStruct()))) {
throw new ResourceInvalidException("The last HttpFilter must be a terminal filter: "
+ filterName);
}
if (filterConfig == null) {
continue;
}
if (filterConfig.getErrorDetail() != null) {
throw new ResourceInvalidException(
"HttpConnectionManager contains invalid HttpFilter: "
+ filterConfig.getErrorDetail());
}
if ((i < proto.getHttpFiltersCount() - 1) && isTerminalFilter(filterConfig.getStruct())) {
throw new ResourceInvalidException("A terminal HttpFilter must be the last filter: "
+ filterName);
}
filterConfigs.add(new Filter.NamedFilterConfig(filterName, filterConfig.getStruct()));
}
}
// Parse inlined RouteConfiguration or RDS.
if (proto.hasRouteConfig()) {
List<VirtualHost> virtualHosts = extractVirtualHosts(
proto.getRouteConfig(), filterRegistry, parseHttpFilter);
return io.grpc.xds.HttpConnectionManager.forVirtualHosts(
maxStreamDuration, virtualHosts, filterConfigs);
}
if (proto.hasRds()) {
Rds rds = proto.getRds();
if (!rds.hasConfigSource()) {
throw new ResourceInvalidException(
"HttpConnectionManager contains invalid RDS: missing config_source");
}
if (!rds.getConfigSource().hasAds() && !rds.getConfigSource().hasSelf()) {
throw new ResourceInvalidException(
"HttpConnectionManager contains invalid RDS: must specify ADS or self ConfigSource");
}
// Collect the RDS resource referenced by this HttpConnectionManager.
rdsResources.add(rds.getRouteConfigName());
return io.grpc.xds.HttpConnectionManager.forRdsName(
maxStreamDuration, rds.getRouteConfigName(), filterConfigs);
}
throw new ResourceInvalidException(
"HttpConnectionManager neither has inlined route_config nor RDS");
}
// hard-coded: currently router config is the only terminal filter.
private static boolean isTerminalFilter(Filter.FilterConfig filterConfig) {
return RouterFilter.ROUTER_CONFIG.equals(filterConfig);
}
@VisibleForTesting
@Nullable // Returns null if the filter is optional but not supported.
static StructOrError<Filter.FilterConfig> parseHttpFilter(
io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter
httpFilter, FilterRegistry filterRegistry, boolean isForClient) {
String filterName = httpFilter.getName();
boolean isOptional = httpFilter.getIsOptional();
if (!httpFilter.hasTypedConfig()) {
if (isOptional) {
return null;
} else {
return StructOrError.fromError(
"HttpFilter [" + filterName + "] is not optional and has no typed config");
}
}
Message rawConfig = httpFilter.getTypedConfig();
String typeUrl = httpFilter.getTypedConfig().getTypeUrl();
try {
if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA)) {
TypedStruct typedStruct = httpFilter.getTypedConfig().unpack(TypedStruct.class);
typeUrl = typedStruct.getTypeUrl();
rawConfig = typedStruct.getValue();
} else if (typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
com.github.xds.type.v3.TypedStruct newTypedStruct =
httpFilter.getTypedConfig().unpack(com.github.xds.type.v3.TypedStruct.class);
typeUrl = newTypedStruct.getTypeUrl();
rawConfig = newTypedStruct.getValue();
}
} catch (InvalidProtocolBufferException e) {
return StructOrError.fromError(
"HttpFilter [" + filterName + "] contains invalid proto: " + e);
}
Filter filter = filterRegistry.get(typeUrl);
if ((isForClient && !(filter instanceof Filter.ClientInterceptorBuilder))
|| (!isForClient && !(filter instanceof Filter.ServerInterceptorBuilder))) {
if (isOptional) {
return null;
} else {
return StructOrError.fromError(
"HttpFilter [" + filterName + "](" + typeUrl + ") is required but unsupported for "
+ (isForClient ? "client" : "server"));
}
}
ConfigOrError<? extends FilterConfig> filterConfig = filter.parseFilterConfig(rawConfig);
if (filterConfig.errorDetail != null) {
return StructOrError.fromError(
"Invalid filter config for HttpFilter [" + filterName + "]: " + filterConfig.errorDetail);
}
return StructOrError.fromStruct(filterConfig.config);
}
@AutoValue
abstract static class LdsUpdate implements ResourceUpdate {
// Http level api listener configuration.
@Nullable
abstract io.grpc.xds.HttpConnectionManager httpConnectionManager();
// Tcp level listener configuration.
@Nullable
abstract EnvoyServerProtoData.Listener listener();
static LdsUpdate forApiListener(io.grpc.xds.HttpConnectionManager httpConnectionManager) {
checkNotNull(httpConnectionManager, "httpConnectionManager");
return new AutoValue_XdsListenerResource_LdsUpdate(httpConnectionManager, null);
}
static LdsUpdate forTcpListener(EnvoyServerProtoData.Listener listener) {
checkNotNull(listener, "listener");
return new AutoValue_XdsListenerResource_LdsUpdate(null, listener);
}
}
}

View File

@ -63,13 +63,12 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.LdsResourceWatcher; import io.grpc.xds.XdsClient.ResourceWatcher;
import io.grpc.xds.XdsClient.LdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsClient.RdsResourceWatcher;
import io.grpc.xds.XdsClient.RdsUpdate;
import io.grpc.xds.XdsLogger.XdsLogLevel; import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import io.grpc.xds.internal.Matchers.FractionMatcher; import io.grpc.xds.internal.Matchers.FractionMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher; import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.util.ArrayList; import java.util.ArrayList;
@ -688,7 +687,7 @@ final class XdsNameResolver extends NameResolver {
} }
} }
private class ResolveState implements LdsResourceWatcher { private class ResolveState implements ResourceWatcher<LdsUpdate> {
private final ConfigOrError emptyServiceConfig = private final ConfigOrError emptyServiceConfig =
serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap()); serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap());
private final String ldsResourceName; private final String ldsResourceName;
@ -723,7 +722,8 @@ final class XdsNameResolver extends NameResolver {
rdsName, httpConnectionManager.httpMaxStreamDurationNano(), rdsName, httpConnectionManager.httpMaxStreamDurationNano(),
httpConnectionManager.httpFilterConfigs()); httpConnectionManager.httpFilterConfigs());
logger.log(XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); logger.log(XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName);
xdsClient.watchRdsResource(rdsName, routeDiscoveryState); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),
rdsName, routeDiscoveryState);
} }
} }
}); });
@ -762,14 +762,14 @@ final class XdsNameResolver extends NameResolver {
private void start() { private void start() {
logger.log(XdsLogLevel.INFO, "Start watching LDS resource {0}", ldsResourceName); logger.log(XdsLogLevel.INFO, "Start watching LDS resource {0}", ldsResourceName);
xdsClient.watchLdsResource(ldsResourceName, this); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), ldsResourceName, this);
} }
private void stop() { private void stop() {
logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", ldsResourceName); logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", ldsResourceName);
stopped = true; stopped = true;
cleanUpRouteDiscoveryState(); cleanUpRouteDiscoveryState();
xdsClient.cancelLdsResourceWatch(ldsResourceName, this); xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), ldsResourceName, this);
} }
// called in syncContext // called in syncContext
@ -905,7 +905,8 @@ final class XdsNameResolver extends NameResolver {
if (routeDiscoveryState != null) { if (routeDiscoveryState != null) {
String rdsName = routeDiscoveryState.resourceName; String rdsName = routeDiscoveryState.resourceName;
logger.log(XdsLogLevel.INFO, "Stop watching RDS resource {0}", rdsName); logger.log(XdsLogLevel.INFO, "Stop watching RDS resource {0}", rdsName);
xdsClient.cancelRdsResourceWatch(rdsName, routeDiscoveryState); xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(), rdsName,
routeDiscoveryState);
routeDiscoveryState = null; routeDiscoveryState = null;
} }
} }
@ -914,7 +915,7 @@ final class XdsNameResolver extends NameResolver {
* Discovery state for RouteConfiguration resource. One instance for each Listener resource * Discovery state for RouteConfiguration resource. One instance for each Listener resource
* update. * update.
*/ */
private class RouteDiscoveryState implements RdsResourceWatcher { private class RouteDiscoveryState implements ResourceWatcher<RdsUpdate> {
private final String resourceName; private final String resourceName;
private final long httpMaxStreamDurationNano; private final long httpMaxStreamDurationNano;
@Nullable @Nullable
@ -936,7 +937,8 @@ final class XdsNameResolver extends NameResolver {
return; return;
} }
logger.log(XdsLogLevel.INFO, "Received RDS resource update: {0}", update); logger.log(XdsLogLevel.INFO, "Received RDS resource update: {0}", update);
updateRoutes(update.virtualHosts, httpMaxStreamDurationNano, filterConfigs); updateRoutes(update.virtualHosts, httpMaxStreamDurationNano,
filterConfigs);
} }
}); });
} }

View File

@ -0,0 +1,308 @@
/*
* Copyright 2022 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.AbstractXdsClient.ResourceType;
import static io.grpc.xds.Bootstrapper.ServerInfo;
import static io.grpc.xds.ClientXdsClient.ResourceInvalidException;
import static io.grpc.xds.XdsClient.ResourceUpdate;
import static io.grpc.xds.XdsClient.canonifyResourceName;
import static io.grpc.xds.XdsClient.isResourceNameValid;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.envoyproxy.envoy.service.discovery.v3.Resource;
import io.grpc.LoadBalancerRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
abstract class XdsResourceType<T extends ResourceUpdate> {
static final String TYPE_URL_RESOURCE_V2 = "type.googleapis.com/envoy.api.v2.Resource";
static final String TYPE_URL_RESOURCE_V3 =
"type.googleapis.com/envoy.service.discovery.v3.Resource";
static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls";
@VisibleForTesting
static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate";
@VisibleForTesting
static final String HASH_POLICY_FILTER_STATE_KEY = "io.grpc.channel_id";
@VisibleForTesting
static boolean enableFaultInjection = getFlag("GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION", true);
@VisibleForTesting
static boolean enableRetry = getFlag("GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY", true);
@VisibleForTesting
static boolean enableRbac = getFlag("GRPC_XDS_EXPERIMENTAL_RBAC", true);
@VisibleForTesting
static boolean enableRouteLookup = getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", false);
@VisibleForTesting
static boolean enableLeastRequest =
!Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
: Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest"));
@VisibleForTesting
static boolean enableCustomLbConfig = getFlag("GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG", true);
@VisibleForTesting
static boolean enableOutlierDetection = getFlag("GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION",
true);
static final String TYPE_URL_CLUSTER_CONFIG_V2 =
"type.googleapis.com/envoy.config.cluster.aggregate.v2alpha.ClusterConfig";
static final String TYPE_URL_CLUSTER_CONFIG =
"type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig";
static final String TYPE_URL_TYPED_STRUCT_UDPA =
"type.googleapis.com/udpa.type.v1.TypedStruct";
static final String TYPE_URL_TYPED_STRUCT =
"type.googleapis.com/xds.type.v3.TypedStruct";
@Nullable
abstract String extractResourceName(Message unpackedResource);
abstract Class<? extends com.google.protobuf.Message> unpackedClassName();
abstract ResourceType typeName();
abstract String typeUrl();
abstract String typeUrlV2();
// Non-null for State of the World resources.
@Nullable
abstract ResourceType dependentResource();
static class Args {
final ServerInfo serverInfo;
final String versionInfo;
final String nonce;
final Bootstrapper.BootstrapInfo bootstrapInfo;
final FilterRegistry filterRegistry;
final LoadBalancerRegistry loadBalancerRegistry;
final TlsContextManager tlsContextManager;
// Management server is required to always send newly requested resources, even if they
// may have been sent previously (proactively). Thus, client does not need to cache
// unrequested resources.
// Only resources in the set needs to be parsed. Null means parse everything.
final @Nullable Set<String> subscribedResources;
public Args(ServerInfo serverInfo, String versionInfo, String nonce,
Bootstrapper.BootstrapInfo bootstrapInfo,
FilterRegistry filterRegistry,
LoadBalancerRegistry loadBalancerRegistry,
TlsContextManager tlsContextManager,
@Nullable Set<String> subscribedResources) {
this.serverInfo = serverInfo;
this.versionInfo = versionInfo;
this.nonce = nonce;
this.bootstrapInfo = bootstrapInfo;
this.filterRegistry = filterRegistry;
this.loadBalancerRegistry = loadBalancerRegistry;
this.tlsContextManager = tlsContextManager;
this.subscribedResources = subscribedResources;
}
}
ValidatedResourceUpdate<T> parse(Args args, List<Any> resources) {
Map<String, ParsedResource<T>> parsedResources = new HashMap<>(resources.size());
Set<String> unpackedResources = new HashSet<>(resources.size());
Set<String> invalidResources = new HashSet<>();
List<String> errors = new ArrayList<>();
Set<String> retainedResources = new HashSet<>();
for (int i = 0; i < resources.size(); i++) {
Any resource = resources.get(i);
boolean isResourceV3;
Message unpackedMessage;
try {
resource = maybeUnwrapResources(resource);
isResourceV3 = resource.getTypeUrl().equals(typeUrl());
unpackedMessage = unpackCompatibleType(resource, unpackedClassName(),
typeUrl(), typeUrlV2());
} catch (InvalidProtocolBufferException e) {
errors.add(String.format("%s response Resource index %d - can't decode %s: %s",
typeName(), i, unpackedClassName().getSimpleName(), e.getMessage()));
continue;
}
String name = extractResourceName(unpackedMessage);
if (name == null || !isResourceNameValid(name, resource.getTypeUrl())) {
errors.add(
"Unsupported resource name: " + name + " for type: " + typeName());
continue;
}
String cname = canonifyResourceName(name);
if (args.subscribedResources != null && !args.subscribedResources.contains(name)) {
continue;
}
unpackedResources.add(cname);
T resourceUpdate;
try {
resourceUpdate = doParse(args, unpackedMessage, retainedResources, isResourceV3);
} catch (ClientXdsClient.ResourceInvalidException e) {
errors.add(String.format("%s response %s '%s' validation error: %s",
typeName(), unpackedClassName().getSimpleName(), cname, e.getMessage()));
invalidResources.add(cname);
continue;
}
// Resource parsed successfully.
parsedResources.put(cname, new ParsedResource<T>(resourceUpdate, resource));
}
return new ValidatedResourceUpdate<T>(parsedResources, unpackedResources, invalidResources,
errors, retainedResources);
}
abstract T doParse(Args args, Message unpackedMessage, Set<String> retainedResources,
boolean isResourceV3)
throws ResourceInvalidException;
/**
* Helper method to unpack serialized {@link com.google.protobuf.Any} message, while replacing
* Type URL {@code compatibleTypeUrl} with {@code typeUrl}.
*
* @param <T> The type of unpacked message
* @param any serialized message to unpack
* @param clazz the class to unpack the message to
* @param typeUrl type URL to replace message Type URL, when it's compatible
* @param compatibleTypeUrl compatible Type URL to be replaced with {@code typeUrl}
* @return Unpacked message
* @throws InvalidProtocolBufferException if the message couldn't be unpacked
*/
static <T extends com.google.protobuf.Message> T unpackCompatibleType(
Any any, Class<T> clazz, String typeUrl, String compatibleTypeUrl)
throws InvalidProtocolBufferException {
if (any.getTypeUrl().equals(compatibleTypeUrl)) {
any = any.toBuilder().setTypeUrl(typeUrl).build();
}
return any.unpack(clazz);
}
private Any maybeUnwrapResources(Any resource)
throws InvalidProtocolBufferException {
if (resource.getTypeUrl().equals(TYPE_URL_RESOURCE_V2)
|| resource.getTypeUrl().equals(TYPE_URL_RESOURCE_V3)) {
return unpackCompatibleType(resource, Resource.class, TYPE_URL_RESOURCE_V3,
TYPE_URL_RESOURCE_V2).getResource();
} else {
return resource;
}
}
static final class ParsedResource<T extends ResourceUpdate> {
private final T resourceUpdate;
private final Any rawResource;
public ParsedResource(T resourceUpdate, Any rawResource) {
this.resourceUpdate = checkNotNull(resourceUpdate, "resourceUpdate");
this.rawResource = checkNotNull(rawResource, "rawResource");
}
T getResourceUpdate() {
return resourceUpdate;
}
Any getRawResource() {
return rawResource;
}
}
static final class ValidatedResourceUpdate<T extends ResourceUpdate> {
Map<String, ParsedResource<T>> parsedResources;
Set<String> unpackedResources;
Set<String> invalidResources;
List<String> errors;
Set<String> retainedResources;
// validated resource update
public ValidatedResourceUpdate(Map<String, ParsedResource<T>> parsedResources,
Set<String> unpackedResources,
Set<String> invalidResources,
List<String> errors,
Set<String> retainedResources) {
this.parsedResources = parsedResources;
this.unpackedResources = unpackedResources;
this.invalidResources = invalidResources;
this.errors = errors;
this.retainedResources = retainedResources;
}
}
private static boolean getFlag(String envVarName, boolean enableByDefault) {
String envVar = System.getenv(envVarName);
if (enableByDefault) {
return Strings.isNullOrEmpty(envVar) || Boolean.parseBoolean(envVar);
} else {
return !Strings.isNullOrEmpty(envVar) && Boolean.parseBoolean(envVar);
}
}
@VisibleForTesting
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.
*/
@VisibleForTesting
@Nullable
T getStruct() {
return struct;
}
/**
* Returns error detail if exists, otherwise null.
*/
@VisibleForTesting
@Nullable
String getErrorDetail() {
return errorDetail;
}
}
}

View File

@ -0,0 +1,707 @@
/*
* Copyright 2022 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.AbstractXdsClient.ResourceType.RDS;
import static io.grpc.xds.AbstractXdsClient.ResourceType;
import static io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import com.github.udpa.udpa.type.v1.TypedStruct;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.Any;
import com.google.protobuf.Duration;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.util.Durations;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig;
import io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin;
import io.envoyproxy.envoy.config.route.v3.RetryPolicy.RetryBackOff;
import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
import io.envoyproxy.envoy.type.v3.FractionalPercent;
import io.grpc.Status;
import io.grpc.xds.ClientXdsClient.ResourceInvalidException;
import io.grpc.xds.ClusterSpecifierPlugin.NamedPluginConfig;
import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteAction;
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.ResourceUpdate;
import io.grpc.xds.internal.Matchers.FractionMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import io.grpc.xds.internal.Matchers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
class XdsRouteConfigureResource extends XdsResourceType<RdsUpdate> {
static final String ADS_TYPE_URL_RDS_V2 =
"type.googleapis.com/envoy.api.v2.RouteConfiguration";
static final String ADS_TYPE_URL_RDS =
"type.googleapis.com/envoy.config.route.v3.RouteConfiguration";
private static final String TYPE_URL_FILTER_CONFIG =
"type.googleapis.com/envoy.config.route.v3.FilterConfig";
// TODO(zdapeng): need to discuss how to handle unsupported values.
private static final Set<Status.Code> SUPPORTED_RETRYABLE_CODES =
Collections.unmodifiableSet(EnumSet.of(
Status.Code.CANCELLED, Status.Code.DEADLINE_EXCEEDED, Status.Code.INTERNAL,
Status.Code.RESOURCE_EXHAUSTED, Status.Code.UNAVAILABLE));
private static final XdsRouteConfigureResource instance = new XdsRouteConfigureResource();
public static XdsRouteConfigureResource getInstance() {
return instance;
}
@Override
@Nullable
String extractResourceName(Message unpackedResource) {
if (!(unpackedResource instanceof RouteConfiguration)) {
return null;
}
return ((RouteConfiguration) unpackedResource).getName();
}
@Override
ResourceType typeName() {
return RDS;
}
@Override
String typeUrl() {
return ADS_TYPE_URL_RDS;
}
@Override
String typeUrlV2() {
return ADS_TYPE_URL_RDS_V2;
}
@Nullable
@Override
ResourceType dependentResource() {
return null;
}
@Override
Class<RouteConfiguration> unpackedClassName() {
return RouteConfiguration.class;
}
@Override
RdsUpdate doParse(XdsResourceType.Args args, Message unpackedMessage,
Set<String> retainedResources, boolean isResourceV3)
throws ResourceInvalidException {
if (!(unpackedMessage instanceof RouteConfiguration)) {
throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
}
return processRouteConfiguration((RouteConfiguration) unpackedMessage,
args.filterRegistry, enableFaultInjection && isResourceV3);
}
private static RdsUpdate processRouteConfiguration(
RouteConfiguration routeConfig, FilterRegistry filterRegistry, boolean parseHttpFilter)
throws ResourceInvalidException {
return new RdsUpdate(extractVirtualHosts(routeConfig, filterRegistry, parseHttpFilter));
}
static List<VirtualHost> extractVirtualHosts(
RouteConfiguration routeConfig, FilterRegistry filterRegistry, boolean parseHttpFilter)
throws ResourceInvalidException {
Map<String, PluginConfig> pluginConfigMap = new HashMap<>();
ImmutableSet.Builder<String> optionalPlugins = ImmutableSet.builder();
if (enableRouteLookup) {
List<ClusterSpecifierPlugin> plugins = routeConfig.getClusterSpecifierPluginsList();
for (ClusterSpecifierPlugin plugin : plugins) {
String pluginName = plugin.getExtension().getName();
PluginConfig pluginConfig = parseClusterSpecifierPlugin(plugin);
if (pluginConfig != null) {
if (pluginConfigMap.put(pluginName, pluginConfig) != null) {
throw new ResourceInvalidException(
"Multiple ClusterSpecifierPlugins with the same name: " + pluginName);
}
} else {
// The plugin parsed successfully, and it's not supported, but it's marked as optional.
optionalPlugins.add(pluginName);
}
}
}
List<VirtualHost> virtualHosts = new ArrayList<>(routeConfig.getVirtualHostsCount());
for (io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHostProto
: routeConfig.getVirtualHostsList()) {
StructOrError<VirtualHost> virtualHost =
parseVirtualHost(virtualHostProto, filterRegistry, parseHttpFilter, pluginConfigMap,
optionalPlugins.build());
if (virtualHost.getErrorDetail() != null) {
throw new ResourceInvalidException(
"RouteConfiguration contains invalid virtual host: " + virtualHost.getErrorDetail());
}
virtualHosts.add(virtualHost.getStruct());
}
return virtualHosts;
}
private static StructOrError<VirtualHost> parseVirtualHost(
io.envoyproxy.envoy.config.route.v3.VirtualHost proto, FilterRegistry filterRegistry,
boolean parseHttpFilter, Map<String, PluginConfig> pluginConfigMap,
Set<String> optionalPlugins) {
String name = proto.getName();
List<Route> routes = new ArrayList<>(proto.getRoutesCount());
for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) {
StructOrError<Route> route = parseRoute(
routeProto, filterRegistry, parseHttpFilter, pluginConfigMap, optionalPlugins);
if (route == null) {
continue;
}
if (route.getErrorDetail() != null) {
return StructOrError.fromError(
"Virtual host [" + name + "] contains invalid route : " + route.getErrorDetail());
}
routes.add(route.getStruct());
}
if (!parseHttpFilter) {
return StructOrError.fromStruct(VirtualHost.create(
name, proto.getDomainsList(), routes, new HashMap<String, Filter.FilterConfig>()));
}
StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
if (overrideConfigs.getErrorDetail() != null) {
return StructOrError.fromError(
"VirtualHost [" + proto.getName() + "] contains invalid HttpFilter config: "
+ overrideConfigs.getErrorDetail());
}
return StructOrError.fromStruct(VirtualHost.create(
name, proto.getDomainsList(), routes, overrideConfigs.getStruct()));
}
@VisibleForTesting
static StructOrError<Map<String, FilterConfig>> parseOverrideFilterConfigs(
Map<String, Any> rawFilterConfigMap, FilterRegistry filterRegistry) {
Map<String, FilterConfig> overrideConfigs = new HashMap<>();
for (String name : rawFilterConfigMap.keySet()) {
Any anyConfig = rawFilterConfigMap.get(name);
String typeUrl = anyConfig.getTypeUrl();
boolean isOptional = false;
if (typeUrl.equals(TYPE_URL_FILTER_CONFIG)) {
io.envoyproxy.envoy.config.route.v3.FilterConfig filterConfig;
try {
filterConfig =
anyConfig.unpack(io.envoyproxy.envoy.config.route.v3.FilterConfig.class);
} catch (InvalidProtocolBufferException e) {
return StructOrError.fromError(
"FilterConfig [" + name + "] contains invalid proto: " + e);
}
isOptional = filterConfig.getIsOptional();
anyConfig = filterConfig.getConfig();
typeUrl = anyConfig.getTypeUrl();
}
Message rawConfig = anyConfig;
try {
if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA)) {
TypedStruct typedStruct = anyConfig.unpack(TypedStruct.class);
typeUrl = typedStruct.getTypeUrl();
rawConfig = typedStruct.getValue();
} else if (typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
com.github.xds.type.v3.TypedStruct newTypedStruct =
anyConfig.unpack(com.github.xds.type.v3.TypedStruct.class);
typeUrl = newTypedStruct.getTypeUrl();
rawConfig = newTypedStruct.getValue();
}
} catch (InvalidProtocolBufferException e) {
return StructOrError.fromError(
"FilterConfig [" + name + "] contains invalid proto: " + e);
}
Filter filter = filterRegistry.get(typeUrl);
if (filter == null) {
if (isOptional) {
continue;
}
return StructOrError.fromError(
"HttpFilter [" + name + "](" + typeUrl + ") is required but unsupported");
}
ConfigOrError<? extends Filter.FilterConfig> filterConfig =
filter.parseFilterConfigOverride(rawConfig);
if (filterConfig.errorDetail != null) {
return StructOrError.fromError(
"Invalid filter config for HttpFilter [" + name + "]: " + filterConfig.errorDetail);
}
overrideConfigs.put(name, filterConfig.config);
}
return StructOrError.fromStruct(overrideConfigs);
}
@VisibleForTesting
@Nullable
static StructOrError<Route> parseRoute(
io.envoyproxy.envoy.config.route.v3.Route proto, FilterRegistry filterRegistry,
boolean parseHttpFilter, Map<String, PluginConfig> pluginConfigMap,
Set<String> optionalPlugins) {
StructOrError<RouteMatch> routeMatch = parseRouteMatch(proto.getMatch());
if (routeMatch == null) {
return null;
}
if (routeMatch.getErrorDetail() != null) {
return StructOrError.fromError(
"Route [" + proto.getName() + "] contains invalid RouteMatch: "
+ routeMatch.getErrorDetail());
}
Map<String, FilterConfig> overrideConfigs = Collections.emptyMap();
if (parseHttpFilter) {
StructOrError<Map<String, FilterConfig>> overrideConfigsOrError =
parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
if (overrideConfigsOrError.getErrorDetail() != null) {
return StructOrError.fromError(
"Route [" + proto.getName() + "] contains invalid HttpFilter config: "
+ overrideConfigsOrError.getErrorDetail());
}
overrideConfigs = overrideConfigsOrError.getStruct();
}
switch (proto.getActionCase()) {
case ROUTE:
StructOrError<RouteAction> routeAction =
parseRouteAction(proto.getRoute(), filterRegistry, parseHttpFilter, pluginConfigMap,
optionalPlugins);
if (routeAction == null) {
return null;
}
if (routeAction.getErrorDetail() != null) {
return StructOrError.fromError(
"Route [" + proto.getName() + "] contains invalid RouteAction: "
+ routeAction.getErrorDetail());
}
return StructOrError.fromStruct(
Route.forAction(routeMatch.getStruct(), routeAction.getStruct(), overrideConfigs));
case NON_FORWARDING_ACTION:
return StructOrError.fromStruct(
Route.forNonForwardingAction(routeMatch.getStruct(), overrideConfigs));
case REDIRECT:
case DIRECT_RESPONSE:
case FILTER_ACTION:
case ACTION_NOT_SET:
default:
return StructOrError.fromError(
"Route [" + proto.getName() + "] with unknown action type: " + proto.getActionCase());
}
}
@VisibleForTesting
@Nullable
static StructOrError<RouteMatch> parseRouteMatch(
io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
if (proto.getQueryParametersCount() != 0) {
return null;
}
StructOrError<PathMatcher> pathMatch = parsePathMatcher(proto);
if (pathMatch.getErrorDetail() != null) {
return StructOrError.fromError(pathMatch.getErrorDetail());
}
FractionMatcher fractionMatch = null;
if (proto.hasRuntimeFraction()) {
StructOrError<FractionMatcher> parsedFraction =
parseFractionMatcher(proto.getRuntimeFraction().getDefaultValue());
if (parsedFraction.getErrorDetail() != null) {
return StructOrError.fromError(parsedFraction.getErrorDetail());
}
fractionMatch = parsedFraction.getStruct();
}
List<HeaderMatcher> headerMatchers = new ArrayList<>();
for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher hmProto : proto.getHeadersList()) {
StructOrError<HeaderMatcher> headerMatcher = parseHeaderMatcher(hmProto);
if (headerMatcher.getErrorDetail() != null) {
return StructOrError.fromError(headerMatcher.getErrorDetail());
}
headerMatchers.add(headerMatcher.getStruct());
}
return StructOrError.fromStruct(RouteMatch.create(
pathMatch.getStruct(), headerMatchers, fractionMatch));
}
@VisibleForTesting
static StructOrError<PathMatcher> parsePathMatcher(
io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
boolean caseSensitive = proto.getCaseSensitive().getValue();
switch (proto.getPathSpecifierCase()) {
case PREFIX:
return StructOrError.fromStruct(
PathMatcher.fromPrefix(proto.getPrefix(), caseSensitive));
case PATH:
return StructOrError.fromStruct(PathMatcher.fromPath(proto.getPath(), caseSensitive));
case SAFE_REGEX:
String rawPattern = proto.getSafeRegex().getRegex();
Pattern safeRegEx;
try {
safeRegEx = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
}
return StructOrError.fromStruct(PathMatcher.fromRegEx(safeRegEx));
case PATHSPECIFIER_NOT_SET:
default:
return StructOrError.fromError("Unknown path match type");
}
}
private static StructOrError<FractionMatcher> parseFractionMatcher(FractionalPercent proto) {
int numerator = proto.getNumerator();
int denominator = 0;
switch (proto.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: " + proto.getDenominator());
}
return StructOrError.fromStruct(FractionMatcher.create(numerator, denominator));
}
@VisibleForTesting
static StructOrError<HeaderMatcher> parseHeaderMatcher(
io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
switch (proto.getHeaderMatchSpecifierCase()) {
case EXACT_MATCH:
return StructOrError.fromStruct(HeaderMatcher.forExactValue(
proto.getName(), proto.getExactMatch(), proto.getInvertMatch()));
case SAFE_REGEX_MATCH:
String rawPattern = proto.getSafeRegexMatch().getRegex();
Pattern safeRegExMatch;
try {
safeRegExMatch = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
return StructOrError.fromError(
"HeaderMatcher [" + proto.getName() + "] contains malformed safe regex pattern: "
+ e.getMessage());
}
return StructOrError.fromStruct(Matchers.HeaderMatcher.forSafeRegEx(
proto.getName(), safeRegExMatch, proto.getInvertMatch()));
case RANGE_MATCH:
Matchers.HeaderMatcher.Range rangeMatch = Matchers.HeaderMatcher.Range.create(
proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd());
return StructOrError.fromStruct(Matchers.HeaderMatcher.forRange(
proto.getName(), rangeMatch, proto.getInvertMatch()));
case PRESENT_MATCH:
return StructOrError.fromStruct(Matchers.HeaderMatcher.forPresent(
proto.getName(), proto.getPresentMatch(), proto.getInvertMatch()));
case PREFIX_MATCH:
return StructOrError.fromStruct(Matchers.HeaderMatcher.forPrefix(
proto.getName(), proto.getPrefixMatch(), proto.getInvertMatch()));
case SUFFIX_MATCH:
return StructOrError.fromStruct(Matchers.HeaderMatcher.forSuffix(
proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch()));
case HEADERMATCHSPECIFIER_NOT_SET:
default:
return StructOrError.fromError("Unknown header matcher type");
}
}
/**
* Parses the RouteAction config. The returned result may contain a (parsed form)
* {@link RouteAction} or an error message. Returns {@code null} if the RouteAction
* should be ignored.
*/
@VisibleForTesting
@Nullable
static StructOrError<RouteAction> parseRouteAction(
io.envoyproxy.envoy.config.route.v3.RouteAction proto, FilterRegistry filterRegistry,
boolean parseHttpFilter, Map<String, PluginConfig> pluginConfigMap,
Set<String> optionalPlugins) {
Long timeoutNano = null;
if (proto.hasMaxStreamDuration()) {
io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration
= proto.getMaxStreamDuration();
if (maxStreamDuration.hasGrpcTimeoutHeaderMax()) {
timeoutNano = Durations.toNanos(maxStreamDuration.getGrpcTimeoutHeaderMax());
} else if (maxStreamDuration.hasMaxStreamDuration()) {
timeoutNano = Durations.toNanos(maxStreamDuration.getMaxStreamDuration());
}
}
RetryPolicy retryPolicy = null;
if (enableRetry && proto.hasRetryPolicy()) {
StructOrError<RetryPolicy> retryPolicyOrError = parseRetryPolicy(proto.getRetryPolicy());
if (retryPolicyOrError != null) {
if (retryPolicyOrError.getErrorDetail() != null) {
return StructOrError.fromError(retryPolicyOrError.getErrorDetail());
}
retryPolicy = retryPolicyOrError.getStruct();
}
}
List<HashPolicy> hashPolicies = new ArrayList<>();
for (io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy config
: proto.getHashPolicyList()) {
HashPolicy policy = null;
boolean terminal = config.getTerminal();
switch (config.getPolicySpecifierCase()) {
case HEADER:
io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy.Header headerCfg =
config.getHeader();
Pattern regEx = null;
String regExSubstitute = null;
if (headerCfg.hasRegexRewrite() && headerCfg.getRegexRewrite().hasPattern()
&& headerCfg.getRegexRewrite().getPattern().hasGoogleRe2()) {
regEx = Pattern.compile(headerCfg.getRegexRewrite().getPattern().getRegex());
regExSubstitute = headerCfg.getRegexRewrite().getSubstitution();
}
policy = HashPolicy.forHeader(
terminal, headerCfg.getHeaderName(), regEx, regExSubstitute);
break;
case FILTER_STATE:
if (config.getFilterState().getKey().equals(HASH_POLICY_FILTER_STATE_KEY)) {
policy = HashPolicy.forChannelId(terminal);
}
break;
default:
// Ignore
}
if (policy != null) {
hashPolicies.add(policy);
}
}
switch (proto.getClusterSpecifierCase()) {
case CLUSTER:
return StructOrError.fromStruct(RouteAction.forCluster(
proto.getCluster(), hashPolicies, timeoutNano, retryPolicy));
case CLUSTER_HEADER:
return null;
case WEIGHTED_CLUSTERS:
List<io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight> clusterWeights
= proto.getWeightedClusters().getClustersList();
if (clusterWeights.isEmpty()) {
return StructOrError.fromError("No cluster found in weighted cluster list");
}
List<ClusterWeight> weightedClusters = new ArrayList<>();
for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight
: clusterWeights) {
StructOrError<ClusterWeight> clusterWeightOrError =
parseClusterWeight(clusterWeight, filterRegistry, parseHttpFilter);
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.
return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forWeightedClusters(
weightedClusters, hashPolicies, timeoutNano, retryPolicy));
case CLUSTER_SPECIFIER_PLUGIN:
if (enableRouteLookup) {
String pluginName = proto.getClusterSpecifierPlugin();
PluginConfig pluginConfig = pluginConfigMap.get(pluginName);
if (pluginConfig == null) {
// Skip route if the plugin is not registered, but it's optional.
if (optionalPlugins.contains(pluginName)) {
return null;
}
return StructOrError.fromError(
"ClusterSpecifierPlugin for [" + pluginName + "] not found");
}
NamedPluginConfig namedPluginConfig = NamedPluginConfig.create(pluginName, pluginConfig);
return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forClusterSpecifierPlugin(
namedPluginConfig, hashPolicies, timeoutNano, retryPolicy));
} else {
return null;
}
case CLUSTERSPECIFIER_NOT_SET:
default:
return null;
}
}
@Nullable // Return null if we ignore the given policy.
private static StructOrError<VirtualHost.Route.RouteAction.RetryPolicy> parseRetryPolicy(
io.envoyproxy.envoy.config.route.v3.RetryPolicy retryPolicyProto) {
int maxAttempts = 2;
if (retryPolicyProto.hasNumRetries()) {
maxAttempts = retryPolicyProto.getNumRetries().getValue() + 1;
}
Duration initialBackoff = Durations.fromMillis(25);
Duration maxBackoff = Durations.fromMillis(250);
if (retryPolicyProto.hasRetryBackOff()) {
RetryBackOff retryBackOff = retryPolicyProto.getRetryBackOff();
if (!retryBackOff.hasBaseInterval()) {
return StructOrError.fromError("No base_interval specified in retry_backoff");
}
Duration originalInitialBackoff = initialBackoff = retryBackOff.getBaseInterval();
if (Durations.compare(initialBackoff, Durations.ZERO) <= 0) {
return StructOrError.fromError("base_interval in retry_backoff must be positive");
}
if (Durations.compare(initialBackoff, Durations.fromMillis(1)) < 0) {
initialBackoff = Durations.fromMillis(1);
}
if (retryBackOff.hasMaxInterval()) {
maxBackoff = retryPolicyProto.getRetryBackOff().getMaxInterval();
if (Durations.compare(maxBackoff, originalInitialBackoff) < 0) {
return StructOrError.fromError(
"max_interval in retry_backoff cannot be less than base_interval");
}
if (Durations.compare(maxBackoff, Durations.fromMillis(1)) < 0) {
maxBackoff = Durations.fromMillis(1);
}
} else {
maxBackoff = Durations.fromNanos(Durations.toNanos(initialBackoff) * 10);
}
}
Iterable<String> retryOns =
Splitter.on(',').omitEmptyStrings().trimResults().split(retryPolicyProto.getRetryOn());
ImmutableList.Builder<Status.Code> retryableStatusCodesBuilder = ImmutableList.builder();
for (String retryOn : retryOns) {
Status.Code code;
try {
code = Status.Code.valueOf(retryOn.toUpperCase(Locale.US).replace('-', '_'));
} catch (IllegalArgumentException e) {
// unsupported value, such as "5xx"
continue;
}
if (!SUPPORTED_RETRYABLE_CODES.contains(code)) {
// unsupported value
continue;
}
retryableStatusCodesBuilder.add(code);
}
List<Status.Code> retryableStatusCodes = retryableStatusCodesBuilder.build();
return StructOrError.fromStruct(
VirtualHost.Route.RouteAction.RetryPolicy.create(
maxAttempts, retryableStatusCodes, initialBackoff, maxBackoff,
/* perAttemptRecvTimeout= */ null));
}
@VisibleForTesting
static StructOrError<VirtualHost.Route.RouteAction.ClusterWeight> parseClusterWeight(
io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto,
FilterRegistry filterRegistry, boolean parseHttpFilter) {
if (!parseHttpFilter) {
return StructOrError.fromStruct(ClusterWeight.create(proto.getName(),
proto.getWeight().getValue(), new HashMap<String, Filter.FilterConfig>()));
}
StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
if (overrideConfigs.getErrorDetail() != null) {
return StructOrError.fromError(
"ClusterWeight [" + proto.getName() + "] contains invalid HttpFilter config: "
+ overrideConfigs.getErrorDetail());
}
return StructOrError.fromStruct(VirtualHost.Route.RouteAction.ClusterWeight.create(
proto.getName(), proto.getWeight().getValue(), overrideConfigs.getStruct()));
}
@Nullable // null if the plugin is not supported, but it's marked as optional.
private static PluginConfig parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto)
throws ResourceInvalidException {
return parseClusterSpecifierPlugin(
pluginProto, ClusterSpecifierPluginRegistry.getDefaultRegistry());
}
@Nullable // null if the plugin is not supported, but it's marked as optional.
@VisibleForTesting
static PluginConfig parseClusterSpecifierPlugin(
ClusterSpecifierPlugin pluginProto, ClusterSpecifierPluginRegistry registry)
throws ResourceInvalidException {
TypedExtensionConfig extension = pluginProto.getExtension();
String pluginName = extension.getName();
Any anyConfig = extension.getTypedConfig();
String typeUrl = anyConfig.getTypeUrl();
Message rawConfig = anyConfig;
if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA) || typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
try {
TypedStruct typedStruct = unpackCompatibleType(
anyConfig, TypedStruct.class, TYPE_URL_TYPED_STRUCT_UDPA, TYPE_URL_TYPED_STRUCT);
typeUrl = typedStruct.getTypeUrl();
rawConfig = typedStruct.getValue();
} catch (InvalidProtocolBufferException e) {
throw new ResourceInvalidException(
"ClusterSpecifierPlugin [" + pluginName + "] contains invalid proto", e);
}
}
io.grpc.xds.ClusterSpecifierPlugin plugin = registry.get(typeUrl);
if (plugin == null) {
if (!pluginProto.getIsOptional()) {
throw new ResourceInvalidException("Unsupported ClusterSpecifierPlugin type: " + typeUrl);
}
return null;
}
ConfigOrError<? extends PluginConfig> pluginConfigOrError = plugin.parsePlugin(rawConfig);
if (pluginConfigOrError.errorDetail != null) {
throw new ResourceInvalidException(pluginConfigOrError.errorDetail);
}
return pluginConfigOrError.config;
}
static final class RdsUpdate implements ResourceUpdate {
// The list virtual hosts that make up the route table.
final List<VirtualHost> virtualHosts;
RdsUpdate(List<VirtualHost> virtualHosts) {
this.virtualHosts = Collections.unmodifiableList(
new ArrayList<>(checkNotNull(virtualHosts, "virtualHosts")));
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("virtualHosts", virtualHosts)
.toString();
}
@Override
public int hashCode() {
return Objects.hash(virtualHosts);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RdsUpdate that = (RdsUpdate) o;
return Objects.equals(virtualHosts, that.virtualHosts);
}
}
}

View File

@ -50,11 +50,10 @@ import io.grpc.xds.Filter.ServerInterceptorBuilder;
import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector;
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.XdsClient.LdsResourceWatcher; import io.grpc.xds.XdsClient.ResourceWatcher;
import io.grpc.xds.XdsClient.LdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsClient.RdsResourceWatcher;
import io.grpc.xds.XdsClient.RdsUpdate;
import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener; import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener;
import io.grpc.xds.internal.security.SslContextProviderSupplier; import io.grpc.xds.internal.security.SslContextProviderSupplier;
import java.io.IOException; import java.io.IOException;
@ -344,7 +343,7 @@ final class XdsServerWrapper extends Server {
} }
} }
private final class DiscoveryState implements LdsResourceWatcher { private final class DiscoveryState implements ResourceWatcher<LdsUpdate> {
private final String resourceName; private final String resourceName;
// RDS resource name is the key. // RDS resource name is the key.
private final Map<String, RouteDiscoveryState> routeDiscoveryStates = new HashMap<>(); private final Map<String, RouteDiscoveryState> routeDiscoveryStates = new HashMap<>();
@ -368,7 +367,7 @@ final class XdsServerWrapper extends Server {
private DiscoveryState(String resourceName) { private DiscoveryState(String resourceName) {
this.resourceName = checkNotNull(resourceName, "resourceName"); this.resourceName = checkNotNull(resourceName, "resourceName");
xdsClient.watchLdsResource(resourceName, this); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), resourceName, this);
} }
@Override @Override
@ -402,7 +401,8 @@ final class XdsServerWrapper extends Server {
if (rdsState == null) { if (rdsState == null) {
rdsState = new RouteDiscoveryState(hcm.rdsName()); rdsState = new RouteDiscoveryState(hcm.rdsName());
routeDiscoveryStates.put(hcm.rdsName(), rdsState); routeDiscoveryStates.put(hcm.rdsName(), rdsState);
xdsClient.watchRdsResource(hcm.rdsName(), rdsState); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),
hcm.rdsName(), rdsState);
} }
if (rdsState.isPending) { if (rdsState.isPending) {
pendingRds.add(hcm.rdsName()); pendingRds.add(hcm.rdsName());
@ -412,7 +412,8 @@ final class XdsServerWrapper extends Server {
} }
for (Map.Entry<String, RouteDiscoveryState> entry: routeDiscoveryStates.entrySet()) { for (Map.Entry<String, RouteDiscoveryState> entry: routeDiscoveryStates.entrySet()) {
if (!allRds.contains(entry.getKey())) { if (!allRds.contains(entry.getKey())) {
xdsClient.cancelRdsResourceWatch(entry.getKey(), entry.getValue()); xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(),
entry.getKey(), entry.getValue());
} }
} }
routeDiscoveryStates.keySet().retainAll(allRds); routeDiscoveryStates.keySet().retainAll(allRds);
@ -458,7 +459,7 @@ final class XdsServerWrapper extends Server {
stopped = true; stopped = true;
cleanUpRouteDiscoveryStates(); cleanUpRouteDiscoveryStates();
logger.log(Level.FINE, "Stop watching LDS resource {0}", resourceName); logger.log(Level.FINE, "Stop watching LDS resource {0}", resourceName);
xdsClient.cancelLdsResourceWatch(resourceName, this); xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), resourceName, this);
List<SslContextProviderSupplier> toRelease = getSuppliersInUse(); List<SslContextProviderSupplier> toRelease = getSuppliersInUse();
filterChainSelectorManager.updateSelector(FilterChainSelector.NO_FILTER_CHAIN); filterChainSelectorManager.updateSelector(FilterChainSelector.NO_FILTER_CHAIN);
for (SslContextProviderSupplier s: toRelease) { for (SslContextProviderSupplier s: toRelease) {
@ -588,7 +589,8 @@ final class XdsServerWrapper extends Server {
for (RouteDiscoveryState rdsState : routeDiscoveryStates.values()) { for (RouteDiscoveryState rdsState : routeDiscoveryStates.values()) {
String rdsName = rdsState.resourceName; String rdsName = rdsState.resourceName;
logger.log(Level.FINE, "Stop watching RDS resource {0}", rdsName); logger.log(Level.FINE, "Stop watching RDS resource {0}", rdsName);
xdsClient.cancelRdsResourceWatch(rdsName, rdsState); xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(), rdsName,
rdsState);
} }
routeDiscoveryStates.clear(); routeDiscoveryStates.clear();
savedRdsRoutingConfigRef.clear(); savedRdsRoutingConfigRef.clear();
@ -626,7 +628,7 @@ final class XdsServerWrapper extends Server {
} }
} }
private final class RouteDiscoveryState implements RdsResourceWatcher { private final class RouteDiscoveryState implements ResourceWatcher<RdsUpdate> {
private final String resourceName; private final String resourceName;
private ImmutableList<VirtualHost> savedVirtualHosts; private ImmutableList<VirtualHost> savedVirtualHosts;
private boolean isPending = true; private boolean isPending = true;

View File

@ -17,6 +17,7 @@
package io.grpc.xds; package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static io.grpc.xds.AbstractXdsClient.ResourceType.CDS;
import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -55,7 +56,7 @@ import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig;
import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig;
import io.grpc.xds.XdsClient.CdsUpdate; import io.grpc.xds.XdsClusterResource.CdsUpdate;
import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -91,7 +92,7 @@ public class CdsLoadBalancer2Test {
null, null, null, null, SuccessRateEjection.create(null, null, null, null), null); null, null, null, null, SuccessRateEjection.create(null, null, null, null), null);
private final SynchronizationContext syncContext = new SynchronizationContext( private static final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() { new Thread.UncaughtExceptionHandler() {
@Override @Override
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(Thread t, Throwable e) {
@ -649,17 +650,24 @@ public class CdsLoadBalancer2Test {
} }
} }
private static final class FakeXdsClient extends XdsClient { private final class FakeXdsClient extends XdsClient {
private final Map<String, CdsResourceWatcher> watchers = new HashMap<>(); private final Map<String, ResourceWatcher<CdsUpdate>> watchers = new HashMap<>();
@Override @Override
void watchCdsResource(String resourceName, CdsResourceWatcher watcher) { @SuppressWarnings("unchecked")
<T extends ResourceUpdate> void watchXdsResource(XdsResourceType<T> type, String resourceName,
ResourceWatcher<T> watcher) {
assertThat(type.typeName()).isEqualTo(CDS);
assertThat(watchers).doesNotContainKey(resourceName); assertThat(watchers).doesNotContainKey(resourceName);
watchers.put(resourceName, watcher); watchers.put(resourceName, (ResourceWatcher<CdsUpdate>)watcher);
} }
@Override @Override
void cancelCdsResourceWatch(String resourceName, CdsResourceWatcher watcher) { @SuppressWarnings("unchecked")
<T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
String resourceName,
ResourceWatcher<T> watcher) {
assertThat(type.typeName()).isEqualTo(CDS);
assertThat(watchers).containsKey(resourceName); assertThat(watchers).containsKey(resourceName);
watchers.remove(resourceName); watchers.remove(resourceName);
} }
@ -677,7 +685,7 @@ public class CdsLoadBalancer2Test {
} }
private void deliverError(Status error) { private void deliverError(Status error) {
for (CdsResourceWatcher watcher : watchers.values()) { for (ResourceWatcher<CdsUpdate> watcher : watchers.values()) {
watcher.onError(error); watcher.onError(error);
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -463,7 +463,7 @@ public class ClientXdsClientV2Test extends ClientXdsClientTestBase {
ClusterConfig clusterConfig = ClusterConfig.newBuilder().addAllClusters(clusters).build(); ClusterConfig clusterConfig = ClusterConfig.newBuilder().addAllClusters(clusters).build();
CustomClusterType type = CustomClusterType type =
CustomClusterType.newBuilder() CustomClusterType.newBuilder()
.setName(ClientXdsClient.AGGREGATE_CLUSTER_TYPE_NAME) .setName(XdsResourceType.AGGREGATE_CLUSTER_TYPE_NAME)
.setTypedConfig(Any.pack(clusterConfig)) .setTypedConfig(Any.pack(clusterConfig))
.build(); .build();
Cluster.Builder builder = Cluster.newBuilder().setName(clusterName).setClusterType(type); Cluster.Builder builder = Cluster.newBuilder().setName(clusterName).setClusterType(type);

View File

@ -519,7 +519,7 @@ public class ClientXdsClientV3Test extends ClientXdsClientTestBase {
ClusterConfig clusterConfig = ClusterConfig.newBuilder().addAllClusters(clusters).build(); ClusterConfig clusterConfig = ClusterConfig.newBuilder().addAllClusters(clusters).build();
CustomClusterType type = CustomClusterType type =
CustomClusterType.newBuilder() CustomClusterType.newBuilder()
.setName(ClientXdsClient.AGGREGATE_CLUSTER_TYPE_NAME) .setName(XdsResourceType.AGGREGATE_CLUSTER_TYPE_NAME)
.setTypedConfig(Any.pack(clusterConfig)) .setTypedConfig(Any.pack(clusterConfig))
.build(); .build();
Cluster.Builder builder = Cluster.newBuilder().setName(clusterName).setClusterType(type); Cluster.Builder builder = Cluster.newBuilder().setName(clusterName).setClusterType(type);

View File

@ -17,6 +17,7 @@
package io.grpc.xds; package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static io.grpc.xds.AbstractXdsClient.ResourceType.EDS;
import static io.grpc.xds.XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME;
import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME;
import static io.grpc.xds.XdsLbPolicies.WEIGHTED_TARGET_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.WEIGHTED_TARGET_POLICY_NAME;
@ -76,6 +77,7 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig;
import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig;
import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig; import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig;
import io.grpc.xds.XdsEndpointResource.EdsUpdate;
import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.URI; import java.net.URI;
@ -185,7 +187,6 @@ public class ClusterResolverLoadBalancerTest {
private int xdsClientRefs; private int xdsClientRefs;
private ClusterResolverLoadBalancer loadBalancer; private ClusterResolverLoadBalancer loadBalancer;
@Before @Before
public void setUp() throws URISyntaxException { public void setUp() throws URISyntaxException {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
@ -1168,16 +1169,24 @@ public class ClusterResolverLoadBalancerTest {
} }
private static final class FakeXdsClient extends XdsClient { private static final class FakeXdsClient extends XdsClient {
private final Map<String, EdsResourceWatcher> watchers = new HashMap<>(); private final Map<String, ResourceWatcher<EdsUpdate>> watchers = new HashMap<>();
@Override @Override
void watchEdsResource(String resourceName, EdsResourceWatcher watcher) { @SuppressWarnings("unchecked")
<T extends ResourceUpdate> void watchXdsResource(XdsResourceType<T> type, String resourceName,
ResourceWatcher<T> watcher) {
assertThat(type.typeName()).isEqualTo(EDS);
assertThat(watchers).doesNotContainKey(resourceName); assertThat(watchers).doesNotContainKey(resourceName);
watchers.put(resourceName, watcher); watchers.put(resourceName, (ResourceWatcher<EdsUpdate>) watcher);
} }
@Override @Override
void cancelEdsResourceWatch(String resourceName, EdsResourceWatcher watcher) { @SuppressWarnings("unchecked")
<T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
String resourceName,
ResourceWatcher<T> watcher) {
assertThat(type.typeName()).isEqualTo(EDS);
assertThat(watchers).containsKey(resourceName); assertThat(watchers).containsKey(resourceName);
watchers.remove(resourceName); watchers.remove(resourceName);
} }
@ -1192,7 +1201,7 @@ public class ClusterResolverLoadBalancerTest {
Map<Locality, LocalityLbEndpoints> localityLbEndpointsMap) { Map<Locality, LocalityLbEndpoints> localityLbEndpointsMap) {
if (watchers.containsKey(resource)) { if (watchers.containsKey(resource)) {
watchers.get(resource).onChanged( watchers.get(resource).onChanged(
new EdsUpdate(resource, localityLbEndpointsMap, dropOverloads)); new XdsEndpointResource.EdsUpdate(resource, localityLbEndpointsMap, dropOverloads));
} }
} }
@ -1203,7 +1212,7 @@ public class ClusterResolverLoadBalancerTest {
} }
void deliverError(Status error) { void deliverError(Status error) {
for (EdsResourceWatcher watcher : watchers.values()) { for (ResourceWatcher<EdsUpdate> watcher : watchers.values()) {
watcher.onError(error); watcher.onError(error);
} }
} }

View File

@ -44,7 +44,7 @@ import io.grpc.netty.ProtocolNegotiationEvent;
import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext;
import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler;
import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector;
import io.grpc.xds.XdsClient.LdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener; import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener;
import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; import io.grpc.xds.XdsServerTestHelper.FakeXdsClient;
import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory; import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory;

View File

@ -86,7 +86,9 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import io.grpc.xds.internal.Matchers.HeaderMatcher; import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -2072,8 +2074,8 @@ public class XdsNameResolverTest {
// Should never be subscribing to more than one LDS and RDS resource at any point of time. // Should never be subscribing to more than one LDS and RDS resource at any point of time.
private String ldsResource; // should always be AUTHORITY private String ldsResource; // should always be AUTHORITY
private String rdsResource; private String rdsResource;
private LdsResourceWatcher ldsWatcher; private ResourceWatcher<LdsUpdate> ldsWatcher;
private RdsResourceWatcher rdsWatcher; private ResourceWatcher<RdsUpdate> rdsWatcher;
@Override @Override
BootstrapInfo getBootstrapInfo() { BootstrapInfo getBootstrapInfo() {
@ -2081,37 +2083,49 @@ public class XdsNameResolverTest {
} }
@Override @Override
void watchLdsResource(String resourceName, LdsResourceWatcher watcher) { @SuppressWarnings("unchecked")
assertThat(ldsResource).isNull(); <T extends ResourceUpdate> void watchXdsResource(XdsResourceType<T> resourceType,
assertThat(ldsWatcher).isNull(); String resourceName,
assertThat(resourceName).isEqualTo(expectedLdsResourceName); ResourceWatcher<T> watcher) {
ldsResource = resourceName;
ldsWatcher = watcher; switch (resourceType.typeName()) {
case LDS:
assertThat(ldsResource).isNull();
assertThat(ldsWatcher).isNull();
assertThat(resourceName).isEqualTo(expectedLdsResourceName);
ldsResource = resourceName;
ldsWatcher = (ResourceWatcher<LdsUpdate>) watcher;
break;
case RDS:
assertThat(rdsResource).isNull();
assertThat(rdsWatcher).isNull();
rdsResource = resourceName;
rdsWatcher = (ResourceWatcher<RdsUpdate>) watcher;
break;
default:
}
} }
@Override @Override
void cancelLdsResourceWatch(String resourceName, LdsResourceWatcher watcher) { <T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
assertThat(ldsResource).isNotNull(); String resourceName,
assertThat(ldsWatcher).isNotNull(); ResourceWatcher<T> watcher) {
assertThat(resourceName).isEqualTo(expectedLdsResourceName); switch (type.typeName()) {
ldsResource = null; case LDS:
ldsWatcher = null; assertThat(ldsResource).isNotNull();
} assertThat(ldsWatcher).isNotNull();
assertThat(resourceName).isEqualTo(expectedLdsResourceName);
@Override ldsResource = null;
void watchRdsResource(String resourceName, RdsResourceWatcher watcher) { ldsWatcher = null;
assertThat(rdsResource).isNull(); break;
assertThat(rdsWatcher).isNull(); case RDS:
rdsResource = resourceName; assertThat(rdsResource).isNotNull();
rdsWatcher = watcher; assertThat(rdsWatcher).isNotNull();
} rdsResource = null;
rdsWatcher = null;
@Override break;
void cancelRdsResourceWatch(String resourceName, RdsResourceWatcher watcher) { default:
assertThat(rdsResource).isNotNull(); }
assertThat(rdsWatcher).isNotNull();
rdsResource = null;
rdsWatcher = null;
} }
void deliverLdsUpdate(long httpMaxStreamDurationNano, List<VirtualHost> virtualHosts) { void deliverLdsUpdate(long httpMaxStreamDurationNano, List<VirtualHost> virtualHosts) {

View File

@ -57,7 +57,7 @@ import io.grpc.xds.Filter.NamedFilterConfig;
import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.LdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; import io.grpc.xds.XdsServerTestHelper.FakeXdsClient;
import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory; import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory;
import io.grpc.xds.internal.Matchers.HeaderMatcher; import io.grpc.xds.internal.Matchers.HeaderMatcher;

View File

@ -30,7 +30,8 @@ import io.grpc.xds.EnvoyServerProtoData.Listener;
import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.Filter.NamedFilterConfig;
import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.XdsClient.LdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -163,9 +164,9 @@ public class XdsServerTestHelper {
static final class FakeXdsClient extends XdsClient { static final class FakeXdsClient extends XdsClient {
boolean shutdown; boolean shutdown;
SettableFuture<String> ldsResource = SettableFuture.create(); SettableFuture<String> ldsResource = SettableFuture.create();
LdsResourceWatcher ldsWatcher; ResourceWatcher<LdsUpdate> ldsWatcher;
CountDownLatch rdsCount = new CountDownLatch(1); CountDownLatch rdsCount = new CountDownLatch(1);
final Map<String, RdsResourceWatcher> rdsWatchers = new HashMap<>(); final Map<String, ResourceWatcher<RdsUpdate>> rdsWatchers = new HashMap<>();
@Override @Override
public TlsContextManager getTlsContextManager() { public TlsContextManager getTlsContextManager() {
@ -178,28 +179,40 @@ public class XdsServerTestHelper {
} }
@Override @Override
void watchLdsResource(String resourceName, LdsResourceWatcher watcher) { @SuppressWarnings("unchecked")
assertThat(ldsWatcher).isNull(); <T extends ResourceUpdate> void watchXdsResource(XdsResourceType<T> resourceType,
ldsWatcher = watcher; String resourceName,
ldsResource.set(resourceName); ResourceWatcher<T> watcher) {
switch (resourceType.typeName()) {
case LDS:
assertThat(ldsWatcher).isNull();
ldsWatcher = (ResourceWatcher<LdsUpdate>) watcher;
ldsResource.set(resourceName);
break;
case RDS:
//re-register is not allowed.
assertThat(rdsWatchers.put(resourceName, (ResourceWatcher<RdsUpdate>)watcher)).isNull();
rdsCount.countDown();
break;
default:
}
} }
@Override @Override
void cancelLdsResourceWatch(String resourceName, LdsResourceWatcher watcher) { <T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
assertThat(ldsWatcher).isNotNull(); String resourceName,
ldsResource = null; ResourceWatcher<T> watcher) {
ldsWatcher = null; switch (type.typeName()) {
} case LDS:
assertThat(ldsWatcher).isNotNull();
@Override ldsResource = null;
void watchRdsResource(String resourceName, RdsResourceWatcher watcher) { ldsWatcher = null;
assertThat(rdsWatchers.put(resourceName, watcher)).isNull(); //re-register is not allowed. break;
rdsCount.countDown(); case RDS:
} rdsWatchers.remove(resourceName);
break;
@Override default:
void cancelRdsResourceWatch(String resourceName, RdsResourceWatcher watcher) { }
rdsWatchers.remove(resourceName);
} }
@Override @Override
@ -213,7 +226,7 @@ public class XdsServerTestHelper {
} }
void deliverLdsUpdate(List<FilterChain> filterChains, void deliverLdsUpdate(List<FilterChain> filterChains,
FilterChain defaultFilterChain) { FilterChain defaultFilterChain) {
ldsWatcher.onChanged(LdsUpdate.forTcpListener(Listener.create( ldsWatcher.onChanged(LdsUpdate.forTcpListener(Listener.create(
"listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain))); "listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain)));
} }

View File

@ -57,9 +57,8 @@ import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHan
import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.LdsResourceWatcher; import io.grpc.xds.XdsClient.ResourceWatcher;
import io.grpc.xds.XdsClient.RdsResourceWatcher; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import io.grpc.xds.XdsClient.RdsUpdate;
import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener; import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener;
import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; import io.grpc.xds.XdsServerTestHelper.FakeXdsClient;
import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory; import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory;
@ -177,6 +176,7 @@ public class XdsServerWrapperTest {
} }
@Test @Test
@SuppressWarnings("unchecked")
public void testBootstrap_templateWithXdstp() throws Exception { public void testBootstrap_templateWithXdstp() throws Exception {
Bootstrapper.BootstrapInfo b = Bootstrapper.BootstrapInfo.builder() Bootstrapper.BootstrapInfo b = Bootstrapper.BootstrapInfo.builder()
.servers(Arrays.asList( .servers(Arrays.asList(
@ -187,6 +187,7 @@ public class XdsServerWrapperTest {
"xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/%s") "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/%s")
.build(); .build();
XdsClient xdsClient = mock(XdsClient.class); XdsClient xdsClient = mock(XdsClient.class);
XdsListenerResource listenerResource = XdsListenerResource.getInstance();
when(xdsClient.getBootstrapInfo()).thenReturn(b); when(xdsClient.getBootstrapInfo()).thenReturn(b);
xdsServerWrapper = new XdsServerWrapper("[::FFFF:129.144.52.38]:80", mockBuilder, listener, xdsServerWrapper = new XdsServerWrapper("[::FFFF:129.144.52.38]:80", mockBuilder, listener,
selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry);
@ -200,10 +201,11 @@ public class XdsServerWrapperTest {
} }
} }
}); });
verify(xdsClient, timeout(5000)).watchLdsResource( verify(xdsClient, timeout(5000)).watchXdsResource(
eq(listenerResource),
eq("xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/" eq("xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/"
+ "%5B::FFFF:129.144.52.38%5D:80"), + "%5B::FFFF:129.144.52.38%5D:80"),
any(LdsResourceWatcher.class)); any(ResourceWatcher.class));
} }
@Test @Test
@ -727,7 +729,7 @@ public class XdsServerWrapperTest {
xdsClient.ldsWatcher.onError(Status.INTERNAL); xdsClient.ldsWatcher.onError(Status.INTERNAL);
assertThat(selectorManager.getSelectorToUpdateSelector()) assertThat(selectorManager.getSelectorToUpdateSelector())
.isSameInstanceAs(FilterChainSelector.NO_FILTER_CHAIN); .isSameInstanceAs(FilterChainSelector.NO_FILTER_CHAIN);
RdsResourceWatcher saveRdsWatcher = xdsClient.rdsWatchers.get("rds"); ResourceWatcher<RdsUpdate> saveRdsWatcher = xdsClient.rdsWatchers.get("rds");
verify(mockBuilder, times(1)).build(); verify(mockBuilder, times(1)).build();
verify(listener, times(2)).onNotServing(any(StatusException.class)); verify(listener, times(2)).onNotServing(any(StatusException.class));
assertThat(sslSupplier0.isShutdown()).isFalse(); assertThat(sslSupplier0.isShutdown()).isFalse();