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..badbef34b7c3 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 @@ -134,6 +134,9 @@ public final class GrpcStorageOptions extends StorageOptions private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); private static final String DEFAULT_HOST = "https://storage.googleapis.com"; + private static final String DEFAULT_HOST_DIRECT_PATH = "https://storage-direct.googleapis.com"; + private static final String DEFAULT_HOST_NO_SCHEME = "storage.googleapis.com"; + private static final String DEFAULT_HOST_DIRECT_PATH_NO_SCHEME = "storage-direct.googleapis.com"; // If true, disable the bound-token-by-default feature for DirectPath. private static final boolean DIRECT_PATH_BOUND_TOKEN_DISABLED = Boolean.parseBoolean( @@ -142,6 +145,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 +164,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; @@ -230,6 +235,21 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE */ private Tuple> resolveSettingsAndOpts() throws IOException { String endpoint = getHost(); + if (attemptDirectPathXdsOverInterconnect) { + if (endpoint.startsWith(DEFAULT_HOST) + && (endpoint.length() == DEFAULT_HOST.length() + || endpoint.charAt(DEFAULT_HOST.length()) == ':' + || endpoint.charAt(DEFAULT_HOST.length()) == '/')) { + endpoint = DEFAULT_HOST_DIRECT_PATH + endpoint.substring(DEFAULT_HOST.length()); + } else if (endpoint.startsWith(DEFAULT_HOST_NO_SCHEME) + && (endpoint.length() == DEFAULT_HOST_NO_SCHEME.length() + || endpoint.charAt(DEFAULT_HOST_NO_SCHEME.length()) == ':' + || endpoint.charAt(DEFAULT_HOST_NO_SCHEME.length()) == '/')) { + endpoint = + DEFAULT_HOST_DIRECT_PATH_NO_SCHEME + + endpoint.substring(DEFAULT_HOST_NO_SCHEME.length()); + } + } URI uri = URI.create(endpoint); String scheme = uri.getScheme(); int port = uri.getPort(); @@ -322,7 +342,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( @@ -337,6 +358,18 @@ private Tuple> resolveSettingsAndOpts() throw channelProviderBuilder.setAttemptDirectPathXds(); } + if (attemptDirectPathXdsOverInterconnect) { + com.google.api.core.ApiFunction + existingConfigurator = channelProviderBuilder.getChannelConfigurator(); + channelProviderBuilder.setChannelConfigurator( + channelBuilder -> { + if (existingConfigurator != null) { + channelBuilder = existingConfigurator.apply(channelBuilder); + } + return channelBuilder.overrideAuthority("storage.googleapis.com"); + }); + } + if (scheme.equals("http")) { channelProviderBuilder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext); } @@ -428,6 +461,7 @@ public int hashCode() { retryAlgorithmManager, terminationAwaitDuration, attemptDirectPath, + attemptDirectPathXdsOverInterconnect, enableGrpcClientMetrics, grpcInterceptorProvider, blobWriteSessionConfig, @@ -445,6 +479,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 +529,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 +548,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 +593,18 @@ 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). + * + * @since 2.45.0 + */ + 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..ec979b348803 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,37 @@ 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-direct.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; + + // Verify attemptDirectPathXdsOverInterconnect is set to true on the provider using reflection + java.lang.reflect.Field field = + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.class.getDeclaredField( + "attemptDirectPathXdsOverInterconnect"); + field.setAccessible(true); + Boolean value = (Boolean) field.get(provider); + assertThat(value).isTrue(); + } + @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/ITStorageOptionsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java index 0d8114a7fb2d..f33ae01c45c4 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,32 @@ 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 clientShouldWork_directPathXdsOverInterconnect() throws Exception { + assumeTrue( + "Environment cannot resolve storage.direct.googleapis.com", canResolveDirectPathAddress()); + StorageOptions options = + StorageOptions.grpc() + .setCredentials(credentials) + .setAttemptDirectPathXdsOverInterconnect(true) + .setEnableGrpcClientMetrics(false) + .build(); + try (Storage storage = options.getService()) { + storage.list(Storage.BucketListOption.pageSize(1)); + } + } + @Test public void lackOfProjectIdDoesNotPreventConstruction_http() throws Exception { StorageOptions options = StorageOptions.http().setCredentials(credentials).build(); @@ -105,4 +131,13 @@ private static void doTest(StorageOptions options) throws Exception { //noinspection EmptyTryBlock try (Storage ignore = options.getService()) {} } + + 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/sdk-platform-java/gax-java/gax-grpc/pom.xml b/sdk-platform-java/gax-java/gax-grpc/pom.xml index 0969dd07fd19..e8de1c8d58bb 100644 --- a/sdk-platform-java/gax-java/gax-grpc/pom.xml +++ b/sdk-platform-java/gax-java/gax-grpc/pom.xml @@ -162,7 +162,7 @@ maven-surefire-plugin - !InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfig_AttemptDirectPathNotSetAndAttemptDirectPathXdsSetViaEnv_warns,!InstantiatingGrpcChannelProviderTest#canUseDirectPath_directPathEnvVarNotSet_attemptDirectPathIsTrue,InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfigWrongCredential + !InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfig_AttemptDirectPathNotSetAndAttemptDirectPathXdsSetViaEnv_warns,!InstantiatingGrpcChannelProviderTest#canUseDirectPath_directPathEnvVarNotSet_attemptDirectPathIsTrue 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 ac42396f006a..d38c5dd66a68 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 @@ -147,13 +147,14 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP private final java.time.@Nullable Duration keepAliveTimeout; private final @Nullable Boolean keepAliveWithoutCalls; private final ChannelPoolSettings channelPoolSettings; - private final @Nullable Credentials credentials; - private final @Nullable CallCredentials altsCallCredentials; - private final @Nullable CallCredentials mtlsS2ACallCredentials; - private final @Nullable ChannelPrimer channelPrimer; - private final @Nullable Boolean attemptDirectPath; - private final @Nullable Boolean attemptDirectPathXds; - private final @Nullable Boolean allowNonDefaultServiceAccount; + @Nullable private final Credentials credentials; + @Nullable private final CallCredentials altsCallCredentials; + @Nullable private final CallCredentials mtlsS2ACallCredentials; + @Nullable private final ChannelPrimer channelPrimer; + @Nullable private final Boolean attemptDirectPath; + @Nullable private final Boolean attemptDirectPathXds; + @Nullable private final Boolean attemptDirectPathXdsOverInterconnect; + @Nullable private final Boolean allowNonDefaultServiceAccount; @VisibleForTesting final ImmutableMap directPathServiceConfig; private final @Nullable MtlsProvider mtlsProvider; private final CertificateBasedAccess certificateBasedAccess; @@ -236,6 +237,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 @@ -433,6 +435,10 @@ private boolean isDirectPathXdsEnabledViaEnv() { return Boolean.parseBoolean(directPathXdsEnv); } + private boolean isAttemptDirectPathXdsOverInterconnect() { + 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 @@ -486,7 +492,7 @@ else if (isDirectPathXdsEnabledViaBuilderOption()) { + " ."); } // Case 4: not running on GCE - if (!isOnComputeEngine()) { + if (!isOnComputeEngine() && !isAttemptDirectPathXdsOverInterconnect()) { LOG.log( level, "DirectPath is misconfigured. DirectPath is only available in a GCE environment."); @@ -500,6 +506,10 @@ boolean isCredentialDirectPathCompatible() { if (needsCredentials()) { return false; } + // xDS over Interconnect is designed to work on-premise using arbitrary service credentials. + if (isAttemptDirectPathXdsOverInterconnect()) { + return true; + } if (allowNonDefaultServiceAccount != null && allowNonDefaultServiceAccount) { return true; } @@ -707,75 +717,108 @@ ChannelCredentials createS2ASecuredChannelCredentials() { return s2aChannelCredentials; } - @InternalApi("For internal use by google-cloud-java clients only") - public ManagedChannelBuilder createChannelBuilder() throws IOException { - int colon = endpoint.lastIndexOf(':'); - if (colon < 0) { - throw new IllegalStateException("invalid endpoint - should have been validated: " + endpoint); + private ChannelCredentials getGoogleDefaultChannelCredentials() { + GoogleDefaultChannelCredentials.Builder builder = GoogleDefaultChannelCredentials.newBuilder(); + if (credentials != null) { + builder.callCredentials(MoreCallCredentials.from(credentials)); + } + if (altsCallCredentials != null) { + builder.altsCallCredentials(altsCallCredentials); } - int port = Integer.parseInt(endpoint.substring(colon + 1)); - String serviceAddress = endpoint.substring(0, colon); + return builder.build(); + } + @InternalApi("For internal use by google-cloud-java clients only") + public ManagedChannelBuilder createChannelBuilder() throws IOException { ManagedChannelBuilder builder; - - // 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(); - 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); - } else { - builder = Grpc.newChannelBuilderForAddress(serviceAddress, port, channelCreds); - builder.defaultServiceConfig(directPathServiceConfig); + String resolvedTarget; + + // If the endpoint is already a custom URI scheme target (e.g. google-c2p:///), use it directly. + if (endpoint.contains(":///")) { + ChannelCredentials channelCreds = getGoogleDefaultChannelCredentials(); + builder = Grpc.newChannelBuilder(endpoint, channelCreds); + resolvedTarget = endpoint; + if (endpoint.startsWith("google-c2p:///")) { + useDirectPathXds = true; + // Set default keepAliveTime and keepAliveTimeout when directpath environment is enabled. + // Will be overridden by user defined values if any. + builder.keepAliveTime(DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS, TimeUnit.SECONDS); + builder.keepAliveTimeout(DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } - // Set default keepAliveTime and keepAliveTimeout when directpath environment is enabled. - // Will be overridden by user defined values if any. - builder.keepAliveTime(DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS, TimeUnit.SECONDS); - builder.keepAliveTimeout(DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } else { - ChannelCredentials channelCredentials; - try { - // Try and create credentials via DCA. See https://google.aip.dev/auth/4114. - channelCredentials = createMtlsChannelCredentials(); - } catch (GeneralSecurityException e) { - throw new IOException(e); + int colon = endpoint.lastIndexOf(':'); + if (colon < 0) { + throw new IllegalStateException( + "invalid endpoint - should have been validated: " + endpoint); } - if (channelCredentials != null) { - // Create the channel using channel credentials created via DCA. - builder = Grpc.newChannelBuilder(endpoint, channelCredentials); + int port = Integer.parseInt(endpoint.substring(colon + 1)); + String serviceAddress = endpoint.substring(0, colon); + + // Check DirectPath traffic. + if (canUseDirectPath()) { + ChannelCredentials channelCreds = getGoogleDefaultChannelCredentials(); + useDirectPathXds = isDirectPathXdsEnabled() || isAttemptDirectPathXdsOverInterconnect(); + 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. + String target = "google-c2p:///" + serviceAddress; + if (isAttemptDirectPathXdsOverInterconnect()) { + target += "?force-xds"; + } + builder = Grpc.newChannelBuilder(target, channelCreds); + resolvedTarget = target; + } else { + builder = Grpc.newChannelBuilderForAddress(serviceAddress, port, channelCreds); + builder.defaultServiceConfig(directPathServiceConfig); + resolvedTarget = serviceAddress + ":" + port; + } + // Set default keepAliveTime and keepAliveTimeout when directpath environment is enabled. + // Will be overridden by user defined values if any. + builder.keepAliveTime(DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS, TimeUnit.SECONDS); + builder.keepAliveTimeout(DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } else { - // Could not create channel credentials via DCA. In accordance with - // https://google.aip.dev/auth/4115, if credentials not available through - // DCA, try mTLS with credentials held by the S2A (Secure Session Agent). - if (useS2A) { - channelCredentials = createS2ASecuredChannelCredentials(); + if (isDirectPathEnabled() || isAttemptDirectPathXdsOverInterconnect()) { + LOG.log( + Level.WARNING, + "DirectPath was requested but is not available. Falling back to CloudPath."); + } + ChannelCredentials channelCredentials; + try { + // Try and create credentials via DCA. See https://google.aip.dev/auth/4114. + channelCredentials = createMtlsChannelCredentials(); + } catch (GeneralSecurityException e) { + throw new IOException(e); } if (channelCredentials != null) { - // Create the channel using S2A-secured channel credentials. - if (mtlsS2ACallCredentials != null) { - // Set {@code mtlsS2ACallCredentials} to be per-RPC call credentials, - // which will be used to fetch MTLS_S2A hard bound tokens from the metdata server. - channelCredentials = - CompositeChannelCredentials.create(channelCredentials, mtlsS2ACallCredentials); - } - // Connect to the MTLS endpoint when using S2A because S2A is used to perform an MTLS - // handshake. - builder = Grpc.newChannelBuilder(mtlsEndpoint, channelCredentials); + // Create the channel using channel credentials created via DCA. + builder = Grpc.newChannelBuilder(endpoint, channelCredentials); + resolvedTarget = endpoint; } else { - // Use default if we cannot initialize channel credentials via DCA or S2A. - builder = ManagedChannelBuilder.forAddress(serviceAddress, port); + // Could not create channel credentials via DCA. In accordance with + // https://google.aip.dev/auth/4115, if credentials not available through + // DCA, try mTLS with credentials held by the S2A (Secure Session Agent). + if (useS2A) { + channelCredentials = createS2ASecuredChannelCredentials(); + } + if (channelCredentials != null) { + // Create the channel using S2A-secured channel credentials. + if (mtlsS2ACallCredentials != null) { + // Set {@code mtlsS2ACallCredentials} to be per-RPC call credentials, + // which will be used to fetch MTLS_S2A hard bound tokens from the metdata server. + channelCredentials = + CompositeChannelCredentials.create(channelCredentials, mtlsS2ACallCredentials); + } + // Connect to the MTLS endpoint when using S2A because S2A is used to perform an MTLS + // handshake. + builder = Grpc.newChannelBuilder(mtlsEndpoint, channelCredentials); + resolvedTarget = mtlsEndpoint; + } else { + // Use default if we cannot initialize channel credentials via DCA or S2A. + builder = ManagedChannelBuilder.forAddress(serviceAddress, port); + resolvedTarget = serviceAddress + ":" + port; + } } } } @@ -784,6 +827,7 @@ public ManagedChannelBuilder createChannelBuilder() throws IOException { // See https://github.com/googleapis/gapic-generator/issues/2816 builder.disableServiceConfigLookUp(); } + LOG.log(Level.INFO, "Channel initialized with target {0}", resolvedTarget); return builder; } @@ -866,13 +910,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() @@ -966,10 +1014,11 @@ public static final class Builder { private @Nullable CallCredentials mtlsS2ACallCredentials; private @Nullable ChannelPrimer channelPrimer; private ChannelPoolSettings channelPoolSettings; - private @Nullable Boolean attemptDirectPath; - private @Nullable Boolean attemptDirectPathXds; - private @Nullable Boolean allowNonDefaultServiceAccount; - private @Nullable ImmutableMap directPathServiceConfig; + @Nullable private Boolean attemptDirectPath; + @Nullable private Boolean attemptDirectPathXds; + @Nullable private Boolean attemptDirectPathXdsOverInterconnect; + @Nullable private Boolean allowNonDefaultServiceAccount; + @Nullable private ImmutableMap directPathServiceConfig; private List allowedHardBoundTokenTypes; private Builder() { @@ -1001,6 +1050,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; @@ -1308,6 +1358,14 @@ public Builder setAttemptDirectPathXds() { return this; } + /** Use DirectPath xDS over Interconnect. Bypasses GCP GCE environment checks. */ + @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; @@ -1462,11 +1520,22 @@ public Builder setChannelConfigurator( } private static void validateEndpoint(String endpoint) { + if (endpoint.contains(":///")) { + try { + java.net.URI.create(endpoint); + return; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid endpoint URI: " + endpoint, e); + } + } int colon = endpoint.lastIndexOf(':'); if (colon < 0) { - throw new IllegalArgumentException( - String.format("invalid endpoint, expecting \":\"")); + throw new IllegalArgumentException("invalid endpoint, expecting \":\""); + } + try { + Integer.parseInt(endpoint.substring(colon + 1)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid endpoint, expecting \":\"", e); } - Integer.parseInt(endpoint.substring(colon + 1)); } } diff --git a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java index fad4cd468b95..c93db599d575 100644 --- a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java +++ b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java @@ -32,7 +32,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -83,7 +82,7 @@ void testInterceptor_basic() { void testInterceptor_responseListener() { when(channel.newCall(Mockito.>any(), any(CallOptions.class))) .thenReturn(call); - GrpcLoggingInterceptor interceptor = spy(new GrpcLoggingInterceptor()); + GrpcLoggingInterceptor interceptor = new GrpcLoggingInterceptor(); Channel intercepted = ClientInterceptors.intercept(channel, interceptor); @SuppressWarnings("unchecked") ClientCall.Listener listener = mock(ClientCall.Listener.class); 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 be0365866615..5f92259b65e8 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; @@ -129,17 +131,41 @@ void testEndpoint() { } @Test - void testEndpointNoPort() { + void testEndpointCustomUriScheme() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setEndpoint("google-c2p:///storage.googleapis.com"); + assertEquals("google-c2p:///storage.googleapis.com", builder.getEndpoint()); + } + + @Test + void testEndpointCustomUriSchemeInvalid() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder(); assertThrows( - IllegalArgumentException.class, - () -> InstantiatingGrpcChannelProvider.newBuilder().setEndpoint("localhost")); + IllegalArgumentException.class, () -> builder.setEndpoint("google-c2p://:invalid")); } @Test - void testEndpointBadPort() { + void testEndpointCustomUriSchemeMalformed() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder(); assertThrows( - IllegalArgumentException.class, - () -> InstantiatingGrpcChannelProvider.newBuilder().setEndpoint("localhost:abcd")); + IllegalArgumentException.class, () -> builder.setEndpoint("google-c2p:///foo bar")); + } + + @Test + void testEndpointNoPort() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder(); + assertThrows(IllegalArgumentException.class, () -> builder.setEndpoint("localhost")); + } + + @Test + void testEndpointBadPort() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder(); + assertThrows(IllegalArgumentException.class, () -> builder.setEndpoint("localhost:abcd")); } @Test @@ -707,15 +733,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) - .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(); @@ -735,16 +765,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(); @@ -924,6 +958,268 @@ 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_customResolverTargetUri_nonGoogleC2p_usesUriDirectly() + 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) + .setCredentials(credentials) + .setEndpoint("dns:///localhost:8080") + .setEnvProvider(envProvider) + .setChannelConfigurator(channelConfigurator); + + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + + InstantiatingGrpcChannelProvider configuredProvider = + (InstantiatingGrpcChannelProvider) + provider + .withHeaders(Collections.emptyMap()) + .withEndpoint("dns:///localhost:8080"); + + TransportChannel transportChannel = configuredProvider.getTransportChannel(); + transportChannel.shutdownNow(); + transportChannel.awaitTermination(5, TimeUnit.SECONDS); + + Truth.assertThat(capturedTarget.get()).isEqualTo("dns:///localhost:8080"); + } + + @Test + void testLogDirectPathFallbackWarning() 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() + .setAttemptDirectPath(true) + .setHeaderProvider(mock(HeaderProvider.class, withSettings().withoutAnnotations())) + .setExecutor(mock(Executor.class)) + .setEndpoint(DEFAULT_ENDPOINT) + .setCertificateBasedAccess(certificateBasedAccess) + .setEnvProvider(envProvider) + .build(); + + TransportChannel transportChannel = provider.getTransportChannel(); + + assertThat(logHandler.getAllMessages()) + .contains("DirectPath was requested but is not available. Falling back to CloudPath."); + InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + + transportChannel.close(); + transportChannel.awaitTermination(10, TimeUnit.SECONDS); + } + + @Test + public void getTransportChannel_attemptDirectPathXdsOverInterconnect_usesForceXdsTarget() + 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.googleapis.com?force-xds"); + } + + @Test + public void getTransportChannel_attemptDirectPathXdsOverInterconnect_nullCredentials() + 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.googleapis.com?force-xds"); + } + + @Test + public void getTransportChannel_customResolverTargetUri_usesUriDirectly() + 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) + .setCredentials(credentials) + .setEndpoint("google-c2p:///storage-direct.googleapis.com?force-xds") + .setEnvProvider(envProvider) + .setChannelConfigurator(channelConfigurator); + + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "not-gce-product-name"); + + InstantiatingGrpcChannelProvider configuredProvider = + (InstantiatingGrpcChannelProvider) + provider + .withHeaders(Collections.emptyMap()) + .withEndpoint("google-c2p:///storage-direct.googleapis.com?force-xds"); + + TransportChannel transportChannel = configuredProvider.getTransportChannel(); + transportChannel.shutdownNow(); + transportChannel.awaitTermination(5, TimeUnit.SECONDS); + + Truth.assertThat(capturedTarget.get()) + .isEqualTo("google-c2p:///storage-direct.googleapis.com?force-xds"); + } + @Test public void canUseDirectPath_isNotOnComputeEngine_invalidOsNameSystemProperty() { System.setProperty("os.name", "Not Linux"); @@ -1342,6 +1638,44 @@ 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) { + e.printStackTrace(); + } + return channelBuilder.toString(); + } + private static class FakeLogHandler extends Handler { List records = new ArrayList<>();