mirror of
https://github.com/google/nomulus.git
synced 2025-06-29 15:53:35 +02:00
Set up JpaTransactionManager in BEAM pipelines (#639)
* Set up JpaTransactionManager in BEAM pipelines Added modules and utilities to create JpaTransactionManager in BEAM pipelines. Not wanting to set up AppEngine Remote API to access Keyring in the Datastore, we instead use the credential files in GCS, which are used by Spinnaker/Cloud Build and desktop access. Added utility to download, decrypt, and parse the file. Also added/modified dagger modules.
This commit is contained in:
parent
ec9ca23507
commit
92f579ce24
12 changed files with 550 additions and 8 deletions
|
@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Streams;
|
import com.google.common.collect.Streams;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
|
@ -36,6 +37,23 @@ public final class BackupPaths {
|
||||||
public static final String COMMIT_LOG_NAME_PREFIX = "commit_diff_until_";
|
public static final String COMMIT_LOG_NAME_PREFIX = "commit_diff_until_";
|
||||||
private static final String COMMIT_LOG_PATTERN_TEMPLATE = "%s/" + COMMIT_LOG_NAME_PREFIX + "*";
|
private static final String COMMIT_LOG_PATTERN_TEMPLATE = "%s/" + COMMIT_LOG_NAME_PREFIX + "*";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern of the per-project file with Cloud SQL connection information. To get a concrete path,
|
||||||
|
* user needs to provide the name of the environment, alpha, crash, sandbox, or production. This
|
||||||
|
* file is meant for applications without access to secrets stored in Datastore.
|
||||||
|
*
|
||||||
|
* <p>In production, this is an base-64 encoded encrypted file with one line, which contains
|
||||||
|
* space-separated values of Cloud SQL instance name, login, and password.
|
||||||
|
*
|
||||||
|
* <p>A plain text may be used for tests to a local database. Replace Cloud SQL instance name with
|
||||||
|
* JDBC URL.
|
||||||
|
*/
|
||||||
|
private static final String SQL_CONN_INFO_FILE_PATTERN =
|
||||||
|
"gs://domain-registry-dev-deploy/cloudsql-credentials/%s/admin_credential.enc";
|
||||||
|
|
||||||
|
private static final ImmutableSet<String> ALLOWED_ENV =
|
||||||
|
ImmutableSet.of("alpha", "crash", "sandbox", "production");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a regex pattern that matches all Datastore export files of a given {@code kind}.
|
* Returns a regex pattern that matches all Datastore export files of a given {@code kind}.
|
||||||
*
|
*
|
||||||
|
@ -90,4 +108,10 @@ public final class BackupPaths {
|
||||||
checkArgument(start >= 0, "Illegal file name %s.", fileName);
|
checkArgument(start >= 0, "Illegal file name %s.", fileName);
|
||||||
return DateTime.parse(fileName.substring(start + COMMIT_LOG_NAME_PREFIX.length()));
|
return DateTime.parse(fileName.substring(start + COMMIT_LOG_NAME_PREFIX.length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ImmutableList<String> getCloudSQLCredentialFilePatterns(String environmentName) {
|
||||||
|
checkArgument(
|
||||||
|
ALLOWED_ENV.contains(environmentName), "Invalid environment name %s", environmentName);
|
||||||
|
return ImmutableList.of(String.format(SQL_CONN_INFO_FILE_PATTERN, environmentName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.beam.initsql;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||||
|
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Component;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import google.registry.beam.initsql.BeamJpaModule.BindModule;
|
||||||
|
import google.registry.config.CredentialModule;
|
||||||
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
import google.registry.keyring.kms.KmsModule;
|
||||||
|
import google.registry.persistence.PersistenceModule;
|
||||||
|
import google.registry.persistence.PersistenceModule.JdbcJpaTm;
|
||||||
|
import google.registry.persistence.PersistenceModule.SocketFactoryJpaTm;
|
||||||
|
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||||
|
import google.registry.util.Clock;
|
||||||
|
import google.registry.util.Sleeper;
|
||||||
|
import google.registry.util.SystemClock;
|
||||||
|
import google.registry.util.SystemSleeper;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import org.apache.beam.sdk.io.FileSystems;
|
||||||
|
import org.apache.beam.sdk.io.fs.ResourceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides bindings for {@link JpaTransactionManager} to Cloud SQL.
|
||||||
|
*
|
||||||
|
* <p>This module is intended for use in BEAM pipelines, and uses a BEAM utility to access GCS like
|
||||||
|
* a regular file system.
|
||||||
|
*
|
||||||
|
* <p>Note that {@link google.registry.config.RegistryConfig.ConfigModule} cannot be used here,
|
||||||
|
* since many bindings, especially KMS-related ones, are different.
|
||||||
|
*/
|
||||||
|
@Module(includes = {BindModule.class})
|
||||||
|
class BeamJpaModule {
|
||||||
|
|
||||||
|
private static final String GCS_SCHEME = "gs://";
|
||||||
|
|
||||||
|
private final String credentialFilePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of {@link BeamJpaModule}.
|
||||||
|
*
|
||||||
|
* @param credentialFilePath the path to a Cloud SQL credential file. This must refer to either a
|
||||||
|
* real encrypted file on GCS as returned by {@link
|
||||||
|
* BackupPaths#getCloudSQLCredentialFilePatterns} or an unencrypted file on local filesystem
|
||||||
|
* with credentials to a test database.
|
||||||
|
*/
|
||||||
|
BeamJpaModule(String credentialFilePath) {
|
||||||
|
checkArgument(!isNullOrEmpty(credentialFilePath), "Null or empty credentialFilePath");
|
||||||
|
this.credentialFilePath = credentialFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the credential file is on GCS (and therefore expected to be encrypted). */
|
||||||
|
private boolean isCloudSqlCredential() {
|
||||||
|
return credentialFilePath.startsWith(GCS_SCHEME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
SqlAccessInfo provideCloudSqlAccessInfo(Lazy<CloudSqlCredentialDecryptor> lazyDecryptor) {
|
||||||
|
String line = readOnlyLineFromCredentialFile();
|
||||||
|
if (isCloudSqlCredential()) {
|
||||||
|
line = lazyDecryptor.get().decrypt(line);
|
||||||
|
}
|
||||||
|
// See ./BackupPaths.java for explanation of the line format.
|
||||||
|
List<String> parts = Splitter.on(' ').splitToList(line.trim());
|
||||||
|
checkState(parts.size() == 3, "Expecting three phrases in %s", line);
|
||||||
|
if (isCloudSqlCredential()) {
|
||||||
|
return SqlAccessInfo.createCloudSqlAccessInfo(parts.get(0), parts.get(1), parts.get(2));
|
||||||
|
} else {
|
||||||
|
return SqlAccessInfo.createLocalSqlAccessInfo(parts.get(0), parts.get(1), parts.get(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String readOnlyLineFromCredentialFile() {
|
||||||
|
try {
|
||||||
|
ResourceId resourceId = FileSystems.matchSingleFileSpec(credentialFilePath).resourceId();
|
||||||
|
try (BufferedReader reader =
|
||||||
|
new BufferedReader(
|
||||||
|
new InputStreamReader(
|
||||||
|
Channels.newInputStream(FileSystems.open(resourceId)), StandardCharsets.UTF_8))) {
|
||||||
|
return reader.readLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("cloudSqlJdbcUrl")
|
||||||
|
String provideJdbcUrl(SqlAccessInfo sqlAccessInfo) {
|
||||||
|
return sqlAccessInfo.jdbcUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("cloudSqlInstanceConnectionName")
|
||||||
|
String provideSqlInstanceName(SqlAccessInfo sqlAccessInfo) {
|
||||||
|
return sqlAccessInfo
|
||||||
|
.cloudSqlInstanceName()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Cloud SQL not provisioned."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("cloudSqlUsername")
|
||||||
|
String provideSqlUsername(SqlAccessInfo sqlAccessInfo) {
|
||||||
|
return sqlAccessInfo.user();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("cloudSqlPassword")
|
||||||
|
String provideSqlPassword(SqlAccessInfo sqlAccessInfo) {
|
||||||
|
return sqlAccessInfo.password();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("cloudKmsProjectId")
|
||||||
|
static String kmsProjectId() {
|
||||||
|
return "domain-registry-dev";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("cloudKmsKeyRing")
|
||||||
|
static String keyRingName() {
|
||||||
|
return "nomulus-tool-keyring";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("defaultCredentialOauthScopes")
|
||||||
|
static ImmutableList<String> defaultCredentialOauthScopes() {
|
||||||
|
return ImmutableList.of("https://www.googleapis.com/auth/cloud-platform");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("transientFailureRetries")
|
||||||
|
static int transientFailureRetries() {
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
interface BindModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
Sleeper sleeper(SystemSleeper sleeper);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
Clock clock(SystemClock clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Component(
|
||||||
|
modules = {
|
||||||
|
CredentialModule.class,
|
||||||
|
BeamJpaModule.class,
|
||||||
|
KmsModule.class,
|
||||||
|
PersistenceModule.class
|
||||||
|
})
|
||||||
|
public interface JpaTransactionManagerComponent {
|
||||||
|
@SocketFactoryJpaTm
|
||||||
|
JpaTransactionManager cloudSqlJpaTransactionManager();
|
||||||
|
|
||||||
|
@JdbcJpaTm
|
||||||
|
JpaTransactionManager localDbJpaTransactionManager();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.beam.initsql;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
import com.google.api.services.cloudkms.v1.model.DecryptRequest;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import google.registry.keyring.kms.KmsConnection;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts data using Cloud KMS, with the same crypto key with which Cloud SQL credential files on
|
||||||
|
* GCS was encrypted. See {@link BackupPaths#getCloudSQLCredentialFilePatterns} for more
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
public class CloudSqlCredentialDecryptor {
|
||||||
|
|
||||||
|
private static final String CRYPTO_KEY_NAME = "nomulus-tool-key";
|
||||||
|
private final KmsConnection kmsConnection;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CloudSqlCredentialDecryptor(KmsConnection kmsConnection) {
|
||||||
|
this.kmsConnection = kmsConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decrypt(String data) {
|
||||||
|
checkArgument(!Strings.isNullOrEmpty(data), "Null or empty data.");
|
||||||
|
byte[] ciphertext = Base64.getDecoder().decode(data);
|
||||||
|
// Re-encode for Cloud KMS JSON REST API, invoked through kmsConnection.
|
||||||
|
String urlSafeCipherText = new DecryptRequest().encodeCiphertext(ciphertext).getCiphertext();
|
||||||
|
return new String(
|
||||||
|
kmsConnection.decrypt(CRYPTO_KEY_NAME, urlSafeCipherText), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.beam.initsql;
|
||||||
|
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information needed to connect to a database, including JDBC URL, user name, password, and in the
|
||||||
|
* case of Cloud SQL, the database instance's name.
|
||||||
|
*/
|
||||||
|
@AutoValue
|
||||||
|
abstract class SqlAccessInfo {
|
||||||
|
|
||||||
|
abstract String jdbcUrl();
|
||||||
|
|
||||||
|
abstract String user();
|
||||||
|
|
||||||
|
abstract String password();
|
||||||
|
|
||||||
|
abstract Optional<String> cloudSqlInstanceName();
|
||||||
|
|
||||||
|
public static SqlAccessInfo createCloudSqlAccessInfo(
|
||||||
|
String sqlInstanceName, String username, String password) {
|
||||||
|
return new AutoValue_SqlAccessInfo(
|
||||||
|
"jdbc:postgresql://google/postgres", username, password, Optional.of(sqlInstanceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SqlAccessInfo createLocalSqlAccessInfo(
|
||||||
|
String jdbcUrl, String username, String password) {
|
||||||
|
return new AutoValue_SqlAccessInfo(jdbcUrl, username, password, Optional.empty());
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,11 @@ public final class Transforms {
|
||||||
return toStringPCollection(getExportFilePatterns(exportDir, kinds));
|
return toStringPCollection(getExportFilePatterns(exportDir, kinds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PTransform<PBegin, PCollection<String>> getCloudSqlConnectionInfoFilePatterns(
|
||||||
|
String gcpProjectName) {
|
||||||
|
return toStringPCollection(BackupPaths.getCloudSQLCredentialFilePatterns(gcpProjectName));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link PTransform} from file name patterns to file {@link Metadata Metadata records}.
|
* Returns a {@link PTransform} from file name patterns to file {@link Metadata Metadata records}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,7 +17,7 @@ package google.registry.keyring.kms;
|
||||||
import google.registry.keyring.api.KeyringException;
|
import google.registry.keyring.api.KeyringException;
|
||||||
|
|
||||||
/** An abstraction to simplify Cloud KMS operations. */
|
/** An abstraction to simplify Cloud KMS operations. */
|
||||||
interface KmsConnection {
|
public interface KmsConnection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum allowable secret size, as set by Cloud KMS.
|
* The maximum allowable secret size, as set by Cloud KMS.
|
||||||
|
|
|
@ -58,9 +58,10 @@ public class PersistenceModule {
|
||||||
public static final String HIKARI_DS_CLOUD_SQL_INSTANCE =
|
public static final String HIKARI_DS_CLOUD_SQL_INSTANCE =
|
||||||
"hibernate.hikari.dataSource.cloudSqlInstance";
|
"hibernate.hikari.dataSource.cloudSqlInstance";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
@Provides
|
@Provides
|
||||||
@DefaultHibernateConfigs
|
@DefaultHibernateConfigs
|
||||||
public static ImmutableMap<String, String> providesDefaultDatabaseConfigs() {
|
public static ImmutableMap<String, String> provideDefaultDatabaseConfigs() {
|
||||||
ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
|
ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
|
||||||
|
|
||||||
properties.put(Environment.DRIVER, "org.postgresql.Driver");
|
properties.put(Environment.DRIVER, "org.postgresql.Driver");
|
||||||
|
@ -89,7 +90,7 @@ public class PersistenceModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@PartialCloudSqlConfigs
|
@PartialCloudSqlConfigs
|
||||||
public static ImmutableMap<String, String> providesPartialCloudSqlConfigs(
|
static ImmutableMap<String, String> providePartialCloudSqlConfigs(
|
||||||
@Config("cloudSqlJdbcUrl") String jdbcUrl,
|
@Config("cloudSqlJdbcUrl") String jdbcUrl,
|
||||||
@Config("cloudSqlInstanceConnectionName") String instanceConnectionName,
|
@Config("cloudSqlInstanceConnectionName") String instanceConnectionName,
|
||||||
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs) {
|
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs) {
|
||||||
|
@ -103,7 +104,7 @@ public class PersistenceModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@AppEngineJpaTm
|
@AppEngineJpaTm
|
||||||
public static JpaTransactionManager providesAppEngineJpaTm(
|
static JpaTransactionManager provideAppEngineJpaTm(
|
||||||
@Config("cloudSqlUsername") String username,
|
@Config("cloudSqlUsername") String username,
|
||||||
KmsKeyring kmsKeyring,
|
KmsKeyring kmsKeyring,
|
||||||
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
||||||
|
@ -117,7 +118,7 @@ public class PersistenceModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@NomulusToolJpaTm
|
@NomulusToolJpaTm
|
||||||
public static JpaTransactionManager providesNomulusToolJpaTm(
|
static JpaTransactionManager provideNomulusToolJpaTm(
|
||||||
@Config("toolsCloudSqlUsername") String username,
|
@Config("toolsCloudSqlUsername") String username,
|
||||||
KmsKeyring kmsKeyring,
|
KmsKeyring kmsKeyring,
|
||||||
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
||||||
|
@ -130,9 +131,39 @@ public class PersistenceModule {
|
||||||
return new JpaTransactionManagerImpl(create(overrides), clock);
|
return new JpaTransactionManagerImpl(create(overrides), clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@SocketFactoryJpaTm
|
||||||
|
static JpaTransactionManager provideSocketFactoryJpaTm(
|
||||||
|
@Config("cloudSqlUsername") String username,
|
||||||
|
@Config("cloudSqlPassword") String password,
|
||||||
|
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
||||||
|
Clock clock) {
|
||||||
|
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
|
||||||
|
overrides.put(Environment.USER, username);
|
||||||
|
overrides.put(Environment.PASS, password);
|
||||||
|
return new JpaTransactionManagerImpl(create(overrides), clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@JdbcJpaTm
|
||||||
|
static JpaTransactionManager provideLocalJpaTm(
|
||||||
|
@Config("cloudSqlJdbcUrl") String jdbcUrl,
|
||||||
|
@Config("cloudSqlUsername") String username,
|
||||||
|
@Config("cloudSqlPassword") String password,
|
||||||
|
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs,
|
||||||
|
Clock clock) {
|
||||||
|
HashMap<String, String> overrides = Maps.newHashMap(defaultConfigs);
|
||||||
|
overrides.put(Environment.URL, jdbcUrl);
|
||||||
|
overrides.put(Environment.USER, username);
|
||||||
|
overrides.put(Environment.PASS, password);
|
||||||
|
return new JpaTransactionManagerImpl(create(overrides), clock);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructs the {@link EntityManagerFactory} instance. */
|
/** Constructs the {@link EntityManagerFactory} instance. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static EntityManagerFactory create(
|
static EntityManagerFactory create(
|
||||||
String jdbcUrl, String username, String password, ImmutableMap<String, String> configs) {
|
String jdbcUrl, String username, String password, ImmutableMap<String, String> configs) {
|
||||||
HashMap<String, String> properties = Maps.newHashMap(configs);
|
HashMap<String, String> properties = Maps.newHashMap(configs);
|
||||||
properties.put(Environment.URL, jdbcUrl);
|
properties.put(Environment.URL, jdbcUrl);
|
||||||
|
@ -165,6 +196,23 @@ public class PersistenceModule {
|
||||||
@Documented
|
@Documented
|
||||||
public @interface NomulusToolJpaTm {}
|
public @interface NomulusToolJpaTm {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger qualifier for {@link JpaTransactionManager} that accesses Cloud SQL using socket
|
||||||
|
* factory. This is meant for applications not running on AppEngine, therefore without access to a
|
||||||
|
* {@link google.registry.keyring.api.Keyring}.
|
||||||
|
*/
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
public @interface SocketFactoryJpaTm {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger qualifier for {@link JpaTransactionManager} backed by plain JDBC connections. This is
|
||||||
|
* mainly used by tests.
|
||||||
|
*/
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
public @interface JdbcJpaTm {}
|
||||||
|
|
||||||
/** Dagger qualifier for the partial Cloud SQL configs. */
|
/** Dagger qualifier for the partial Cloud SQL configs. */
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Documented
|
@Documented
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.beam.initsql;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.beam.initsql.BackupPaths.getCloudSQLCredentialFilePatterns;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Unit tests for {@link google.registry.beam.initsql.BackupPaths}. */
|
||||||
|
public class BackupPathsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCloudSQLCredentialFilePatterns_alpha() {
|
||||||
|
assertThat(getCloudSQLCredentialFilePatterns("alpha"))
|
||||||
|
.containsExactly(
|
||||||
|
"gs://domain-registry-dev-deploy/cloudsql-credentials/alpha/admin_credential.enc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCloudSQLCredentialFilePatterns_crash() {
|
||||||
|
assertThat(getCloudSQLCredentialFilePatterns("crash"))
|
||||||
|
.containsExactly(
|
||||||
|
"gs://domain-registry-dev-deploy/cloudsql-credentials/crash/admin_credential.enc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCloudSQLCredentialFilePatterns_sandbox() {
|
||||||
|
assertThat(getCloudSQLCredentialFilePatterns("sandbox"))
|
||||||
|
.containsExactly(
|
||||||
|
"gs://domain-registry-dev-deploy/cloudsql-credentials/sandbox/admin_credential.enc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCloudSQLCredentialFilePatterns_production() {
|
||||||
|
assertThat(getCloudSQLCredentialFilePatterns("production"))
|
||||||
|
.containsExactly(
|
||||||
|
"gs://domain-registry-dev-deploy/cloudsql-credentials/production/admin_credential.enc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEnvFromProject_illegal() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> getCloudSQLCredentialFilePatterns("bad"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEnvFromProject_null() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> getCloudSQLCredentialFilePatterns(null));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.beam.initsql;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.junit.Assume.assumeThat;
|
||||||
|
|
||||||
|
import google.registry.persistence.NomulusPostgreSql;
|
||||||
|
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import org.apache.beam.sdk.io.FileSystems;
|
||||||
|
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
import org.testcontainers.containers.PostgreSQLContainer;
|
||||||
|
|
||||||
|
/** Unit tests for {@link BeamJpaModule}. */
|
||||||
|
@RunWith(JUnit4.class) // TODO(weiminyu): upgrade to JUnit 5.
|
||||||
|
public class BeamJpaModuleTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public PostgreSQLContainer database = new PostgreSQLContainer(NomulusPostgreSql.getDockerTag());
|
||||||
|
|
||||||
|
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private File credentialFile;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeEach() throws IOException {
|
||||||
|
credentialFile = temporaryFolder.newFile();
|
||||||
|
new PrintStream(credentialFile)
|
||||||
|
.printf("%s %s %s", database.getJdbcUrl(), database.getUsername(), database.getPassword())
|
||||||
|
.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getJpaTransactionManager_local() {
|
||||||
|
JpaTransactionManager jpa =
|
||||||
|
DaggerBeamJpaModule_JpaTransactionManagerComponent.builder()
|
||||||
|
.beamJpaModule(new BeamJpaModule(credentialFile.getAbsolutePath()))
|
||||||
|
.build()
|
||||||
|
.localDbJpaTransactionManager();
|
||||||
|
assertThat(
|
||||||
|
jpa.transact(
|
||||||
|
() -> jpa.getEntityManager().createNativeQuery("select 1").getSingleResult()))
|
||||||
|
.isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test with a GCP project, only run when the 'test.gcp_integration.env' property is
|
||||||
|
* defined. Otherwise this test is ignored. This is meant to be run from a developer's desktop,
|
||||||
|
* with auth already set up by gcloud.
|
||||||
|
*
|
||||||
|
* <p>Example: {@code gradlew test -P test.gcp_integration.env=alpha}.
|
||||||
|
*
|
||||||
|
* <p>See <a href="../../../../../../../../java_common.gradle">java_common.gradle</a> for more
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void getJpaTransactionManager_cloudSql_authRequired() {
|
||||||
|
String environmentName = System.getProperty("test.gcp_integration.env");
|
||||||
|
assumeThat(environmentName, notNullValue());
|
||||||
|
|
||||||
|
FileSystems.setDefaultPipelineOptions(PipelineOptionsFactory.create());
|
||||||
|
JpaTransactionManager jpa =
|
||||||
|
DaggerBeamJpaModule_JpaTransactionManagerComponent.builder()
|
||||||
|
.beamJpaModule(
|
||||||
|
new BeamJpaModule(
|
||||||
|
BackupPaths.getCloudSQLCredentialFilePatterns(environmentName).get(0)))
|
||||||
|
.build()
|
||||||
|
.cloudSqlJpaTransactionManager();
|
||||||
|
assertThat(
|
||||||
|
jpa.transact(
|
||||||
|
() -> jpa.getEntityManager().createNativeQuery("select 1").getSingleResult()))
|
||||||
|
.isEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ public class PersistenceModuleTest {
|
||||||
database.getJdbcUrl(),
|
database.getJdbcUrl(),
|
||||||
database.getUsername(),
|
database.getUsername(),
|
||||||
database.getPassword(),
|
database.getPassword(),
|
||||||
PersistenceModule.providesDefaultDatabaseConfigs());
|
PersistenceModule.provideDefaultDatabaseConfigs());
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
|
|
@ -166,7 +166,7 @@ abstract class JpaTransactionManagerRule extends ExternalResource {
|
||||||
new String(Files.readAllBytes(tempSqlFile.toPath()), StandardCharsets.UTF_8));
|
new String(Files.readAllBytes(tempSqlFile.toPath()), StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmutableMap properties = PersistenceModule.providesDefaultDatabaseConfigs();
|
ImmutableMap properties = PersistenceModule.provideDefaultDatabaseConfigs();
|
||||||
if (!userProperties.isEmpty()) {
|
if (!userProperties.isEmpty()) {
|
||||||
// If there are user properties, create a new properties object with these added.
|
// If there are user properties, create a new properties object with these added.
|
||||||
Map<String, String> mergedProperties = Maps.newHashMap();
|
Map<String, String> mergedProperties = Maps.newHashMap();
|
||||||
|
|
|
@ -76,6 +76,27 @@ test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets up integration test with a registry environment. The target environment is
|
||||||
|
// passed by the 'test.gcp_integration.env' property. Test runner must have been
|
||||||
|
// authorized to access the corresponding GCP project, e.g., by running 'gcloud auth'
|
||||||
|
// or placing a credential file at a well known place.
|
||||||
|
//
|
||||||
|
// A typical use case is to run tests from desktop that accesses Cloud resources. See
|
||||||
|
// core/src/test/java/google/registry/beam/initsql/BeamJpaModuleTest.java for an example.
|
||||||
|
tasks.withType(Test).configureEach {
|
||||||
|
def gcp_integration_env_property = 'test.gcp_integration.env'
|
||||||
|
|
||||||
|
if (project.hasProperty(gcp_integration_env_property)) {
|
||||||
|
String targetEnv = project.property(gcp_integration_env_property)
|
||||||
|
|
||||||
|
if (targetEnv in ['sandbox', 'production']) {
|
||||||
|
throw new RuntimeException("Integration test with production or sandbox not allowed.")
|
||||||
|
}
|
||||||
|
systemProperty gcp_integration_env_property, targetEnv
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
// The -Werror flag causes Intellij to fail on deprecated api use.
|
// The -Werror flag causes Intellij to fail on deprecated api use.
|
||||||
// Allow IDE user to turn off this flag by specifying a Gradle VM
|
// Allow IDE user to turn off this flag by specifying a Gradle VM
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue