From 180e1282f85fd03fc97fbad9ad230064f918dbdf Mon Sep 17 00:00:00 2001 From: sagnghos Date: Sat, 22 Feb 2025 18:49:49 +0000 Subject: [PATCH 01/10] feat: default authentication support for external hosts --- .../google/cloud/spanner/SpannerOptions.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 5b63ff4fe44..b2bc8f239b5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -34,6 +34,8 @@ import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; import com.google.api.gax.tracing.OpencensusTracerFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.NoCredentials; import com.google.cloud.ServiceDefaults; import com.google.cloud.ServiceOptions; @@ -79,8 +81,11 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -92,6 +97,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; @@ -110,6 +116,9 @@ public class SpannerOptions extends ServiceOptions { private static final String API_SHORT_NAME = "Spanner"; private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; + private static final String CLOUD_SPANNER_HOST_FORMAT = ".*\\.googleapis\\.com.*"; + private static final Pattern CLOUD_SPANNER_HOST_PATTERN = + Pattern.compile(CLOUD_SPANNER_HOST_FORMAT); private static final ImmutableSet SCOPES = ImmutableSet.of( "https://www.googleapis.com/auth/spanner.admin", @@ -799,6 +808,18 @@ protected SpannerOptions(Builder builder) { enableBuiltInMetrics = builder.enableBuiltInMetrics; enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; + String externalHostTokenPath = System.getenv("EXTERNAL_HOST_AUTH_TOKEN"); + if (builder.isExternalHost && externalHostTokenPath != null) { + String token; + try { + token = + Base64.getEncoder() + .encodeToString(Files.readAllBytes(Paths.get(externalHostTokenPath))); + } catch (IOException e) { + throw SpannerExceptionFactory.newSpannerException(e); + } + credentials = new GoogleCredentials(new AccessToken(token, null)); + } } /** @@ -967,6 +988,7 @@ public static class Builder private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics(); private String monitoringHost = SpannerOptions.environment.getMonitoringHost(); private SslContext mTLSContext = null; + private boolean isExternalHost = false; private static String createCustomClientLibToken(String token) { return token + " " + ServiceOptions.getGoogApiClientLibName(); @@ -1459,6 +1481,9 @@ public Builder setDecodeMode(DecodeMode decodeMode) { @Override public Builder setHost(String host) { super.setHost(host); + if (this.emulatorHost == null && !CLOUD_SPANNER_HOST_PATTERN.matcher(host).matches()) { + this.isExternalHost = true; + } // Setting a host should override any SPANNER_EMULATOR_HOST setting. setEmulatorHost(null); return this; From 61ee038e29138d55b735f776209aabd79c4b7d1d Mon Sep 17 00:00:00 2001 From: sagnghos Date: Sun, 23 Feb 2025 03:14:23 +0000 Subject: [PATCH 02/10] unit test added --- .../java/com/google/cloud/spanner/SpannerOptions.java | 6 ++++-- .../java/com/google/cloud/spanner/SpannerOptionsTest.java | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index b2bc8f239b5..f1d86423d3f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -117,8 +117,10 @@ public class SpannerOptions extends ServiceOptions { private static final String API_SHORT_NAME = "Spanner"; private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; private static final String CLOUD_SPANNER_HOST_FORMAT = ".*\\.googleapis\\.com.*"; - private static final Pattern CLOUD_SPANNER_HOST_PATTERN = - Pattern.compile(CLOUD_SPANNER_HOST_FORMAT); + + @VisibleForTesting + static final Pattern CLOUD_SPANNER_HOST_PATTERN = Pattern.compile(CLOUD_SPANNER_HOST_FORMAT); + private static final ImmutableSet SCOPES = ImmutableSet.of( "https://www.googleapis.com/auth/spanner.admin", diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 70482c0ffdd..920659d44ed 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.SpannerOptions.CLOUD_SPANNER_HOST_PATTERN; import static com.google.common.truth.Truth.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -1164,4 +1165,11 @@ public void checkGlobalOpenTelemetryWhenNotInjected() { .build(); assertEquals(GlobalOpenTelemetry.get(), options.getOpenTelemetry()); } + + @Test + public void testCloudSpannerHostPattern() { + assertTrue(CLOUD_SPANNER_HOST_PATTERN.matcher("https://spanner.googleapis.com").matches()); + assertTrue(CLOUD_SPANNER_HOST_PATTERN.matcher("https://product-area.googleapis.com").matches()); + assertFalse(CLOUD_SPANNER_HOST_PATTERN.matcher("https://some-company.com").matches()); + } } From 004cea269e5b7ffc0f375dd58ed343ac2247f454 Mon Sep 17 00:00:00 2001 From: sagnghos Date: Sun, 23 Feb 2025 03:17:10 +0000 Subject: [PATCH 03/10] unit test typo --- .../java/com/google/cloud/spanner/SpannerOptionsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 920659d44ed..72bbdf82eae 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -1169,7 +1169,8 @@ public void checkGlobalOpenTelemetryWhenNotInjected() { @Test public void testCloudSpannerHostPattern() { assertTrue(CLOUD_SPANNER_HOST_PATTERN.matcher("https://spanner.googleapis.com").matches()); - assertTrue(CLOUD_SPANNER_HOST_PATTERN.matcher("https://product-area.googleapis.com").matches()); - assertFalse(CLOUD_SPANNER_HOST_PATTERN.matcher("https://some-company.com").matches()); + assertTrue( + CLOUD_SPANNER_HOST_PATTERN.matcher("https://product-area.googleapis.com:443").matches()); + assertFalse(CLOUD_SPANNER_HOST_PATTERN.matcher("https://some-company.com:443").matches()); } } From e65c55558c27399060d3b9119b7d9e8763079ad5 Mon Sep 17 00:00:00 2001 From: sagnghos Date: Mon, 24 Feb 2025 05:23:29 +0000 Subject: [PATCH 04/10] skip auth_token in emulator host for jdbc --- .../main/java/com/google/cloud/spanner/SpannerOptions.java | 3 +++ .../com/google/cloud/spanner/connection/SpannerPool.java | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index f1d86423d3f..b050533531b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -740,6 +740,9 @@ public static CloseableExecutorProvider createAsyncExecutorProvider( protected SpannerOptions(Builder builder) { super(SpannerFactory.class, SpannerRpcFactory.class, builder, new SpannerDefaults()); + if (builder.emulatorHost != null) { + builder.setHost(builder.emulatorHost); + } numChannels = builder.numChannels == null ? DEFAULT_CHANNELS : builder.numChannels; Preconditions.checkArgument( numChannels >= 1 && numChannels <= MAX_CHANNELS, diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index 9558947156c..6fb0843ec40 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -363,10 +363,14 @@ private void initialize() { @VisibleForTesting Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { ConnectionSpannerOptions.Builder builder = ConnectionSpannerOptions.newBuilder(); + if (options.usesEmulator()) { + builder.setEmulatorHost(key.host); + } else { + builder.setHost(key.host); + } builder .setUseVirtualThreads(key.useVirtualGrpcTransportThreads) .setClientLibToken(MoreObjects.firstNonNull(key.userAgent, CONNECTION_API_CLIENT_LIB_TOKEN)) - .setHost(key.host) .setProjectId(key.projectId) // Use lazy decoding, so we can use the protobuf values for calculating the checksum that is // needed for read/write transactions. From 2aa18db94182d569541c725b2684c64c6e3ccdc5 Mon Sep 17 00:00:00 2001 From: sagnghos Date: Mon, 24 Feb 2025 05:42:45 +0000 Subject: [PATCH 05/10] skip auth_token for emulator --- .../java/com/google/cloud/spanner/SpannerOptions.java | 5 +---- .../com/google/cloud/spanner/connection/SpannerPool.java | 9 ++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index b050533531b..ba450c9b70b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -740,9 +740,6 @@ public static CloseableExecutorProvider createAsyncExecutorProvider( protected SpannerOptions(Builder builder) { super(SpannerFactory.class, SpannerRpcFactory.class, builder, new SpannerDefaults()); - if (builder.emulatorHost != null) { - builder.setHost(builder.emulatorHost); - } numChannels = builder.numChannels == null ? DEFAULT_CHANNELS : builder.numChannels; Preconditions.checkArgument( numChannels >= 1 && numChannels <= MAX_CHANNELS, @@ -814,7 +811,7 @@ protected SpannerOptions(Builder builder) { enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; String externalHostTokenPath = System.getenv("EXTERNAL_HOST_AUTH_TOKEN"); - if (builder.isExternalHost && externalHostTokenPath != null) { + if (builder.isExternalHost && builder.emulatorHost == null && externalHostTokenPath != null) { String token; try { token = diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index 6fb0843ec40..dedc2a2deed 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -363,14 +363,10 @@ private void initialize() { @VisibleForTesting Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { ConnectionSpannerOptions.Builder builder = ConnectionSpannerOptions.newBuilder(); - if (options.usesEmulator()) { - builder.setEmulatorHost(key.host); - } else { - builder.setHost(key.host); - } builder .setUseVirtualThreads(key.useVirtualGrpcTransportThreads) .setClientLibToken(MoreObjects.firstNonNull(key.userAgent, CONNECTION_API_CLIENT_LIB_TOKEN)) + .setHost(key.host) .setProjectId(key.projectId) // Use lazy decoding, so we can use the protobuf values for calculating the checksum that is // needed for read/write transactions. @@ -411,6 +407,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { if (options.getConfigurator() != null) { options.getConfigurator().configure(builder); } + if (options.usesEmulator()) { + builder.setEmulatorHost(key.host); + } return builder.build().getService(); } From c33ae1b186832d07676545f5d9dd1499fe582a84 Mon Sep 17 00:00:00 2001 From: sagnghos Date: Mon, 24 Feb 2025 07:07:49 +0000 Subject: [PATCH 06/10] env variable prefix change --- .../src/main/java/com/google/cloud/spanner/SpannerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index ba450c9b70b..149dc49d87e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -810,7 +810,7 @@ protected SpannerOptions(Builder builder) { enableBuiltInMetrics = builder.enableBuiltInMetrics; enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; - String externalHostTokenPath = System.getenv("EXTERNAL_HOST_AUTH_TOKEN"); + String externalHostTokenPath = System.getenv("SPANNER_EXTERNAL_HOST_AUTH_TOKEN"); if (builder.isExternalHost && builder.emulatorHost == null && externalHostTokenPath != null) { String token; try { From 996e26ff73680eda31b1998cb9d38eec1eeb8300 Mon Sep 17 00:00:00 2001 From: sagnghos Date: Mon, 24 Feb 2025 18:20:06 +0000 Subject: [PATCH 07/10] code refactor to handle edge cases --- .../google/cloud/spanner/SpannerOptions.java | 28 ++++++++++--------- .../spanner/connection/ConnectionOptions.java | 14 ++++++++++ .../cloud/spanner/connection/SpannerPool.java | 3 -- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 149dc49d87e..a38fd20bc39 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -58,6 +58,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ThreadFactoryBuilder; @@ -810,18 +811,6 @@ protected SpannerOptions(Builder builder) { enableBuiltInMetrics = builder.enableBuiltInMetrics; enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; - String externalHostTokenPath = System.getenv("SPANNER_EXTERNAL_HOST_AUTH_TOKEN"); - if (builder.isExternalHost && builder.emulatorHost == null && externalHostTokenPath != null) { - String token; - try { - token = - Base64.getEncoder() - .encodeToString(Files.readAllBytes(Paths.get(externalHostTokenPath))); - } catch (IOException e) { - throw SpannerExceptionFactory.newSpannerException(e); - } - credentials = new GoogleCredentials(new AccessToken(token, null)); - } } /** @@ -1483,7 +1472,7 @@ public Builder setDecodeMode(DecodeMode decodeMode) { @Override public Builder setHost(String host) { super.setHost(host); - if (this.emulatorHost == null && !CLOUD_SPANNER_HOST_PATTERN.matcher(host).matches()) { + if (!CLOUD_SPANNER_HOST_PATTERN.matcher(host).matches()) { this.isExternalHost = true; } // Setting a host should override any SPANNER_EMULATOR_HOST setting. @@ -1656,6 +1645,19 @@ public SpannerOptions build() { this.setChannelConfigurator(ManagedChannelBuilder::usePlaintext); // As we are using plain text, we should never send any credentials. this.setCredentials(NoCredentials.getInstance()); + } else if (isExternalHost && credentials == null) { + String externalHostTokenPath = System.getenv("SPANNER_EXTERNAL_HOST_AUTH_TOKEN"); + if (!Strings.isNullOrEmpty(externalHostTokenPath)) { + String token; + try { + token = + Base64.getEncoder() + .encodeToString(Files.readAllBytes(Paths.get(externalHostTokenPath))); + } catch (IOException e) { + throw SpannerExceptionFactory.newSpannerException(e); + } + credentials = GoogleCredentials.create(new AccessToken(token, null)); + } } if (this.numChannels == null) { this.numChannels = diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index d205699bb9e..b6176c90cc3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -82,9 +82,12 @@ import io.opentelemetry.api.OpenTelemetry; import java.io.IOException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -921,6 +924,7 @@ private ConnectionOptions(Builder builder) { getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR), usePlainText, System.getenv()); + String externalHostTokenPath = System.getenv("SPANNER_EXTERNAL_HOST_AUTH_TOKEN"); // Using credentials on a plain text connection is not allowed, so if the user has not specified // any credentials and is using a plain text connection, we should not try to get the // credentials from the environment, but default to NoCredentials. @@ -935,6 +939,16 @@ && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null this.credentials = new GoogleCredentials( new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null)); + } else if (isExternalHost && !Strings.isNullOrEmpty(externalHostTokenPath)) { + String token; + try { + token = + Base64.getEncoder() + .encodeToString(Files.readAllBytes(Paths.get(externalHostTokenPath))); + } catch (IOException e) { + throw SpannerExceptionFactory.newSpannerException(e); + } + this.credentials = GoogleCredentials.create(new AccessToken(token, null)); } else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) { try { this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index dedc2a2deed..9558947156c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -407,9 +407,6 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { if (options.getConfigurator() != null) { options.getConfigurator().configure(builder); } - if (options.usesEmulator()) { - builder.setEmulatorHost(key.host); - } return builder.build().getService(); } From 6ec5a8e51d8415823813916d794cad7f2b633d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 25 Feb 2025 10:21:58 +0100 Subject: [PATCH 08/10] chore: create util method for reading token from file --- .../google/cloud/spanner/SpannerOptions.java | 43 +++++++++++++------ .../spanner/connection/ConnectionOptions.java | 18 ++------ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index a38fd20bc39..43978a1d04c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -855,8 +855,15 @@ default boolean isEnableEndToEndTracing() { default String getMonitoringHost() { return null; } + + default GoogleCredentials getDefaultExternalHostCredentials() { + return null; + } } + static final String DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS = + "SPANNER_EXTERNAL_HOST_AUTH_TOKEN"; + /** * Default implementation of {@link SpannerEnvironment}. Reads all configuration from environment * variables. @@ -912,6 +919,11 @@ public boolean isEnableEndToEndTracing() { public String getMonitoringHost() { return System.getenv(SPANNER_MONITORING_HOST); } + + @Override + public GoogleCredentials getDefaultExternalHostCredentials() { + return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS)); + } } /** Builder for {@link SpannerOptions} instances. */ @@ -1646,18 +1658,7 @@ public SpannerOptions build() { // As we are using plain text, we should never send any credentials. this.setCredentials(NoCredentials.getInstance()); } else if (isExternalHost && credentials == null) { - String externalHostTokenPath = System.getenv("SPANNER_EXTERNAL_HOST_AUTH_TOKEN"); - if (!Strings.isNullOrEmpty(externalHostTokenPath)) { - String token; - try { - token = - Base64.getEncoder() - .encodeToString(Files.readAllBytes(Paths.get(externalHostTokenPath))); - } catch (IOException e) { - throw SpannerExceptionFactory.newSpannerException(e); - } - credentials = GoogleCredentials.create(new AccessToken(token, null)); - } + credentials = environment.getDefaultExternalHostCredentials(); } if (this.numChannels == null) { this.numChannels = @@ -1698,6 +1699,24 @@ public static void useDefaultEnvironment() { SpannerOptions.environment = SpannerEnvironmentImpl.INSTANCE; } + @InternalApi + public static GoogleCredentials getDefaultExternalHostCredentialsFromSysEnv() { + return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS)); + } + + private static @Nullable GoogleCredentials getOAuthTokenFromFile(@Nullable String file) { + if (!Strings.isNullOrEmpty(file)) { + String token; + try { + token = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(file))); + } catch (IOException e) { + throw SpannerExceptionFactory.newSpannerException(e); + } + return GoogleCredentials.create(new AccessToken(token, null)); + } + return null; + } + /** * Enables OpenTelemetry traces. Enabling OpenTelemetry traces will disable OpenCensus traces. By * default, OpenCensus traces are enabled. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index b6176c90cc3..deb279d8e3e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -82,12 +82,9 @@ import io.opentelemetry.api.OpenTelemetry; import java.io.IOException; import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -924,7 +921,8 @@ private ConnectionOptions(Builder builder) { getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR), usePlainText, System.getenv()); - String externalHostTokenPath = System.getenv("SPANNER_EXTERNAL_HOST_AUTH_TOKEN"); + GoogleCredentials defaultExternalHostCredentials = + SpannerOptions.getDefaultExternalHostCredentialsFromSysEnv(); // Using credentials on a plain text connection is not allowed, so if the user has not specified // any credentials and is using a plain text connection, we should not try to get the // credentials from the environment, but default to NoCredentials. @@ -939,16 +937,8 @@ && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null this.credentials = new GoogleCredentials( new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null)); - } else if (isExternalHost && !Strings.isNullOrEmpty(externalHostTokenPath)) { - String token; - try { - token = - Base64.getEncoder() - .encodeToString(Files.readAllBytes(Paths.get(externalHostTokenPath))); - } catch (IOException e) { - throw SpannerExceptionFactory.newSpannerException(e); - } - this.credentials = GoogleCredentials.create(new AccessToken(token, null)); + } else if (isExternalHost && defaultExternalHostCredentials != null) { + this.credentials = defaultExternalHostCredentials; } else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) { try { this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials(); From 46645ffac6f9f43726aeb9fe82cebb5a0855ff2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 25 Feb 2025 10:30:28 +0100 Subject: [PATCH 09/10] chore: fix clirr failure --- google-cloud-spanner/clirr-ignored-differences.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index c6796085d83..4e03926c66e 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -822,4 +822,10 @@ java.lang.Object runTransaction(com.google.cloud.spanner.connection.Connection$TransactionCallable) + + + 7012 + com/google/cloud/spanner/SpannerOptions$SpannerEnvironment + com.google.auth.oauth2.GoogleCredentials getDefaultExternalHostCredentials() + From 29658472a6c9db3ca5c490080dd9534500d95998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 25 Feb 2025 16:23:05 +0100 Subject: [PATCH 10/10] test: reset unimplemented flag after test --- ...edSessionDatabaseClientMockServerTest.java | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index 87877caf21a..c65bab64603 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -1569,49 +1569,55 @@ public void testInitialBeginTransactionWithRW_receivesUnimplemented_fallsBackToR // Tests the behavior of the server-side kill switch for read-write multiplexed sessions. @Test public void testPartitionedQuery_receivesUnimplemented_fallsBackToRegularSession() { - mockSpanner.setPartitionQueryExecutionTime( - SimulatedExecutionTime.ofException( - Status.INVALID_ARGUMENT - .withDescription( - "Partitioned operations are not supported with multiplexed sessions") - .asRuntimeException())); - BatchClientImpl client = (BatchClientImpl) spanner.getBatchClient(DatabaseId.of("p", "i", "d")); - - try (BatchReadOnlyTransaction transaction = - client.batchReadOnlyTransaction(TimestampBound.strong())) { - // Partitioned Query should fail - SpannerException spannerException = - assertThrows( - SpannerException.class, - () -> { - transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT); - }); - assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode()); - - // Verify that we received one PartitionQueryRequest. - List partitionQueryRequests = - mockSpanner.getRequestsOfType(PartitionQueryRequest.class); - assertEquals(1, partitionQueryRequests.size()); - // Verify the requests were executed using multiplexed sessions - Session session2 = mockSpanner.getSession(partitionQueryRequests.get(0).getSession()); - assertNotNull(session2); - assertTrue(session2.getMultiplexed()); - assertTrue(client.unimplementedForPartitionedOps.get()); - } - try (BatchReadOnlyTransaction transaction = - client.batchReadOnlyTransaction(TimestampBound.strong())) { - // Partitioned Query should fail - transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT); - - // // Verify that we received two PartitionQueryRequest. and it uses a regular session due to - // fallback. - List partitionQueryRequests = - mockSpanner.getRequestsOfType(PartitionQueryRequest.class); - assertEquals(2, partitionQueryRequests.size()); - // Verify the requests are not executed using multiplexed sessions - Session session2 = mockSpanner.getSession(partitionQueryRequests.get(1).getSession()); - assertNotNull(session2); - assertFalse(session2.getMultiplexed()); + try { + mockSpanner.setPartitionQueryExecutionTime( + SimulatedExecutionTime.ofException( + Status.INVALID_ARGUMENT + .withDescription( + "Partitioned operations are not supported with multiplexed sessions") + .asRuntimeException())); + BatchClientImpl client = + (BatchClientImpl) spanner.getBatchClient(DatabaseId.of("p", "i", "d")); + + try (BatchReadOnlyTransaction transaction = + client.batchReadOnlyTransaction(TimestampBound.strong())) { + // Partitioned Query should fail + SpannerException spannerException = + assertThrows( + SpannerException.class, + () -> { + transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT); + }); + assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode()); + + // Verify that we received one PartitionQueryRequest. + List partitionQueryRequests = + mockSpanner.getRequestsOfType(PartitionQueryRequest.class); + assertEquals(1, partitionQueryRequests.size()); + // Verify the requests were executed using multiplexed sessions + Session session2 = mockSpanner.getSession(partitionQueryRequests.get(0).getSession()); + assertNotNull(session2); + assertTrue(session2.getMultiplexed()); + assertTrue(BatchClientImpl.unimplementedForPartitionedOps.get()); + } + try (BatchReadOnlyTransaction transaction = + client.batchReadOnlyTransaction(TimestampBound.strong())) { + // Partitioned Query should fail + transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT); + + // // Verify that we received two PartitionQueryRequest. and it uses a regular session due + // to + // fallback. + List partitionQueryRequests = + mockSpanner.getRequestsOfType(PartitionQueryRequest.class); + assertEquals(2, partitionQueryRequests.size()); + // Verify the requests are not executed using multiplexed sessions + Session session2 = mockSpanner.getSession(partitionQueryRequests.get(1).getSession()); + assertNotNull(session2); + assertFalse(session2.getMultiplexed()); + } + } finally { + BatchClientImpl.unimplementedForPartitionedOps.set(false); } }