Skip to content

Commit 23147be

Browse files
committed
feat(storage): add support for DirectPath over Interconnect
1 parent 50ebc70 commit 23147be

7 files changed

Lines changed: 805 additions & 97 deletions

File tree

java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ public final class GrpcStorageOptions extends StorageOptions
134134
private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control";
135135
private static final Set<String> SCOPES = ImmutableSet.of(GCS_SCOPE);
136136
private static final String DEFAULT_HOST = "https://storage.googleapis.com";
137+
private static final String DEFAULT_HOST_DIRECT_PATH = "https://storage-direct.googleapis.com";
138+
private static final String DEFAULT_HOST_NO_SCHEME = "storage.googleapis.com";
139+
private static final String DEFAULT_HOST_DIRECT_PATH_NO_SCHEME = "storage-direct.googleapis.com";
137140
// If true, disable the bound-token-by-default feature for DirectPath.
138141
private static final boolean DIRECT_PATH_BOUND_TOKEN_DISABLED =
139142
Boolean.parseBoolean(
@@ -142,6 +145,7 @@ public final class GrpcStorageOptions extends StorageOptions
142145
private final GrpcRetryAlgorithmManager retryAlgorithmManager;
143146
private final java.time.Duration terminationAwaitDuration;
144147
private final boolean attemptDirectPath;
148+
private final boolean attemptDirectPathXdsOverInterconnect;
145149
private final boolean enableGrpcClientMetrics;
146150

147151
private final boolean grpcClientMetricsManuallyEnabled;
@@ -160,6 +164,7 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults)
160164
builder.terminationAwaitDuration,
161165
serviceDefaults.getTerminationAwaitDurationJavaTime());
162166
this.attemptDirectPath = builder.attemptDirectPath;
167+
this.attemptDirectPathXdsOverInterconnect = builder.attemptDirectPathXdsOverInterconnect;
163168
this.enableGrpcClientMetrics = builder.enableGrpcClientMetrics;
164169
this.grpcClientMetricsManuallyEnabled = builder.grpcMetricsManuallyEnabled;
165170
this.grpcInterceptorProvider = builder.grpcInterceptorProvider;
@@ -197,6 +202,23 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
197202
this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry();
198203
}
199204

205+
private static String rewriteHost(String endpoint, String oldHost, String newHost) {
206+
String prefix = "";
207+
String rest = endpoint;
208+
int schemeIndex = endpoint.indexOf("://");
209+
if (schemeIndex >= 0) {
210+
prefix = endpoint.substring(0, schemeIndex + 3);
211+
rest = endpoint.substring(schemeIndex + 3);
212+
}
213+
if (rest.startsWith(oldHost)) {
214+
int len = oldHost.length();
215+
if (rest.length() == len || rest.charAt(len) == ':' || rest.charAt(len) == '/') {
216+
return prefix + newHost + rest.substring(len);
217+
}
218+
}
219+
return endpoint;
220+
}
221+
200222
/**
201223
* We have to perform several introspections and detections to cross-wire/support several features
202224
* that are either gapic primitives, ServiceOption primitives or GCS semantic requirements.
@@ -230,6 +252,9 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
230252
*/
231253
private Tuple<StorageSettings, Opts<UserProject>> resolveSettingsAndOpts() throws IOException {
232254
String endpoint = getHost();
255+
if (attemptDirectPathXdsOverInterconnect) {
256+
endpoint = rewriteHost(endpoint, "storage.googleapis.com", "storage-direct.googleapis.com");
257+
}
233258
URI uri = URI.create(endpoint);
234259
String scheme = uri.getScheme();
235260
int port = uri.getPort();
@@ -322,7 +347,8 @@ private Tuple<StorageSettings, Opts<UserProject>> resolveSettingsAndOpts() throw
322347
InstantiatingGrpcChannelProvider.newBuilder()
323348
.setEndpoint(endpoint)
324349
.setAllowNonDefaultServiceAccount(true)
325-
.setAttemptDirectPath(attemptDirectPath);
350+
.setAttemptDirectPath(attemptDirectPath || attemptDirectPathXdsOverInterconnect)
351+
.setAttemptDirectPathXdsOverInterconnect(attemptDirectPathXdsOverInterconnect);
326352

327353
if (!DIRECT_PATH_BOUND_TOKEN_DISABLED) {
328354
channelProviderBuilder.setAllowHardBoundTokenTypes(
@@ -337,6 +363,18 @@ private Tuple<StorageSettings, Opts<UserProject>> resolveSettingsAndOpts() throw
337363
channelProviderBuilder.setAttemptDirectPathXds();
338364
}
339365

366+
if (attemptDirectPathXdsOverInterconnect) {
367+
com.google.api.core.ApiFunction<ManagedChannelBuilder, ManagedChannelBuilder>
368+
existingConfigurator = channelProviderBuilder.getChannelConfigurator();
369+
channelProviderBuilder.setChannelConfigurator(
370+
channelBuilder -> {
371+
if (existingConfigurator != null) {
372+
channelBuilder = existingConfigurator.apply(channelBuilder);
373+
}
374+
return channelBuilder.overrideAuthority("storage.googleapis.com");
375+
});
376+
}
377+
340378
if (scheme.equals("http")) {
341379
channelProviderBuilder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
342380
}
@@ -428,6 +466,7 @@ public int hashCode() {
428466
retryAlgorithmManager,
429467
terminationAwaitDuration,
430468
attemptDirectPath,
469+
attemptDirectPathXdsOverInterconnect,
431470
enableGrpcClientMetrics,
432471
grpcInterceptorProvider,
433472
blobWriteSessionConfig,
@@ -445,6 +484,7 @@ public boolean equals(Object o) {
445484
}
446485
GrpcStorageOptions that = (GrpcStorageOptions) o;
447486
return attemptDirectPath == that.attemptDirectPath
487+
&& attemptDirectPathXdsOverInterconnect == that.attemptDirectPathXdsOverInterconnect
448488
&& enableGrpcClientMetrics == that.enableGrpcClientMetrics
449489
&& Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager)
450490
&& Objects.equals(terminationAwaitDuration, that.terminationAwaitDuration)
@@ -494,6 +534,7 @@ public static final class Builder extends StorageOptions.Builder {
494534
private StorageRetryStrategy storageRetryStrategy;
495535
private java.time.Duration terminationAwaitDuration;
496536
private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath();
537+
private boolean attemptDirectPathXdsOverInterconnect = false;
497538
private boolean enableGrpcClientMetrics =
498539
GrpcStorageDefaults.INSTANCE.isEnableGrpcClientMetrics();
499540
private GrpcInterceptorProvider grpcInterceptorProvider =
@@ -512,6 +553,7 @@ public static final class Builder extends StorageOptions.Builder {
512553
this.storageRetryStrategy = gso.getRetryAlgorithmManager().retryStrategy;
513554
this.terminationAwaitDuration = gso.getTerminationAwaitDuration();
514555
this.attemptDirectPath = gso.attemptDirectPath;
556+
this.attemptDirectPathXdsOverInterconnect = gso.attemptDirectPathXdsOverInterconnect;
515557
this.enableGrpcClientMetrics = gso.enableGrpcClientMetrics;
516558
this.grpcInterceptorProvider = gso.grpcInterceptorProvider;
517559
this.blobWriteSessionConfig = gso.blobWriteSessionConfig;
@@ -556,6 +598,18 @@ public GrpcStorageOptions.Builder setAttemptDirectPath(boolean attemptDirectPath
556598
return this;
557599
}
558600

601+
/**
602+
* Option for whether this client should attempt to use DirectPath over Interconnect (on-premise
603+
* xDS name resolution).
604+
*
605+
* @since 2.45.0
606+
*/
607+
public GrpcStorageOptions.Builder setAttemptDirectPathXdsOverInterconnect(
608+
boolean attemptDirectPathXdsOverInterconnect) {
609+
this.attemptDirectPathXdsOverInterconnect = attemptDirectPathXdsOverInterconnect;
610+
return this;
611+
}
612+
559613
/**
560614
* Option for whether this client should emit internal gRPC client internal metrics to Cloud
561615
* Monitoring. To disable metric reporting, set this to false. True by default. Emitting metrics

java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ public void grpc() throws Exception {
6969
() -> assertThat(rebuilt.hashCode()).isEqualTo(base.hashCode()));
7070
}
7171

72+
@Test
73+
public void grpc_attemptDirectPathXdsOverInterconnect() throws Exception {
74+
com.google.auth.Credentials mockCreds = com.google.cloud.NoCredentials.getInstance();
75+
GrpcStorageOptions options =
76+
GrpcStorageOptions.grpc()
77+
.setCredentials(mockCreds)
78+
.setAttemptDirectPathXdsOverInterconnect(true)
79+
.build();
80+
81+
GrpcStorageOptions rebuilt = options.toBuilder().build();
82+
assertAll(
83+
() -> assertThat(rebuilt).isEqualTo(options),
84+
() -> assertThat(rebuilt.hashCode()).isEqualTo(options.hashCode()));
85+
86+
com.google.storage.v2.StorageSettings settings = options.getStorageSettings();
87+
assertThat(settings.getEndpoint()).isEqualTo("storage-direct.googleapis.com:443");
88+
89+
com.google.api.gax.rpc.TransportChannelProvider tcp = settings.getTransportChannelProvider();
90+
assertThat(tcp).isInstanceOf(com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.class);
91+
com.google.api.gax.grpc.InstantiatingGrpcChannelProvider provider =
92+
(com.google.api.gax.grpc.InstantiatingGrpcChannelProvider) tcp;
93+
94+
// Verify attemptDirectPathXdsOverInterconnect is set to true on the provider using reflection
95+
java.lang.reflect.Field field =
96+
com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.class.getDeclaredField(
97+
"attemptDirectPathXdsOverInterconnect");
98+
field.setAccessible(true);
99+
Boolean value = (Boolean) field.get(provider);
100+
assertThat(value).isTrue();
101+
}
102+
72103
@Test
73104
public void useJwtAccessWithScope_defaultsToFalse() {
74105
HttpStorageOptions httpOptions = HttpStorageOptions.http().build();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.storage.it;
18+
19+
import static org.junit.Assume.assumeTrue;
20+
21+
import com.google.cloud.storage.Storage;
22+
import com.google.cloud.storage.StorageOptions;
23+
import com.google.cloud.storage.TransportCompatibility.Transport;
24+
import com.google.cloud.storage.it.runner.StorageITRunner;
25+
import com.google.cloud.storage.it.runner.annotations.Backend;
26+
import com.google.cloud.storage.it.runner.annotations.Inject;
27+
import com.google.cloud.storage.it.runner.annotations.SingleBackend;
28+
import com.google.cloud.storage.it.runner.annotations.StorageFixture;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
32+
@RunWith(StorageITRunner.class)
33+
@SingleBackend(Backend.PROD)
34+
public final class ITGrpcDirectPathTest {
35+
36+
@Inject
37+
@StorageFixture(Transport.GRPC)
38+
public Storage storage;
39+
40+
@Test
41+
public void clientShouldWork_directPathXdsOverInterconnect() throws Exception {
42+
assumeTrue(
43+
"Environment cannot resolve storage-direct.googleapis.com", canResolveDirectPathAddress());
44+
StorageOptions options =
45+
StorageOptions.grpc()
46+
.setCredentials(storage.getOptions().getCredentials())
47+
.setAttemptDirectPathXdsOverInterconnect(true)
48+
.setEnableGrpcClientMetrics(false)
49+
.build();
50+
try (Storage client = options.getService()) {
51+
client.list(Storage.BucketListOption.pageSize(1));
52+
}
53+
}
54+
55+
private static boolean canResolveDirectPathAddress() {
56+
try {
57+
java.net.InetAddress.getAllByName("storage-direct.googleapis.com");
58+
return true;
59+
} catch (java.net.UnknownHostException e) {
60+
return false;
61+
}
62+
}
63+
}

java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ public void clientShouldConstructCleanly_directPath() throws Exception {
8484
doTest(options);
8585
}
8686

87+
@Test
88+
public void clientShouldConstructCleanly_directPathXdsOverInterconnect() throws Exception {
89+
StorageOptions options =
90+
StorageOptions.grpc()
91+
.setCredentials(credentials)
92+
.setAttemptDirectPathXdsOverInterconnect(true)
93+
.setEnableGrpcClientMetrics(false)
94+
.build();
95+
doTest(options);
96+
}
97+
8798
@Test
8899
public void lackOfProjectIdDoesNotPreventConstruction_http() throws Exception {
89100
StorageOptions options = StorageOptions.http().setCredentials(credentials).build();

0 commit comments

Comments
 (0)