diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 98dd288b28..f340f5e196 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -319,7 +319,7 @@ com/google/cloud/spanner/Value java.util.List getNumericArray() - + 7012 @@ -406,7 +406,7 @@ com/google/cloud/spanner/AbstractLazyInitializer java.lang.Object initialize() - + 7004 @@ -504,4 +504,71 @@ com/google/cloud/spanner/DatabaseAdminClient com.google.api.gax.longrunning.OperationFuture createBackup(com.google.cloud.spanner.Backup) + + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.longrunning.OperationFuture createDatabase(java.lang.String, java.lang.String, java.lang.Iterable) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.longrunning.OperationFuture createBackup(java.lang.String, java.lang.String, com.google.spanner.admin.database.v1.Backup) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.longrunning.OperationFuture restoreDatabase(java.lang.String, java.lang.String, java.lang.String) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.api.gax.longrunning.OperationFuture createDatabase(java.lang.String, java.lang.String, java.lang.Iterable) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.api.gax.longrunning.OperationFuture createBackup(java.lang.String, java.lang.String, com.google.spanner.admin.database.v1.Backup) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.api.gax.longrunning.OperationFuture restoreDatabase(java.lang.String, java.lang.String, java.lang.String) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.longrunning.OperationFuture createDatabase(com.google.cloud.spanner.Database, java.lang.Iterable) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.longrunning.OperationFuture createBackup(com.google.cloud.spanner.Backup) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.longrunning.OperationFuture restoreDatabase(com.google.cloud.spanner.Restore) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.cloud.spanner.Database$Builder newDatabaseBuilder(com.google.cloud.spanner.DatabaseId) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.cloud.spanner.Restore$Builder newRestoreBuilder(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.DatabaseId) + + + 7013 + com/google/cloud/spanner/DatabaseInfo$Builder + com.google.cloud.spanner.DatabaseInfo$Builder setEncryptionConfig(com.google.cloud.spanner.encryption.CustomerManagedEncryption) + + + 7013 + com/google/cloud/spanner/BackupInfo$Builder + com.google.cloud.spanner.BackupInfo$Builder setEncryptionConfig(com.google.cloud.spanner.encryption.BackupEncryptionConfig) + diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 1cd5665190..4534486af9 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -73,6 +73,7 @@ com.google.cloud.spanner.GceTestEnvConfig projects/gcloud-devel/instances/spanner-testing gcloud-devel + projects/gcloud-devel/locations/us-central1/keyRings/spanner-test-keyring/cryptoKeys/spanner-test-key 3000 @@ -383,7 +384,7 @@ - generate-test-sql-scripts diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java index a0052cd381..27308c0400 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java @@ -23,6 +23,7 @@ import com.google.api.gax.paging.Page; import com.google.cloud.Policy; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.encryption.EncryptionInfo; import com.google.longrunning.Operation; import com.google.spanner.admin.database.v1.CreateBackupMetadata; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; @@ -61,10 +62,6 @@ public Backup build() { /** Creates a backup on the server based on the source of this {@link Backup} instance. */ public OperationFuture 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 {