From dc79366f05f28d4b1a68240989b5ad06621e4a01 Mon Sep 17 00:00:00 2001 From: Astha Mohta <35952883+asthamohta@users.noreply.github.com> Date: Mon, 28 Mar 2022 12:24:06 +0530 Subject: [PATCH] feat: Copy Backup Support (#1778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: copy backup - porting code changes * feat: copy backup - porting partial sample * feat: copy backup - cleaning up tests * feat: copy backup - cleaning up samples * feat: copy backup - cleaning up samples * feat: copy backup signature fixes * feat: copy backup sample fixes * feat: copy backup - review fixes * feat: copy backup - review fixes * feat: copy backup checkstyle fixes * feat: make CopyBackup sample runnable * fix: checkstyle violation * feat: adding max expire time and get referencing database support * samples: adding copy backup operation support * adding documentation * linting changes * changes as per review * removing samples * review changes * changes as per review * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Anshul Goyal Co-authored-by: Knut Olav Løite Co-authored-by: Owl Bot --- README.md | 4 +- .../clirr-ignored-differences.xml | 25 ++++++ .../java/com/google/cloud/spanner/Backup.java | 2 + .../com/google/cloud/spanner/BackupInfo.java | 73 +++++++++++++++++- .../cloud/spanner/DatabaseAdminClient.java | 68 ++++++++++++++++ .../spanner/DatabaseAdminClientImpl.java | 48 +++++++++++- .../EncryptionConfigProtoMapper.java | 23 ++++++ .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 62 +++++++++++++++ .../cloud/spanner/spi/v1/SpannerRpc.java | 38 ++++----- .../com/google/cloud/spanner/BackupTest.java | 30 +++++++- .../spanner/DatabaseAdminClientImplTest.java | 77 +++++++++++++++++++ 11 files changed, 415 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 00209a3ee31..3cc889c220c 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-spanner' If you are using Gradle without BOM, add this to your dependencies ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.21.2' +implementation 'com.google.cloud:google-cloud-spanner:6.22.0' ``` If you are using SBT, add this to your dependencies ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.21.2" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.22.0" ``` ## Authentication diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 464f6e9e9f8..899b44e3546 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -35,4 +35,29 @@ com/google/cloud/spanner/connection/ConnectionOptions com.google.cloud.spanner.Dialect getDialect() + + 7013 + com/google/cloud/spanner/BackupInfo$Builder + com.google.cloud.spanner.BackupInfo$Builder setMaxExpireTime(com.google.cloud.Timestamp) + + + 7013 + com/google/cloud/spanner/BackupInfo$Builder + com.google.cloud.spanner.BackupInfo$Builder setReferencingBackup(com.google.protobuf.ProtocolStringList) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.longrunning.OperationFuture copyBackup(java.lang.String, java.lang.String, java.lang.String, com.google.cloud.Timestamp) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.longrunning.OperationFuture copyBackup(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.Backup) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.longrunning.OperationFuture copyBackup(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.Backup) + 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 27308c04009..6d694b0052a 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 @@ -183,6 +183,8 @@ static Backup fromProto( .setDatabase(DatabaseId.of(proto.getDatabase())) .setEncryptionInfo(EncryptionInfo.fromProtoOrNull(proto.getEncryptionInfo())) .setProto(proto) + .setMaxExpireTime(Timestamp.fromProto(proto.getMaxExpireTime())) + .addAllReferencingBackups(proto.getReferencingBackupsList()) .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 0657ff2b7b8..3ea3329a6db 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 @@ -21,6 +21,7 @@ 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.List; import java.util.Objects; import javax.annotation.Nullable; @@ -84,6 +85,24 @@ public abstract static class Builder { /** Builds the backup from this builder. */ public abstract Backup build(); + + /** + * Output Only. + * + *

Returns the max allowed expiration time of the backup, with microseconds granularity. + */ + protected Builder setMaxExpireTime(Timestamp maxExpireTime) { + throw new UnsupportedOperationException("Unimplemented"); + } + + /** + * Output Only. + * + *

Returns the names of the destination backups being created by copying this source backup. + */ + protected Builder addAllReferencingBackups(List referencingBackups) { + throw new UnsupportedOperationException("Unimplemented"); + } } abstract static class BuilderImpl extends Builder { @@ -96,6 +115,8 @@ abstract static class BuilderImpl extends Builder { private BackupEncryptionConfig encryptionConfig; private EncryptionInfo encryptionInfo; private com.google.spanner.admin.database.v1.Backup proto; + private Timestamp maxExpireTime; + private List referencingBackups; BuilderImpl(BackupId id) { this.id = Preconditions.checkNotNull(id); @@ -111,6 +132,8 @@ abstract static class BuilderImpl extends Builder { this.encryptionConfig = other.encryptionConfig; this.encryptionInfo = other.encryptionInfo; this.proto = other.proto; + this.maxExpireTime = other.maxExpireTime; + this.referencingBackups = other.referencingBackups; } @Override @@ -163,6 +186,18 @@ Builder setProto(@Nullable com.google.spanner.admin.database.v1.Backup proto) { this.proto = proto; return this; } + + @Override + protected Builder setMaxExpireTime(Timestamp maxExpireTime) { + this.maxExpireTime = Preconditions.checkNotNull(maxExpireTime); + return this; + } + + @Override + protected Builder addAllReferencingBackups(List referencingBackups) { + this.referencingBackups = Preconditions.checkNotNull(referencingBackups); + return this; + } } /** State of the backup. */ @@ -184,6 +219,8 @@ public enum State { private final BackupEncryptionConfig encryptionConfig; private final EncryptionInfo encryptionInfo; private final com.google.spanner.admin.database.v1.Backup proto; + private final Timestamp maxExpireTime; + private final List referencingBackups; BackupInfo(BuilderImpl builder) { this.id = builder.id; @@ -195,6 +232,8 @@ public enum State { this.versionTime = builder.versionTime; this.database = builder.database; this.proto = builder.proto; + this.maxExpireTime = builder.maxExpireTime; + this.referencingBackups = builder.referencingBackups; } /** Returns the backup id. */ @@ -253,6 +292,19 @@ public DatabaseId getDatabase() { return proto; } + /** Returns the max expire time of this {@link Backup}. */ + public Timestamp getMaxExpireTime() { + return maxExpireTime; + } + + /** + * Returns the names of the destination backups being created by copying this source backup {@link + * Backup}. + */ + public List getReferencingBackups() { + return referencingBackups; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -269,19 +321,30 @@ public boolean equals(Object o) { && Objects.equals(encryptionInfo, that.encryptionInfo) && Objects.equals(expireTime, that.expireTime) && Objects.equals(versionTime, that.versionTime) - && Objects.equals(database, that.database); + && Objects.equals(database, that.database) + && Objects.equals(maxExpireTime, that.maxExpireTime) + && Objects.equals(referencingBackups, that.referencingBackups); } @Override public int hashCode() { return Objects.hash( - id, state, size, encryptionConfig, encryptionInfo, expireTime, versionTime, database); + id, + state, + size, + encryptionConfig, + encryptionInfo, + expireTime, + versionTime, + database, + maxExpireTime, + referencingBackups); } @Override public String toString() { return String.format( - "Backup[%s, %s, %d, %s, %s, %s, %s, %s]", + "Backup[%s, %s, %d, %s, %s, %s, %s, %s, %s, %s]", id.getName(), state, size, @@ -289,6 +352,8 @@ public String toString() { encryptionInfo, expireTime, versionTime, - database); + database, + maxExpireTime, + referencingBackups); } } 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 2e4fd09951e..d3aa8b4d18f 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 @@ -22,6 +22,7 @@ import com.google.cloud.Timestamp; import com.google.cloud.spanner.Options.ListOption; import com.google.longrunning.Operation; +import com.google.spanner.admin.database.v1.CopyBackupMetadata; import com.google.spanner.admin.database.v1.CreateBackupMetadata; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.CreateDatabaseRequest; @@ -178,6 +179,73 @@ OperationFuture createBackup( */ OperationFuture createBackup(Backup backup) throws SpannerException; + /** + * Creates a copy of backup from an existing backup in a Cloud Spanner instance. + * + *

Example to copy a backup. + * + *

{@code
+   * String instanceId                  ="my_instance_id";
+   * String sourceBackupId              ="source_backup_id";
+   * String destinationBackupId         ="destination_backup_id";
+   * Timestamp expireTime               =Timestamp.ofTimeMicroseconds(micros);
+   * OperationFuture op = dbAdminClient
+   *     .copyBackup(
+   *         instanceId,
+   *         sourceBackupId,
+   *         destinationBackupId,
+   *         expireTime);
+   * Backup backup = op.get();
+   * }
+ * + * @param instanceId the id of the instance where the source backup is located and where the new + * backup will be created. + * @param sourceBackupId the source backup id. + * @param destinationBackupId 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 expireTime the time that the new backup will automatically expire. + */ + default OperationFuture copyBackup( + String instanceId, String sourceBackupId, String destinationBackupId, Timestamp expireTime) { + throw new UnsupportedOperationException("Unimplemented"); + } + + /** + * Creates a copy of backup from an existing backup in Cloud Spanner in the same instance. Any + * configuration options in the {@link Backup} instance will be included in the {@link + * com.google.spanner.admin.database.v1.CopyBackupRequest}. + * + *

The expire time of the new backup must be set and be at least 6 hours and at most 366 days + * after the creation time of the existing backup that is being copied. + * + *

Example to create a copy of a backup. + * + *

{@code
+   * BackupId sourceBackupId = BackupId.of("source-project", "source-instance", "source-backup-id");
+   * BackupId destinationBackupId = BackupId.of("destination-project", "destination-instance", "new-backup-id");
+   * Timestamp expireTime = Timestamp.ofTimeMicroseconds(expireTimeMicros);
+   * EncryptionConfig encryptionConfig =
+   *         EncryptionConfig.ofKey(
+   *             "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"));
+   *
+   * Backup destinationBackup = dbAdminClient
+   *     .newBackupBuilder(destinationBackupId)
+   *     .setExpireTime(expireTime)
+   *     .setEncryptionConfig(encryptionConfig)
+   *     .build();
+   *
+   * OperationFuture op = dbAdminClient.copyBackup(sourceBackupId, destinationBackup);
+   * Backup copiedBackup = op.get();
+   * }
+ * + * @param sourceBackupId the backup to be copied + * @param destinationBackup the new backup to create + */ + default OperationFuture copyBackup( + BackupId sourceBackupId, Backup destinationBackup) { + throw new UnsupportedOperationException("Unimplemented"); + } + /** * Restore a database from a backup. The database that is restored will be created and may not * already exist. 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 52df8701556..3285dde5f5e 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 @@ -32,10 +32,7 @@ import com.google.longrunning.Operation; import com.google.protobuf.Empty; import com.google.protobuf.FieldMask; -import com.google.spanner.admin.database.v1.CreateBackupMetadata; -import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; -import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.database.v1.*; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; @@ -165,6 +162,49 @@ public OperationFuture createBackup(Backup backupI }); } + @Override + public OperationFuture copyBackup( + String instanceId, String sourceBackupId, String destinationBackupId, Timestamp expireTime) + throws SpannerException { + final Backup destinationBackup = + newBackupBuilder(BackupId.of(projectId, instanceId, destinationBackupId)) + .setExpireTime(expireTime) + .build(); + + return copyBackup(BackupId.of(projectId, instanceId, sourceBackupId), destinationBackup); + } + + @Override + public OperationFuture copyBackup( + BackupId sourceBackupId, Backup destinationBackup) throws SpannerException { + Preconditions.checkNotNull(sourceBackupId); + Preconditions.checkNotNull(destinationBackup); + + final OperationFuture + rawOperationFuture = rpc.copyBackup(sourceBackupId, destinationBackup); + + return new OperationFutureImpl<>( + rawOperationFuture.getPollingFuture(), + rawOperationFuture.getInitialFuture(), + snapshot -> { + com.google.spanner.admin.database.v1.Backup proto = + ProtoOperationTransformers.ResponseTransformer.create( + com.google.spanner.admin.database.v1.Backup.class) + .apply(snapshot); + return Backup.fromProto( + com.google.spanner.admin.database.v1.Backup.newBuilder(proto) + .setName(proto.getName()) + .setExpireTime(proto.getExpireTime()) + .setEncryptionInfo(proto.getEncryptionInfo()) + .build(), + DatabaseAdminClientImpl.this); + }, + ProtoOperationTransformers.MetadataTransformer.create(CopyBackupMetadata.class), + e -> { + throw SpannerExceptionFactory.newSpannerException(e); + }); + } + @Override public Backup updateBackup(String instanceId, String backupId, Timestamp expireTime) { String backupName = getBackupName(instanceId, backupId); 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 index 0a18e9844a1..62d51bf76ed 100644 --- 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 @@ -16,6 +16,7 @@ package com.google.cloud.spanner.encryption; +import com.google.spanner.admin.database.v1.CopyBackupEncryptionConfig; import com.google.spanner.admin.database.v1.CreateBackupEncryptionConfig; import com.google.spanner.admin.database.v1.EncryptionConfig; import com.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig; @@ -50,6 +51,28 @@ public static CreateBackupEncryptionConfig createBackupEncryptionConfig( } } + /** Returns an encryption config to be used for a copy backup. */ + public static CopyBackupEncryptionConfig copyBackupEncryptionConfig( + BackupEncryptionConfig config) { + if (config instanceof CustomerManagedEncryption) { + return CopyBackupEncryptionConfig.newBuilder() + .setEncryptionType(CopyBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION) + .setKmsKeyName(((CustomerManagedEncryption) config).getKmsKeyName()) + .build(); + } else if (config instanceof GoogleDefaultEncryption) { + return CopyBackupEncryptionConfig.newBuilder() + .setEncryptionType(CopyBackupEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION) + .build(); + } else if (config instanceof UseBackupEncryption) { + return CopyBackupEncryptionConfig.newBuilder() + .setEncryptionType( + CopyBackupEncryptionConfig.EncryptionType.USE_CONFIG_DEFAULT_OR_BACKUP_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) { 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 98a7e0927f3..3a20c2a3c78 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 @@ -60,6 +60,7 @@ import com.google.cloud.grpc.GcpManagedChannelOptions.GcpMetricsOptions; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.AdminRequestsPerMinuteExceededException; +import com.google.cloud.spanner.BackupId; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Restore; import com.google.cloud.spanner.SpannerException; @@ -102,6 +103,8 @@ import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import com.google.spanner.admin.database.v1.Backup; +import com.google.spanner.admin.database.v1.CopyBackupMetadata; +import com.google.spanner.admin.database.v1.CopyBackupRequest; import com.google.spanner.admin.database.v1.CreateBackupMetadata; import com.google.spanner.admin.database.v1.CreateBackupRequest; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; @@ -1264,6 +1267,65 @@ public OperationFuture createBackup( NanoClock.getDefaultClock()); } + @Override + public OperationFuture copyBackup( + BackupId sourceBackupId, final com.google.cloud.spanner.Backup destinationBackup) + throws SpannerException { + Preconditions.checkNotNull(sourceBackupId); + Preconditions.checkNotNull(destinationBackup); + final String instanceName = destinationBackup.getInstanceId().getName(); + final String backupId = destinationBackup.getId().getBackup(); + + final CopyBackupRequest.Builder requestBuilder = + CopyBackupRequest.newBuilder() + .setParent(instanceName) + .setBackupId(backupId) + .setSourceBackup(sourceBackupId.getName()) + .setExpireTime(destinationBackup.getExpireTime().toProto()); + + if (destinationBackup.getEncryptionConfig() != null) { + requestBuilder.setEncryptionConfig( + EncryptionConfigProtoMapper.copyBackupEncryptionConfig( + destinationBackup.getEncryptionConfig())); + } + final CopyBackupRequest request = requestBuilder.build(); + final OperationFutureCallable callable = + new OperationFutureCallable<>( + databaseAdminStub.copyBackupOperationCallable(), + request, + // calling copy backup method of dbClientImpl + DatabaseAdminGrpc.getCopyBackupMethod(), + instanceName, + nextPageToken -> + listBackupOperations( + instanceName, + 0, + String.format( + "(metadata.@type:type.googleapis.com/%s) AND (metadata.name:%s)", + CopyBackupMetadata.getDescriptor().getFullName(), + String.format("%s/backups/%s", instanceName, backupId)), + nextPageToken), + input -> { + try { + return input + .getMetadata() + .unpack(CopyBackupMetadata.class) + .getProgress() + .getStartTime(); + } catch (InvalidProtocolBufferException e) { + return null; + } + }); + return RetryHelper.runWithRetries( + callable, + databaseAdminStubSettings + .copyBackupOperationSettings() + .getInitialCallSettings() + .getRetrySettings(), + new OperationFutureRetryAlgorithm<>(), + NanoClock.getDefaultClock()); + } + @Override public OperationFuture restoreDatabase(final Restore restore) { final String databaseInstanceName = restore.getDestination().getInstanceId().getName(); 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 cce92e0f378..00382e228f3 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.BackupId; import com.google.cloud.spanner.Restore; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; @@ -32,31 +33,12 @@ import com.google.longrunning.Operation; import com.google.protobuf.Empty; import com.google.protobuf.FieldMask; -import com.google.spanner.admin.database.v1.Backup; -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.RestoreDatabaseMetadata; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.database.v1.*; import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; import com.google.spanner.admin.instance.v1.Instance; import com.google.spanner.admin.instance.v1.InstanceConfig; import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata; -import com.google.spanner.v1.BeginTransactionRequest; -import com.google.spanner.v1.CommitRequest; -import com.google.spanner.v1.CommitResponse; -import com.google.spanner.v1.ExecuteBatchDmlRequest; -import com.google.spanner.v1.ExecuteBatchDmlResponse; -import com.google.spanner.v1.ExecuteSqlRequest; -import com.google.spanner.v1.PartialResultSet; -import com.google.spanner.v1.PartitionQueryRequest; -import com.google.spanner.v1.PartitionReadRequest; -import com.google.spanner.v1.PartitionResponse; -import com.google.spanner.v1.ReadRequest; -import com.google.spanner.v1.ResultSet; -import com.google.spanner.v1.RollbackRequest; -import com.google.spanner.v1.Session; -import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.*; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -230,6 +212,20 @@ Paginated listBackups( OperationFuture createBackup( com.google.cloud.spanner.Backup backupInfo) throws SpannerException; + /** + * Creates a copy backup from the source backup specified. + * + * @param destinationBackup the backup to create. The instance, database, and expireTime fields of + * the backup must be filled. It may also optionally have an encryption config set. If no + * encryption config has been set, the new backup will use the same encryption config as the + * source backup. + * @return the operation that monitors the backup creation. + */ + default OperationFuture copyBackup( + BackupId sourceBackupId, com.google.cloud.spanner.Backup destinationBackup) { + throw new UnsupportedOperationException("Unimplemented"); + } + /** * Restore a backup into the given database. * 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 2f12790437c..ea1d3724c41 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,10 +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.assertThrows; +import static org.junit.Assert.*; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -47,6 +44,8 @@ public class BackupTest { private static final String NAME = "projects/test-project/instances/test-instance/backups/backup-1"; + private static final String REFERENCING_BACKUP_NAME = + "projects/test-project/instances/test-instance/backups/backup-2"; 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); @@ -293,9 +292,11 @@ public void fromProto() { public void testEqualsAndHashCode() { final Backup backup1 = createBackup(); final Backup backup2 = createBackup(); + final Backup copyBackup1 = copyBackup(); assertEquals(backup1, backup2); assertEquals(backup1.hashCode(), backup2.hashCode()); + assertEquals(backup1.hashCode(), copyBackup1.hashCode()); } private Backup createBackup() { @@ -309,6 +310,27 @@ private Backup createBackup() { com.google.protobuf.Timestamp.newBuilder().setSeconds(2000L).setNanos(2000).build()) .setEncryptionInfo(ENCRYPTION_INFO) .setState(com.google.spanner.admin.database.v1.Backup.State.CREATING) + .setMaxExpireTime( + com.google.protobuf.Timestamp.newBuilder().setSeconds(3000L).setNanos(3000).build()) + .addAllReferencingBackups(Collections.singletonList(REFERENCING_BACKUP_NAME)) + .build(); + return Backup.fromProto(proto, dbClient); + } + + private Backup copyBackup() { + com.google.spanner.admin.database.v1.Backup proto = + com.google.spanner.admin.database.v1.Backup.newBuilder() + .setName(NAME) + .setDatabase(DB) + .setExpireTime( + 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) + .setMaxExpireTime( + com.google.protobuf.Timestamp.newBuilder().setSeconds(3000L).setNanos(3000).build()) + .addAllReferencingBackups(Collections.singletonList(REFERENCING_BACKUP_NAME)) .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 44a531d4a34..56dfd707307 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 @@ -44,6 +44,7 @@ import com.google.protobuf.FieldMask; import com.google.protobuf.Message; import com.google.spanner.admin.database.v1.Backup; +import com.google.spanner.admin.database.v1.CopyBackupMetadata; import com.google.spanner.admin.database.v1.CreateBackupMetadata; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.Database; @@ -72,6 +73,7 @@ public class DatabaseAdminClientImplTest { private static final String DB_NAME2 = "projects/my-project/instances/my-instance/databases/my-db2"; private static final String BK_ID = "my-bk"; + private static final String SOURCE_BK = "my-source-bk"; private static final String BK_NAME = "projects/my-project/instances/my-instance/backups/my-bk"; private static final String BK_NAME2 = "projects/my-project/instances/my-instance/backups/my-bk2"; private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now(); @@ -454,6 +456,81 @@ public void createEncryptedBackup() throws ExecutionException, InterruptedExcept assertThat(op.get().getEncryptionInfo().getKmsKeyVersion()).isEqualTo(KMS_KEY_VERSION); } + @Test + public void copyBackupWithParams() throws Exception { + OperationFuture rawOperationFuture = + OperationFutureUtil.immediateOperationFuture( + "copyBackup", getBackupProto(), CopyBackupMetadata.getDefaultInstance()); + 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)) + .setExpireTime(t) + .build(); + when(rpc.copyBackup(BackupId.of(PROJECT_ID, INSTANCE_ID, SOURCE_BK), backup)) + .thenReturn(rawOperationFuture); + OperationFuture op = + client.copyBackup(INSTANCE_ID, SOURCE_BK, BK_ID, t); + assertThat(op.isDone()).isTrue(); + assertThat(op.get().getId().getName()).isEqualTo(BK_NAME); + } + + @Test + public void copyBackupWithBackupObject() throws ExecutionException, InterruptedException { + final OperationFuture rawOperationFuture = + OperationFutureUtil.immediateOperationFuture( + "copyBackup", getBackupProto(), CopyBackupMetadata.getDefaultInstance()); + final Timestamp expireTime = + Timestamp.ofTimeMicroseconds( + TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()) + + TimeUnit.HOURS.toMicros(28)); + final Timestamp versionTime = + Timestamp.ofTimeMicroseconds( + TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()) - TimeUnit.DAYS.toMicros(2)); + final com.google.cloud.spanner.Backup requestBackup = + client + .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID)) + .setExpireTime(expireTime) + .setVersionTime(versionTime) + .build(); + BackupId sourceBackupId = BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID); + + when(rpc.copyBackup(sourceBackupId, requestBackup)).thenReturn(rawOperationFuture); + + final OperationFuture op = + client.copyBackup(sourceBackupId, requestBackup); + assertThat(op.isDone()).isTrue(); + assertThat(op.get().getId().getName()).isEqualTo(BK_NAME); + } + + @Test + public void copyEncryptedBackup() throws ExecutionException, InterruptedException { + final OperationFuture rawOperationFuture = + OperationFutureUtil.immediateOperationFuture( + "copyBackup", getEncryptedBackupProto(), CopyBackupMetadata.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(); + BackupId sourceBackupId = BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID); + when(rpc.copyBackup(sourceBackupId, backup)).thenReturn(rawOperationFuture); + final OperationFuture op = + client.copyBackup(sourceBackupId, 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);