diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index 1a6726b9c01b..18ceeb91f115 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -142,6 +142,7 @@ public final class GrpcStorageOptions extends StorageOptions private final GrpcRetryAlgorithmManager retryAlgorithmManager; private final java.time.Duration terminationAwaitDuration; private final boolean attemptDirectPath; + private final boolean attemptDirectPathXdsOverInterconnect; private final boolean enableGrpcClientMetrics; private final boolean grpcClientMetricsManuallyEnabled; @@ -160,6 +161,7 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) builder.terminationAwaitDuration, serviceDefaults.getTerminationAwaitDurationJavaTime()); this.attemptDirectPath = builder.attemptDirectPath; + this.attemptDirectPathXdsOverInterconnect = builder.attemptDirectPathXdsOverInterconnect; this.enableGrpcClientMetrics = builder.enableGrpcClientMetrics; this.grpcClientMetricsManuallyEnabled = builder.grpcMetricsManuallyEnabled; this.grpcInterceptorProvider = builder.grpcInterceptorProvider; @@ -322,7 +324,8 @@ private Tuple> resolveSettingsAndOpts() throw InstantiatingGrpcChannelProvider.newBuilder() .setEndpoint(endpoint) .setAllowNonDefaultServiceAccount(true) - .setAttemptDirectPath(attemptDirectPath); + .setAttemptDirectPath(attemptDirectPath || attemptDirectPathXdsOverInterconnect) + .setAttemptDirectPathXdsOverInterconnect(attemptDirectPathXdsOverInterconnect); if (!DIRECT_PATH_BOUND_TOKEN_DISABLED) { channelProviderBuilder.setAllowHardBoundTokenTypes( @@ -428,6 +431,7 @@ public int hashCode() { retryAlgorithmManager, terminationAwaitDuration, attemptDirectPath, + attemptDirectPathXdsOverInterconnect, enableGrpcClientMetrics, grpcInterceptorProvider, blobWriteSessionConfig, @@ -445,6 +449,7 @@ public boolean equals(Object o) { } GrpcStorageOptions that = (GrpcStorageOptions) o; return attemptDirectPath == that.attemptDirectPath + && attemptDirectPathXdsOverInterconnect == that.attemptDirectPathXdsOverInterconnect && enableGrpcClientMetrics == that.enableGrpcClientMetrics && Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager) && Objects.equals(terminationAwaitDuration, that.terminationAwaitDuration) @@ -494,6 +499,7 @@ public static final class Builder extends StorageOptions.Builder { private StorageRetryStrategy storageRetryStrategy; private java.time.Duration terminationAwaitDuration; private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath(); + private boolean attemptDirectPathXdsOverInterconnect = false; private boolean enableGrpcClientMetrics = GrpcStorageDefaults.INSTANCE.isEnableGrpcClientMetrics(); private GrpcInterceptorProvider grpcInterceptorProvider = @@ -512,6 +518,7 @@ public static final class Builder extends StorageOptions.Builder { this.storageRetryStrategy = gso.getRetryAlgorithmManager().retryStrategy; this.terminationAwaitDuration = gso.getTerminationAwaitDuration(); this.attemptDirectPath = gso.attemptDirectPath; + this.attemptDirectPathXdsOverInterconnect = gso.attemptDirectPathXdsOverInterconnect; this.enableGrpcClientMetrics = gso.enableGrpcClientMetrics; this.grpcInterceptorProvider = gso.grpcInterceptorProvider; this.blobWriteSessionConfig = gso.blobWriteSessionConfig; @@ -556,6 +563,23 @@ public GrpcStorageOptions.Builder setAttemptDirectPath(boolean attemptDirectPath return this; } + /** + * Option for whether this client should attempt to use DirectPath over Interconnect (on-premise + * xDS name resolution). + * + *

This can also be enabled or disabled via the {@code + * GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT} environment variable ({@code "true"} + * or {@code "false"}), which takes precedence over this builder option. + * + * @since 2.76.0 + */ + @BetaApi + public GrpcStorageOptions.Builder setAttemptDirectPathXdsOverInterconnect( + boolean attemptDirectPathXdsOverInterconnect) { + this.attemptDirectPathXdsOverInterconnect = attemptDirectPathXdsOverInterconnect; + return this; + } + /** * Option for whether this client should emit internal gRPC client internal metrics to Cloud * Monitoring. To disable metric reporting, set this to false. True by default. Emitting metrics diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java index 240040519635..87136e5c69af 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java @@ -69,6 +69,56 @@ public void grpc() throws Exception { () -> assertThat(rebuilt.hashCode()).isEqualTo(base.hashCode())); } + @Test + public void grpc_attemptDirectPathXdsOverInterconnect() throws Exception { + com.google.auth.Credentials mockCreds = com.google.cloud.NoCredentials.getInstance(); + GrpcStorageOptions options = + GrpcStorageOptions.grpc() + .setCredentials(mockCreds) + .setAttemptDirectPathXdsOverInterconnect(true) + .build(); + + GrpcStorageOptions rebuilt = options.toBuilder().build(); + assertAll( + () -> assertThat(rebuilt).isEqualTo(options), + () -> assertThat(rebuilt.hashCode()).isEqualTo(options.hashCode())); + + com.google.storage.v2.StorageSettings settings = options.getStorageSettings(); + assertThat(settings.getEndpoint()).isEqualTo("storage.googleapis.com:443"); + + com.google.api.gax.rpc.TransportChannelProvider tcp = settings.getTransportChannelProvider(); + assertThat(tcp).isInstanceOf(com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.class); + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider provider = + (com.google.api.gax.grpc.InstantiatingGrpcChannelProvider) tcp; + + assertThat(provider.getEndpoint()).isEqualTo("storage.googleapis.com:443"); + assertThat(provider.isDirectPathXdsEnabled()).isTrue(); + if (Boolean.parseBoolean(System.getenv("GOOGLE_CLOUD_DISABLE_DIRECT_PATH"))) { + assertThat(provider.canUseDirectPath()).isFalse(); + } else { + assertThat(provider.canUseDirectPath()).isTrue(); + } + } + + @Test + public void grpc_fallbackFromDirectPathXdsOverInterconnect_nonGduUniverse() throws Exception { + GrpcStorageOptions options = + GrpcStorageOptions.grpc() + .setHost("https://storage.my-universe.com") + .setAttemptDirectPathXdsOverInterconnect(true) + .setCredentials(com.google.cloud.NoCredentials.getInstance()) + .build(); + com.google.storage.v2.StorageSettings settings = options.getStorageSettings(); + assertThat(settings.getEndpoint()).isEqualTo("storage.my-universe.com:443"); + + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider provider = + (com.google.api.gax.grpc.InstantiatingGrpcChannelProvider) + settings.getTransportChannelProvider(); + assertThat(provider.getEndpoint()).isEqualTo("storage.my-universe.com:443"); + assertThat(provider.isDirectPathXdsEnabled()).isTrue(); + assertThat(provider.canUseDirectPath()).isFalse(); + } + @Test public void useJwtAccessWithScope_defaultsToFalse() { HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITGrpcDirectPathTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITGrpcDirectPathTest.java new file mode 100644 index 000000000000..f994d17e60be --- /dev/null +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITGrpcDirectPathTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Google LLC + * + * 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 com.google.cloud.storage.it; + +import static org.junit.Assume.assumeTrue; + +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.Inject; +import com.google.cloud.storage.it.runner.annotations.SingleBackend; +import com.google.cloud.storage.it.runner.annotations.StorageFixture; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@SingleBackend(Backend.PROD) +public final class ITGrpcDirectPathTest { + + @Inject + @StorageFixture(Transport.GRPC) + public Storage storage; + + @Ignore( + "Bypassed because DirectPath over Interconnect (GCI) requires a specialized hybrid network environment (Interconnect and Traffic Director configured for storage-direct) and cannot be validated in standard CI or local workstations.") + @Test + public void clientShouldWork_directPathXdsOverInterconnect() throws Exception { + // Added assumeTrue to ensure the test skips gracefully if storage-direct.googleapis.com + // cannot be resolved rather than failing CI. + assumeTrue( + "Environment cannot resolve storage-direct.googleapis.com", canResolveDirectPathAddress()); + StorageOptions options = + StorageOptions.grpc() + .setCredentials(storage.getOptions().getCredentials()) + .setAttemptDirectPathXdsOverInterconnect(true) + .setEnableGrpcClientMetrics(false) + .build(); + try (Storage client = options.getService()) { + client.list(Storage.BucketListOption.pageSize(1)); + } + } + + private static boolean canResolveDirectPathAddress() { + try { + java.net.InetAddress.getAllByName("storage-direct.googleapis.com"); + return true; + } catch (java.net.UnknownHostException e) { + return false; + } + } +} diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java index 0d8114a7fb2d..898725de2633 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java @@ -84,6 +84,17 @@ public void clientShouldConstructCleanly_directPath() throws Exception { doTest(options); } + @Test + public void clientShouldConstructCleanly_directPathXdsOverInterconnect() throws Exception { + StorageOptions options = + StorageOptions.grpc() + .setCredentials(credentials) + .setAttemptDirectPathXdsOverInterconnect(true) + .setEnableGrpcClientMetrics(false) + .build(); + doTest(options); + } + @Test public void lackOfProjectIdDoesNotPreventConstruction_http() throws Exception { StorageOptions options = StorageOptions.http().setCredentials(credentials).build(); diff --git a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java index 91299357da10..a6db148b992d 100644 --- a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java +++ b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java @@ -110,6 +110,12 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP @VisibleForTesting static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS"; + @VisibleForTesting + static final String DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT = + "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT"; + + private static final String DIRECT_PATH_INTERCONNECT_INFIX = "-direct."; + // The public portion of the mTLS MDS root certificate is stored for performing // cert verification when establishing an mTLS connection with the MDS. See // {@link directPathServiceConfig; private final @Nullable MtlsProvider mtlsProvider; @@ -235,6 +242,7 @@ private InstantiatingGrpcChannelProvider(Builder builder) { this.channelPrimer = builder.channelPrimer; this.attemptDirectPath = builder.attemptDirectPath; this.attemptDirectPathXds = builder.attemptDirectPathXds; + this.attemptDirectPathXdsOverInterconnect = builder.attemptDirectPathXdsOverInterconnect; this.allowNonDefaultServiceAccount = builder.allowNonDefaultServiceAccount; this.directPathServiceConfig = builder.directPathServiceConfig == null @@ -440,30 +448,44 @@ private boolean isDirectPathXdsEnabledViaEnv() { return Boolean.parseBoolean(directPathXdsEnv); } + private boolean isAttemptDirectPathXdsOverInterconnect() { + String directPathInterconnectEnv = + envProvider.getenv(DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT); + if (directPathInterconnectEnv != null) { + String trimmed = directPathInterconnectEnv.trim(); + if ("true".equalsIgnoreCase(trimmed)) { + return true; + } + if ("false".equalsIgnoreCase(trimmed)) { + return false; + } + } + return Boolean.TRUE.equals(attemptDirectPathXdsOverInterconnect); + } + /** - * This method tells if Direct Path xDS was enabled. There are two ways of enabling it: via - * environment variable (by setting GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS=true) or when building - * this channel provider (via the {@link Builder#setAttemptDirectPathXds()} method). + * This method tells if Direct Path xDS was enabled. It can be enabled via environment variables + * ({@code GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS=true} or {@code + * GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT=true}) or when building this channel + * provider (via {@link Builder#setAttemptDirectPathXds()} or {@link + * Builder#setAttemptDirectPathXdsOverInterconnect(boolean)}). * * @return true if Direct Path xDS was either enabled via env var or via builder option */ @InternalApi public boolean isDirectPathXdsEnabled() { - return isDirectPathXdsEnabledViaEnv() || isDirectPathXdsEnabledViaBuilderOption(); + return isDirectPathXdsEnabledViaEnv() + || isDirectPathXdsEnabledViaBuilderOption() + || isAttemptDirectPathXdsOverInterconnect(); } // This method should be called once per client initialization, hence can not be called in the // builder or createSingleChannel, only in getTransportChannel which creates the first channel // for a client. private void logDirectPathMisconfig() { - if (!isDirectPathXdsEnabled()) { - return; - } - Level level = isOnComputeEngine() ? Level.WARNING : Level.FINE; if (!isDirectPathEnabled()) { - // This misconfiguration occurs when Direct Path xDS is enabled, but Direct Path is not // Direct Path xDS can be enabled two ways: via environment variable or via builder. // Case 1: Direct Path is only enabled via xDS env var. We will _warn_ the user that this is // a misconfiguration if they intended to set the env var. @@ -487,7 +509,14 @@ else if (isDirectPathXdsEnabledViaBuilderOption()) { + " attemptDirectPathXds options."); } } else { - // Case 3: credential is not correctly set + // Case 3: DirectPath is enabled, but xDS is not. + if (!isDirectPathXdsEnabled()) { + LOG.log( + level, + "DirectPath is enabled, but DirectPath xDS is not. Please note that DirectPath will" + + " soon require xDS to be enabled. Please set the attemptDirectPathXds option."); + } + // Case 4: credential is not correctly set if (!isCredentialDirectPathCompatible()) { LOG.log( level, @@ -495,8 +524,8 @@ else if (isDirectPathXdsEnabledViaBuilderOption()) { + ComputeEngineCredentials.class.getName() + " ."); } - // Case 4: not running on GCE - if (!isOnComputeEngine()) { + // Case 5: not running on GCE + if (!isOnComputeEngine() && !isAttemptDirectPathXdsOverInterconnect()) { LOG.log( level, "DirectPath is misconfigured. DirectPath is only available in a GCE environment."); @@ -506,6 +535,10 @@ else if (isDirectPathXdsEnabledViaBuilderOption()) { @VisibleForTesting boolean isCredentialDirectPathCompatible() { + // xDS over Interconnect is designed to work on-premise using arbitrary service credentials. + if (isAttemptDirectPathXdsOverInterconnect()) { + return true; + } // DirectPath requires a call credential during gRPC channel construction. if (needsCredentials()) { return false; @@ -733,21 +766,30 @@ public ManagedChannelBuilder createChannelBuilder() throws IOException { // Check DirectPath traffic. boolean useDirectPathXds = false; if (canUseDirectPath()) { - CallCredentials callCreds = MoreCallCredentials.from(credentials); - // altsCallCredentials may be null and GoogleDefaultChannelCredentials - // will solely use callCreds. Otherwise it uses altsCallCredentials - // for DirectPath connections and callCreds for CloudPath fallbacks. - ChannelCredentials channelCreds = - GoogleDefaultChannelCredentials.newBuilder() - .callCredentials(callCreds) - .altsCallCredentials(altsCallCredentials) - .build(); + GoogleDefaultChannelCredentials.Builder credsBuilder = + GoogleDefaultChannelCredentials.newBuilder(); + if (credentials != null) { + credsBuilder.callCredentials(MoreCallCredentials.from(credentials)); + } + if (altsCallCredentials != null) { + credsBuilder.altsCallCredentials(altsCallCredentials); + } + ChannelCredentials channelCreds = credsBuilder.build(); useDirectPathXds = isDirectPathXdsEnabled(); if (useDirectPathXds) { // google-c2p: CloudToProd(C2P) Directpath. This scheme is defined in // io.grpc.googleapis.GoogleCloudToProdNameResolverProvider. // This resolver target must not have a port number. - builder = Grpc.newChannelBuilder("google-c2p:///" + serviceAddress, channelCreds); + String target = "google-c2p:///" + serviceAddress; + if (isAttemptDirectPathXdsOverInterconnect()) { + // DirectPath over Interconnect traffic is identified by the -direct hostname in the RPC + // :authority, which GoogleCloudToProdNameResolver natively derives from the target URI + // without needing overrideAuthority. Wildcard TLS certificates (*.googleapis.com) + // already match -direct.googleapis.com. + target = + "google-c2p:///" + rewriteDirectPathInterconnectHost(serviceAddress) + "?force-xds"; + } + builder = Grpc.newChannelBuilder(target, channelCreds); } else { builder = Grpc.newChannelBuilderForAddress(serviceAddress, port, channelCreds); builder.defaultServiceConfig(directPathServiceConfig); @@ -880,13 +922,17 @@ private void removeApiKeyCredentialDuplicateHeaders() { * settings and a few other configurations/settings must also be valid for the request to go * through DirectPath. * - *

Checks: 1. Credentials are compatible 2.Running on Compute Engine 3. Universe Domain is - * configured to for the Google Default Universe + *

Checks: 1. Credentials are compatible 2. Running on Compute Engine (bypassed if + * attemptDirectPathXdsOverInterconnect is enabled) 3. Universe Domain is configured for the + * Google Default Universe * * @return if DirectPath is enabled for the client AND if the configurations are valid */ @InternalApi public boolean canUseDirectPath() { + if (isAttemptDirectPathXdsOverInterconnect()) { + return isDirectPathEnabled() && canUseDirectPathWithUniverseDomain(); + } return isDirectPathEnabled() && isCredentialDirectPathCompatible() && isOnComputeEngine() @@ -982,6 +1028,7 @@ public static final class Builder { private ChannelPoolSettings channelPoolSettings; private @Nullable Boolean attemptDirectPath; private @Nullable Boolean attemptDirectPathXds; + private @Nullable Boolean attemptDirectPathXdsOverInterconnect; private @Nullable Boolean allowNonDefaultServiceAccount; private @Nullable ImmutableMap directPathServiceConfig; private List allowedHardBoundTokenTypes; @@ -1015,6 +1062,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) { this.channelPoolSettings = provider.channelPoolSettings; this.attemptDirectPath = provider.attemptDirectPath; this.attemptDirectPathXds = provider.attemptDirectPathXds; + this.attemptDirectPathXdsOverInterconnect = provider.attemptDirectPathXdsOverInterconnect; this.allowNonDefaultServiceAccount = provider.allowNonDefaultServiceAccount; this.allowedHardBoundTokenTypes = provider.allowedHardBoundTokenTypes; this.directPathServiceConfig = provider.directPathServiceConfig; @@ -1322,6 +1370,20 @@ public Builder setAttemptDirectPathXds() { return this; } + /** + * Use DirectPath xDS over Interconnect. Bypasses GCP GCE environment checks. + * + *

This can also be enabled or disabled via the {@code + * GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT} environment variable ({@code "true"} + * or {@code "false"}), which takes precedence over this builder option. + */ + @InternalApi("For internal use by google-cloud-java clients only") + public Builder setAttemptDirectPathXdsOverInterconnect( + boolean attemptDirectPathXdsOverInterconnect) { + this.attemptDirectPathXdsOverInterconnect = attemptDirectPathXdsOverInterconnect; + return this; + } + @VisibleForTesting Builder setEnvProvider(EnvironmentProvider envProvider) { this.envProvider = envProvider; @@ -1481,6 +1543,18 @@ public Builder setChannelConfigurator( "loadBalancingConfig", ImmutableList.of(pickFirstStrategy)); } + private static String rewriteDirectPathInterconnectHost(String serviceAddress) { + if (!serviceAddress.contains(DIRECT_PATH_INTERCONNECT_INFIX)) { + int dotIndex = serviceAddress.indexOf('.'); + if (dotIndex > 0) { + return serviceAddress.substring(0, dotIndex) + + DIRECT_PATH_INTERCONNECT_INFIX + + serviceAddress.substring(dotIndex + 1); + } + } + return serviceAddress; + } + private static void validateEndpoint(String endpoint) { int colon = endpoint.lastIndexOf(':'); if (colon < 0) { diff --git a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java index 5ffb6943a4fe..1612744a43ee 100644 --- a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java +++ b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java @@ -37,6 +37,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import com.google.api.core.ApiFunction; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder; @@ -712,16 +714,19 @@ void testLogDirectPathMisconfigWrongCredential() throws Exception { FakeLogHandler logHandler = new FakeLogHandler(); InstantiatingGrpcChannelProvider.LOG.setLevel(Level.FINE); InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); InstantiatingGrpcChannelProvider provider = InstantiatingGrpcChannelProvider.newBuilder() .setAttemptDirectPathXds() .setAttemptDirectPath(true) - .setEnvProvider(name -> null) - .setHeaderProvider( - mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations())) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) .setExecutor(mock(Executor.class)) .setEndpoint(DEFAULT_ENDPOINT) .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) .build(); TransportChannel transportChannel = provider.getTransportChannel(); @@ -741,16 +746,20 @@ void testLogDirectPathMisconfigNotOnGCE() throws Exception { FakeLogHandler logHandler = new FakeLogHandler(); InstantiatingGrpcChannelProvider.LOG.setLevel(Level.FINE); InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); InstantiatingGrpcChannelProvider provider = InstantiatingGrpcChannelProvider.newBuilder() .setAttemptDirectPathXds() .setAttemptDirectPath(true) .setAllowNonDefaultServiceAccount(true) - .setHeaderProvider( - mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations())) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) .setExecutor(mock(Executor.class)) .setEndpoint(DEFAULT_ENDPOINT) .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) .build(); TransportChannel transportChannel = provider.getTransportChannel(); @@ -934,6 +943,158 @@ public void canUseDirectPath_nonComputeCredentials() { Truth.assertThat(provider.canUseDirectPath()).isFalse(); } + @Test + public void canUseDirectPath_attemptDirectPathXdsOverInterconnect_bypassesGceCheck() { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + Credentials credentials = mock(Credentials.class, withSettings().withoutAnnotations()); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setCredentials(credentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + Truth.assertThat(provider.canUseDirectPath()).isTrue(); + } + + @Test + public void + canUseDirectPath_attemptDirectPathXdsOverInterconnect_directPathDisabled_returnsFalse() { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + Credentials credentials = mock(Credentials.class, withSettings().withoutAnnotations()); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(false) + .setAttemptDirectPathXdsOverInterconnect(true) + .setCredentials(credentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void + canUseDirectPath_attemptDirectPathXdsOverInterconnect_nonGDUUniverseDomain_returnsFalse() { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + Credentials credentials = mock(Credentials.class, withSettings().withoutAnnotations()); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setCredentials(credentials) + .setEndpoint("test.random.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void getTransportChannel_storageTarget_withInterconnect() + throws IOException, InterruptedException { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + Credentials credentials = mock(Credentials.class, withSettings().withoutAnnotations()); + final java.util.concurrent.atomic.AtomicReference capturedTarget = + new java.util.concurrent.atomic.AtomicReference<>(); + ApiFunction channelConfigurator = + channelBuilder -> { + capturedTarget.set(extractTargetFromChannelBuilder(channelBuilder)); + return channelBuilder; + }; + + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setCredentials(credentials) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider) + .setChannelConfigurator(channelConfigurator); + + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + + InstantiatingGrpcChannelProvider configuredProvider = + (InstantiatingGrpcChannelProvider) + provider + .withHeaders(Collections.emptyMap()) + .withEndpoint("storage.googleapis.com:443"); + + TransportChannel transportChannel = configuredProvider.getTransportChannel(); + transportChannel.shutdownNow(); + transportChannel.awaitTermination(5, TimeUnit.SECONDS); + + Truth.assertThat(capturedTarget.get()) + .contains("google-c2p:///storage-direct.googleapis.com?force-xds"); + } + + @Test + public void getTransportChannel_storageTarget_withInterconnectAndNullCredentials() + throws IOException, InterruptedException { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + final java.util.concurrent.atomic.AtomicReference capturedTarget = + new java.util.concurrent.atomic.AtomicReference<>(); + ApiFunction channelConfigurator = + channelBuilder -> { + capturedTarget.set(extractTargetFromChannelBuilder(channelBuilder)); + return channelBuilder; + }; + + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setCredentials(null) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider) + .setChannelConfigurator(channelConfigurator); + + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + + InstantiatingGrpcChannelProvider configuredProvider = + (InstantiatingGrpcChannelProvider) + provider + .withHeaders(Collections.emptyMap()) + .withEndpoint("storage.googleapis.com:443"); + + TransportChannel transportChannel = configuredProvider.getTransportChannel(); + transportChannel.shutdownNow(); + transportChannel.awaitTermination(5, TimeUnit.SECONDS); + + Truth.assertThat(capturedTarget.get()) + .contains("google-c2p:///storage-direct.googleapis.com?force-xds"); + } + @Test public void canUseDirectPath_isNotOnComputeEngine_invalidOsNameSystemProperty() { System.setProperty("os.name", "Not Linux"); @@ -1354,6 +1515,504 @@ void testSettingBackgroundExecutor() { assertThat(provider.getBackgroundExecutor()).isEqualTo(mockExecutor); } + private static String extractTargetFromChannelBuilder(ManagedChannelBuilder channelBuilder) { + try { + Class nettyBuilderClass = channelBuilder.getClass(); + java.lang.reflect.Field delegateField = null; + while (nettyBuilderClass != null && delegateField == null) { + try { + delegateField = nettyBuilderClass.getDeclaredField("delegate"); + } catch (NoSuchFieldException e) { + try { + delegateField = nettyBuilderClass.getDeclaredField("managedChannelImplBuilder"); + } catch (NoSuchFieldException e2) { + nettyBuilderClass = nettyBuilderClass.getSuperclass(); + } + } + } + if (delegateField != null) { + delegateField.setAccessible(true); + Object delegate = delegateField.get(channelBuilder); + Class delegateClass = delegate.getClass(); + java.lang.reflect.Field targetField = null; + while (delegateClass != null && targetField == null) { + try { + targetField = delegateClass.getDeclaredField("target"); + } catch (NoSuchFieldException e) { + delegateClass = delegateClass.getSuperclass(); + } + } + if (targetField != null) { + targetField.setAccessible(true); + return (String) targetField.get(delegate); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return channelBuilder.toString(); + } + + @Test + void testLogDirectPathMisconfigXdsSetDirectPathNotSet() throws Exception { + FakeLogHandler logHandler = new FakeLogHandler(); + InstantiatingGrpcChannelProvider.LOG.setLevel(Level.FINE); + InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, Mockito.withSettings().withoutAnnotations()); + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPathXds() + .setAttemptDirectPath(false) + .setHeaderProvider( + mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class, Mockito.withSettings().withoutAnnotations())) + .setEndpoint(DEFAULT_ENDPOINT) + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + try { + provider.getTransportChannel(); + } catch (Exception e) { + // ignore + } + + assertThat(logHandler.getAllMessages()) + .contains( + "DirectPath is misconfigured. The DirectPath XDS option was set, but the" + + " attemptDirectPath option was not. Please set both the attemptDirectPath and" + + " attemptDirectPathXds options."); + InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + } + + @Test + void testLogDirectPathMisconfigDirectPathSetXdsNotSet() throws Exception { + FakeLogHandler logHandler = new FakeLogHandler(); + InstantiatingGrpcChannelProvider.LOG.setLevel(Level.FINE); + InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, Mockito.withSettings().withoutAnnotations()); + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setHeaderProvider( + mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class, Mockito.withSettings().withoutAnnotations())) + .setEndpoint(DEFAULT_ENDPOINT) + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + try { + provider.getTransportChannel(); + } catch (Exception e) { + // ignore + } + + assertThat(logHandler.getAllMessages()) + .contains( + "DirectPath is enabled, but DirectPath xDS is not. Please note that DirectPath will" + + " soon require xDS to be enabled. Please set the attemptDirectPathXds option."); + InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + } + + @Test + void canUseDirectPath_interconnectEnabledButDirectPathDisabled_fallsBackToCloudPath() + throws Exception { + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(false) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint(DEFAULT_ENDPOINT) + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)).isEqualTo(DEFAULT_ENDPOINT); + Truth.assertThat(extractAuthorityFromChannelBuilder(channelBuilder)).isNull(); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()).isEqualTo(DEFAULT_ENDPOINT); + } finally { + channel.shutdownNow(); + } + } + + @Test + void canUseDirectPath_interconnectAndDirectPathEnabledButNonGduUniverse_fallsBackToCloudPath() + throws Exception { + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("storage-direct.some-other-universe.com:443") + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)) + .isEqualTo("storage-direct.some-other-universe.com:443"); + Truth.assertThat(extractAuthorityFromChannelBuilder(channelBuilder)).isNull(); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()).isEqualTo("storage-direct.some-other-universe.com:443"); + } finally { + channel.shutdownNow(); + } + } + + @Test + void + createChannelBuilder_directPathOverInterconnect_storageEndpoint_rewritesTargetWithoutAuthorityOverride() + throws Exception { + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("storage.googleapis.com:443") + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)) + .isEqualTo("google-c2p:///storage-direct.googleapis.com?force-xds"); + Truth.assertThat(extractAuthorityFromChannelBuilder(channelBuilder)).isNull(); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()).isEqualTo("storage-direct.googleapis.com"); + } finally { + channel.shutdownNow(); + } + } + + @Test + void + createChannelBuilder_directPathOverInterconnect_storagePreprodSandboxEndpoint_rewritesTargetWithoutAuthorityOverride() + throws Exception { + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("storage-preprod.sandbox.googleapis.com:443") + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)) + .isEqualTo("google-c2p:///storage-preprod-direct.sandbox.googleapis.com?force-xds"); + Truth.assertThat(extractAuthorityFromChannelBuilder(channelBuilder)).isNull(); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()) + .isEqualTo("storage-preprod-direct.sandbox.googleapis.com"); + } finally { + channel.shutdownNow(); + } + } + + @Test + void + createChannelBuilder_directPathOverInterconnect_bigtableEndpoint_rewritesTargetWithoutAuthorityOverride() + throws Exception { + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + // Test with bigtable.googleapis.com:443 (rewritten to bigtable-direct) + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("bigtable.googleapis.com:443") + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)) + .isEqualTo("google-c2p:///bigtable-direct.googleapis.com?force-xds"); + Truth.assertThat(extractAuthorityFromChannelBuilder(channelBuilder)).isNull(); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()).isEqualTo("bigtable-direct.googleapis.com"); + } finally { + channel.shutdownNow(); + } + + // Also test if endpoint already has -direct infix + InstantiatingGrpcChannelProvider directProvider = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("bigtable-direct.googleapis.com:443") + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + ManagedChannelBuilder directChannelBuilder = directProvider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(directChannelBuilder)) + .isEqualTo("google-c2p:///bigtable-direct.googleapis.com?force-xds"); + Truth.assertThat(extractAuthorityFromChannelBuilder(directChannelBuilder)).isNull(); + ManagedChannel directChannel = (ManagedChannel) directChannelBuilder.build(); + try { + Truth.assertThat(directChannel.authority()).isEqualTo("bigtable-direct.googleapis.com"); + } finally { + directChannel.shutdownNow(); + } + } + + @Test + void createChannelBuilder_directPathXdsOnGce_doesNotRewriteHost() throws Exception { + System.setProperty("os.name", "Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXds() + .setAttemptDirectPathXdsOverInterconnect(false) + .setCredentials(computeEngineCredentials) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)); + + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isTrue(); + + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)) + .isEqualTo("google-c2p:///storage.googleapis.com"); + Truth.assertThat(extractAuthorityFromChannelBuilder(channelBuilder)).isNull(); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()).isEqualTo("storage.googleapis.com"); + } finally { + channel.shutdownNow(); + } + } + + @Test + void canUseDirectPath_interconnectEnabledViaEnvVar_bypassesGceCheckAndRewritesTarget() + throws Exception { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + when(envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT)) + .thenReturn("true"); + + // 1. Builder option not set (null) + InstantiatingGrpcChannelProvider.Builder unsetBuilder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider unsetProvider = + new InstantiatingGrpcChannelProvider(unsetBuilder, "not-gce-product-name"); + + Truth.assertThat(unsetProvider.isDirectPathXdsEnabled()).isTrue(); + Truth.assertThat(unsetProvider.canUseDirectPath()).isTrue(); + ManagedChannelBuilder unsetChannelBuilder = unsetProvider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(unsetChannelBuilder)) + .isEqualTo("google-c2p:///storage-direct.googleapis.com?force-xds"); + ManagedChannel unsetChannel = (ManagedChannel) unsetChannelBuilder.build(); + try { + Truth.assertThat(unsetChannel.authority()).isEqualTo("storage-direct.googleapis.com"); + } finally { + unsetChannel.shutdownNow(); + } + + // 2. Builder option explicitly false (simulating GrpcStorageOptions default), env var overrides + when(envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT)) + .thenReturn(" TRUE "); + InstantiatingGrpcChannelProvider.Builder falseBuilder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(false) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider falseProvider = + new InstantiatingGrpcChannelProvider(falseBuilder, "not-gce-product-name"); + + Truth.assertThat(falseProvider.isDirectPathXdsEnabled()).isTrue(); + Truth.assertThat(falseProvider.canUseDirectPath()).isTrue(); + ManagedChannelBuilder falseChannelBuilder = falseProvider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(falseChannelBuilder)) + .isEqualTo("google-c2p:///storage-direct.googleapis.com?force-xds"); + ManagedChannel falseChannel = (ManagedChannel) falseChannelBuilder.build(); + try { + Truth.assertThat(falseChannel.authority()).isEqualTo("storage-direct.googleapis.com"); + } finally { + falseChannel.shutdownNow(); + } + } + + @Test + void + canUseDirectPath_interconnectDisabledViaEnvVar_overridesBuilderOptionAndFallsBackToCloudPath() + throws Exception { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + when(envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT)) + .thenReturn("false"); + + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + + Truth.assertThat(provider.isDirectPathXdsEnabled()).isFalse(); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + ManagedChannelBuilder channelBuilder = provider.createChannelBuilder(); + Truth.assertThat(extractTargetFromChannelBuilder(channelBuilder)) + .isEqualTo("storage.googleapis.com:443"); + ManagedChannel channel = (ManagedChannel) channelBuilder.build(); + try { + Truth.assertThat(channel.authority()).isEqualTo("storage.googleapis.com:443"); + } finally { + channel.shutdownNow(); + } + } + + @Test + void canUseDirectPath_interconnectEnabledViaEnvVar_respectsDirectPathKillSwitch() { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT)) + .thenReturn("true"); + + // Case 1: setAttemptDirectPath(false) + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + InstantiatingGrpcChannelProvider.Builder disabledOptionBuilder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(false) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider disabledOptionProvider = + new InstantiatingGrpcChannelProvider(disabledOptionBuilder, "not-gce-product-name"); + Truth.assertThat(disabledOptionProvider.canUseDirectPath()).isFalse(); + + // Case 2: GOOGLE_CLOUD_DISABLE_DIRECT_PATH=true + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("true"); + InstantiatingGrpcChannelProvider.Builder disabledEnvBuilder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider disabledEnvProvider = + new InstantiatingGrpcChannelProvider(disabledEnvBuilder, "not-gce-product-name"); + Truth.assertThat(disabledEnvProvider.canUseDirectPath()).isFalse(); + } + + @Test + void canUseDirectPath_interconnectInvalidOrEmptyEnvVar_fallsBackToBuilderOption() { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = + mock(EnvironmentProvider.class, withSettings().withoutAnnotations()); + when(envProvider.getenv(InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + + for (String envVal : new String[] {"", " ", "invalid", "1", "0"}) { + when(envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_ENABLE_XDS_OVER_INTERCONNECT)) + .thenReturn(envVal); + + InstantiatingGrpcChannelProvider.Builder enabledBuilder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(true) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider enabledProvider = + new InstantiatingGrpcChannelProvider(enabledBuilder, "not-gce-product-name"); + Truth.assertThat(enabledProvider.canUseDirectPath()).isTrue(); + + InstantiatingGrpcChannelProvider.Builder disabledBuilder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCertificateBasedAccess(certificateBasedAccess) + .setAttemptDirectPath(true) + .setAttemptDirectPathXdsOverInterconnect(false) + .setEndpoint("storage.googleapis.com:443") + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider disabledProvider = + new InstantiatingGrpcChannelProvider(disabledBuilder, "not-gce-product-name"); + Truth.assertThat(disabledProvider.canUseDirectPath()).isFalse(); + } + } + @Test void createChannel_whenDirectPathEnabled_ignoresWorkloadCertPath() throws Exception { System.setProperty("os.name", "Linux"); @@ -1507,4 +2166,66 @@ List getAllMessages() { return records.stream().map(LogRecord::getMessage).collect(Collectors.toList()); } } + + private static String extractAuthorityFromChannelBuilder( + ManagedChannelBuilder channelBuilder) { + try { + Object current = channelBuilder; + while (current != null) { + Class clazz = current.getClass(); + java.lang.reflect.Field field = null; + while (clazz != null) { + try { + field = clazz.getDeclaredField("authority"); + break; + } catch (NoSuchFieldException e) { + try { + field = clazz.getDeclaredField("authorityOverride"); + break; + } catch (NoSuchFieldException e2) { + try { + field = clazz.getDeclaredField("overrideAuthority"); + break; + } catch (Exception ignored) { + // Expected if neither field exists on this class; continue scanning superclasses. + } + } + clazz = clazz.getSuperclass(); + } + } + if (field != null) { + field.setAccessible(true); + Object val = field.get(current); + if (val instanceof String && !((String) val).isEmpty()) { + return (String) val; + } + } + Class unwrapClass = current.getClass(); + java.lang.reflect.Field delegateField = null; + while (unwrapClass != null && delegateField == null) { + try { + delegateField = unwrapClass.getDeclaredField("delegate"); + } catch (NoSuchFieldException e) { + try { + delegateField = unwrapClass.getDeclaredField("managedChannelImplBuilder"); + } catch (NoSuchFieldException e2) { + unwrapClass = unwrapClass.getSuperclass(); + } + } + } + if (delegateField != null) { + delegateField.setAccessible(true); + Object next = delegateField.get(current); + if (next != null && next != current) { + current = next; + continue; + } + } + break; + } + return null; + } catch (Exception e) { + throw new RuntimeException(e); + } + } }