Skip to content

Commit 0c14209

Browse files
committed
Merge branch 'main' into default-sequence-kind
2 parents a69248c + ace11d5 commit 0c14209

File tree

19 files changed

+1277
-1028
lines changed

19 files changed

+1277
-1028
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# For syntax help see:
55
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
66

7-
# The @googleapis/api-spanner-java is the default owner for changes in this repo
7+
# The @googleapis/spanner-client-libraries-java is the default owner for changes in this repo
88
* @googleapis/yoshi-java @googleapis/spanner-client-libraries-java

.repo-metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"api_id": "spanner.googleapis.com",
1414
"library_type": "GAPIC_COMBO",
1515
"requires_billing": true,
16-
"codeowner_team": "@googleapis/api-spanner-java",
16+
"codeowner_team": "@googleapis/spanner-client-libraries-java",
1717
"excluded_poms": "google-cloud-spanner-bom",
1818
"issue_tracker": "https://issuetracker.google.com/issues?q=componentid:190851%2B%20status:open",
1919
"recommended_package": "com.google.cloud.spanner",

generation_config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
gapic_generator_version: 2.53.0
2-
googleapis_commitish: 6bc8e91bf92cc985da5ed0c227b48f12315cb695
2+
googleapis_commitish: fbbbf5023815f9a662c85aa8af8f3b72467fcb6f
33
libraries_bom_version: 26.55.0
44
libraries:
55
- api_shortname: spanner
@@ -17,7 +17,7 @@ libraries:
1717
api_id: spanner.googleapis.com
1818
transport: grpc
1919
requires_billing: true
20-
codeowner_team: '@googleapis/api-spanner-java'
20+
codeowner_team: '@googleapis/spanner-client-libraries-java'
2121
library_type: GAPIC_COMBO
2222
excluded_poms: google-cloud-spanner-bom
2323
recommended_package: com.google.cloud.spanner

google-cloud-spanner/clirr-ignored-differences.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,13 @@
822822
<method>java.lang.Object runTransaction(com.google.cloud.spanner.connection.Connection$TransactionCallable)</method>
823823
</difference>
824824

825+
<!-- Added external host option -->
826+
<difference>
827+
<differenceType>7012</differenceType>
828+
<className>com/google/cloud/spanner/SpannerOptions$SpannerEnvironment</className>
829+
<method>com.google.auth.oauth2.GoogleCredentials getDefaultExternalHostCredentials()</method>
830+
</difference>
831+
825832
<!-- Default sequence kind -->
826833
<difference>
827834
<differenceType>7012</differenceType>
@@ -833,5 +840,4 @@
833840
<className>com/google/cloud/spanner/connection/Connection</className>
834841
<method>java.lang.String getDefaultSequenceKind()</method>
835842
</difference>
836-
837843
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import com.google.api.gax.tracing.ApiTracerFactory;
3535
import com.google.api.gax.tracing.BaseApiTracerFactory;
3636
import com.google.api.gax.tracing.OpencensusTracerFactory;
37+
import com.google.auth.oauth2.AccessToken;
38+
import com.google.auth.oauth2.GoogleCredentials;
3739
import com.google.cloud.NoCredentials;
3840
import com.google.cloud.ServiceDefaults;
3941
import com.google.cloud.ServiceOptions;
@@ -56,6 +58,7 @@
5658
import com.google.common.annotations.VisibleForTesting;
5759
import com.google.common.base.MoreObjects;
5860
import com.google.common.base.Preconditions;
61+
import com.google.common.base.Strings;
5962
import com.google.common.collect.ImmutableMap;
6063
import com.google.common.collect.ImmutableSet;
6164
import com.google.common.util.concurrent.ThreadFactoryBuilder;
@@ -79,8 +82,11 @@
7982
import java.io.IOException;
8083
import java.net.MalformedURLException;
8184
import java.net.URL;
85+
import java.nio.file.Files;
86+
import java.nio.file.Paths;
8287
import java.time.Duration;
8388
import java.util.ArrayList;
89+
import java.util.Base64;
8490
import java.util.HashMap;
8591
import java.util.List;
8692
import java.util.Map;
@@ -92,6 +98,7 @@
9298
import java.util.concurrent.ThreadFactory;
9399
import java.util.concurrent.TimeUnit;
94100
import java.util.concurrent.atomic.AtomicInteger;
101+
import java.util.regex.Pattern;
95102
import javax.annotation.Nonnull;
96103
import javax.annotation.Nullable;
97104
import javax.annotation.concurrent.GuardedBy;
@@ -110,6 +117,11 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
110117

111118
private static final String API_SHORT_NAME = "Spanner";
112119
private static final String DEFAULT_HOST = "https://spanner.googleapis.com";
120+
private static final String CLOUD_SPANNER_HOST_FORMAT = ".*\\.googleapis\\.com.*";
121+
122+
@VisibleForTesting
123+
static final Pattern CLOUD_SPANNER_HOST_PATTERN = Pattern.compile(CLOUD_SPANNER_HOST_FORMAT);
124+
113125
private static final ImmutableSet<String> SCOPES =
114126
ImmutableSet.of(
115127
"https://www.googleapis.com/auth/spanner.admin",
@@ -843,8 +855,15 @@ default boolean isEnableEndToEndTracing() {
843855
default String getMonitoringHost() {
844856
return null;
845857
}
858+
859+
default GoogleCredentials getDefaultExternalHostCredentials() {
860+
return null;
861+
}
846862
}
847863

864+
static final String DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS =
865+
"SPANNER_EXTERNAL_HOST_AUTH_TOKEN";
866+
848867
/**
849868
* Default implementation of {@link SpannerEnvironment}. Reads all configuration from environment
850869
* variables.
@@ -900,6 +919,11 @@ public boolean isEnableEndToEndTracing() {
900919
public String getMonitoringHost() {
901920
return System.getenv(SPANNER_MONITORING_HOST);
902921
}
922+
923+
@Override
924+
public GoogleCredentials getDefaultExternalHostCredentials() {
925+
return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS));
926+
}
903927
}
904928

905929
/** Builder for {@link SpannerOptions} instances. */
@@ -967,6 +991,7 @@ public static class Builder
967991
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
968992
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
969993
private SslContext mTLSContext = null;
994+
private boolean isExternalHost = false;
970995

971996
private static String createCustomClientLibToken(String token) {
972997
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1459,6 +1484,9 @@ public Builder setDecodeMode(DecodeMode decodeMode) {
14591484
@Override
14601485
public Builder setHost(String host) {
14611486
super.setHost(host);
1487+
if (!CLOUD_SPANNER_HOST_PATTERN.matcher(host).matches()) {
1488+
this.isExternalHost = true;
1489+
}
14621490
// Setting a host should override any SPANNER_EMULATOR_HOST setting.
14631491
setEmulatorHost(null);
14641492
return this;
@@ -1629,6 +1657,8 @@ public SpannerOptions build() {
16291657
this.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
16301658
// As we are using plain text, we should never send any credentials.
16311659
this.setCredentials(NoCredentials.getInstance());
1660+
} else if (isExternalHost && credentials == null) {
1661+
credentials = environment.getDefaultExternalHostCredentials();
16321662
}
16331663
if (this.numChannels == null) {
16341664
this.numChannels =
@@ -1669,6 +1699,24 @@ public static void useDefaultEnvironment() {
16691699
SpannerOptions.environment = SpannerEnvironmentImpl.INSTANCE;
16701700
}
16711701

1702+
@InternalApi
1703+
public static GoogleCredentials getDefaultExternalHostCredentialsFromSysEnv() {
1704+
return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS));
1705+
}
1706+
1707+
private static @Nullable GoogleCredentials getOAuthTokenFromFile(@Nullable String file) {
1708+
if (!Strings.isNullOrEmpty(file)) {
1709+
String token;
1710+
try {
1711+
token = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(file)));
1712+
} catch (IOException e) {
1713+
throw SpannerExceptionFactory.newSpannerException(e);
1714+
}
1715+
return GoogleCredentials.create(new AccessToken(token, null));
1716+
}
1717+
return null;
1718+
}
1719+
16721720
/**
16731721
* Enables OpenTelemetry traces. Enabling OpenTelemetry traces will disable OpenCensus traces. By
16741722
* default, OpenCensus traces are enabled.

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,8 @@ private ConnectionOptions(Builder builder) {
923923
getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR),
924924
usePlainText,
925925
System.getenv());
926+
GoogleCredentials defaultExternalHostCredentials =
927+
SpannerOptions.getDefaultExternalHostCredentialsFromSysEnv();
926928
// Using credentials on a plain text connection is not allowed, so if the user has not specified
927929
// any credentials and is using a plain text connection, we should not try to get the
928930
// credentials from the environment, but default to NoCredentials.
@@ -937,6 +939,8 @@ && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null
937939
this.credentials =
938940
new GoogleCredentials(
939941
new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null));
942+
} else if (isExternalHost && defaultExternalHostCredentials != null) {
943+
this.credentials = defaultExternalHostCredentials;
940944
} else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) {
941945
try {
942946
this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials();

google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,49 +1569,55 @@ public void testInitialBeginTransactionWithRW_receivesUnimplemented_fallsBackToR
15691569
// Tests the behavior of the server-side kill switch for read-write multiplexed sessions.
15701570
@Test
15711571
public void testPartitionedQuery_receivesUnimplemented_fallsBackToRegularSession() {
1572-
mockSpanner.setPartitionQueryExecutionTime(
1573-
SimulatedExecutionTime.ofException(
1574-
Status.INVALID_ARGUMENT
1575-
.withDescription(
1576-
"Partitioned operations are not supported with multiplexed sessions")
1577-
.asRuntimeException()));
1578-
BatchClientImpl client = (BatchClientImpl) spanner.getBatchClient(DatabaseId.of("p", "i", "d"));
1579-
1580-
try (BatchReadOnlyTransaction transaction =
1581-
client.batchReadOnlyTransaction(TimestampBound.strong())) {
1582-
// Partitioned Query should fail
1583-
SpannerException spannerException =
1584-
assertThrows(
1585-
SpannerException.class,
1586-
() -> {
1587-
transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT);
1588-
});
1589-
assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode());
1590-
1591-
// Verify that we received one PartitionQueryRequest.
1592-
List<PartitionQueryRequest> partitionQueryRequests =
1593-
mockSpanner.getRequestsOfType(PartitionQueryRequest.class);
1594-
assertEquals(1, partitionQueryRequests.size());
1595-
// Verify the requests were executed using multiplexed sessions
1596-
Session session2 = mockSpanner.getSession(partitionQueryRequests.get(0).getSession());
1597-
assertNotNull(session2);
1598-
assertTrue(session2.getMultiplexed());
1599-
assertTrue(client.unimplementedForPartitionedOps.get());
1600-
}
1601-
try (BatchReadOnlyTransaction transaction =
1602-
client.batchReadOnlyTransaction(TimestampBound.strong())) {
1603-
// Partitioned Query should fail
1604-
transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT);
1605-
1606-
// // Verify that we received two PartitionQueryRequest. and it uses a regular session due to
1607-
// fallback.
1608-
List<PartitionQueryRequest> partitionQueryRequests =
1609-
mockSpanner.getRequestsOfType(PartitionQueryRequest.class);
1610-
assertEquals(2, partitionQueryRequests.size());
1611-
// Verify the requests are not executed using multiplexed sessions
1612-
Session session2 = mockSpanner.getSession(partitionQueryRequests.get(1).getSession());
1613-
assertNotNull(session2);
1614-
assertFalse(session2.getMultiplexed());
1572+
try {
1573+
mockSpanner.setPartitionQueryExecutionTime(
1574+
SimulatedExecutionTime.ofException(
1575+
Status.INVALID_ARGUMENT
1576+
.withDescription(
1577+
"Partitioned operations are not supported with multiplexed sessions")
1578+
.asRuntimeException()));
1579+
BatchClientImpl client =
1580+
(BatchClientImpl) spanner.getBatchClient(DatabaseId.of("p", "i", "d"));
1581+
1582+
try (BatchReadOnlyTransaction transaction =
1583+
client.batchReadOnlyTransaction(TimestampBound.strong())) {
1584+
// Partitioned Query should fail
1585+
SpannerException spannerException =
1586+
assertThrows(
1587+
SpannerException.class,
1588+
() -> {
1589+
transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT);
1590+
});
1591+
assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode());
1592+
1593+
// Verify that we received one PartitionQueryRequest.
1594+
List<PartitionQueryRequest> partitionQueryRequests =
1595+
mockSpanner.getRequestsOfType(PartitionQueryRequest.class);
1596+
assertEquals(1, partitionQueryRequests.size());
1597+
// Verify the requests were executed using multiplexed sessions
1598+
Session session2 = mockSpanner.getSession(partitionQueryRequests.get(0).getSession());
1599+
assertNotNull(session2);
1600+
assertTrue(session2.getMultiplexed());
1601+
assertTrue(BatchClientImpl.unimplementedForPartitionedOps.get());
1602+
}
1603+
try (BatchReadOnlyTransaction transaction =
1604+
client.batchReadOnlyTransaction(TimestampBound.strong())) {
1605+
// Partitioned Query should fail
1606+
transaction.partitionQuery(PartitionOptions.getDefaultInstance(), STATEMENT);
1607+
1608+
// // Verify that we received two PartitionQueryRequest. and it uses a regular session due
1609+
// to
1610+
// fallback.
1611+
List<PartitionQueryRequest> partitionQueryRequests =
1612+
mockSpanner.getRequestsOfType(PartitionQueryRequest.class);
1613+
assertEquals(2, partitionQueryRequests.size());
1614+
// Verify the requests are not executed using multiplexed sessions
1615+
Session session2 = mockSpanner.getSession(partitionQueryRequests.get(1).getSession());
1616+
assertNotNull(session2);
1617+
assertFalse(session2.getMultiplexed());
1618+
}
1619+
} finally {
1620+
BatchClientImpl.unimplementedForPartitionedOps.set(false);
16151621
}
16161622
}
16171623

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import static com.google.cloud.spanner.SpannerOptions.CLOUD_SPANNER_HOST_PATTERN;
1920
import static com.google.common.truth.Truth.assertThat;
2021
import static org.hamcrest.CoreMatchers.is;
2122
import static org.hamcrest.MatcherAssert.assertThat;
@@ -1164,4 +1165,12 @@ public void checkGlobalOpenTelemetryWhenNotInjected() {
11641165
.build();
11651166
assertEquals(GlobalOpenTelemetry.get(), options.getOpenTelemetry());
11661167
}
1168+
1169+
@Test
1170+
public void testCloudSpannerHostPattern() {
1171+
assertTrue(CLOUD_SPANNER_HOST_PATTERN.matcher("https://spanner.googleapis.com").matches());
1172+
assertTrue(
1173+
CLOUD_SPANNER_HOST_PATTERN.matcher("https://product-area.googleapis.com:443").matches());
1174+
assertFalse(CLOUD_SPANNER_HOST_PATTERN.matcher("https://some-company.com:443").matches());
1175+
}
11671176
}

google-cloud-spanner/src/test/java/com/google/cloud/spanner/v1/SpannerClientTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ public void executeStreamingSqlTest() throws Exception {
635635
.setResumeToken(ByteString.EMPTY)
636636
.setStats(ResultSetStats.newBuilder().build())
637637
.setPrecommitToken(MultiplexedSessionPrecommitToken.newBuilder().build())
638+
.setLast(true)
638639
.build();
639640
mockSpanner.addResponse(expectedResponse);
640641
ExecuteSqlRequest request =
@@ -861,6 +862,7 @@ public void streamingReadTest() throws Exception {
861862
.setResumeToken(ByteString.EMPTY)
862863
.setStats(ResultSetStats.newBuilder().build())
863864
.setPrecommitToken(MultiplexedSessionPrecommitToken.newBuilder().build())
865+
.setLast(true)
864866
.build();
865867
mockSpanner.addResponse(expectedResponse);
866868
ReadRequest request =

0 commit comments

Comments
 (0)