Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: add autoConfigEmulator connection option (#931)
Adds `autoConfigEmulator` connection option. When this option is set to true for a connection:
1. The connection will by default try to connect to `localhost:9010` (unless a specific host/port is set in the connection URL).
2. Plain text communication will be enabled.
3. Authentication will be disabled.
4. The instance and database in the connection string will automatically be created on the emulator if any of them do not yet exist. Any existing instance and/or database will remain untouched.

Towards googleapis/java-spanner-jdbc#380
  • Loading branch information
olavloite committed Mar 17, 2021
1 parent 213dddc commit 32fdd60
Show file tree
Hide file tree
Showing 5 changed files with 462 additions and 6 deletions.
Expand Up @@ -210,6 +210,9 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
this.spannerPool = SpannerPool.INSTANCE;
this.options = options;
this.spanner = spannerPool.getSpanner(options, this);
if (options.isAutoConfigEmulator()) {
EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId());
}
this.dbClient = spanner.getDatabaseClient(options.getDatabaseId());
this.retryAbortsInternally = options.isRetryAbortsInternally();
this.readOnly = options.isReadOnly();
Expand Down
Expand Up @@ -161,6 +161,7 @@ public String[] getValidValues() {
private static final String PLAIN_TEXT_PROTOCOL = "http:";
private static final String HOST_PROTOCOL = "https:";
private static final String DEFAULT_HOST = "https://spanner.googleapis.com";
private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010";
/** Use plain text is only for local testing purposes. */
private static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText";
/** Name of the 'autocommit' connection property. */
Expand Down Expand Up @@ -231,6 +232,10 @@ public String[] getValidValues() {
OPTIMIZER_VERSION_PROPERTY_NAME,
"Sets the default query optimizer version to use for this connection."),
ConnectionProperty.createBooleanProperty("returnCommitStats", "", false),
ConnectionProperty.createBooleanProperty(
"autoConfigEmulator",
"Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). The instance and database in the connection string will automatically be created if these do not yet exist on the emulator.",
false),
ConnectionProperty.createBooleanProperty(
LENIENT_PROPERTY_NAME,
"Silently ignore unknown properties in the connection string/properties (true/false)",
Expand Down Expand Up @@ -347,6 +352,14 @@ private boolean isValidUri(String uri) {
* <li>retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the
* connection. Default is true.
* <li>optimizerVersion (string): Sets the query optimizer version to use for the connection.
* <li>autoConfigEmulator (boolean): Automatically configures the connection to connect to the
* Cloud Spanner emulator. If no host and port is specified in the connection string, the
* connection will automatically use the default emulator host/port combination
* (localhost:9010). Plain text communication will be enabled and authentication will be
* disabled. The instance and database in the connection string will automatically be
* created on the emulator if any of them do not yet exist. Any existing instance or
* database on the emulator will remain untouched. No other configuration is needed in
* order to connect to the emulator than setting this property.
* </ul>
*
* @param uri The URI of the Spanner database to connect to.
Expand Down Expand Up @@ -459,6 +472,7 @@ public static Builder newBuilder() {
private final String userAgent;
private final QueryOptions queryOptions;
private final boolean returnCommitStats;
private final boolean autoConfigEmulator;

private final boolean autocommit;
private final boolean readOnly;
Expand All @@ -483,18 +497,15 @@ private ConnectionOptions(Builder builder) {
(builder.credentials == null && this.credentialsUrl == null) || this.oauthToken == null,
"Cannot specify both credentials and an OAuth token.");

this.usePlainText = parseUsePlainText(this.uri);
this.userAgent = parseUserAgent(this.uri);
QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder();
queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri));
this.queryOptions = queryOptionsBuilder.build();
this.returnCommitStats = parseReturnCommitStats(this.uri);
this.autoConfigEmulator = parseAutoConfigEmulator(this.uri);
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
this.host = determineHost(matcher, autoConfigEmulator, usePlainText);

this.host =
matcher.group(Builder.HOST_GROUP) == null
? DEFAULT_HOST
: (usePlainText ? PLAIN_TEXT_PROTOCOL : HOST_PROTOCOL)
+ matcher.group(Builder.HOST_GROUP);
this.instanceId = matcher.group(Builder.INSTANCE_GROUP);
this.databaseName = matcher.group(Builder.DATABASE_GROUP);
// Using credentials on a plain text connection is not allowed, so if the user has not specified
Expand Down Expand Up @@ -549,6 +560,23 @@ private ConnectionOptions(Builder builder) {
}
}

private static String determineHost(
Matcher matcher, boolean autoConfigEmulator, boolean usePlainText) {
if (matcher.group(Builder.HOST_GROUP) == null) {
if (autoConfigEmulator) {
return DEFAULT_EMULATOR_HOST;
} else {
return DEFAULT_HOST;
}
} else {
if (usePlainText) {
return PLAIN_TEXT_PROTOCOL + matcher.group(Builder.HOST_GROUP);
} else {
return HOST_PROTOCOL + matcher.group(Builder.HOST_GROUP);
}
}
}

private static Integer parseIntegerProperty(String propertyName, String value) {
if (value != null) {
try {
Expand Down Expand Up @@ -644,6 +672,11 @@ static boolean parseReturnCommitStats(String uri) {
return value != null ? Boolean.valueOf(value) : false;
}

static boolean parseAutoConfigEmulator(String uri) {
String value = parseUriProperty(uri, "autoConfigEmulator");
return value != null ? Boolean.valueOf(value) : false;
}

@VisibleForTesting
static boolean parseLenient(String uri) {
String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME);
Expand Down Expand Up @@ -838,6 +871,16 @@ public boolean isReturnCommitStats() {
return returnCommitStats;
}

/**
* Whether connections created by this {@link ConnectionOptions} will automatically try to connect
* to the emulator using the default host/port of the emulator, and automatically create the
* instance and database that is specified in the connection string if these do not exist on the
* emulator instance.
*/
public boolean isAutoConfigEmulator() {
return autoConfigEmulator;
}

/** Interceptors that should be executed after each statement */
List<StatementExecutionInterceptor> getStatementExecutionInterceptors() {
return statementExecutionInterceptors;
Expand Down
@@ -0,0 +1,85 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.connection;

import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.InstanceConfigId;
import com.google.cloud.spanner.InstanceInfo;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.ExecutionException;

/**
* Util class for automatically generating a test instance and test database on a Cloud Spanner
* emulator instance. This makes it easier to automatically start a working emulator and test an
* application when working with JDBC.
*/
class EmulatorUtil {

/**
* Creates the instance and the database that are specified in the connection string on the
* emulator that the given {@link Spanner} instance connects to if these do not already exist.
*
* @param spanner a {@link Spanner} instance that connects to an emulator instance
* @param databaseId the id of the instance and the database to create
*/
static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseId) {
Preconditions.checkArgument(
NoCredentials.getInstance().equals(spanner.getOptions().getCredentials()));
try {
spanner
.getInstanceAdminClient()
.createInstance(
InstanceInfo.newBuilder(databaseId.getInstanceId())
.setDisplayName("Automatically Generated Test Instance")
.setNodeCount(1)
.setInstanceConfigId(
InstanceConfigId.of(
databaseId.getInstanceId().getProject(), "emulator-config"))
.build())
.get();
} catch (ExecutionException executionException) {
SpannerException spannerException = (SpannerException) executionException.getCause();
if (spannerException.getErrorCode() != ErrorCode.ALREADY_EXISTS) {
throw spannerException;
}
} catch (InterruptedException e) {
throw SpannerExceptionFactory.propagateInterrupt(e);
}
try {
spanner
.getDatabaseAdminClient()
.createDatabase(
databaseId.getInstanceId().getInstance(),
databaseId.getDatabase(),
ImmutableList.<String>of())
.get();
} catch (ExecutionException executionException) {
SpannerException spannerException = (SpannerException) executionException.getCause();
if (spannerException.getErrorCode() != ErrorCode.ALREADY_EXISTS) {
throw spannerException;
}
} catch (InterruptedException e) {
throw SpannerExceptionFactory.propagateInterrupt(e);
}
}
}
Expand Up @@ -17,10 +17,13 @@
package com.google.cloud.spanner.connection;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.SpannerOptions;
import java.util.Arrays;
import org.junit.Test;
Expand Down Expand Up @@ -118,6 +121,34 @@ public void testBuildWithLocalhostPortAndValidURI() {
assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY);
}

@Test
public void testBuildWithAutoConfigEmulator() {
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
builder.setUri(
"cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autoConfigEmulator=true");
ConnectionOptions options = builder.build();
assertEquals("http://localhost:9010", options.getHost());
assertEquals("test-project-123", options.getProjectId());
assertEquals("test-instance-123", options.getInstanceId());
assertEquals("test-database-123", options.getDatabaseName());
assertEquals(NoCredentials.getInstance(), options.getCredentials());
assertTrue(options.isUsePlainText());
}

@Test
public void testBuildWithAutoConfigEmulatorAndHost() {
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
builder.setUri(
"cloudspanner://central-emulator.local:8080/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autoConfigEmulator=true");
ConnectionOptions options = builder.build();
assertEquals("http://central-emulator.local:8080", options.getHost());
assertEquals("test-project-123", options.getProjectId());
assertEquals("test-instance-123", options.getInstanceId());
assertEquals("test-database-123", options.getDatabaseName());
assertEquals(NoCredentials.getInstance(), options.getCredentials());
assertTrue(options.isUsePlainText());
}

@Test
public void testBuildWithDefaultProjectPlaceholder() {
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
Expand Down

0 comments on commit 32fdd60

Please sign in to comment.