diff --git a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java index 427c0658531..cd6e49bb3ad 100644 --- a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java +++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java @@ -27,6 +27,7 @@ import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.NameResolverRegistry; +import io.grpc.QueryParams; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.Uri; @@ -47,7 +48,6 @@ import java.io.Reader; import java.net.HttpURLConnection; import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.List; @@ -81,18 +81,26 @@ final class GoogleCloudToProdNameResolver extends NameResolver { private static HttpConnectionProvider httpConnectionProvider = HttpConnectionFactory.INSTANCE; private static int c2pId = new Random().nextInt(); - private static synchronized BootstrapInfo getBootstrapInfo() + private static synchronized BootstrapInfo getBootstrapInfo(boolean isForcedXds) throws XdsInitializationException, IOException { if (bootstrapInfo != null) { return bootstrapInfo; } - BootstrapInfo bootstrapInfoTmp = - InternalGrpcBootstrapperImpl.parseBootstrap(generateBootstrap()); + BootstrapInfo newInfo; + if (isForcedXds) { + newInfo = InternalGrpcBootstrapperImpl.parseBootstrap( + generateBootstrap("", true)); + } else { + newInfo = InternalGrpcBootstrapperImpl.parseBootstrap( + generateBootstrap( + queryZoneMetadata(METADATA_URL_ZONE), + queryIpv6SupportMetadata(METADATA_URL_SUPPORT_IPV6))); + } // Avoid setting global when testing if (httpConnectionProvider == HttpConnectionFactory.INSTANCE) { - bootstrapInfo = bootstrapInfoTmp; + bootstrapInfo = newInfo; } - return bootstrapInfoTmp; + return newInfo; } private final String authority; @@ -102,7 +110,8 @@ private static synchronized BootstrapInfo getBootstrapInfo() private final MetricRecorder metricRecorder; private final NameResolver delegate; private final boolean usingExecutorResource; - private final String schemeOverride = !isOnGcp ? "dns" : "xds"; + private final boolean forceXds; + private final String schemeOverride; private XdsClientResult xdsClientPool; private XdsClient xdsClient; private Executor executor; @@ -122,6 +131,13 @@ private static synchronized BootstrapInfo getBootstrapInfo() NameResolver.Factory nameResolverFactory) { this.executorResource = checkNotNull(executorResource, "executorResource"); String targetPath = checkNotNull(checkNotNull(targetUri, "targetUri").getPath(), "targetPath"); + Uri grpcUri = Uri.create(targetUri.toString()); + QueryParams queryParams = QueryParams.fromRawQuery(grpcUri.getRawQuery()); + this.forceXds = checkForceXds(queryParams); + this.schemeOverride = (forceXds || isOnGcp) ? "xds" : "dns"; + stripForceXds(queryParams); + String newQuery = queryParams.toRawQuery(); + Preconditions.checkArgument( targetPath.startsWith("/"), "the path component (%s) of the target (%s) must start with '/'", @@ -129,9 +145,19 @@ private static synchronized BootstrapInfo getBootstrapInfo() targetUri); authority = GrpcUtil.checkAuthority(targetPath.substring(1)); syncContext = checkNotNull(args, "args").getSynchronizationContext(); - targetUri = overrideUriScheme(targetUri, schemeOverride); + + Uri.Builder modifiedTargetBuilder = grpcUri.toBuilder().setScheme(schemeOverride); + if (newQuery != null) { + modifiedTargetBuilder.setRawQuery(newQuery); + } else { + modifiedTargetBuilder.setRawQuery(null); + } + if (schemeOverride.equals("xds")) { + modifiedTargetBuilder.setRawAuthority(C2P_AUTHORITY); + } + targetUri = URI.create(modifiedTargetBuilder.build().toString()); + if (schemeOverride.equals("xds")) { - targetUri = overrideUriAuthority(targetUri, C2P_AUTHORITY); args = args.toBuilder() .setArg(XdsNameResolverProvider.XDS_CLIENT_SUPPLIER, () -> xdsClient) .build(); @@ -155,6 +181,12 @@ private static synchronized BootstrapInfo getBootstrapInfo() Resource executorResource, NameResolver.Factory nameResolverFactory) { this.executorResource = checkNotNull(executorResource, "executorResource"); + QueryParams queryParams = QueryParams.fromRawQuery(targetUri.getRawQuery()); + this.forceXds = checkForceXds(queryParams); + this.schemeOverride = (forceXds || isOnGcp) ? "xds" : "dns"; + stripForceXds(queryParams); + String newQuery = queryParams.toRawQuery(); + Preconditions.checkArgument( targetUri.isPathAbsolute(), "the path component of the target (%s) must start with '/'", @@ -167,6 +199,12 @@ private static synchronized BootstrapInfo getBootstrapInfo() authority = GrpcUtil.checkAuthority(pathSegments.get(0)); syncContext = checkNotNull(args, "args").getSynchronizationContext(); Uri.Builder modifiedTargetBuilder = targetUri.toBuilder().setScheme(schemeOverride); + if (newQuery != null) { + modifiedTargetBuilder.setRawQuery(newQuery); + } else { + modifiedTargetBuilder.setRawQuery(null); + } + if (schemeOverride.equals("xds")) { modifiedTargetBuilder.setRawAuthority(C2P_AUTHORITY); args = @@ -226,7 +264,7 @@ class Resolve implements Runnable { public void run() { BootstrapInfo bootstrapInfo = null; try { - bootstrapInfo = getBootstrapInfo(); + bootstrapInfo = getBootstrapInfo(forceXds); } catch (IOException e) { listener.onError( Status.INTERNAL.withDescription("Unable to get metadata").withCause(e)); @@ -259,16 +297,11 @@ public void run() { executor.execute(new Resolve()); } - @VisibleForTesting - static ImmutableMap generateBootstrap() throws IOException { - return generateBootstrap( - queryZoneMetadata(METADATA_URL_ZONE), - queryIpv6SupportMetadata(METADATA_URL_SUPPORT_IPV6)); - } - - private static ImmutableMap generateBootstrap(String zone, boolean supportIpv6) { + private static ImmutableMap generateBootstrap( + String zone, boolean supportIpv6) { ImmutableMap.Builder nodeBuilder = ImmutableMap.builder(); - nodeBuilder.put("id", "C2P-" + (c2pId & Integer.MAX_VALUE)); + String nodeIdPrefix = isOnGcp ? "C2P-" : "C2P-non-gcp-"; + nodeBuilder.put("id", nodeIdPrefix + (c2pId & Integer.MAX_VALUE)); if (!zone.isEmpty()) { nodeBuilder.put("locality", ImmutableMap.of("zone", zone)); } @@ -373,24 +406,17 @@ static void setC2pId(int c2pId) { GoogleCloudToProdNameResolver.c2pId = c2pId; } - private static URI overrideUriScheme(URI uri, String scheme) { - URI res; - try { - res = new URI(scheme, uri.getAuthority(), uri.getPath(), uri.getQuery(), uri.getFragment()); - } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid scheme: " + scheme, ex); + private static boolean checkForceXds(QueryParams params) { + for (QueryParams.Entry entry : params.asList()) { + if ("force-xds".equals(entry.getKey())) { + return true; + } } - return res; + return false; } - private static URI overrideUriAuthority(URI uri, String authority) { - URI res; - try { - res = new URI(uri.getScheme(), authority, uri.getPath(), uri.getQuery(), uri.getFragment()); - } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid authority: " + authority, ex); - } - return res; + private static void stripForceXds(QueryParams params) { + params.asList().removeIf(entry -> "force-xds".equals(entry.getKey())); } private enum HttpConnectionFactory implements HttpConnectionProvider { diff --git a/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java index d3d3cfc4bff..bbd3ba3ef05 100644 --- a/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java +++ b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java @@ -21,8 +21,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import io.grpc.ChannelLogger; import io.grpc.MetricRecorder; @@ -46,7 +44,6 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.Executor; @@ -103,6 +100,8 @@ public void close(Executor instance) {} private final NameResolverRegistry nsRegistry = new NameResolverRegistry(); private final Map delegatedResolver = new HashMap<>(); + private final Map delegatedUri = new HashMap<>(); + private final Map delegatedRfcUri = new HashMap<>(); @Mock private NameResolver.Listener2 mockListener; @@ -187,57 +186,125 @@ public void onGcpAndNoProvidedBootstrap_DelegateToXds() { verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); } - @SuppressWarnings("unchecked") @Test - public void generateBootstrap_ipv6() throws IOException { - Map bootstrap = GoogleCloudToProdNameResolver.generateBootstrap(); - Map node = (Map) bootstrap.get("node"); - assertThat(node).containsExactly( - "id", "C2P-991614323", - "locality", ImmutableMap.of("zone", ZONE), - "metadata", ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true)); - Map server = Iterables.getOnlyElement( - (List>) bootstrap.get("xds_servers")); - assertThat(server).containsExactly( - "server_uri", "directpath-pa.googleapis.com", - "channel_creds", ImmutableList.of(ImmutableMap.of("type", "google_default")), - "server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion")); - Map authorities = (Map) bootstrap.get("authorities"); - assertThat(authorities).containsExactly( - "traffic-director-c2p.xds.googleapis.com", - ImmutableMap.of("xds_servers", ImmutableList.of(server))); + public void notOnGcpButForceXds_DelegateToXds() { + GoogleCloudToProdNameResolver.isOnGcp = false; + String target = TARGET_URI + "?force-xds"; + resolver = + enableRfc3986UrisParam + ? new GoogleCloudToProdNameResolver( + Uri.create(target), args, fakeExecutorResource, nsRegistry.asFactory()) + : new GoogleCloudToProdNameResolver( + URI.create(target), args, fakeExecutorResource, nsRegistry.asFactory()); + resolver.start(mockListener); + fakeExecutor.runDueTasks(); + assertThat(delegatedResolver.keySet()).containsExactly("xds"); + + if (enableRfc3986UrisParam) { + Uri delegatedRfcUriValue = delegatedRfcUri.get("xds"); + assertThat(delegatedRfcUriValue).isNotNull(); + assertThat(delegatedRfcUriValue.getRawQuery()).isNull(); + } else { + URI delegatedUriValue = delegatedUri.get("xds"); + assertThat(delegatedUriValue).isNotNull(); + assertThat(delegatedUriValue.getQuery()).isNull(); + } + } + + @Test + public void notOnGcpButForceXds_KeyValueTrue_DelegateToXds() { + GoogleCloudToProdNameResolver.isOnGcp = false; + String target = TARGET_URI + "?force-xds=true"; + resolver = enableRfc3986UrisParam + ? new GoogleCloudToProdNameResolver( + Uri.create(target), args, fakeExecutorResource, nsRegistry.asFactory()) + : new GoogleCloudToProdNameResolver( + URI.create(target), args, fakeExecutorResource, nsRegistry.asFactory()); + resolver.start(mockListener); + fakeExecutor.runDueTasks(); + assertThat(delegatedResolver.keySet()).containsExactly("xds"); + + if (enableRfc3986UrisParam) { + Uri delegatedRfcUriValue = delegatedRfcUri.get("xds"); + assertThat(delegatedRfcUriValue).isNotNull(); + assertThat(delegatedRfcUriValue.getRawQuery()).isNull(); + } else { + URI delegatedUriValue = delegatedUri.get("xds"); + assertThat(delegatedUriValue).isNotNull(); + assertThat(delegatedUriValue.getQuery()).isNull(); + } + } + + + @Test + public void notOnGcpButForceXds_WithMultipleParams_DelegateToXds() { + GoogleCloudToProdNameResolver.isOnGcp = false; + String target = TARGET_URI + "?foo=bar&force-xds&baz=qux"; + resolver = enableRfc3986UrisParam + ? new GoogleCloudToProdNameResolver( + Uri.create(target), args, fakeExecutorResource, nsRegistry.asFactory()) + : new GoogleCloudToProdNameResolver( + URI.create(target), args, fakeExecutorResource, nsRegistry.asFactory()); + resolver.start(mockListener); + fakeExecutor.runDueTasks(); + assertThat(delegatedResolver.keySet()).containsExactly("xds"); + + if (enableRfc3986UrisParam) { + Uri delegatedRfcUriValue = delegatedRfcUri.get("xds"); + assertThat(delegatedRfcUriValue).isNotNull(); + assertThat(delegatedRfcUriValue.getRawQuery()).isEqualTo("foo=bar&baz=qux"); + } else { + URI delegatedUriValue = delegatedUri.get("xds"); + assertThat(delegatedUriValue).isNotNull(); + assertThat(delegatedUriValue.getQuery()).isEqualTo("foo=bar&baz=qux"); + } } - @SuppressWarnings("unchecked") @Test - public void generateBootstrap_noIpV6() throws IOException { - responseToIpV6 = null; - Map bootstrap = GoogleCloudToProdNameResolver.generateBootstrap(); - Map node = (Map) bootstrap.get("node"); - assertThat(node).containsExactly( - "id", "C2P-991614323", - "locality", ImmutableMap.of("zone", ZONE)); - Map server = Iterables.getOnlyElement( - (List>) bootstrap.get("xds_servers")); - assertThat(server).containsExactly( - "server_uri", "directpath-pa.googleapis.com", - "channel_creds", ImmutableList.of(ImmutableMap.of("type", "google_default")), - "server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion")); - Map authorities = (Map) bootstrap.get("authorities"); - assertThat(authorities).containsExactly( - "traffic-director-c2p.xds.googleapis.com", - ImmutableMap.of("xds_servers", ImmutableList.of(server))); + public void notOnGcpButForceXds_WithEncodedAmpersand_DelegateToXds() { + GoogleCloudToProdNameResolver.isOnGcp = false; + String target = TARGET_URI + "?force-xds&foo=bar%26baz"; + resolver = enableRfc3986UrisParam + ? new GoogleCloudToProdNameResolver( + Uri.create(target), args, fakeExecutorResource, nsRegistry.asFactory()) + : new GoogleCloudToProdNameResolver( + URI.create(target), args, fakeExecutorResource, nsRegistry.asFactory()); + resolver.start(mockListener); + fakeExecutor.runDueTasks(); + assertThat(delegatedResolver.keySet()).containsExactly("xds"); + + if (enableRfc3986UrisParam) { + Uri delegatedRfcUriValue = delegatedRfcUri.get("xds"); + assertThat(delegatedRfcUriValue).isNotNull(); + assertThat(delegatedRfcUriValue.getRawQuery()).isEqualTo("foo=bar%26baz"); + } else { + URI delegatedUriValue = delegatedUri.get("xds"); + assertThat(delegatedUriValue).isNotNull(); + assertThat(delegatedUriValue.getRawQuery()).isEqualTo("foo=bar%26baz"); + } } - @SuppressWarnings("unchecked") @Test - public void emptyResolverMeetadataValue() throws IOException { - responseToIpV6 = ""; - Map bootstrap = GoogleCloudToProdNameResolver.generateBootstrap(); - Map node = (Map) bootstrap.get("node"); - assertThat(node).containsExactly( - "id", "C2P-991614323", - "locality", ImmutableMap.of("zone", ZONE)); + public void notOnGcpButForceXds_CaseSensitive_DelegateToDns() { + GoogleCloudToProdNameResolver.isOnGcp = false; + String target = TARGET_URI + "?FORCE-XDS"; + resolver = enableRfc3986UrisParam + ? new GoogleCloudToProdNameResolver( + Uri.create(target), args, fakeExecutorResource, nsRegistry.asFactory()) + : new GoogleCloudToProdNameResolver( + URI.create(target), args, fakeExecutorResource, nsRegistry.asFactory()); + resolver.start(mockListener); + assertThat(delegatedResolver.keySet()).containsExactly("dns"); + + if (enableRfc3986UrisParam) { + Uri delegatedRfcUriValue = delegatedRfcUri.get("dns"); + assertThat(delegatedRfcUriValue).isNotNull(); + assertThat(delegatedRfcUriValue.getRawQuery()).isEqualTo("FORCE-XDS"); + } else { + URI delegatedUriValue = delegatedUri.get("dns"); + assertThat(delegatedUriValue).isNotNull(); + assertThat(delegatedUriValue.getQuery()).isEqualTo("FORCE-XDS"); + } } @Test @@ -270,6 +337,18 @@ private FakeNsProvider(String scheme) { @Override public NameResolver newNameResolver(URI targetUri, Args args) { if (scheme.equals(targetUri.getScheme())) { + delegatedUri.put(scheme, targetUri); + NameResolver resolver = mock(NameResolver.class); + delegatedResolver.put(scheme, resolver); + return resolver; + } + return null; + } + + @Override + public NameResolver newNameResolver(Uri targetUri, Args args) { + if (scheme.equals(targetUri.getScheme())) { + delegatedRfcUri.put(scheme, targetUri); NameResolver resolver = mock(NameResolver.class); delegatedResolver.put(scheme, resolver); return resolver;