From 5f3a5f8b375367571c0660e0b9e46c38a4175b6e Mon Sep 17 00:00:00 2001 From: ZHANG Dapeng Date: Mon, 22 Nov 2021 09:02:35 -0800 Subject: [PATCH] xds: support xdstp scheme in resource URIs for federation (#8716) Implement applying `server_listener_resource_name_template` and `client_listener_resource_name_template` with xdstp scheme, extracting authorities from xdstp resource URI and lookup authorities map in bootstrap. --- .../main/java/io/grpc/xds/Bootstrapper.java | 9 +- .../java/io/grpc/xds/BootstrapperImpl.java | 2 +- .../java/io/grpc/xds/ClientXdsClient.java | 21 +++- .../java/io/grpc/xds/XdsNameResolver.java | 62 ++++++++-- .../io/grpc/xds/XdsNameResolverProvider.java | 7 +- .../java/io/grpc/xds/XdsServerWrapper.java | 8 +- .../io/grpc/xds/ClientXdsClientTestBase.java | 75 +++++++++++- .../java/io/grpc/xds/XdsNameResolverTest.java | 113 ++++++++++++++++-- .../io/grpc/xds/XdsServerWrapperTest.java | 32 +++++ 9 files changed, 295 insertions(+), 34 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/Bootstrapper.java index 862f8691080..57cdc6f324b 100644 --- a/xds/src/main/java/io/grpc/xds/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/Bootstrapper.java @@ -16,6 +16,8 @@ package io.grpc.xds; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -33,6 +35,8 @@ @Internal public abstract class Bootstrapper { + static final String XDSTP_SCHEME = "xdstp:"; + /** * Returns system-loaded bootstrap configuration. */ @@ -104,12 +108,13 @@ abstract static class AuthorityInfo { *

If the same server is listed in multiple authorities, the entries will be de-duped (i.e., * resources for both authorities will be fetched on the same ADS stream). * - *

If empty, the top-level server list {@link BootstrapInfo#servers()} will be used. + *

Defaults to the top-level server list {@link BootstrapInfo#servers()}. Must not be empty. */ abstract ImmutableList xdsServers(); static AuthorityInfo create( String clientListenerResourceNameTemplate, List xdsServers) { + checkArgument(!xdsServers.isEmpty(), "xdsServers must not be empty"); return new AutoValue_Bootstrapper_AuthorityInfo( clientListenerResourceNameTemplate, ImmutableList.copyOf(xdsServers)); } @@ -121,7 +126,7 @@ static AuthorityInfo create( @AutoValue @Internal public abstract static class BootstrapInfo { - /** Returns the list of xDS servers to be connected to. */ + /** Returns the list of xDS servers to be connected to. Must not be empty. */ abstract ImmutableList servers(); /** Returns the node identifier to be included in xDS requests. */ diff --git a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java index 35ff5e55a42..23b14357096 100644 --- a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java @@ -229,7 +229,7 @@ BootstrapInfo bootstrap(Map rawData) throws XdsInitializationExceptio JsonUtil.getString(rawAuthority, "client_listener_resource_name_template"); logger.log( XdsLogLevel.INFO, "client_listener_resource_name_template: {0}", clientListnerTemplate); - String prefix = "xdstp://" + authorityName + "/"; + String prefix = XDSTP_SCHEME + "//" + authorityName + "/"; if (clientListnerTemplate == null) { clientListnerTemplate = prefix + "envoy.config.listener.v3.Listener/%s"; } else if (!clientListnerTemplate.startsWith(prefix)) { diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index 0a11ad47288..2b676553838 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME; import com.github.udpa.udpa.type.v1.TypedStruct; import com.google.common.annotations.VisibleForTesting; @@ -70,6 +71,7 @@ import io.grpc.internal.BackoffPolicy; import io.grpc.internal.TimeProvider; import io.grpc.xds.AbstractXdsClient.ResourceType; +import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.Endpoints.LbEndpoint; @@ -98,6 +100,7 @@ import io.grpc.xds.internal.Matchers.FractionMatcher; import io.grpc.xds.internal.Matchers.HeaderMatcher; import java.net.InetSocketAddress; +import java.net.URI; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; @@ -2267,8 +2270,12 @@ private final class ResourceSubscriber { ResourceSubscriber(ResourceType type, String resource) { syncContext.throwIfNotInThisSynchronizationContext(); this.type = type; + // TODO(zdapeng): Validate authority in resource URI for new-style resource name + // when parsing XDS response. + // TODO(zdapeng): Canonicalize the resource name by sorting the context params in normal + // lexicographic order. this.resource = resource; - this.serverInfo = getServerInfo(); + this.serverInfo = getServerInfo(resource); // Initialize metadata in UNKNOWN state to cover the case when resource subscriber, // is created but not yet requested because the client is in backoff. this.metadata = ResourceMetadata.newResourceMetadataUnknown(); @@ -2280,8 +2287,16 @@ private final class ResourceSubscriber { restartTimer(); } - // TODO(zdapeng): add resourceName arg and support xdstp:// resources - private ServerInfo getServerInfo() { + private ServerInfo getServerInfo(String resource) { + if (resource.startsWith(XDSTP_SCHEME)) { + URI uri = URI.create(resource); + String authority = uri.getAuthority(); + if (authority == null) { + authority = ""; + } + AuthorityInfo authorityInfo = bootstrapInfo.authorities().get(authority); + return authorityInfo.xdsServers().get(0); + } return bootstrapInfo.servers().get(0); // use first server } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 4cd52c8b3f9..b6b66327525 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -18,12 +18,14 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; +import com.google.common.net.UrlEscapers; import com.google.gson.Gson; import com.google.protobuf.util.Durations; import io.grpc.Attributes; @@ -45,6 +47,8 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; +import io.grpc.xds.Bootstrapper.AuthorityInfo; +import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; @@ -101,7 +105,9 @@ final class XdsNameResolver extends NameResolver { private final InternalLogId logId; private final XdsLogger logger; - private final String authority; + @Nullable + private final String targetAuthority; + private final String serviceAuthority; private final ServiceConfigParser serviceConfigParser; private final SynchronizationContext syncContext; private final ScheduledExecutorService scheduler; @@ -120,20 +126,23 @@ final class XdsNameResolver extends NameResolver { private CallCounterProvider callCounterProvider; private ResolveState resolveState; - XdsNameResolver(String name, ServiceConfigParser serviceConfigParser, + XdsNameResolver( + @Nullable String targetAuthority, String name, ServiceConfigParser serviceConfigParser, SynchronizationContext syncContext, ScheduledExecutorService scheduler, @Nullable Map bootstrapOverride) { - this(name, serviceConfigParser, syncContext, scheduler, + this(targetAuthority, name, serviceConfigParser, syncContext, scheduler, SharedXdsClientPoolProvider.getDefaultProvider(), ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride); } @VisibleForTesting - XdsNameResolver(String name, ServiceConfigParser serviceConfigParser, + XdsNameResolver( + @Nullable String targetAuthority, String name, ServiceConfigParser serviceConfigParser, SynchronizationContext syncContext, ScheduledExecutorService scheduler, XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random, FilterRegistry filterRegistry, @Nullable Map bootstrapOverride) { - authority = GrpcUtil.checkAuthority(checkNotNull(name, "name")); + this.targetAuthority = targetAuthority; + serviceAuthority = GrpcUtil.checkAuthority(checkNotNull(name, "name")); this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.scheduler = checkNotNull(scheduler, "scheduler"); @@ -149,7 +158,7 @@ final class XdsNameResolver extends NameResolver { @Override public String getServiceAuthority() { - return authority; + return serviceAuthority; } @Override @@ -163,11 +172,33 @@ public void start(Listener2 listener) { return; } xdsClient = xdsClientPool.getObject(); + BootstrapInfo bootstrapInfo = xdsClient.getBootstrapInfo(); + String listenerNameTemplate; + if (targetAuthority == null) { + listenerNameTemplate = bootstrapInfo.clientDefaultListenerResourceNameTemplate(); + } else { + AuthorityInfo authorityInfo = bootstrapInfo.authorities().get(targetAuthority); + if (authorityInfo == null) { + listener.onError(Status.INVALID_ARGUMENT.withDescription( + "invalid target URI: target authority not found in the bootstrap")); + return; + } + listenerNameTemplate = authorityInfo.clientListenerResourceNameTemplate(); + } + String replacement = serviceAuthority; + if (listenerNameTemplate.startsWith(XDSTP_SCHEME)) { + replacement = UrlEscapers.urlFragmentEscaper().escape(replacement); + } + String ldsResourceName = expandPercentS(listenerNameTemplate, replacement); callCounterProvider = SharedCallCounterMap.getInstance(); - resolveState = new ResolveState(); + resolveState = new ResolveState(ldsResourceName); resolveState.start(); } + private static String expandPercentS(String template, String replacement) { + return template.replace("%s", replacement); + } + @Override public void shutdown() { logger.log(XdsLogLevel.INFO, "Shutdown"); @@ -624,12 +655,17 @@ private class ResolveState implements LdsResourceWatcher { .setServiceConfig(emptyServiceConfig) // let channel take action for no config selector .build(); + private final String ldsResourceName; private boolean stopped; @Nullable private Set existingClusters; // clusters to which new requests can be routed @Nullable private RouteDiscoveryState routeDiscoveryState; + ResolveState(String ldsResourceName) { + this.ldsResourceName = ldsResourceName; + } + @Override public void onChanged(final LdsUpdate update) { syncContext.execute(new Runnable() { @@ -686,23 +722,23 @@ public void run() { } private void start() { - logger.log(XdsLogLevel.INFO, "Start watching LDS resource {0}", authority); - xdsClient.watchLdsResource(authority, this); + logger.log(XdsLogLevel.INFO, "Start watching LDS resource {0}", ldsResourceName); + xdsClient.watchLdsResource(ldsResourceName, this); } private void stop() { - logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", authority); + logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", ldsResourceName); stopped = true; cleanUpRouteDiscoveryState(); - xdsClient.cancelLdsResourceWatch(authority, this); + xdsClient.cancelLdsResourceWatch(ldsResourceName, this); } private void updateRoutes(List virtualHosts, long httpMaxStreamDurationNano, @Nullable List filterConfigs) { - VirtualHost virtualHost = findVirtualHostForHostName(virtualHosts, authority); + VirtualHost virtualHost = findVirtualHostForHostName(virtualHosts, ldsResourceName); if (virtualHost == null) { logger.log(XdsLogLevel.WARNING, - "Failed to find virtual host matching hostname {0}", authority); + "Failed to find virtual host matching hostname {0}", ldsResourceName); cleanUpRoutes(); return; } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java index 03d88a9752e..a02e27c37c7 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java @@ -74,9 +74,10 @@ public XdsNameResolver newNameResolver(URI targetUri, Args args) { targetPath, targetUri); String name = targetPath.substring(1); - return new XdsNameResolver(name, args.getServiceConfigParser(), - args.getSynchronizationContext(), args.getScheduledExecutorService(), - bootstrapOverride); + return new XdsNameResolver( + targetUri.getAuthority(), name, args.getServiceConfigParser(), + args.getSynchronizationContext(), args.getScheduledExecutorService(), + bootstrapOverride); } return null; } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 6eefcf63411..0946f621487 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -18,11 +18,13 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.net.UrlEscapers; import com.google.common.util.concurrent.SettableFuture; import io.grpc.Attributes; import io.grpc.InternalServerInterceptors; @@ -192,7 +194,11 @@ private void internalStart() { xdsClient = xdsClientPool.returnObject(xdsClient); return; } - discoveryState = new DiscoveryState(listenerTemplate.replaceAll("%s", listenerAddress)); + String replacement = listenerAddress; + if (listenerTemplate.startsWith(XDSTP_SCHEME)) { + replacement = UrlEscapers.urlFragmentEscaper().escape(replacement); + } + discoveryState = new DiscoveryState(listenerTemplate.replaceAll("%s", replacement)); } @Override diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java index 9d0c735ba95..6fdf5e1b811 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java @@ -59,6 +59,7 @@ import io.grpc.internal.TimeProvider; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.AbstractXdsClient.ResourceType; +import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.CertificateProviderInfo; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.ClientXdsClient.XdsChannelFactory; @@ -114,6 +115,8 @@ @RunWith(JUnit4.class) public abstract class ClientXdsClientTestBase { private static final String SERVER_URI = "trafficdirector.googleapis.com"; + private static final String SERVER_URI_CUSTOME_AUTHORITY = "trafficdirector2.googleapis.com"; + private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; private static final String LDS_RESOURCE = "listener.googleapis.com"; private static final String RDS_RESOURCE = "route-configuration.googleapis.com"; private static final String CDS_RESOURCE = "cluster.googleapis.com"; @@ -250,6 +253,8 @@ public long currentTimeNanos() { private TlsContextManager tlsContextManager; private ManagedChannel channel; + private ManagedChannel channelForCustomAuthority; + private ManagedChannel channelForEmptyAuthority; private ClientXdsClient xdsClient; private boolean originalEnableFaultInjection; private boolean originalEnableRbac; @@ -281,7 +286,24 @@ public void setUp() throws IOException { XdsChannelFactory xdsChannelFactory = new XdsChannelFactory() { @Override ManagedChannel create(ServerInfo serverInfo) { - return channel; + if (serverInfo.target().equals(SERVER_URI)) { + return channel; + } + if (serverInfo.target().equals(SERVER_URI_CUSTOME_AUTHORITY)) { + if (channelForCustomAuthority == null) { + channelForCustomAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return channelForCustomAuthority; + } + if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { + if (channelForEmptyAuthority == null) { + channelForEmptyAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return channelForEmptyAuthority; + } + throw new IllegalArgumentException("Can not create channel for " + serverInfo); } }; @@ -290,6 +312,17 @@ ManagedChannel create(ServerInfo serverInfo) { .servers(Arrays.asList( Bootstrapper.ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, useProtocolV3()))) .node(EnvoyProtoData.Node.newBuilder().build()) + .authorities(ImmutableMap.of( + "authority.xds.com", + AuthorityInfo.create( + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS, useProtocolV3()))), + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS, useProtocolV3()))))) .certProviders(ImmutableMap.of("cert-instance-name", CertificateProviderInfo.create("file-watcher", ImmutableMap.of()))) .build(); @@ -706,6 +739,46 @@ public void ldsResourceUpdated() { .isEqualTo(RDS_RESOURCE); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerRds, VERSION_2, TIME_INCREMENT * 2); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); + assertThat(channelForCustomAuthority).isNull(); + assertThat(channelForEmptyAuthority).isNull(); + } + + @Test + public void ldsResourceUpdated_withXdstpResourceName() { + String ldsResourceName = + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1"; + DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); + assertThat(channelForCustomAuthority).isNotNull(); + verifyResourceMetadataRequested(LDS, ldsResourceName); + + Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(ldsResourceName, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest(LDS, ldsResourceName, VERSION_1, "0000", NODE); + verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); + assertThat(ldsUpdateCaptor.getValue().httpConnectionManager().virtualHosts()) + .hasSize(VHOST_SIZE); + verifyResourceMetadataAcked( + LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT); + } + + @Test + public void ldsResourceUpdated_withXdstpResourceName_withEmptyAuthority() { + String ldsResourceName = + "xdstp:///envoy.config.listener.v3.Listener/listener1"; + DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); + assertThat(channelForEmptyAuthority).isNotNull(); + verifyResourceMetadataRequested(LDS, ldsResourceName); + + Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(ldsResourceName, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest(LDS, ldsResourceName, VERSION_1, "0000", NODE); + verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); + assertThat(ldsUpdateCaptor.getValue().httpConnectionManager().virtualHosts()) + .hasSize(VHOST_SIZE); + verifyResourceMetadataAcked( + LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT); } @Test diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index babaa2b3034..7968b7fb366 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -45,6 +46,7 @@ import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; import io.grpc.Deadline; +import io.grpc.InsecureChannelCredentials; import io.grpc.InternalConfigSelector; import io.grpc.InternalConfigSelector.Result; import io.grpc.Metadata; @@ -67,6 +69,10 @@ import io.grpc.internal.PickSubchannelArgsImpl; import io.grpc.internal.ScParser; import io.grpc.testing.TestMethodDescriptors; +import io.grpc.xds.Bootstrapper.AuthorityInfo; +import io.grpc.xds.Bootstrapper.BootstrapInfo; +import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.FaultConfig.FaultAbort; import io.grpc.xds.FaultConfig.FaultDelay; import io.grpc.xds.Filter.FilterConfig; @@ -132,6 +138,12 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { private final CallInfo call1 = new CallInfo("HelloService", "hi"); private final CallInfo call2 = new CallInfo("GreetService", "bye"); private final TestChannel channel = new TestChannel(); + private BootstrapInfo bootstrapInfo = BootstrapInfo.builder() + .servers(ImmutableList.of(ServerInfo.create( + "td.googleapis.com", InsecureChannelCredentials.create(), true))) + .node(Node.newBuilder().build()) + .build(); + private String expectedLdsResourceName = AUTHORITY; @Mock private ThreadSafeRandom mockRandom; @@ -152,7 +164,7 @@ public void setUp() { FilterRegistry filterRegistry = FilterRegistry.newRegistry().register( new FaultFilter(mockRandom, new AtomicLong()), RouterFilter.INSTANCE); - resolver = new XdsNameResolver(AUTHORITY, serviceConfigParser, syncContext, scheduler, + resolver = new XdsNameResolver(null, AUTHORITY, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null); } @@ -185,7 +197,7 @@ public ObjectPool getOrCreate() throws XdsInitializationException { throw new XdsInitializationException("Fail to read bootstrap file"); } }; - resolver = new XdsNameResolver(AUTHORITY, serviceConfigParser, syncContext, scheduler, + resolver = new XdsNameResolver(null, AUTHORITY, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); @@ -195,6 +207,79 @@ public ObjectPool getOrCreate() throws XdsInitializationException { assertThat(error.getCause()).hasMessageThat().isEqualTo("Fail to read bootstrap file"); } + @Test + public void resolving_withTargetAuthorityNotFound() { + resolver = new XdsNameResolver( + "notfound.google.com", AUTHORITY, serviceConfigParser, syncContext, scheduler, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + resolver.start(mockListener); + verify(mockListener).onError(errorCaptor.capture()); + Status error = errorCaptor.getValue(); + assertThat(error.getCode()).isEqualTo(Code.INVALID_ARGUMENT); + assertThat(error.getDescription()).isEqualTo( + "invalid target URI: target authority not found in the bootstrap"); + } + + @Test + public void resolving_noTargetAuthority_templateWithoutXdstp() { + bootstrapInfo = BootstrapInfo.builder() + .servers(ImmutableList.of(ServerInfo.create( + "td.googleapis.com", InsecureChannelCredentials.create(), true))) + .node(Node.newBuilder().build()) + .clientDefaultListenerResourceNameTemplate("%s/id=1") + .build(); + String serviceAuthority = "[::FFFF:129.144.52.38]:80"; + expectedLdsResourceName = "[::FFFF:129.144.52.38]:80/id=1"; + resolver = new XdsNameResolver( + null, serviceAuthority, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, + mockRandom, FilterRegistry.getDefaultRegistry(), null); + resolver.start(mockListener); + verify(mockListener, never()).onError(any(Status.class)); + } + + @Test + public void resolving_noTargetAuthority_templateWithXdstp() { + bootstrapInfo = BootstrapInfo.builder() + .servers(ImmutableList.of(ServerInfo.create( + "td.googleapis.com", InsecureChannelCredentials.create(), true))) + .node(Node.newBuilder().build()) + .clientDefaultListenerResourceNameTemplate( + "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%s?id=1") + .build(); + String serviceAuthority = "[::FFFF:129.144.52.38]:80"; + expectedLdsResourceName = + "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + + "%5B::FFFF:129.144.52.38%5D:80?id=1"; + resolver = new XdsNameResolver( + null, serviceAuthority, serviceConfigParser, syncContext, scheduler, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + resolver.start(mockListener); + verify(mockListener, never()).onError(any(Status.class)); + } + + @Test + public void resolving_targetAuthorityInAuthoritiesMap() { + String targetAuthority = "xds.authority.com"; + String serviceAuthority = "[::FFFF:129.144.52.38]:80"; + bootstrapInfo = BootstrapInfo.builder() + .servers(ImmutableList.of(ServerInfo.create( + "td.googleapis.com", InsecureChannelCredentials.create(), true))) + .node(Node.newBuilder().build()) + .authorities( + ImmutableMap.of(targetAuthority, AuthorityInfo.create( + "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s", + ImmutableList.of(ServerInfo.create( + "td.googleapis.com", InsecureChannelCredentials.create(), true))))) + .build(); + expectedLdsResourceName = + "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%5B::FFFF:129.144.52.38%5D:80"; + resolver = new XdsNameResolver( + "xds.authority.com", serviceAuthority, serviceConfigParser, syncContext, scheduler, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + resolver.start(mockListener); + verify(mockListener, never()).onError(any(Status.class)); + } + @Test public void resolving_ldsResourceNotFound() { resolver.start(mockListener); @@ -435,7 +520,7 @@ public void resolved_fallbackToHttpMaxStreamDurationAsTimeout() { public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { ServiceConfigParser realParser = new ScParser( true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick-first")); - resolver = new XdsNameResolver(AUTHORITY, realParser, syncContext, scheduler, + resolver = new XdsNameResolver(null, AUTHORITY, realParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -638,7 +723,7 @@ public void resolved_rpcHashingByChannelId() { // A different resolver/Channel. resolver.shutdown(); reset(mockListener); - resolver = new XdsNameResolver(AUTHORITY, serviceConfigParser, syncContext, scheduler, + resolver = new XdsNameResolver(null, AUTHORITY, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); resolver.start(mockListener); xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -1698,8 +1783,11 @@ public void routeMatching_withHeaders() { } private final class FakeXdsClientPoolFactory implements XdsClientPoolFactory { + Map bootstrap; + @Override public void setBootstrapOverride(Map bootstrap) { + this.bootstrap = bootstrap; } @Override @@ -1731,11 +1819,16 @@ private class FakeXdsClient extends XdsClient { private LdsResourceWatcher ldsWatcher; private RdsResourceWatcher rdsWatcher; + @Override + BootstrapInfo getBootstrapInfo() { + return bootstrapInfo; + } + @Override void watchLdsResource(String resourceName, LdsResourceWatcher watcher) { assertThat(ldsResource).isNull(); assertThat(ldsWatcher).isNull(); - assertThat(resourceName).isEqualTo(AUTHORITY); + assertThat(resourceName).isEqualTo(expectedLdsResourceName); ldsResource = resourceName; ldsWatcher = watcher; } @@ -1744,7 +1837,7 @@ void watchLdsResource(String resourceName, LdsResourceWatcher watcher) { void cancelLdsResourceWatch(String resourceName, LdsResourceWatcher watcher) { assertThat(ldsResource).isNotNull(); assertThat(ldsWatcher).isNotNull(); - assertThat(resourceName).isEqualTo(AUTHORITY); + assertThat(resourceName).isEqualTo(expectedLdsResourceName); ldsResource = null; ldsWatcher = null; } @@ -1773,7 +1866,7 @@ void deliverLdsUpdate(long httpMaxStreamDurationNano, List virtualH void deliverLdsUpdate(final List routes) { VirtualHost virtualHost = VirtualHost.create( - "virtual-host", Collections.singletonList(AUTHORITY), routes, + "virtual-host", Collections.singletonList(expectedLdsResourceName), routes, ImmutableMap.of()); ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( 0L, Collections.singletonList(virtualHost), null))); @@ -1817,7 +1910,7 @@ void deliverLdsUpdateWithFaultInjection( FAULT_FILTER_INSTANCE_NAME, virtualHostFaultConfig); VirtualHost virtualHost = VirtualHost.create( "virtual-host", - Collections.singletonList(AUTHORITY), + Collections.singletonList(expectedLdsResourceName), Collections.singletonList(route), overrideConfig); ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( @@ -1843,7 +1936,7 @@ void deliverLdsUpdateForRdsName(String rdsName) { } void deliverLdsResourceNotFound() { - ldsWatcher.onResourceDoesNotExist(AUTHORITY); + ldsWatcher.onResourceDoesNotExist(expectedLdsResourceName); } void deliverRdsUpdateWithFaultInjection( @@ -1876,7 +1969,7 @@ void deliverRdsUpdateWithFaultInjection( FAULT_FILTER_INSTANCE_NAME, virtualHostFaultConfig); VirtualHost virtualHost = VirtualHost.create( "virtual-host", - Collections.singletonList(AUTHORITY), + Collections.singletonList(expectedLdsResourceName), Collections.singletonList(route), overrideConfig); rdsWatcher.onChanged(new RdsUpdate(Collections.singletonList(virtualHost))); diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index bfb16c9745e..f011b789da9 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -55,6 +56,7 @@ import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; +import io.grpc.xds.XdsClient.LdsResourceWatcher; import io.grpc.xds.XdsClient.RdsResourceWatcher; import io.grpc.xds.XdsClient.RdsUpdate; import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener; @@ -173,6 +175,36 @@ public void run() { } } + @Test + public void testBootstrap_templateWithXdstp() throws Exception { + Bootstrapper.BootstrapInfo b = Bootstrapper.BootstrapInfo.builder() + .servers(Arrays.asList( + Bootstrapper.ServerInfo.create( + "uri", InsecureChannelCredentials.create(), true))) + .node(EnvoyProtoData.Node.newBuilder().setId("id").build()) + .serverListenerResourceNameTemplate( + "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/%s") + .build(); + XdsClient xdsClient = mock(XdsClient.class); + when(xdsClient.getBootstrapInfo()).thenReturn(b); + xdsServerWrapper = new XdsServerWrapper("[::FFFF:129.144.52.38]:80", mockBuilder, listener, + selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + xdsServerWrapper.start(); + } catch (IOException ex) { + // ignore + } + } + }); + verify(xdsClient, timeout(5000)).watchLdsResource( + eq("xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/" + + "%5B::FFFF:129.144.52.38%5D:80"), + any(LdsResourceWatcher.class)); + } + @Test public void shutdown() throws Exception { final SettableFuture start = SettableFuture.create();