From 8338116dffe847931cae1212333af04338ea1d45 Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Thu, 18 Mar 2021 15:58:42 +1100 Subject: [PATCH 1/9] feat!: customer-managed encryption keys for Spanner (#666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for encrypted databases * fix: fix deps and clirr failures * tests: add additional tests for keys * tests: remove IT and add unit * fix: set null instead of default instance * fix: does not set encryption info if null Does not set encryption info in the request if it is null * fix: fixes dependencies * feature: adds support for encrypted backup Adds the possibility to set encryption config info in the creation of a backup. * feature: adds support for restoring encrypted dbs * Revert "tests: remove IT and add unit" This reverts commit cc19cf2efd32007ecd351c3b0c1b5942256c31ce. * fix: makes the setEncryptionConfigInfo public This is so a backup can be encrypted * feature: adds tests for cmek Adds tests for creating encrypted database, creating encrypted backups and restoring encrypted databases. * fix: removes keys after test finishes Destroy keys used in CMEK tests * fix: fixes clirr errors * fix: ignores failing cmek tests Ignores the failing CMEK tests until the backend support is enabled in production. * fix: uses wrapper encryption info for backups * fix: fixes clirr issues * fix: re-orders clirr issues * fix: addresses PR comments * test: fixes database admin client tests * chore: re-formats the code * chore: fixes clirr checks * tests: adds unit tests for domain classes Adds unit tests for EncryptionConfigInfo, EncryptionConfig, Backup and Restore. * chore: renames EncryptionConfigInfo Renames EncryptionConfigInfo to EncryptionConfig in order to mirror what is the protobuf definition. * tests: do not create a key on CMEK test Instead use an existing key and fails if the key is not present. * feat: allows multiple encryption configs Allows customer managed encryption for create databases (google default encryption is just nullifying the value here). Allows customer managed encryption, google default encryption and database encryption for create backups. Allows customer managed encryption, google default encryption and backup encryption for restore databases. * docs: adds java doc to Restore class * chore: refactors pom.xml Uses variables to define project id and instance id for running integration tests. * test: fixes cmek integration test * chore: fixes linting * Revert "chore: refactors pom.xml" This reverts commit d182b8316bf78322f22dcc7d48d6955cecd844f7. * test: unifies cmek backup and restore tests * chore: adds toString to encryption classes * docs: updates DatabaseInfo javadoc Co-authored-by: Knut Olav Løite * docs: updates Restore javadocs Co-authored-by: Knut Olav Løite * docs: updates DatabaseInfo javadocs Co-authored-by: Knut Olav Løite * fix: addresses PR comments * tests: reformats Co-authored-by: Olav Loite --- .../clirr-ignored-differences.xml | 71 +++++++- google-cloud-spanner/pom.xml | 3 +- .../java/com/google/cloud/spanner/Backup.java | 6 +- .../com/google/cloud/spanner/BackupInfo.java | 75 ++++++++- .../com/google/cloud/spanner/Database.java | 2 + .../cloud/spanner/DatabaseAdminClient.java | 94 ++++++++++- .../spanner/DatabaseAdminClientImpl.java | 70 +++++--- .../google/cloud/spanner/DatabaseInfo.java | 50 +++++- .../com/google/cloud/spanner/Restore.java | 109 ++++++++++++ .../encryption/BackupEncryptionConfig.java | 23 +++ .../encryption/CustomerManagedEncryption.java | 66 ++++++++ .../EncryptionConfigProtoMapper.java | 77 +++++++++ .../spanner/encryption/EncryptionConfigs.java | 45 +++++ .../spanner/encryption/EncryptionInfo.java | 92 +++++++++++ .../encryption/GoogleDefaultEncryption.java | 30 ++++ .../encryption/RestoreEncryptionConfig.java | 23 +++ .../encryption/UseBackupEncryption.java | 30 ++++ .../encryption/UseDatabaseEncryption.java | 33 ++++ .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 125 ++++++++------ .../cloud/spanner/spi/v1/SpannerRpc.java | 24 +-- .../com/google/cloud/spanner/BackupTest.java | 56 +++---- .../spanner/DatabaseAdminClientImplTest.java | 155 ++++++++++++++++-- .../google/cloud/spanner/DatabaseTest.java | 71 ++++++++ .../com/google/cloud/spanner/RestoreTest.java | 54 ++++++ .../CustomerManagedEncryptionTest.java | 56 +++++++ .../EncryptionConfigProtoMapperTest.java | 139 ++++++++++++++++ .../encryption/EncryptionConfigsTest.java | 55 +++++++ .../encryption/EncryptionInfoTest.java | 79 +++++++++ .../google/cloud/spanner/it/ITBackupTest.java | 66 ++++++-- 29 files changed, 1614 insertions(+), 165 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/Restore.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/BackupEncryptionConfig.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigs.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionInfo.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/GoogleDefaultEncryption.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/RestoreEncryptionConfig.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseBackupEncryption.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/UseDatabaseEncryption.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/RestoreTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/CustomerManagedEncryptionTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapperTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionConfigsTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/encryption/EncryptionInfoTest.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 98dd288b28a..f340f5e1963 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 1cd56651900..4534486af9a 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 a0052cd381a..27308c04009 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 199e6ae2ae4..0657ff2b7b8 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 a442ad2399c..05ba3f2edfb 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 eae1a3cdf4e..2e4fd09951e 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 a5eed214a4e..6129e0fa2d0 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 5ba9f0aa765..101fdd4e641 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 00000000000..d6a9c28850b --- /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 00000000000..a6e9fc13562 --- /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 00000000000..fdc48cf3d2c --- /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 00000000000..0a18e9844a1 --- /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 00000000000..6f77da2c872 --- /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 00000000000..f811cfc1010 --- /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 00000000000..fa03da8bd51 --- /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 00000000000..b23fbe69d04 --- /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 00000000000..b3604597ab6 --- /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 00000000000..1fc7233496d --- /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 608d9e23d53..94a7a121d67 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 6b42c0a7544..d4a96508307 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 80b99f679b4..f2b479b6660 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 fb617797aa0..b7828476296 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 7c61720b219..7b3b533874b 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 00000000000..409efb20469 --- /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 00000000000..5a8fe204fb3 --- /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 00000000000..4ce300c2f66 --- /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 00000000000..82f997a1c40 --- /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 00000000000..88a11d19c8f --- /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 786e7b3d6d5..53dcc7907f8 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 { From 7af19514dfae5f87ba50572d8867568d2c09daab Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Thu, 18 Mar 2021 15:58:55 +1100 Subject: [PATCH 2/9] feat!: drops support of Java 7 (#946) * feat!: drops support of Java 7 From the next major release we will be only supporting Java 8+ * tests: do not run unit tests for java 7 --- .github/sync-repo-settings.yaml | 1 - .github/workflows/ci.yaml | 4 ++-- .kokoro/nightly/java7.cfg | 7 ------- .kokoro/presubmit/java7.cfg | 7 ------- CONTRIBUTING.md | 7 ++----- README.md | 5 +---- 6 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 .kokoro/nightly/java7.cfg delete mode 100644 .kokoro/presubmit/java7.cfg diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index a87cb7bbd2b..2b81a9785dd 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -34,7 +34,6 @@ branchProtectionRules: - "linkage-monitor" - "lint" - "clirr" - - "units (7)" - "units (8)" - "units (11)" - "Kokoro - Test: Integration" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index def8b3a2c84..cf64069a5ed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [7, 8, 11] + java: [8, 11] steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 @@ -80,4 +80,4 @@ jobs: - run: java -version - run: .kokoro/build.sh env: - JOB_TYPE: clirr \ No newline at end of file + JOB_TYPE: clirr diff --git a/.kokoro/nightly/java7.cfg b/.kokoro/nightly/java7.cfg deleted file mode 100644 index cb24f44eea3..00000000000 --- a/.kokoro/nightly/java7.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java7" -} diff --git a/.kokoro/presubmit/java7.cfg b/.kokoro/presubmit/java7.cfg deleted file mode 100644 index cb24f44eea3..00000000000 --- a/.kokoro/presubmit/java7.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java7" -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2dbdee06bc..3e5cbba5a3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,12 +57,9 @@ Code Samples must be bundled in separate Maven modules, and guarded by a Maven profile with the name `enable-samples`. The samples must be separate from the primary project for a few reasons: -1. Primary projects have a minimum Java version of Java 7 whereas samples have - a minimum Java version of Java 8. Due to this we need the ability to - selectively exclude samples from a build run. -2. Many code samples depend on external GCP services and need +1. Many code samples depend on external GCP services and need credentials to access the service. -3. Code samples are not released as Maven artifacts and must be excluded from +2. Code samples are not released as Maven artifacts and must be excluded from release builds. ### Building diff --git a/README.md b/README.md index c103612f20c..65772740c1b 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ Cloud Spanner uses gRPC for the transport layer. ## Java Versions -Java 7 or above is required for using this client. +Java 8 or above is required for using this client. ## Versioning @@ -285,7 +285,6 @@ Apache 2.0 - See [LICENSE][license] for more information. Java Version | Status ------------ | ------ -Java 7 | [![Kokoro CI][kokoro-badge-image-1]][kokoro-badge-link-1] Java 8 | [![Kokoro CI][kokoro-badge-image-2]][kokoro-badge-link-2] Java 8 OSX | [![Kokoro CI][kokoro-badge-image-3]][kokoro-badge-link-3] Java 8 Windows | [![Kokoro CI][kokoro-badge-image-4]][kokoro-badge-link-4] @@ -295,8 +294,6 @@ Java is a registered trademark of Oracle and/or its affiliates. [product-docs]: https://cloud.google.com/spanner/docs/ [javadocs]: https://googleapis.dev/java/google-cloud-spanner/latest/ -[kokoro-badge-image-1]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java7.svg -[kokoro-badge-link-1]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java7.html [kokoro-badge-image-2]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java8.svg [kokoro-badge-link-2]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java8.html [kokoro-badge-image-3]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java8-osx.svg From 29a200decf8bdbf205f7e370d7f4273ab291dab4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 18 Mar 2021 05:10:03 +0000 Subject: [PATCH 3/9] chore: release 5.2.1-SNAPSHOT (#992) :robot: I have created a release \*beep\* \*boop\* --- ### Updating meta-information for bleeding-edge SNAPSHOT release. --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- google-cloud-spanner-bom/pom.xml | 18 +++++++++--------- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 ++++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 14 +++++++------- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 94279ff033f..b681006bc4f 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 5.2.0 + 5.2.1-SNAPSHOT pom com.google.cloud @@ -64,43 +64,43 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.cloud google-cloud-spanner - 5.2.0 + 5.2.1-SNAPSHOT com.google.cloud google-cloud-spanner test-jar - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 4534486af9a..9fd4dafea8d 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 5.2.0 + 5.2.1-SNAPSHOT jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index a6d49f54c2e..2e829b6546a 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 5.2.0 + 5.2.1-SNAPSHOT grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index c874313e4e3..3ffd913ac70 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 5.2.0 + 5.2.1-SNAPSHOT grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 36b9bdde336..240126d05de 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 5.2.0 + 5.2.1-SNAPSHOT grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index cf7712e9579..f91c634b456 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 5.2.0 + 5.2.1-SNAPSHOT Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -71,37 +71,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 5.2.0 + 5.2.1-SNAPSHOT com.google.cloud google-cloud-spanner - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 407a9723faa..03a6f845371 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 5.2.0 + 5.2.1-SNAPSHOT proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 0768962deba..77af2d3bc3e 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 5.2.0 + 5.2.1-SNAPSHOT proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 249e100580d..8031e55e086 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 5.2.0 + 5.2.1-SNAPSHOT proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 1ccaf82ceaf..0dec641ccf2 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 5.2.0 + 5.2.1-SNAPSHOT diff --git a/versions.txt b/versions.txt index f857ad69666..14d117e2d19 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:5.2.0:5.2.0 -proto-google-cloud-spanner-v1:5.2.0:5.2.0 -proto-google-cloud-spanner-admin-database-v1:5.2.0:5.2.0 -grpc-google-cloud-spanner-v1:5.2.0:5.2.0 -grpc-google-cloud-spanner-admin-instance-v1:5.2.0:5.2.0 -grpc-google-cloud-spanner-admin-database-v1:5.2.0:5.2.0 -google-cloud-spanner:5.2.0:5.2.0 \ No newline at end of file +proto-google-cloud-spanner-admin-instance-v1:5.2.0:5.2.1-SNAPSHOT +proto-google-cloud-spanner-v1:5.2.0:5.2.1-SNAPSHOT +proto-google-cloud-spanner-admin-database-v1:5.2.0:5.2.1-SNAPSHOT +grpc-google-cloud-spanner-v1:5.2.0:5.2.1-SNAPSHOT +grpc-google-cloud-spanner-admin-instance-v1:5.2.0:5.2.1-SNAPSHOT +grpc-google-cloud-spanner-admin-database-v1:5.2.0:5.2.1-SNAPSHOT +google-cloud-spanner:5.2.0:5.2.1-SNAPSHOT \ No newline at end of file From e7ec96ec09a9d273d4f576356d3e4c6cbbb6de9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 18 Mar 2021 06:33:32 +0100 Subject: [PATCH 4/9] feat!: add closeAsync() method to Connection (#984) Co-authored-by: Thiago Nunes --- .../clirr-ignored-differences.xml | 7 ++ .../cloud/spanner/connection/Connection.java | 11 +++- .../spanner/connection/ConnectionImpl.java | 64 +++++++++++++------ 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index f340f5e1963..efc5fd4de36 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -571,4 +571,11 @@ com/google/cloud/spanner/BackupInfo$Builder com.google.cloud.spanner.BackupInfo$Builder setEncryptionConfig(com.google.cloud.spanner.encryption.BackupEncryptionConfig) + + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture closeAsync() + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java index bd1214d6a19..4a0bcf27019 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java @@ -139,10 +139,19 @@ */ @InternalApi public interface Connection extends AutoCloseable { - /** Closes this connection. This is a no-op if the {@link Connection} has alread been closed. */ + + /** Closes this connection. This is a no-op if the {@link Connection} has already been closed. */ @Override void close(); + /** + * Closes this connection without blocking. This is a no-op if the {@link Connection} has already + * been closed. The {@link Connection} is no longer usable directly after calling this method. The + * returned {@link ApiFuture} is done when the running statement(s) (if any) on the connection + * have finished. + */ + ApiFuture closeAsync(); + /** @return true if this connection has been closed. */ boolean isClosed(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java index 4f9703807a8..41fca0fcfbc 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java @@ -18,6 +18,7 @@ import static com.google.cloud.spanner.SpannerApiFutures.get; +import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; import com.google.cloud.Timestamp; @@ -42,6 +43,7 @@ import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import java.util.ArrayList; import java.util.Collections; @@ -49,8 +51,11 @@ import java.util.LinkedList; import java.util.List; import java.util.Stack; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.threeten.bp.Instant; /** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */ @@ -257,28 +262,49 @@ private DdlClient createDdlClient() { @Override public void close() { + try { + closeAsync().get(10L, TimeUnit.SECONDS); + } catch (SpannerException | InterruptedException | ExecutionException | TimeoutException e) { + // ignore and continue to close the connection. + } finally { + statementExecutor.shutdownNow(); + } + } + + public ApiFuture closeAsync() { if (!isClosed()) { - try { - if (isTransactionStarted()) { - try { - rollback(); - } catch (Exception e) { - // Ignore as we are closing the connection. - } - } - // Try to wait for the current statement to finish (if any) before we actually close the - // connection. - this.closed = true; - statementExecutor.shutdown(); - leakedException = null; - spannerPool.removeConnection(options, this); - statementExecutor.awaitTermination(10L, TimeUnit.SECONDS); - } catch (InterruptedException e) { - // ignore and continue to close the connection. - } finally { - statementExecutor.shutdownNow(); + List> futures = new ArrayList<>(); + if (isTransactionStarted()) { + futures.add(rollbackAsync()); } + // Try to wait for the current statement to finish (if any) before we actually close the + // connection. + this.closed = true; + // Add a no-op statement to the execute. Once this has been executed, we know that all + // preceeding statements have also been executed, as the executor is single-threaded and + // executes all statements in order of submitting. + futures.add( + statementExecutor.submit( + new Callable() { + @Override + public Void call() throws Exception { + return null; + } + })); + statementExecutor.shutdown(); + leakedException = null; + spannerPool.removeConnection(options, this); + return ApiFutures.transform( + ApiFutures.allAsList(futures), + new ApiFunction, Void>() { + @Override + public Void apply(List input) { + return null; + } + }, + MoreExecutors.directExecutor()); } + return ApiFutures.immediateFuture(null); } /** Get the current unit-of-work type of this connection. */ From fc305b7cbf59cbdeecf1d0486b7f5ca86925148e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 17 Mar 2021 22:34:03 -0700 Subject: [PATCH 5/9] chore: regenerate README (#993) This PR was generated using Autosynth. :rainbow:

Log from Synthtool ``` 2021-03-18 05:00:56,730 synthtool [DEBUG] > Executing /root/.cache/synthtool/java-spanner/.github/readme/synth.py. On branch autosynth-readme nothing to commit, working tree clean 2021-03-18 05:00:57,698 synthtool [DEBUG] > Wrote metadata to .github/readme/synth.metadata/synth.metadata. ```
Full log will be available here: https://source.cloud.google.com/results/invocations/4feedb3a-2e39-4824-b0f1-48e0dce90a14/targets - [ ] To automatically regenerate this PR, check this box. --- .github/readme/synth.metadata/synth.metadata | 2 +- README.md | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/readme/synth.metadata/synth.metadata b/.github/readme/synth.metadata/synth.metadata index f2f3015d804..dac2f0ec35a 100644 --- a/.github/readme/synth.metadata/synth.metadata +++ b/.github/readme/synth.metadata/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "8eab323893fda4a464f66cffa87f553f6888c8f8" + "sha": "7af19514dfae5f87ba50572d8867568d2c09daab" } }, { diff --git a/README.md b/README.md index 65772740c1b..c103612f20c 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ Cloud Spanner uses gRPC for the transport layer. ## Java Versions -Java 8 or above is required for using this client. +Java 7 or above is required for using this client. ## Versioning @@ -285,6 +285,7 @@ Apache 2.0 - See [LICENSE][license] for more information. Java Version | Status ------------ | ------ +Java 7 | [![Kokoro CI][kokoro-badge-image-1]][kokoro-badge-link-1] Java 8 | [![Kokoro CI][kokoro-badge-image-2]][kokoro-badge-link-2] Java 8 OSX | [![Kokoro CI][kokoro-badge-image-3]][kokoro-badge-link-3] Java 8 Windows | [![Kokoro CI][kokoro-badge-image-4]][kokoro-badge-link-4] @@ -294,6 +295,8 @@ Java is a registered trademark of Oracle and/or its affiliates. [product-docs]: https://cloud.google.com/spanner/docs/ [javadocs]: https://googleapis.dev/java/google-cloud-spanner/latest/ +[kokoro-badge-image-1]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java7.svg +[kokoro-badge-link-1]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java7.html [kokoro-badge-image-2]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java8.svg [kokoro-badge-link-2]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java8.html [kokoro-badge-image-3]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java8-osx.svg From 459a47756becc49c25551799d76c1771b876978f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 18 Mar 2021 09:14:02 +0100 Subject: [PATCH 6/9] test: fail if the same token is seen twice (#983) The backup pagination test should fail if the same page token is returned twice by the backend, instead of fetching the same page indefinitely. Further investigation is necessary if that actually happens, as it is not something that is expected. --- .../com/google/cloud/spanner/it/ITBackupTest.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 53dcc7907f8..6a2742c2d94 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 @@ -63,8 +63,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Random; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -100,7 +101,6 @@ public class ITBackupTest { private RemoteSpannerHelper testHelper; private List databases = new ArrayList<>(); private List backups = new ArrayList<>(); - private final Random random = new Random(); private String projectId; private String instanceId; @@ -580,8 +580,15 @@ private void testPagination(int expectedMinimumTotalBackups) { assertThat(page.getValues()).hasSize(1); numBackups++; assertThat(page.hasNextPage()).isTrue(); + Set seenPageTokens = new HashSet<>(); + seenPageTokens.add(""); while (page.hasNextPage()) { - logger.info(String.format("Fetching page %d", numBackups + 1)); + logger.info( + String.format( + "Fetching page %d with page token %s", numBackups + 1, page.getNextPageToken())); + // The backend should not return the same page token twice. + assertThat(seenPageTokens).doesNotContain(page.getNextPageToken()); + seenPageTokens.add(page.getNextPageToken()); page = dbAdminClient.listBackups( instanceId, Options.pageToken(page.getNextPageToken()), Options.pageSize(1)); From 254cfdcd2fe6c2f383127a7724ec4265106e2400 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 19 Mar 2021 19:04:15 +0100 Subject: [PATCH 7/9] chore(deps): update dependency com.google.cloud:libraries-bom to v19.2.1 (#1000) [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.google.cloud:libraries-bom](https://togithub.com/GoogleCloudPlatform/cloud-opensource-java) | `19.2.0` -> `19.2.1` | [![age](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/19.2.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/19.2.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/19.2.1/compatibility-slim/19.2.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/19.2.1/confidence-slim/19.2.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/snippets/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index f16cf9abc73..c12f5ee3369 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -33,7 +33,7 @@ com.google.cloud libraries-bom - 19.2.0 + 19.2.1 pom import From 1110700c900d8de73554ba633cb0d11f160829df Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 22 Mar 2021 00:31:31 +0100 Subject: [PATCH 8/9] chore(deps): update dependency com.google.cloud:google-cloud-spanner to v5.2.0 (#998) --- samples/install-without-bom/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 0cbc50663cc..f9e3351c52f 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -32,7 +32,7 @@ com.google.cloud google-cloud-spanner - 5.1.0 + 5.2.0 From 27d5343540e284933e93529094c2af1e53642a97 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 11:24:48 +1100 Subject: [PATCH 9/9] chore: release 6.0.0 (#994) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ google-cloud-spanner-bom/pom.xml | 18 +++++++++--------- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 ++++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 14 +++++++------- 12 files changed, 54 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4074a5f462b..2922d4e2158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [6.0.0](https://www.github.com/googleapis/java-spanner/compare/v5.2.0...v6.0.0) (2021-03-21) + + +### ⚠ BREAKING CHANGES + +* add closeAsync() method to Connection (#984) +* drops support of Java 7 (#946) +* customer-managed encryption keys for Spanner (#666) + +### Features + +* add closeAsync() method to Connection ([#984](https://www.github.com/googleapis/java-spanner/issues/984)) ([e7ec96e](https://www.github.com/googleapis/java-spanner/commit/e7ec96ec09a9d273d4f576356d3e4c6cbbb6de9e)) +* customer-managed encryption keys for Spanner ([#666](https://www.github.com/googleapis/java-spanner/issues/666)) ([8338116](https://www.github.com/googleapis/java-spanner/commit/8338116dffe847931cae1212333af04338ea1d45)) +* drops support of Java 7 ([#946](https://www.github.com/googleapis/java-spanner/issues/946)) ([7af1951](https://www.github.com/googleapis/java-spanner/commit/7af19514dfae5f87ba50572d8867568d2c09daab)) + ## [5.2.0](https://www.github.com/googleapis/java-spanner/compare/v5.1.0...v5.2.0) (2021-03-18) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index b681006bc4f..a458d6200df 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 5.2.1-SNAPSHOT + 6.0.0 pom com.google.cloud @@ -64,43 +64,43 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.cloud google-cloud-spanner - 5.2.1-SNAPSHOT + 6.0.0 com.google.cloud google-cloud-spanner test-jar - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 9fd4dafea8d..91cc08157d0 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 5.2.1-SNAPSHOT + 6.0.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index 2e829b6546a..33eaf3d1bff 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 5.2.1-SNAPSHOT + 6.0.0 grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 3ffd913ac70..d5696c1fc9a 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 5.2.1-SNAPSHOT + 6.0.0 grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 240126d05de..906c93fca08 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 5.2.1-SNAPSHOT + 6.0.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/pom.xml b/pom.xml index f91c634b456..bbfe1d23ca4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 5.2.1-SNAPSHOT + 6.0.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -71,37 +71,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 5.2.1-SNAPSHOT + 6.0.0 com.google.cloud google-cloud-spanner - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 03a6f845371..93e782515fb 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 5.2.1-SNAPSHOT + 6.0.0 proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 77af2d3bc3e..b5f6efbcd70 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 5.2.1-SNAPSHOT + 6.0.0 proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 8031e55e086..0921a8f10c4 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 5.2.1-SNAPSHOT + 6.0.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 0dec641ccf2..a566672e8db 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 5.2.1-SNAPSHOT + 6.0.0 diff --git a/versions.txt b/versions.txt index 14d117e2d19..fa0bb9fbe26 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:5.2.0:5.2.1-SNAPSHOT -proto-google-cloud-spanner-v1:5.2.0:5.2.1-SNAPSHOT -proto-google-cloud-spanner-admin-database-v1:5.2.0:5.2.1-SNAPSHOT -grpc-google-cloud-spanner-v1:5.2.0:5.2.1-SNAPSHOT -grpc-google-cloud-spanner-admin-instance-v1:5.2.0:5.2.1-SNAPSHOT -grpc-google-cloud-spanner-admin-database-v1:5.2.0:5.2.1-SNAPSHOT -google-cloud-spanner:5.2.0:5.2.1-SNAPSHOT \ No newline at end of file +proto-google-cloud-spanner-admin-instance-v1:6.0.0:6.0.0 +proto-google-cloud-spanner-v1:6.0.0:6.0.0 +proto-google-cloud-spanner-admin-database-v1:6.0.0:6.0.0 +grpc-google-cloud-spanner-v1:6.0.0:6.0.0 +grpc-google-cloud-spanner-admin-instance-v1:6.0.0:6.0.0 +grpc-google-cloud-spanner-admin-database-v1:6.0.0:6.0.0 +google-cloud-spanner:6.0.0:6.0.0 \ No newline at end of file