create() {
- Preconditions.checkState(
- getExpireTime() != null, "Cannot create a backup without an expire time");
- Preconditions.checkState(
- getDatabase() != null, "Cannot create a backup without a source database");
return dbClient.createBackup(this);
}
@@ -184,6 +181,7 @@ static Backup fromProto(
.setExpireTime(Timestamp.fromProto(proto.getExpireTime()))
.setVersionTime(Timestamp.fromProto(proto.getVersionTime()))
.setDatabase(DatabaseId.of(proto.getDatabase()))
+ .setEncryptionInfo(EncryptionInfo.fromProtoOrNull(proto.getEncryptionInfo()))
.setProto(proto)
.build();
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
index 199e6ae2ae..0657ff2b7b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
@@ -18,6 +18,8 @@
import com.google.api.client.util.Preconditions;
import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.encryption.BackupEncryptionConfig;
+import com.google.cloud.spanner.encryption.EncryptionInfo;
import com.google.spanner.admin.database.v1.Database;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -29,8 +31,29 @@ public abstract static class Builder {
abstract Builder setSize(long size);
+ /**
+ * Returned when retrieving a backup.
+ *
+ * The encryption information for the backup. If the encryption key protecting this resource
+ * is customer managed, then kms_key_version will be filled.
+ */
+ abstract Builder setEncryptionInfo(EncryptionInfo encryptionInfo);
+
abstract Builder setProto(com.google.spanner.admin.database.v1.Backup proto);
+ /**
+ * Optional for creating a new backup.
+ *
+ *
The encryption configuration to be used for the backup. The possible configurations are
+ * {@link com.google.cloud.spanner.encryption.CustomerManagedEncryption}, {@link
+ * com.google.cloud.spanner.encryption.GoogleDefaultEncryption} and {@link
+ * com.google.cloud.spanner.encryption.UseDatabaseEncryption}.
+ *
+ *
If no encryption config is given the backup will be created with the same encryption as
+ * set by the database ({@link com.google.cloud.spanner.encryption.UseDatabaseEncryption}).
+ */
+ public abstract Builder setEncryptionConfig(BackupEncryptionConfig encryptionConfig);
+
/**
* Required for creating a new backup.
*
@@ -70,6 +93,8 @@ abstract static class BuilderImpl extends Builder {
private Timestamp versionTime;
private DatabaseId database;
private long size;
+ private BackupEncryptionConfig encryptionConfig;
+ private EncryptionInfo encryptionInfo;
private com.google.spanner.admin.database.v1.Backup proto;
BuilderImpl(BackupId id) {
@@ -83,6 +108,8 @@ abstract static class BuilderImpl extends Builder {
this.versionTime = other.versionTime;
this.database = other.database;
this.size = other.size;
+ this.encryptionConfig = other.encryptionConfig;
+ this.encryptionInfo = other.encryptionInfo;
this.proto = other.proto;
}
@@ -113,12 +140,24 @@ public Builder setDatabase(DatabaseId database) {
return this;
}
+ @Override
+ public Builder setEncryptionConfig(BackupEncryptionConfig encryptionConfig) {
+ this.encryptionConfig = encryptionConfig;
+ return this;
+ }
+
@Override
Builder setSize(long size) {
this.size = size;
return this;
}
+ @Override
+ Builder setEncryptionInfo(EncryptionInfo encryptionInfo) {
+ this.encryptionInfo = encryptionInfo;
+ return this;
+ }
+
@Override
Builder setProto(@Nullable com.google.spanner.admin.database.v1.Backup proto) {
this.proto = proto;
@@ -142,12 +181,16 @@ public enum State {
private final Timestamp versionTime;
private final DatabaseId database;
private final long size;
+ private final BackupEncryptionConfig encryptionConfig;
+ private final EncryptionInfo encryptionInfo;
private final com.google.spanner.admin.database.v1.Backup proto;
BackupInfo(BuilderImpl builder) {
this.id = builder.id;
this.state = builder.state;
this.size = builder.size;
+ this.encryptionConfig = builder.encryptionConfig;
+ this.encryptionInfo = builder.encryptionInfo;
this.expireTime = builder.expireTime;
this.versionTime = builder.versionTime;
this.database = builder.database;
@@ -174,6 +217,22 @@ public long getSize() {
return size;
}
+ /**
+ * Returns the {@link BackupEncryptionConfig} to encrypt the backup during its creation. Returns
+ * null
if no customer-managed encryption key should be used.
+ */
+ public BackupEncryptionConfig getEncryptionConfig() {
+ return encryptionConfig;
+ }
+
+ /**
+ * Returns the {@link EncryptionInfo} of the backup if the backup is encrypted, or null
+ *
if this backup is not encrypted.
+ */
+ public EncryptionInfo getEncryptionInfo() {
+ return encryptionInfo;
+ }
+
/** Returns the expire time of the backup. */
public Timestamp getExpireTime() {
return expireTime;
@@ -206,6 +265,8 @@ public boolean equals(Object o) {
return id.equals(that.id)
&& state == that.state
&& size == that.size
+ && Objects.equals(encryptionConfig, that.encryptionConfig)
+ && Objects.equals(encryptionInfo, that.encryptionInfo)
&& Objects.equals(expireTime, that.expireTime)
&& Objects.equals(versionTime, that.versionTime)
&& Objects.equals(database, that.database);
@@ -213,13 +274,21 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
- return Objects.hash(id, state, size, expireTime, versionTime, database);
+ return Objects.hash(
+ id, state, size, encryptionConfig, encryptionInfo, expireTime, versionTime, database);
}
@Override
public String toString() {
return String.format(
- "Backup[%s, %s, %d, %s, %s, %s]",
- id.getName(), state, size, expireTime, versionTime, database);
+ "Backup[%s, %s, %d, %s, %s, %s, %s, %s]",
+ id.getName(),
+ state,
+ size,
+ encryptionConfig,
+ encryptionInfo,
+ expireTime,
+ versionTime,
+ database);
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java
index a442ad2399..05ba3f2edf 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java
@@ -22,6 +22,7 @@
import com.google.api.gax.paging.Page;
import com.google.cloud.Policy;
import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.encryption.CustomerManagedEncryption;
import com.google.common.base.Preconditions;
import com.google.longrunning.Operation;
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
@@ -185,6 +186,7 @@ static Database fromProto(
.setRestoreInfo(RestoreInfo.fromProtoOrNullIfDefaultInstance(proto.getRestoreInfo()))
.setVersionRetentionPeriod(proto.getVersionRetentionPeriod())
.setEarliestVersionTime(Timestamp.fromProto(proto.getEarliestVersionTime()))
+ .setEncryptionConfig(CustomerManagedEncryption.fromProtoOrNull(proto.getEncryptionConfig()))
.setProto(proto)
.build();
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
index eae1a3cdf4..2e4fd09951 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
@@ -24,6 +24,7 @@
import com.google.longrunning.Operation;
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
+import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import java.util.List;
@@ -68,9 +69,53 @@ public interface DatabaseAdminClient {
OperationFuture createDatabase(
String instanceId, String databaseId, Iterable statements) throws SpannerException;
+ /**
+ * Creates a database in a Cloud Spanner instance. Any configuration options in the {@link
+ * Database} instance will be included in the {@link CreateDatabaseRequest}.
+ *
+ * Example to create an encrypted database.
+ *
+ *
{@code
+ * Database dbInfo =
+ * dbClient
+ * .newDatabaseBuilder(DatabaseId.of("my-project", "my-instance", "my-database"))
+ * .setEncryptionConfig(
+ * EncryptionConfig.ofKey(
+ * "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"))
+ * .build();
+ * Operation op = dbAdminClient
+ * .createDatabase(
+ * dbInfo,
+ * Arrays.asList(
+ * "CREATE TABLE Singers (\n"
+ * + " SingerId INT64 NOT NULL,\n"
+ * + " FirstName STRING(1024),\n"
+ * + " LastName STRING(1024),\n"
+ * + " SingerInfo BYTES(MAX)\n"
+ * + ") PRIMARY KEY (SingerId)",
+ * "CREATE TABLE Albums (\n"
+ * + " SingerId INT64 NOT NULL,\n"
+ * + " AlbumId INT64 NOT NULL,\n"
+ * + " AlbumTitle STRING(MAX)\n"
+ * + ") PRIMARY KEY (SingerId, AlbumId),\n"
+ * + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"));
+ * Database db = op.waitFor().getResult();
+ * }
+ *
+ * @see also #createDatabase(String, String, Iterable)
+ */
+ OperationFuture createDatabase(
+ Database database, Iterable statements) throws SpannerException;
+
+ /** Returns a builder for a {@code Database} object with the given id. */
+ Database.Builder newDatabaseBuilder(DatabaseId id);
+
/** Returns a builder for a {@code Backup} object with the given id. */
Backup.Builder newBackupBuilder(BackupId id);
+ /** Returns a builder for a {@link Restore} object with the given source and destination */
+ Restore.Builder newRestoreBuilder(BackupId source, DatabaseId destination);
+
/**
* Creates a new backup from a database in a Cloud Spanner instance.
*
@@ -90,8 +135,8 @@ OperationFuture createDatabase(
* Backup backup = op.get();
* }
*
- * @param instanceId the id of the instance where the database to backup is located and where the
- * backup will be created.
+ * @param sourceInstanceId the id of the instance where the database to backup is located and
+ * where the backup will be created.
* @param backupId the id of the backup which will be created. It must conform to the regular
* expression [a-z][a-z0-9_\-]*[a-z0-9] and be between 2 and 60 characters in length.
* @param databaseId the id of the database to backup.
@@ -102,21 +147,27 @@ OperationFuture createBackup(
throws SpannerException;
/**
- * Creates a new backup from a database in a Cloud Spanner instance.
+ * Creates a new backup from a database in a Cloud Spanner. Any configuration options in the
+ * {@link Backup} instance will be included in the {@link
+ * com.google.spanner.admin.database.v1.CreateBackupRequest}.
*
- * Example to create a backup.
+ *
Example to create an encrypted backup.
*
*
{@code
- * BackupId backupId = BackupId.of("project", "instance", "backup-id");
+ * BackupId backupId = BackupId.of("project", "instance", "backup-id");
* DatabaseId databaseId = DatabaseId.of("project", "instance", "database-id");
- * Timestamp expireTime = Timestamp.ofTimeMicroseconds(expireTimeMicros);
+ * Timestamp expireTime = Timestamp.ofTimeMicroseconds(expireTimeMicros);
* Timestamp versionTime = Timestamp.ofTimeMicroseconds(versionTimeMicros);
+ * EncryptionConfig encryptionConfig =
+ * EncryptionConfig.ofKey(
+ * "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"));
*
* Backup backupToCreate = dbAdminClient
* .newBackupBuilder(backupId)
* .setDatabase(databaseId)
* .setExpireTime(expireTime)
* .setVersionTime(versionTime)
+ * .setEncryptionConfig(encryptionConfig)
* .build();
*
* OperationFuture op = dbAdminClient.createBackup(backupToCreate);
@@ -138,7 +189,7 @@ OperationFuture createBackup(
* String backupId = my_backup_id;
* String restoreInstanceId = my_db_instance_id;
* String restoreDatabaseId = my_database_id;
- * OperationFuture op = dbAdminClient
+ * OperationFuture op = dbAdminClient
* .restoreDatabase(
* backupInstanceId,
* backupId,
@@ -153,10 +204,37 @@ OperationFuture createBackup(
* be a different instance than where the backup is stored.
* @param restoreDatabaseId the id of the database to restore to.
*/
- public OperationFuture restoreDatabase(
+ OperationFuture restoreDatabase(
String backupInstanceId, String backupId, String restoreInstanceId, String restoreDatabaseId)
throws SpannerException;
+ /**
+ * Restore a database from a backup. The database that is restored will be created and may not
+ * already exist.
+ *
+ * Example to restore an encrypted database.
+ *
+ *
{@code
+ * final Restore restore = dbAdminClient
+ * .newRestoreBuilder(
+ * BackupId.of("my-project", "my-instance", "my-backup"),
+ * DatabaseId.of("my-project", "my-instance", "my-database")
+ * )
+ * .setEncryptionConfig(EncryptionConfig.ofKey(
+ * "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"))
+ * .build();
+ *
+ * final OperationFuture op = dbAdminClient
+ * .restoreDatabase(restore);
+ *
+ * Database database = op.get();
+ * }
+ *
+ * @param restore a {@link Restore} instance with the backup source and destination database
+ */
+ OperationFuture restoreDatabase(Restore restore)
+ throws SpannerException;
+
/** Lists long-running database operations on the specified instance. */
Page listDatabaseOperations(String instanceId, ListOption... options);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
index a5eed214a4..6129e0fa2d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
@@ -25,6 +25,7 @@
import com.google.cloud.Policy;
import com.google.cloud.Policy.DefaultMarshaller;
import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.DatabaseInfo.State;
import com.google.cloud.spanner.Options.ListOption;
import com.google.cloud.spanner.SpannerImpl.PageFetcher;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
@@ -72,21 +73,37 @@ private static String randomOperationId() {
return ("r" + uuid.toString()).replace("-", "_");
}
+ @Override
+ public Database.Builder newDatabaseBuilder(DatabaseId databaseId) {
+ return new Database.Builder(this, databaseId);
+ }
+
@Override
public Backup.Builder newBackupBuilder(BackupId backupId) {
return new Backup.Builder(this, backupId);
}
+ @Override
+ public Restore.Builder newRestoreBuilder(BackupId source, DatabaseId destination) {
+ return new Restore.Builder(source, destination);
+ }
+
@Override
public OperationFuture restoreDatabase(
String backupInstanceId, String backupId, String restoreInstanceId, String restoreDatabaseId)
throws SpannerException {
- String databaseInstanceName = getInstanceName(restoreInstanceId);
- String backupName = getBackupName(backupInstanceId, backupId);
+ return restoreDatabase(
+ newRestoreBuilder(
+ BackupId.of(projectId, backupInstanceId, backupId),
+ DatabaseId.of(projectId, restoreInstanceId, restoreDatabaseId))
+ .build());
+ }
- OperationFuture
- rawOperationFuture =
- rpc.restoreDatabase(databaseInstanceName, restoreDatabaseId, backupName);
+ @Override
+ public OperationFuture restoreDatabase(Restore restore)
+ throws SpannerException {
+ final OperationFuture
+ rawOperationFuture = rpc.restoreDatabase(restore);
return new OperationFutureImpl(
rawOperationFuture.getPollingFuture(),
@@ -114,29 +131,25 @@ public Database apply(Exception e) {
public OperationFuture createBackup(
String instanceId, String backupId, String databaseId, Timestamp expireTime)
throws SpannerException {
- final Backup backup =
+ final Backup backupInfo =
newBackupBuilder(BackupId.of(projectId, instanceId, backupId))
.setDatabase(DatabaseId.of(projectId, instanceId, databaseId))
.setExpireTime(expireTime)
.build();
- return createBackup(backup);
+
+ return createBackup(backupInfo);
}
@Override
- public OperationFuture createBackup(final Backup backup) {
- final String instanceId = backup.getInstanceId().getInstance();
- final String databaseId = backup.getDatabase().getDatabase();
- final String backupId = backup.getId().getBackup();
- final com.google.spanner.admin.database.v1.Backup.Builder backupBuilder =
- com.google.spanner.admin.database.v1.Backup.newBuilder()
- .setDatabase(getDatabaseName(instanceId, databaseId))
- .setExpireTime(backup.getExpireTime().toProto());
- if (backup.getVersionTime() != null) {
- backupBuilder.setVersionTime(backup.getVersionTime().toProto());
- }
- final String instanceName = getInstanceName(instanceId);
+ public OperationFuture createBackup(Backup backupInfo)
+ throws SpannerException {
+ Preconditions.checkArgument(
+ backupInfo.getExpireTime() != null, "Cannot create a backup without an expire time");
+ Preconditions.checkArgument(
+ backupInfo.getDatabase() != null, "Cannot create a backup without a source database");
+
final OperationFuture
- rawOperationFuture = rpc.createBackup(instanceName, backupId, backupBuilder.build());
+ rawOperationFuture = rpc.createBackup(backupInfo);
return new OperationFutureImpl<>(
rawOperationFuture.getPollingFuture(),
@@ -154,6 +167,7 @@ public Backup apply(OperationSnapshot snapshot) {
.setExpireTime(proto.getExpireTime())
.setVersionTime(proto.getVersionTime())
.setState(proto.getState())
+ .setEncryptionInfo(proto.getEncryptionInfo())
.build(),
DatabaseAdminClientImpl.this);
}
@@ -281,11 +295,19 @@ public Backup fromProto(com.google.spanner.admin.database.v1.Backup proto) {
@Override
public OperationFuture createDatabase(
String instanceId, String databaseId, Iterable statements) throws SpannerException {
- // CreateDatabase() is not idempotent, so we're not retrying this request.
- String instanceName = getInstanceName(instanceId);
- String createStatement = "CREATE DATABASE `" + databaseId + "`";
+ return createDatabase(
+ new Database(DatabaseId.of(projectId, instanceId, databaseId), State.UNSPECIFIED, this),
+ statements);
+ }
+
+ @Override
+ public OperationFuture createDatabase(
+ Database database, Iterable statements) throws SpannerException {
+ String createStatement = "CREATE DATABASE `" + database.getId().getDatabase() + "`";
OperationFuture
- rawOperationFuture = rpc.createDatabase(instanceName, createStatement, statements);
+ rawOperationFuture =
+ rpc.createDatabase(
+ database.getId().getInstanceId().getName(), createStatement, statements, database);
return new OperationFutureImpl(
rawOperationFuture.getPollingFuture(),
rawOperationFuture.getInitialFuture(),
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
index 5ba9f0aa76..101fdd4e64 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.encryption.CustomerManagedEncryption;
import com.google.common.base.Preconditions;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -34,6 +35,15 @@ public abstract static class Builder {
abstract Builder setEarliestVersionTime(Timestamp earliestVersionTime);
+ /**
+ * Optional for creating a new backup.
+ *
+ * The encryption configuration to be used for the database. The only encryption, other than
+ * Google's default encryption, is a customer managed encryption with a provided key. If no
+ * encryption is provided, Google's default encryption will be used.
+ */
+ public abstract Builder setEncryptionConfig(CustomerManagedEncryption encryptionConfig);
+
abstract Builder setProto(com.google.spanner.admin.database.v1.Database proto);
/** Builds the database from this builder. */
@@ -47,6 +57,7 @@ abstract static class BuilderImpl extends Builder {
private RestoreInfo restoreInfo;
private String versionRetentionPeriod;
private Timestamp earliestVersionTime;
+ private CustomerManagedEncryption encryptionConfig;
private com.google.spanner.admin.database.v1.Database proto;
BuilderImpl(DatabaseId id) {
@@ -60,6 +71,7 @@ abstract static class BuilderImpl extends Builder {
this.restoreInfo = other.restoreInfo;
this.versionRetentionPeriod = other.versionRetentionPeriod;
this.earliestVersionTime = other.earliestVersionTime;
+ this.encryptionConfig = other.encryptionConfig;
this.proto = other.proto;
}
@@ -93,6 +105,12 @@ Builder setEarliestVersionTime(Timestamp earliestVersionTime) {
return this;
}
+ @Override
+ public Builder setEncryptionConfig(@Nullable CustomerManagedEncryption encryptionConfig) {
+ this.encryptionConfig = encryptionConfig;
+ return this;
+ }
+
@Override
Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) {
this.proto = proto;
@@ -118,6 +136,7 @@ public enum State {
private final RestoreInfo restoreInfo;
private final String versionRetentionPeriod;
private final Timestamp earliestVersionTime;
+ private final CustomerManagedEncryption encryptionConfig;
private final com.google.spanner.admin.database.v1.Database proto;
public DatabaseInfo(DatabaseId id, State state) {
@@ -127,6 +146,7 @@ public DatabaseInfo(DatabaseId id, State state) {
this.restoreInfo = null;
this.versionRetentionPeriod = null;
this.earliestVersionTime = null;
+ this.encryptionConfig = null;
this.proto = null;
}
@@ -137,6 +157,7 @@ public DatabaseInfo(DatabaseId id, State state) {
this.restoreInfo = builder.restoreInfo;
this.versionRetentionPeriod = builder.versionRetentionPeriod;
this.earliestVersionTime = builder.earliestVersionTime;
+ this.encryptionConfig = builder.encryptionConfig;
this.proto = builder.proto;
}
@@ -180,6 +201,14 @@ public Timestamp getEarliestVersionTime() {
return restoreInfo;
}
+ /**
+ * Returns the {@link CustomerManagedEncryption} of the database if the database is encrypted, or
+ * null
if this database is not encrypted.
+ */
+ public @Nullable CustomerManagedEncryption getEncryptionConfig() {
+ return encryptionConfig;
+ }
+
/** Returns the raw proto instance that was used to construct this {@link Database}. */
public @Nullable com.google.spanner.admin.database.v1.Database getProto() {
return proto;
@@ -199,19 +228,32 @@ public boolean equals(Object o) {
&& Objects.equals(createTime, that.createTime)
&& Objects.equals(restoreInfo, that.restoreInfo)
&& Objects.equals(versionRetentionPeriod, that.versionRetentionPeriod)
- && Objects.equals(earliestVersionTime, that.earliestVersionTime);
+ && Objects.equals(earliestVersionTime, that.earliestVersionTime)
+ && Objects.equals(encryptionConfig, that.encryptionConfig);
}
@Override
public int hashCode() {
return Objects.hash(
- id, state, createTime, restoreInfo, versionRetentionPeriod, earliestVersionTime);
+ id,
+ state,
+ createTime,
+ restoreInfo,
+ versionRetentionPeriod,
+ earliestVersionTime,
+ encryptionConfig);
}
@Override
public String toString() {
return String.format(
- "Database[%s, %s, %s, %s, %s, %s]",
- id.getName(), state, createTime, restoreInfo, versionRetentionPeriod, earliestVersionTime);
+ "Database[%s, %s, %s, %s, %s, %s, %s]",
+ id.getName(),
+ state,
+ createTime,
+ restoreInfo,
+ versionRetentionPeriod,
+ earliestVersionTime,
+ encryptionConfig);
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Restore.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Restore.java
new file mode 100644
index 0000000000..d6a9c28850
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Restore.java
@@ -0,0 +1,109 @@
+/*
+ * 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;
+
+import com.google.cloud.spanner.encryption.RestoreEncryptionConfig;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Objects;
+
+/** Represents a restore operation of a Cloud Spanner backup. */
+public class Restore {
+
+ public static class Builder {
+
+ private final BackupId source;
+ private final DatabaseId destination;
+ private RestoreEncryptionConfig encryptionConfig;
+
+ public Builder(BackupId source, DatabaseId destination) {
+ this.source = source;
+ this.destination = destination;
+ }
+
+ /**
+ * Optional for restoring a backup.
+ *
+ *
The encryption configuration to be used for the backup. The possible configurations are
+ * {@link com.google.cloud.spanner.encryption.CustomerManagedEncryption}, {@link
+ * com.google.cloud.spanner.encryption.GoogleDefaultEncryption} and {@link
+ * com.google.cloud.spanner.encryption.UseBackupEncryption}.
+ *
+ *
If no encryption config is given the database will be restored with the same encryption as
+ * set by the backup ({@link com.google.cloud.spanner.encryption.UseBackupEncryption}).
+ */
+ public Builder setEncryptionConfig(RestoreEncryptionConfig encryptionConfig) {
+ this.encryptionConfig = encryptionConfig;
+ return this;
+ }
+
+ public Restore build() {
+ return new Restore(this);
+ }
+ }
+
+ private final BackupId source;
+ private final DatabaseId destination;
+ private final RestoreEncryptionConfig encryptionConfig;
+
+ Restore(Builder builder) {
+ this(builder.source, builder.destination, builder.encryptionConfig);
+ }
+
+ @VisibleForTesting
+ Restore(BackupId source, DatabaseId destination, RestoreEncryptionConfig encryptionConfig) {
+ this.source = source;
+ this.destination = destination;
+ this.encryptionConfig = encryptionConfig;
+ }
+
+ public BackupId getSource() {
+ return source;
+ }
+
+ public DatabaseId getDestination() {
+ return destination;
+ }
+
+ public RestoreEncryptionConfig getEncryptionConfig() {
+ return encryptionConfig;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Restore restore = (Restore) o;
+ return Objects.equals(source, restore.source)
+ && Objects.equals(destination, restore.destination)
+ && Objects.equals(encryptionConfig, restore.encryptionConfig);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(source, destination, encryptionConfig);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Restore[%s, %s, %s]", source.getName(), destination.getName(), encryptionConfig);
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/BackupEncryptionConfig.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/BackupEncryptionConfig.java
new file mode 100644
index 0000000000..a6e9fc1356
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/BackupEncryptionConfig.java
@@ -0,0 +1,23 @@
+/*
+ * 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.encryption;
+
+import com.google.api.core.InternalApi;
+
+/** Marker interface for encryption configurations that can be applied on backups. */
+@InternalApi
+public interface BackupEncryptionConfig {}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java
new file mode 100644
index 0000000000..fdc48cf3d2
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java
@@ -0,0 +1,66 @@
+/*
+ * 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.encryption;
+
+import com.google.spanner.admin.database.v1.EncryptionConfig;
+import java.util.Objects;
+
+/** The data is encrypted with a key provided by the customer. */
+public class CustomerManagedEncryption implements BackupEncryptionConfig, RestoreEncryptionConfig {
+
+ private final String kmsKeyName;
+
+ CustomerManagedEncryption(String kmsKeyName) {
+ this.kmsKeyName = kmsKeyName;
+ }
+
+ public String getKmsKeyName() {
+ return kmsKeyName;
+ }
+
+ /**
+ * Returns a {@link CustomerManagedEncryption} instance from the given proto, or null
+ * if the given proto is the default proto instance (i.e. there is no encryption config).
+ */
+ public static CustomerManagedEncryption fromProtoOrNull(EncryptionConfig proto) {
+ return proto.equals(EncryptionConfig.getDefaultInstance())
+ ? null
+ : new CustomerManagedEncryption(proto.getKmsKeyName());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CustomerManagedEncryption that = (CustomerManagedEncryption) o;
+ return Objects.equals(kmsKeyName, that.kmsKeyName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(kmsKeyName);
+ }
+
+ @Override
+ public String toString() {
+ return "CustomerManagedEncryption{" + "kmsKeyName='" + kmsKeyName + '\'' + '}';
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java
new file mode 100644
index 0000000000..0a18e9844a
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java
@@ -0,0 +1,77 @@
+/*
+ * 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.encryption;
+
+import com.google.spanner.admin.database.v1.CreateBackupEncryptionConfig;
+import com.google.spanner.admin.database.v1.EncryptionConfig;
+import com.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig;
+
+/** Maps encryption config domain classes to their protobuf counterpart. */
+public class EncryptionConfigProtoMapper {
+
+ /** Returns an encryption config to be used for a database. */
+ public static EncryptionConfig encryptionConfig(CustomerManagedEncryption config) {
+ return EncryptionConfig.newBuilder().setKmsKeyName(config.getKmsKeyName()).build();
+ }
+
+ /** Returns an encryption config to be used for a backup. */
+ public static CreateBackupEncryptionConfig createBackupEncryptionConfig(
+ BackupEncryptionConfig config) {
+ if (config instanceof CustomerManagedEncryption) {
+ return CreateBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION)
+ .setKmsKeyName(((CustomerManagedEncryption) config).getKmsKeyName())
+ .build();
+ } else if (config instanceof GoogleDefaultEncryption) {
+ return CreateBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(CreateBackupEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION)
+ .build();
+ } else if (config instanceof UseDatabaseEncryption) {
+ return CreateBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(CreateBackupEncryptionConfig.EncryptionType.USE_DATABASE_ENCRYPTION)
+ .build();
+ } else {
+ throw new IllegalArgumentException("Unknown backup encryption configuration " + config);
+ }
+ }
+
+ /** Returns an encryption config to be used for a database restore. */
+ public static RestoreDatabaseEncryptionConfig restoreDatabaseEncryptionConfig(
+ RestoreEncryptionConfig config) {
+ if (config instanceof CustomerManagedEncryption) {
+ return RestoreDatabaseEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ RestoreDatabaseEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION)
+ .setKmsKeyName(((CustomerManagedEncryption) config).getKmsKeyName())
+ .build();
+ } else if (config instanceof GoogleDefaultEncryption) {
+ return RestoreDatabaseEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ RestoreDatabaseEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION)
+ .build();
+ } else if (config instanceof UseBackupEncryption) {
+ return RestoreDatabaseEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ RestoreDatabaseEncryptionConfig.EncryptionType
+ .USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION)
+ .build();
+ } else {
+ throw new IllegalArgumentException("Unknown restore encryption configuration " + config);
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigs.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigs.java
new file mode 100644
index 0000000000..6f77da2c87
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigs.java
@@ -0,0 +1,45 @@
+/*
+ * 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.encryption;
+
+import com.google.api.client.util.Preconditions;
+
+/** Encryption configuration factory. */
+public class EncryptionConfigs {
+
+ /** Returns a customer managed encryption configuration for the given key. */
+ public static CustomerManagedEncryption customerManagedEncryption(String kmsKeyName) {
+ Preconditions.checkArgument(
+ kmsKeyName != null, "Customer managed encryption key name must not be null");
+ return new CustomerManagedEncryption(kmsKeyName);
+ }
+
+ /** Returns google default encryption configuration. */
+ public static GoogleDefaultEncryption googleDefaultEncryption() {
+ return GoogleDefaultEncryption.INSTANCE;
+ }
+
+ /** Returns use database encryption configuration. */
+ public static UseDatabaseEncryption useDatabaseEncryption() {
+ return UseDatabaseEncryption.INSTANCE;
+ }
+
+ /** Returns use backup encryption configuration. */
+ public static UseBackupEncryption useBackupEncryption() {
+ return UseBackupEncryption.INSTANCE;
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionInfo.java
new file mode 100644
index 0000000000..f811cfc101
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionInfo.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2020 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.encryption;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.rpc.Status;
+import java.util.Objects;
+
+/** Represents the encryption information for a Cloud Spanner backup. */
+public class EncryptionInfo {
+
+ private final String kmsKeyVersion;
+ private final com.google.spanner.admin.database.v1.EncryptionInfo.Type encryptionType;
+ private final Status encryptionStatus;
+
+ public EncryptionInfo(com.google.spanner.admin.database.v1.EncryptionInfo proto) {
+ this(proto.getKmsKeyVersion(), proto.getEncryptionType(), proto.getEncryptionStatus());
+ }
+
+ @VisibleForTesting
+ public EncryptionInfo(
+ String kmsKeyVersion,
+ com.google.spanner.admin.database.v1.EncryptionInfo.Type encryptionType,
+ Status encryptionStatus) {
+ this.kmsKeyVersion = kmsKeyVersion;
+ this.encryptionType = encryptionType;
+ this.encryptionStatus = encryptionStatus;
+ }
+
+ /**
+ * Returns a {@link EncryptionInfo} instance from the given proto, or null
if the
+ * given proto is the default proto instance (i.e. there is no encryption info).
+ */
+ public static EncryptionInfo fromProtoOrNull(
+ com.google.spanner.admin.database.v1.EncryptionInfo proto) {
+ return proto.equals(com.google.spanner.admin.database.v1.EncryptionInfo.getDefaultInstance())
+ ? null
+ : new EncryptionInfo(proto);
+ }
+
+ public String getKmsKeyVersion() {
+ return kmsKeyVersion;
+ }
+
+ public com.google.spanner.admin.database.v1.EncryptionInfo.Type getEncryptionType() {
+ return encryptionType;
+ }
+
+ public Status getEncryptionStatus() {
+ return encryptionStatus;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ EncryptionInfo that = (EncryptionInfo) o;
+ return Objects.equals(kmsKeyVersion, that.kmsKeyVersion)
+ && encryptionType == that.encryptionType
+ && Objects.equals(encryptionStatus, that.encryptionStatus);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(kmsKeyVersion, encryptionType, encryptionStatus);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "EncryptionInfo[kmsKeyVersion=%s,encryptionType=%s,encryptionStatus=%s]",
+ kmsKeyVersion, encryptionType, encryptionStatus);
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/GoogleDefaultEncryption.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/GoogleDefaultEncryption.java
new file mode 100644
index 0000000000..fa03da8bd5
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/GoogleDefaultEncryption.java
@@ -0,0 +1,30 @@
+/*
+ * 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.encryption;
+
+/** The data is encrypted with a key that is fully managed by Google. */
+public class GoogleDefaultEncryption implements BackupEncryptionConfig, RestoreEncryptionConfig {
+
+ static final GoogleDefaultEncryption INSTANCE = new GoogleDefaultEncryption();
+
+ private GoogleDefaultEncryption() {}
+
+ @Override
+ public String toString() {
+ return "GoogleDefaultEncryption{}";
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/RestoreEncryptionConfig.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/RestoreEncryptionConfig.java
new file mode 100644
index 0000000000..b23fbe69d0
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/RestoreEncryptionConfig.java
@@ -0,0 +1,23 @@
+/*
+ * 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.encryption;
+
+import com.google.api.core.InternalApi;
+
+/** Marker interface for encryption configurations that can be applied on restores. */
+@InternalApi
+public interface RestoreEncryptionConfig {}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseBackupEncryption.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseBackupEncryption.java
new file mode 100644
index 0000000000..b3604597ab
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseBackupEncryption.java
@@ -0,0 +1,30 @@
+/*
+ * 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.encryption;
+
+/** The data is encrypted with the same configuration as specified by the backup being restored. */
+public class UseBackupEncryption implements RestoreEncryptionConfig {
+
+ static final UseBackupEncryption INSTANCE = new UseBackupEncryption();
+
+ private UseBackupEncryption() {}
+
+ @Override
+ public String toString() {
+ return "UseBackupEncryption{}";
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseDatabaseEncryption.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseDatabaseEncryption.java
new file mode 100644
index 0000000000..1fc7233496
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseDatabaseEncryption.java
@@ -0,0 +1,33 @@
+/*
+ * 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.encryption;
+
+/**
+ * The data is encrypted with the same configuration as specified by the source database for a
+ * backup.
+ */
+public class UseDatabaseEncryption implements BackupEncryptionConfig {
+
+ static final UseDatabaseEncryption INSTANCE = new UseDatabaseEncryption();
+
+ private UseDatabaseEncryption() {}
+
+ @Override
+ public String toString() {
+ return "UseDatabaseEncryption{}";
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
index 608d9e23d5..94a7a121d6 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
@@ -57,6 +57,7 @@
import com.google.cloud.grpc.GrpcTransportOptions;
import com.google.cloud.spanner.AdminRequestsPerMinuteExceededException;
import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.Restore;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
@@ -69,6 +70,7 @@
import com.google.cloud.spanner.admin.instance.v1.stub.GrpcInstanceAdminStub;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
+import com.google.cloud.spanner.encryption.EncryptionConfigProtoMapper;
import com.google.cloud.spanner.v1.stub.GrpcSpannerStub;
import com.google.cloud.spanner.v1.stub.SpannerStub;
import com.google.cloud.spanner.v1.stub.SpannerStubSettings;
@@ -971,17 +973,22 @@ public ListDatabasesResponse call() throws Exception {
public OperationFuture createDatabase(
final String instanceName,
String createDatabaseStatement,
- Iterable additionalStatements)
+ Iterable additionalStatements,
+ com.google.cloud.spanner.Database databaseInfo)
throws SpannerException {
final String databaseId =
createDatabaseStatement.substring(
"CREATE DATABASE `".length(), createDatabaseStatement.length() - 1);
- CreateDatabaseRequest request =
+ CreateDatabaseRequest.Builder requestBuilder =
CreateDatabaseRequest.newBuilder()
.setParent(instanceName)
.setCreateStatement(createDatabaseStatement)
- .addAllExtraStatements(additionalStatements)
- .build();
+ .addAllExtraStatements(additionalStatements);
+ if (databaseInfo.getEncryptionConfig() != null) {
+ requestBuilder.setEncryptionConfig(
+ EncryptionConfigProtoMapper.encryptionConfig(databaseInfo.getEncryptionConfig()));
+ }
+ final CreateDatabaseRequest request = requestBuilder.build();
OperationFutureCallable callable =
new OperationFutureCallable(
@@ -1145,15 +1152,31 @@ public List call() throws Exception {
@Override
public OperationFuture createBackup(
- final String instanceName, final String backupId, final Backup backup)
- throws SpannerException {
- CreateBackupRequest request =
+ final com.google.cloud.spanner.Backup backupInfo) throws SpannerException {
+ final String instanceName = backupInfo.getInstanceId().getName();
+ final String databaseName = backupInfo.getDatabase().getName();
+ final String backupId = backupInfo.getId().getBackup();
+ final Backup.Builder backupBuilder =
+ com.google.spanner.admin.database.v1.Backup.newBuilder()
+ .setDatabase(databaseName)
+ .setExpireTime(backupInfo.getExpireTime().toProto());
+ if (backupInfo.getVersionTime() != null) {
+ backupBuilder.setVersionTime(backupInfo.getVersionTime().toProto());
+ }
+ final Backup backup = backupBuilder.build();
+
+ final CreateBackupRequest.Builder requestBuilder =
CreateBackupRequest.newBuilder()
.setParent(instanceName)
.setBackupId(backupId)
- .setBackup(backup)
- .build();
- OperationFutureCallable callable =
+ .setBackup(backup);
+ if (backupInfo.getEncryptionConfig() != null) {
+ requestBuilder.setEncryptionConfig(
+ EncryptionConfigProtoMapper.createBackupEncryptionConfig(
+ backupInfo.getEncryptionConfig()));
+ }
+ final CreateBackupRequest request = requestBuilder.build();
+ final OperationFutureCallable callable =
new OperationFutureCallable(
databaseAdminStub.createBackupOperationCallable(),
request,
@@ -1197,48 +1220,54 @@ public Timestamp apply(Operation input) {
}
@Override
- public OperationFuture restoreDatabase(
- final String databaseInstanceName, final String databaseId, String backupName) {
- RestoreDatabaseRequest request =
+ public OperationFuture restoreDatabase(final Restore restore) {
+ final String databaseInstanceName = restore.getDestination().getInstanceId().getName();
+ final String databaseId = restore.getDestination().getDatabase();
+ final RestoreDatabaseRequest.Builder requestBuilder =
RestoreDatabaseRequest.newBuilder()
.setParent(databaseInstanceName)
.setDatabaseId(databaseId)
- .setBackup(backupName)
- .build();
+ .setBackup(restore.getSource().getName());
+ if (restore.getEncryptionConfig() != null) {
+ requestBuilder.setEncryptionConfig(
+ EncryptionConfigProtoMapper.restoreDatabaseEncryptionConfig(
+ restore.getEncryptionConfig()));
+ }
- OperationFutureCallable callable =
- new OperationFutureCallable(
- databaseAdminStub.restoreDatabaseOperationCallable(),
- request,
- DatabaseAdminGrpc.getRestoreDatabaseMethod(),
- databaseInstanceName,
- new OperationsLister() {
- @Override
- public Paginated listOperations(String nextPageToken) {
- return listDatabaseOperations(
- databaseInstanceName,
- 0,
- String.format(
- "(metadata.@type:type.googleapis.com/%s) AND (metadata.name:%s)",
- RestoreDatabaseMetadata.getDescriptor().getFullName(),
- String.format("%s/databases/%s", databaseInstanceName, databaseId)),
- nextPageToken);
- }
- },
- new Function() {
- @Override
- public Timestamp apply(Operation input) {
- try {
- return input
- .getMetadata()
- .unpack(RestoreDatabaseMetadata.class)
- .getProgress()
- .getStartTime();
- } catch (InvalidProtocolBufferException e) {
- return null;
- }
- }
- });
+ final OperationFutureCallable
+ callable =
+ new OperationFutureCallable(
+ databaseAdminStub.restoreDatabaseOperationCallable(),
+ requestBuilder.build(),
+ DatabaseAdminGrpc.getRestoreDatabaseMethod(),
+ databaseInstanceName,
+ new OperationsLister() {
+ @Override
+ public Paginated listOperations(String nextPageToken) {
+ return listDatabaseOperations(
+ databaseInstanceName,
+ 0,
+ String.format(
+ "(metadata.@type:type.googleapis.com/%s) AND (metadata.name:%s)",
+ RestoreDatabaseMetadata.getDescriptor().getFullName(),
+ String.format("%s/databases/%s", databaseInstanceName, databaseId)),
+ nextPageToken);
+ }
+ },
+ new Function() {
+ @Override
+ public Timestamp apply(Operation input) {
+ try {
+ return input
+ .getMetadata()
+ .unpack(RestoreDatabaseMetadata.class)
+ .getProgress()
+ .getStartTime();
+ } catch (InvalidProtocolBufferException e) {
+ return null;
+ }
+ }
+ });
return RetryHelper.runWithRetries(
callable,
databaseAdminStubSettings
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java
index 6b42c0a754..d4a9650830 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java
@@ -22,6 +22,7 @@
import com.google.api.gax.retrying.RetrySettings;
import com.google.api.gax.rpc.ServerStream;
import com.google.cloud.ServiceRpc;
+import com.google.cloud.spanner.Restore;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub;
@@ -198,7 +199,10 @@ Paginated listDatabases(String instanceName, int pageSize, @Nullable S
throws SpannerException;
OperationFuture createDatabase(
- String instanceName, String createDatabaseStatement, Iterable additionalStatements)
+ String instanceName,
+ String createDatabaseStatement,
+ Iterable additionalStatements,
+ com.google.cloud.spanner.Database database)
throws SpannerException;
OperationFuture updateDatabaseDdl(
@@ -216,26 +220,22 @@ Paginated listBackups(
throws SpannerException;
/**
- * Creates a new backup from the source database specified in the {@link Backup} instance.
+ * Creates a new backup from the source database specified in the {@link
+ * com.google.cloud.spanner.Backup} instance.
*
- * @param instanceName the name of the instance where the backup should be created.
- * @param backupId the id of the backup to create.
- * @param backup the backup to create. The database and expireTime fields of the backup must be
- * filled.
+ * @param backupInfo the backup to create. The instance, database and expireTime fields of the
+ * backup must be filled.
* @return the operation that monitors the backup creation.
*/
OperationFuture createBackup(
- String instanceName, String backupId, Backup backup) throws SpannerException;
+ com.google.cloud.spanner.Backup backupInfo) throws SpannerException;
/**
* Restore a backup into the given database.
*
- * @param instanceName Fully qualified name of instance where to restore the database
- * @param databaseId DatabaseId to restore into
- * @param backupName Fully qualified name of backup to restore from
+ * @param restore a {@link Restore} instance with the backup source and destination database
*/
- OperationFuture restoreDatabase(
- String instanceName, String databaseId, String backupName);
+ OperationFuture restoreDatabase(Restore restore);
/** Gets the backup with the specified name. */
Backup getBackup(String backupName) throws SpannerException;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
index 80b99f679b..f2b479b666 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
@@ -30,6 +31,9 @@
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Backup.Builder;
import com.google.cloud.spanner.BackupInfo.State;
+import com.google.cloud.spanner.encryption.EncryptionInfo;
+import com.google.rpc.Code;
+import com.google.rpc.Status;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
@@ -42,11 +46,20 @@
@RunWith(JUnit4.class)
public class BackupTest {
+
private static final String NAME =
"projects/test-project/instances/test-instance/backups/backup-1";
private static final String DB = "projects/test-project/instances/test-instance/databases/db-1";
private static final Timestamp EXP_TIME = Timestamp.ofTimeSecondsAndNanos(1000L, 1000);
private static final Timestamp VERSION_TIME = Timestamp.ofTimeSecondsAndNanos(2000L, 2000);
+ public static final String KMS_KEY_VERSION = "key-version";
+ private static final com.google.spanner.admin.database.v1.EncryptionInfo ENCRYPTION_INFO =
+ com.google.spanner.admin.database.v1.EncryptionInfo.newBuilder()
+ .setEncryptionType(
+ com.google.spanner.admin.database.v1.EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION)
+ .setEncryptionStatus(Status.newBuilder().setCode(Code.OK.getNumber()))
+ .setKmsKeyVersion(KMS_KEY_VERSION)
+ .build();
@Mock DatabaseAdminClient dbClient;
@@ -100,37 +113,6 @@ public void create() {
verify(dbClient).createBackup(backup);
}
- @Test
- public void createWithoutSource() {
- Timestamp expireTime = Timestamp.now();
- Backup backup =
- dbClient
- .newBackupBuilder(BackupId.of("test-project", "dest-instance", "backup-id"))
- .setExpireTime(expireTime)
- .build();
- try {
- backup.create();
- fail("Expected exception");
- } catch (IllegalStateException e) {
- assertNotNull(e.getMessage());
- }
- }
-
- @Test
- public void createWithoutExpireTime() {
- Backup backup =
- dbClient
- .newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
- .setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
- .build();
- try {
- backup.create();
- fail("Expected exception");
- } catch (IllegalStateException e) {
- assertNotNull(e.getMessage());
- }
- }
-
@Test
public void createWithoutVersionTimeShouldSucceed() {
final Timestamp expireTime = Timestamp.now();
@@ -318,6 +300,17 @@ public void fromProto() {
assertThat(backup.getState()).isEqualTo(BackupInfo.State.CREATING);
assertThat(backup.getExpireTime()).isEqualTo(EXP_TIME);
assertThat(backup.getVersionTime()).isEqualTo(VERSION_TIME);
+ assertThat(backup.getEncryptionInfo())
+ .isEqualTo(EncryptionInfo.fromProtoOrNull(ENCRYPTION_INFO));
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ final Backup backup1 = createBackup();
+ final Backup backup2 = createBackup();
+
+ assertEquals(backup1, backup2);
+ assertEquals(backup1.hashCode(), backup2.hashCode());
}
private Backup createBackup() {
@@ -329,6 +322,7 @@ private Backup createBackup() {
com.google.protobuf.Timestamp.newBuilder().setSeconds(1000L).setNanos(1000).build())
.setVersionTime(
com.google.protobuf.Timestamp.newBuilder().setSeconds(2000L).setNanos(2000).build())
+ .setEncryptionInfo(ENCRYPTION_INFO)
.setState(com.google.spanner.admin.database.v1.Backup.State.CREATING)
.build();
return Backup.fromProto(proto, dbClient);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java
index fb617797aa..b782847629 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -25,6 +26,8 @@
import com.google.cloud.Identity;
import com.google.cloud.Role;
import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.DatabaseInfo.State;
+import com.google.cloud.spanner.encryption.EncryptionConfigs;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated;
import com.google.common.collect.ImmutableList;
@@ -43,6 +46,7 @@
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.Database;
+import com.google.spanner.admin.database.v1.EncryptionInfo;
import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import java.util.Arrays;
@@ -50,14 +54,12 @@
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
-/** Unit tests for {@link com.google.cloud.spanner.SpannerImpl.DatabaseAdminClientImpl}. */
@RunWith(JUnit4.class)
public class DatabaseAdminClientImplTest {
private static final String PROJECT_ID = "my-project";
@@ -72,6 +74,9 @@ public class DatabaseAdminClientImplTest {
private static final String BK_NAME2 = "projects/my-project/instances/my-instance/backups/my-bk2";
private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now();
private static final String VERSION_RETENTION_PERIOD = "7d";
+ private static final String KMS_KEY_NAME =
+ "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key";
+ private static final String KMS_KEY_VERSION = "1";
@Mock SpannerRpc rpc;
DatabaseAdminClientImpl client;
@@ -91,6 +96,16 @@ private Database getDatabaseProto() {
.build();
}
+ private Database getEncryptedDatabaseProto() {
+ return getDatabaseProto()
+ .toBuilder()
+ .setEncryptionConfig(
+ com.google.spanner.admin.database.v1.EncryptionConfig.newBuilder()
+ .setKmsKeyName(KMS_KEY_NAME)
+ .build())
+ .build();
+ }
+
private Database getAnotherDatabaseProto() {
return Database.newBuilder().setName(DB_NAME2).setState(Database.State.READY).build();
}
@@ -110,6 +125,15 @@ private Backup getBackupProto() {
.build();
}
+ private Backup getEncryptedBackupProto() {
+ return Backup.newBuilder()
+ .setName(BK_NAME)
+ .setDatabase(DB_NAME)
+ .setState(Backup.State.READY)
+ .setEncryptionInfo(EncryptionInfo.newBuilder().setKmsKeyVersion(KMS_KEY_VERSION).build())
+ .build();
+ }
+
private Backup getAnotherBackupProto() {
return Backup.newBuilder()
.setName(BK_NAME2)
@@ -134,7 +158,11 @@ public void createDatabase() throws Exception {
OperationFutureUtil.immediateOperationFuture(
"createDatabase", getDatabaseProto(), CreateDatabaseMetadata.getDefaultInstance());
when(rpc.createDatabase(
- INSTANCE_NAME, "CREATE DATABASE `" + DB_ID + "`", Collections.emptyList()))
+ INSTANCE_NAME,
+ "CREATE DATABASE `" + DB_ID + "`",
+ Collections.emptyList(),
+ new com.google.cloud.spanner.Database(
+ DatabaseId.of(DB_NAME), State.UNSPECIFIED, client)))
.thenReturn(rawOperationFuture);
OperationFuture op =
client.createDatabase(INSTANCE_ID, DB_ID, Collections.emptyList());
@@ -142,6 +170,31 @@ public void createDatabase() throws Exception {
assertThat(op.get().getId().getName()).isEqualTo(DB_NAME);
}
+ @Test
+ public void createEncryptedDatabase() throws Exception {
+ com.google.cloud.spanner.Database database =
+ client
+ .newDatabaseBuilder(DatabaseId.of(DB_NAME))
+ .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME))
+ .build();
+
+ OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "createDatabase",
+ getEncryptedDatabaseProto(),
+ CreateDatabaseMetadata.getDefaultInstance());
+ when(rpc.createDatabase(
+ INSTANCE_NAME,
+ "CREATE DATABASE `" + DB_ID + "`",
+ Collections.emptyList(),
+ database))
+ .thenReturn(rawOperationFuture);
+ OperationFuture op =
+ client.createDatabase(database, Collections.emptyList());
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(DB_NAME);
+ }
+
@Test
public void updateDatabaseDdl() throws Exception {
String opName = DB_NAME + "/operations/myop";
@@ -208,7 +261,7 @@ public void listDatabasesError() {
SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "Test error"));
try {
client.listDatabases(INSTANCE_ID, Options.pageSize(1));
- Assert.fail("Missing expected exception");
+ fail("Missing expected exception");
} catch (SpannerException e) {
assertThat(e.getMessage()).contains(INSTANCE_NAME);
// Assert that the call was done without a page token.
@@ -226,7 +279,7 @@ public void listDatabaseErrorWithToken() {
SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "Test error"));
try {
Lists.newArrayList(client.listDatabases(INSTANCE_ID, Options.pageSize(1)).iterateAll());
- Assert.fail("Missing expected exception");
+ fail("Missing expected exception");
} catch (SpannerException e) {
assertThat(e.getMessage()).contains(INSTANCE_NAME);
// Assert that the call was done without a page token.
@@ -310,8 +363,13 @@ public void createBackupWithParams() throws Exception {
Timestamp.ofTimeMicroseconds(
TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ TimeUnit.HOURS.toMicros(28));
- Backup backup = Backup.newBuilder().setDatabase(DB_NAME).setExpireTime(t.toProto()).build();
- when(rpc.createBackup(INSTANCE_NAME, BK_ID, backup)).thenReturn(rawOperationFuture);
+ final com.google.cloud.spanner.Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(t)
+ .build();
+ when(rpc.createBackup(backup)).thenReturn(rawOperationFuture);
OperationFuture op =
client.createBackup(INSTANCE_ID, BK_ID, DB_ID, t);
assertThat(op.isDone()).isTrue();
@@ -330,12 +388,6 @@ public void createBackupWithBackupObject() throws ExecutionException, Interrupte
final Timestamp versionTime =
Timestamp.ofTimeMicroseconds(
TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()) - TimeUnit.DAYS.toMicros(2));
- final Backup expectedCallBackup =
- Backup.newBuilder()
- .setDatabase(DB_NAME)
- .setExpireTime(expireTime.toProto())
- .setVersionTime(versionTime.toProto())
- .build();
final com.google.cloud.spanner.Backup requestBackup =
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
@@ -344,7 +396,7 @@ public void createBackupWithBackupObject() throws ExecutionException, Interrupte
.setVersionTime(versionTime)
.build();
- when(rpc.createBackup(INSTANCE_NAME, BK_ID, expectedCallBackup)).thenReturn(rawOperationFuture);
+ when(rpc.createBackup(requestBackup)).thenReturn(rawOperationFuture);
final OperationFuture op =
client.createBackup(requestBackup);
@@ -352,6 +404,52 @@ public void createBackupWithBackupObject() throws ExecutionException, Interrupte
assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
}
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateBackupNoExpireTime() {
+ final com.google.cloud.spanner.Backup requestBackup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .build();
+
+ client.createBackup(requestBackup);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateBackupNoDatabase() {
+ final com.google.cloud.spanner.Backup requestBackup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setExpireTime(Timestamp.now())
+ .build();
+
+ client.createBackup(requestBackup);
+ }
+
+ @Test
+ public void createEncryptedBackup() throws ExecutionException, InterruptedException {
+ final OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "createBackup", getEncryptedBackupProto(), CreateBackupMetadata.getDefaultInstance());
+ final Timestamp t =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ + TimeUnit.HOURS.toMicros(28));
+ final com.google.cloud.spanner.Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(t)
+ .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME))
+ .build();
+ when(rpc.createBackup(backup)).thenReturn(rawOperationFuture);
+ final OperationFuture op =
+ client.createBackup(backup);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
+ assertThat(op.get().getEncryptionInfo().getKmsKeyVersion()).isEqualTo(KMS_KEY_VERSION);
+ }
+
@Test
public void deleteBackup() {
client.deleteBackup(INSTANCE_ID, BK_ID);
@@ -404,10 +502,35 @@ public void restoreDatabase() throws Exception {
OperationFuture rawOperationFuture =
OperationFutureUtil.immediateOperationFuture(
"restoreDatabase", getDatabaseProto(), RestoreDatabaseMetadata.getDefaultInstance());
- when(rpc.restoreDatabase(INSTANCE_NAME, DB_ID, BK_NAME)).thenReturn(rawOperationFuture);
+ final Restore restore =
+ new Restore.Builder(
+ BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID),
+ DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .build();
+ when(rpc.restoreDatabase(restore)).thenReturn(rawOperationFuture);
+ OperationFuture op =
+ client.restoreDatabase(restore);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(DB_NAME);
+ }
+
+ @Test
+ public void restoreEncryptedDatabase() throws Exception {
+ OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "restoreEncryptedDatabase",
+ getEncryptedDatabaseProto(),
+ RestoreDatabaseMetadata.getDefaultInstance());
+ final Restore restore =
+ new Restore.Builder(
+ BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID),
+ DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .build();
+ when(rpc.restoreDatabase(restore)).thenReturn(rawOperationFuture);
OperationFuture op =
- client.restoreDatabase(INSTANCE_ID, BK_ID, INSTANCE_ID, DB_ID);
+ client.restoreDatabase(restore);
assertThat(op.isDone()).isTrue();
assertThat(op.get().getId().getName()).isEqualTo(DB_NAME);
+ assertThat(op.get().getEncryptionConfig().getKmsKeyName()).isEqualTo(KMS_KEY_NAME);
}
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java
index 7c61720b21..7b3b533874 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -26,7 +27,13 @@
import com.google.cloud.Role;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.DatabaseInfo.State;
+import com.google.cloud.spanner.encryption.EncryptionConfigs;
+import com.google.rpc.Code;
+import com.google.rpc.Status;
+import com.google.spanner.admin.database.v1.EncryptionInfo;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,6 +51,19 @@ public class DatabaseTest {
private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now();
private static final String VERSION_RETENTION_PERIOD = "7d";
+ private static final String KMS_KEY_NAME = "kms-key-name";
+ private static final String KMS_KEY_VERSION = "kms-key-version";
+ private static final com.google.spanner.admin.database.v1.EncryptionConfig ENCRYPTION_CONFIG =
+ com.google.spanner.admin.database.v1.EncryptionConfig.newBuilder()
+ .setKmsKeyName(KMS_KEY_NAME)
+ .build();
+ private static final List ENCRYPTION_INFOS =
+ Collections.singletonList(
+ EncryptionInfo.newBuilder()
+ .setEncryptionType(EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION)
+ .setEncryptionStatus(Status.newBuilder().setCode(Code.OK.getNumber()))
+ .setKmsKeyVersion(KMS_KEY_VERSION)
+ .build());
@Mock DatabaseAdminClient dbClient;
@@ -58,6 +78,14 @@ public Backup.Builder answer(InvocationOnMock invocation) {
return new Backup.Builder(dbClient, (BackupId) invocation.getArguments()[0]);
}
});
+ when(dbClient.newDatabaseBuilder(Mockito.any(DatabaseId.class)))
+ .thenAnswer(
+ new Answer() {
+ @Override
+ public Database.Builder answer(InvocationOnMock invocation) throws Throwable {
+ return new Database.Builder(dbClient, (DatabaseId) invocation.getArguments()[0]);
+ }
+ });
}
@Test
@@ -88,6 +116,38 @@ public void fromProto() {
assertThat(db.getState()).isEqualTo(DatabaseInfo.State.CREATING);
assertThat(db.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
assertThat(db.getEarliestVersionTime()).isEqualTo(EARLIEST_VERSION_TIME);
+ assertThat(db.getEncryptionConfig())
+ .isEqualTo(EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME));
+ }
+
+ @Test
+ public void testFromProtoWithEncryptionConfig() {
+ com.google.spanner.admin.database.v1.Database proto =
+ com.google.spanner.admin.database.v1.Database.newBuilder()
+ .setName(NAME)
+ .setEncryptionConfig(
+ com.google.spanner.admin.database.v1.EncryptionConfig.newBuilder()
+ .setKmsKeyName("some-key")
+ .build())
+ .build();
+ Database db = Database.fromProto(proto, dbClient);
+ assertThat(db.getEncryptionConfig()).isNotNull();
+ assertThat(db.getEncryptionConfig().getKmsKeyName()).isEqualTo("some-key");
+ }
+
+ @Test
+ public void testBuildWithEncryptionConfig() {
+ Database db =
+ dbClient
+ .newDatabaseBuilder(DatabaseId.of("my-project", "my-instance", "my-database"))
+ .setEncryptionConfig(
+ EncryptionConfigs.customerManagedEncryption(
+ "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"))
+ .build();
+ assertThat(db.getEncryptionConfig()).isNotNull();
+ assertThat(db.getEncryptionConfig().getKmsKeyName())
+ .isEqualTo(
+ "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key");
}
@Test
@@ -120,6 +180,15 @@ public void testIAMPermissions() {
verify(dbClient).testDatabaseIAMPermissions("test-instance", "test-database", permissions);
}
+ @Test
+ public void testEqualsAndHashCode() {
+ final Database database1 = createDatabase();
+ final Database database2 = createDatabase();
+
+ assertEquals(database1, database2);
+ assertEquals(database1.hashCode(), database2.hashCode());
+ }
+
private Database createDatabase() {
com.google.spanner.admin.database.v1.Database proto =
com.google.spanner.admin.database.v1.Database.newBuilder()
@@ -127,6 +196,8 @@ private Database createDatabase() {
.setState(com.google.spanner.admin.database.v1.Database.State.CREATING)
.setEarliestVersionTime(EARLIEST_VERSION_TIME.toProto())
.setVersionRetentionPeriod(VERSION_RETENTION_PERIOD)
+ .setEncryptionConfig(ENCRYPTION_CONFIG)
+ .addAllEncryptionInfo(ENCRYPTION_INFOS)
.build();
return Database.fromProto(proto, dbClient);
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RestoreTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RestoreTest.java
new file mode 100644
index 0000000000..409efb2046
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RestoreTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.spanner.encryption.EncryptionConfigs;
+import com.google.cloud.spanner.encryption.RestoreEncryptionConfig;
+import org.junit.Test;
+
+/** Unit tests for {@link com.google.cloud.spanner.Restore} */
+public class RestoreTest {
+
+ private static final BackupId BACKUP_ID =
+ BackupId.of("test-project", "test-instance", "test-backup");
+ private static final DatabaseId DATABASE_ID =
+ DatabaseId.of("test-project", "test-instance", "test-database");
+ private static final String KMS_KEY_NAME = "kms-key-name";
+ private static final RestoreEncryptionConfig ENCRYPTION_CONFIG_INFO =
+ EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME);
+
+ @Test
+ public void testRestore() {
+ final Restore actualRestore =
+ new Restore.Builder(BACKUP_ID, DATABASE_ID)
+ .setEncryptionConfig(ENCRYPTION_CONFIG_INFO)
+ .build();
+ final Restore expectedRestore = new Restore(BACKUP_ID, DATABASE_ID, ENCRYPTION_CONFIG_INFO);
+
+ assertEquals(expectedRestore, actualRestore);
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ final Restore restore1 = new Restore(BACKUP_ID, DATABASE_ID, ENCRYPTION_CONFIG_INFO);
+ final Restore restore2 = new Restore(BACKUP_ID, DATABASE_ID, ENCRYPTION_CONFIG_INFO);
+
+ assertEquals(restore1, restore2);
+ assertEquals(restore1.hashCode(), restore2.hashCode());
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/CustomerManagedEncryptionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/CustomerManagedEncryptionTest.java
new file mode 100644
index 0000000000..5a8fe204fb
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/CustomerManagedEncryptionTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.encryption;
+
+import static org.junit.Assert.*;
+
+import com.google.spanner.admin.database.v1.EncryptionConfig;
+import org.junit.Test;
+
+/** Unit tests for {@link CustomerManagedEncryption}. */
+public class CustomerManagedEncryptionTest {
+
+ @Test
+ public void testFromProtoWithDefaultInstance() {
+ final CustomerManagedEncryption actual =
+ CustomerManagedEncryption.fromProtoOrNull(EncryptionConfig.getDefaultInstance());
+
+ assertNull(actual);
+ }
+
+ @Test
+ public void testFromProto() {
+ final CustomerManagedEncryption expected = new CustomerManagedEncryption("kms-key-name");
+ final EncryptionConfig encryptionConfig =
+ EncryptionConfig.newBuilder().setKmsKeyName("kms-key-name").build();
+
+ final CustomerManagedEncryption actual =
+ CustomerManagedEncryption.fromProtoOrNull(encryptionConfig);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ final CustomerManagedEncryption customerManagedEncryption1 =
+ new CustomerManagedEncryption("kms-key-name");
+ final CustomerManagedEncryption customerManagedEncryption2 =
+ new CustomerManagedEncryption("kms-key-name");
+
+ assertEquals(customerManagedEncryption1, customerManagedEncryption2);
+ assertEquals(customerManagedEncryption1.hashCode(), customerManagedEncryption2.hashCode());
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapperTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapperTest.java
new file mode 100644
index 0000000000..4ce300c2f6
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapperTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.encryption;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.spanner.admin.database.v1.CreateBackupEncryptionConfig;
+import com.google.spanner.admin.database.v1.EncryptionConfig;
+import com.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig;
+import org.junit.Test;
+
+/** Unit tests for {@link com.google.cloud.spanner.encryption.EncryptionConfigProtoMapper} */
+public class EncryptionConfigProtoMapperTest {
+
+ public static final String KMS_KEY_NAME = "kms-key-name";
+
+ @Test
+ public void testEncryptionConfig() {
+ final EncryptionConfig expected =
+ EncryptionConfig.newBuilder().setKmsKeyName(KMS_KEY_NAME).build();
+
+ final EncryptionConfig actual =
+ EncryptionConfigProtoMapper.encryptionConfig(new CustomerManagedEncryption(KMS_KEY_NAME));
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testCreateBackupConfigCustomerManagedEncryption() {
+ final CreateBackupEncryptionConfig expected =
+ CreateBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION)
+ .setKmsKeyName(KMS_KEY_NAME)
+ .build();
+
+ final CreateBackupEncryptionConfig actual =
+ EncryptionConfigProtoMapper.createBackupEncryptionConfig(
+ new CustomerManagedEncryption(KMS_KEY_NAME));
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testCreateBackupConfigGoogleDefaultEncryption() {
+ final CreateBackupEncryptionConfig expected =
+ CreateBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ CreateBackupEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION)
+ .build();
+
+ final CreateBackupEncryptionConfig actual =
+ EncryptionConfigProtoMapper.createBackupEncryptionConfig(GoogleDefaultEncryption.INSTANCE);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testCreateBackupConfigUseDatabaseEncryption() {
+ final CreateBackupEncryptionConfig expected =
+ CreateBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(CreateBackupEncryptionConfig.EncryptionType.USE_DATABASE_ENCRYPTION)
+ .build();
+
+ final CreateBackupEncryptionConfig actual =
+ EncryptionConfigProtoMapper.createBackupEncryptionConfig(UseDatabaseEncryption.INSTANCE);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateBackupInvalidEncryption() {
+ EncryptionConfigProtoMapper.createBackupEncryptionConfig(null);
+ }
+
+ @Test
+ public void testRestoreDatabaseConfigCustomerManagedEncryption() {
+ final RestoreDatabaseEncryptionConfig expected =
+ RestoreDatabaseEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ RestoreDatabaseEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION)
+ .setKmsKeyName(KMS_KEY_NAME)
+ .build();
+
+ final RestoreDatabaseEncryptionConfig actual =
+ EncryptionConfigProtoMapper.restoreDatabaseEncryptionConfig(
+ new CustomerManagedEncryption(KMS_KEY_NAME));
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testRestoreDatabaseConfigGoogleDefaultEncryption() {
+ final RestoreDatabaseEncryptionConfig expected =
+ RestoreDatabaseEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ RestoreDatabaseEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION)
+ .build();
+
+ final RestoreDatabaseEncryptionConfig actual =
+ EncryptionConfigProtoMapper.restoreDatabaseEncryptionConfig(
+ GoogleDefaultEncryption.INSTANCE);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testRestoreDatabaseConfigUseBackupEncryption() {
+ final RestoreDatabaseEncryptionConfig expected =
+ RestoreDatabaseEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ RestoreDatabaseEncryptionConfig.EncryptionType
+ .USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION)
+ .build();
+
+ final RestoreDatabaseEncryptionConfig actual =
+ EncryptionConfigProtoMapper.restoreDatabaseEncryptionConfig(UseBackupEncryption.INSTANCE);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRestoreDatabaseConfigInvalidEncryption() {
+ EncryptionConfigProtoMapper.restoreDatabaseEncryptionConfig(null);
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigsTest.java
new file mode 100644
index 0000000000..82f997a1c4
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigsTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.encryption;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import org.junit.Test;
+
+/** Unit tests for {@link EncryptionConfigs} */
+public class EncryptionConfigsTest {
+
+ @Test
+ public void testCustomerManagedEncryption() {
+ final CustomerManagedEncryption expected = new CustomerManagedEncryption("kms-key-name");
+
+ final CustomerManagedEncryption actual =
+ EncryptionConfigs.customerManagedEncryption("kms-key-name");
+
+ assertEquals(expected, actual);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCustomerManagedEncryptionNullKeyName() {
+ EncryptionConfigs.customerManagedEncryption(null);
+ }
+
+ @Test
+ public void testGoogleDefaultEncryption() {
+ assertSame(EncryptionConfigs.googleDefaultEncryption(), GoogleDefaultEncryption.INSTANCE);
+ }
+
+ @Test
+ public void testUseDatabaseEncryption() {
+ assertSame(EncryptionConfigs.useDatabaseEncryption(), UseDatabaseEncryption.INSTANCE);
+ }
+
+ @Test
+ public void testUseBackupEncryption() {
+ assertSame(EncryptionConfigs.useBackupEncryption(), UseBackupEncryption.INSTANCE);
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionInfoTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionInfoTest.java
new file mode 100644
index 0000000000..88a11d19c8
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionInfoTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.encryption;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.google.rpc.Code;
+import com.google.rpc.Status;
+import org.junit.Test;
+
+/** Unit tests for {@link com.google.cloud.spanner.encryption.EncryptionInfo} */
+public class EncryptionInfoTest {
+
+ private static final String KMS_KEY_VERSION = "kms-key-version";
+ private static final com.google.spanner.admin.database.v1.EncryptionInfo.Type
+ CUSTOMER_MANAGED_ENCRYPTION =
+ com.google.spanner.admin.database.v1.EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION;
+ private static final Status OK_STATUS = Status.newBuilder().setCode(Code.OK_VALUE).build();
+
+ @Test
+ public void testEncryptionInfoFromProtoDefaultInstance() {
+ final EncryptionInfo encryptionInfo =
+ EncryptionInfo.fromProtoOrNull(
+ com.google.spanner.admin.database.v1.EncryptionInfo.getDefaultInstance());
+
+ assertNull(encryptionInfo);
+ }
+
+ @Test
+ public void testEncryptionInfoFromProto() {
+ final EncryptionInfo actualEncryptionInfo =
+ EncryptionInfo.fromProtoOrNull(
+ com.google.spanner.admin.database.v1.EncryptionInfo.newBuilder()
+ .setEncryptionStatus(OK_STATUS)
+ .setEncryptionTypeValue(CUSTOMER_MANAGED_ENCRYPTION.getNumber())
+ .setKmsKeyVersion(KMS_KEY_VERSION)
+ .build());
+
+ final EncryptionInfo expectedEncryptionInfo =
+ new EncryptionInfo(KMS_KEY_VERSION, CUSTOMER_MANAGED_ENCRYPTION, OK_STATUS);
+
+ assertEquals(expectedEncryptionInfo, actualEncryptionInfo);
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ final EncryptionInfo encryptionInfo1 =
+ EncryptionInfo.fromProtoOrNull(
+ com.google.spanner.admin.database.v1.EncryptionInfo.newBuilder()
+ .setEncryptionStatus(OK_STATUS)
+ .setEncryptionTypeValue(CUSTOMER_MANAGED_ENCRYPTION.getNumber())
+ .setKmsKeyVersion(KMS_KEY_VERSION)
+ .build());
+ final EncryptionInfo encryptionInfo2 =
+ EncryptionInfo.fromProtoOrNull(
+ com.google.spanner.admin.database.v1.EncryptionInfo.newBuilder()
+ .setEncryptionStatus(OK_STATUS)
+ .setEncryptionTypeValue(CUSTOMER_MANAGED_ENCRYPTION.getNumber())
+ .setKmsKeyVersion(KMS_KEY_VERSION)
+ .build());
+
+ assertEquals(encryptionInfo1, encryptionInfo2);
+ assertEquals(encryptionInfo1.hashCode(), encryptionInfo2.hashCode());
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
index 786e7b3d6d..53dcc7907f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
@@ -42,11 +42,14 @@
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.Options;
import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.Restore;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.encryption.EncryptionConfigs;
import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Iterables;
@@ -59,6 +62,7 @@
import io.grpc.Status;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
@@ -86,7 +90,9 @@
public class ITBackupTest {
private static final Logger logger = Logger.getLogger(ITBackupTest.class.getName());
private static final String EXPECTED_OP_NAME_FORMAT = "%s/backups/%s/operations/";
+ private static final String KMS_KEY_NAME_PROPERTY = "spanner.testenv.kms_key.name";
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+ private static String keyName;
private DatabaseAdminClient dbAdminClient;
private InstanceAdminClient instanceAdminClient;
@@ -101,6 +107,10 @@ public class ITBackupTest {
@BeforeClass
public static void doNotRunOnEmulator() {
assumeFalse("backups are not supported on the emulator", isUsingEmulator());
+ keyName = System.getProperty(KMS_KEY_NAME_PROPERTY);
+ Preconditions.checkNotNull(
+ keyName,
+ "Key name is null, please set a key to be used for this test. The necessary permissions should be grant to the spanner service account according to the CMEK user guide.");
}
@Before
@@ -199,13 +209,18 @@ private void waitForDbOperations(String backupId) throws InterruptedException {
@Test
public void testBackups() throws InterruptedException, ExecutionException {
// Create two test databases in parallel.
- String db1Id = testHelper.getUniqueDatabaseId() + "_db1";
+ final String db1Id = testHelper.getUniqueDatabaseId() + "_db1";
+ final Database sourceDatabase1 =
+ dbAdminClient
+ .newDatabaseBuilder(DatabaseId.of(projectId, instanceId, db1Id))
+ .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(keyName))
+ .build();
logger.info(String.format("Creating test database %s", db1Id));
OperationFuture dbOp1 =
dbAdminClient.createDatabase(
- testHelper.getInstanceId().getInstance(),
- db1Id,
- Arrays.asList("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)"));
+ sourceDatabase1,
+ Collections.singletonList(
+ "CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)"));
String db2Id = testHelper.getUniqueDatabaseId() + "_db2";
logger.info(String.format("Creating test database %s", db2Id));
OperationFuture dbOp2 =
@@ -229,6 +244,9 @@ public void testBackups() throws InterruptedException, ExecutionException {
.to("TEST")
.build()));
+ // Verifies that the database encryption has been properly set
+ testDatabaseEncryption(db1);
+
// Create two backups in parallel.
String backupId1 = testHelper.getUniqueBackupId() + "_bck1";
String backupId2 = testHelper.getUniqueBackupId() + "_bck2";
@@ -236,12 +254,14 @@ public void testBackups() throws InterruptedException, ExecutionException {
Timestamp versionTime = getCurrentTimestamp(client);
logger.info(String.format("Creating backups %s and %s in parallel", backupId1, backupId2));
// This backup has the version time specified as the server's current timestamp
+ // This backup is encrypted with a customer managed key
final Backup backupToCreate1 =
dbAdminClient
.newBackupBuilder(BackupId.of(projectId, instanceId, backupId1))
.setDatabase(db1.getId())
.setExpireTime(expireTime)
.setVersionTime(versionTime)
+ .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(keyName))
.build();
// This backup has no version time specified
final Backup backupToCreate2 =
@@ -287,12 +307,14 @@ public void testBackups() throws InterruptedException, ExecutionException {
"Backup2 still not finished. Test is giving up waiting for it.");
}
logger.info("Long-running operations finished. Getting backups by id.");
- backup1 = dbAdminClient.getBackup(this.instance.getId().getInstance(), backupId1);
- backup2 = dbAdminClient.getBackup(this.instance.getId().getInstance(), backupId2);
+ backup1 = dbAdminClient.getBackup(instance.getId().getInstance(), backupId1);
+ backup2 = dbAdminClient.getBackup(instance.getId().getInstance(), backupId2);
}
// Verifies that backup version time is the specified one
testBackupVersionTime(backup1, versionTime);
+ // Verifies that backup encryption has been properly set
+ testBackupEncryption(backup1);
// Insert some more data into db2 to get a timestamp from the server.
Timestamp commitTs =
@@ -308,7 +330,7 @@ public void testBackups() throws InterruptedException, ExecutionException {
// Test listing operations.
// List all backups.
logger.info("Listing all backups");
- assertThat(this.instance.listBackups().iterateAll()).containsAtLeast(backup1, backup2);
+ assertThat(instance.listBackups().iterateAll()).containsAtLeast(backup1, backup2);
// List all backups whose names contain 'bck1'.
logger.info("Listing backups with name bck1");
assertThat(
@@ -425,6 +447,20 @@ private void testBackupVersionTime(Backup backup, Timestamp versionTime) {
logger.info("Done verifying backup version time for " + backup.getId());
}
+ private void testDatabaseEncryption(Database database) {
+ logger.info("Verifying database encryption for " + database.getId());
+ assertThat(database.getEncryptionConfig()).isNotNull();
+ assertThat(database.getEncryptionConfig().getKmsKeyName()).isEqualTo(keyName);
+ logger.info("Done verifying database encryption for " + database.getId());
+ }
+
+ private void testBackupEncryption(Backup backup) {
+ logger.info("Verifying backup encryption for " + backup.getId());
+ assertThat(backup.getEncryptionInfo()).isNotNull();
+ assertThat(backup.getEncryptionInfo().getKmsKeyVersion()).isNotNull();
+ logger.info("Done verifying backup encryption for " + backup.getId());
+ }
+
private void testMetadata(
OperationFuture op1,
OperationFuture op2,
@@ -583,15 +619,20 @@ private void testRestore(
// Restore the backup to a new database.
String restoredDb = testHelper.getUniqueDatabaseId();
String restoreOperationName;
- OperationFuture restoreOp;
+ OperationFuture restoreOperation;
int attempts = 0;
while (true) {
try {
logger.info(
String.format(
"Restoring backup %s to database %s", backup.getId().getBackup(), restoredDb));
- restoreOp = backup.restore(DatabaseId.of(testHelper.getInstanceId(), restoredDb));
- restoreOperationName = restoreOp.getName();
+ final Restore restore =
+ dbAdminClient
+ .newRestoreBuilder(backup.getId(), DatabaseId.of(projectId, instanceId, restoredDb))
+ .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(keyName))
+ .build();
+ restoreOperation = dbAdminClient.restoreDatabase(restore);
+ restoreOperationName = restoreOperation.getName();
break;
} catch (ExecutionException e) {
if (e.getCause() instanceof FailedPreconditionException
@@ -617,7 +658,7 @@ private void testRestore(
}
databases.add(restoredDb);
logger.info(String.format("Restore operation %s running", restoreOperationName));
- RestoreDatabaseMetadata metadata = restoreOp.getMetadata().get();
+ RestoreDatabaseMetadata metadata = restoreOperation.getMetadata().get();
assertThat(metadata.getBackupInfo().getBackup()).isEqualTo(backup.getId().getName());
assertThat(metadata.getSourceType()).isEqualTo(RestoreSourceType.BACKUP);
assertThat(metadata.getName())
@@ -630,7 +671,7 @@ private void testRestore(
// verifyRestoreOperations(backupOp.getName(), restoreOperationName);
// Wait until the restore operation has finished successfully.
- Database database = restoreOp.get();
+ Database database = restoreOperation.get();
assertThat(database.getId().getDatabase()).isEqualTo(restoredDb);
// Reloads the database
@@ -639,6 +680,7 @@ private void testRestore(
Timestamp.fromProto(
reloadedDatabase.getProto().getRestoreInfo().getBackupInfo().getVersionTime()))
.isEqualTo(versionTime);
+ testDatabaseEncryption(reloadedDatabase);
// Restoring the backup to an existing database should fail.
try {