diff --git a/spanner/api/Spanner.Samples.Tests/CopyBackupWithMrCmekAsyncTest.cs b/spanner/api/Spanner.Samples.Tests/CopyBackupWithMrCmekAsyncTest.cs new file mode 100644 index 00000000000..12cc4247d1a --- /dev/null +++ b/spanner/api/Spanner.Samples.Tests/CopyBackupWithMrCmekAsyncTest.cs @@ -0,0 +1,49 @@ +// Copyright 2024 Google Inc. +// +// 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. + +using Google.Cloud.Spanner.Admin.Database.V1; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +[Collection(nameof(SpannerFixture))] +public class CopyBackupWithMrCmekAsyncTest +{ + private readonly SpannerFixture _spannerFixture; + + public CopyBackupWithMrCmekAsyncTest(SpannerFixture spannerFixture) + { + _spannerFixture = spannerFixture; + } + + [SkippableFact] + public async Task TestCopyBackupWithMrCmekAsync() + { + CopyBackupWithMrCmekAsyncSample copyBackupWithMrCmekAsyncSample = new CopyBackupWithMrCmekAsyncSample(); + string source_Project_id = _spannerFixture.ProjectId; + string source_Instance_id = _spannerFixture.InstanceId; + string source_backupId = _spannerFixture.MrCmekBackupId; + string target_Project_id = _spannerFixture.ProjectId; + string target_Instance_id = _spannerFixture.InstanceId; + string target_backupId = SpannerFixture.GenerateId("test_", 16); + DateTimeOffset expireTime = DateTimeOffset.UtcNow.AddHours(12); + CryptoKeyName[] kmsKeyNames = _spannerFixture.KmsKeyNames; + + var backup = await copyBackupWithMrCmekAsyncSample.CopyBackupWithMrCmekAsync(source_Instance_id, source_Project_id, source_backupId, + target_Instance_id, target_Project_id, target_backupId, expireTime, kmsKeyNames); + + Assert.All(backup.EncryptionInformation, encryptionInfo => _spannerFixture.KmsKeyNames.Contains(CryptoKeyName.Parse(encryptionInfo.KmsKeyVersionAsCryptoKeyVersionName.CryptoKeyId))); + } +} diff --git a/spanner/api/Spanner.Samples.Tests/CreateBackupWithMrCmekAsyncTest.cs b/spanner/api/Spanner.Samples.Tests/CreateBackupWithMrCmekAsyncTest.cs new file mode 100644 index 00000000000..a3472090a89 --- /dev/null +++ b/spanner/api/Spanner.Samples.Tests/CreateBackupWithMrCmekAsyncTest.cs @@ -0,0 +1,43 @@ +// Copyright 2024 Google Inc. +// +// 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. + +using Google.Cloud.Spanner.Admin.Database.V1; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +/// +/// Tests creating a backup using MR CMEK. +/// +[Collection(nameof(SpannerFixture))] +public class CreateBackupWithMrCmekAsyncTest +{ + private readonly SpannerFixture _fixture; + + public CreateBackupWithMrCmekAsyncTest(SpannerFixture fixture) + { + _fixture = fixture; + } + + [SkippableFact] + public async Task TestCreateBackupWithMrCmekAsync() + { + Skip.If(!_fixture.RunCmekBackupSampleTests, SpannerFixture.SkipCmekBackupSamplesMessage); + // Create a backup with custom encryption keys. + var sample = new CreateBackupWithMrCmekAsyncSample(); + var backup = await sample.CreateBackupWithMrCmekAsync(_fixture.ProjectId, _fixture.InstanceId, _fixture.FixedMrCmekDatabaseId, _fixture.MrCmekBackupId, _fixture.KmsKeyNames); + Assert.All(backup.EncryptionInformation, encryptionInfo => _fixture.KmsKeyNames.Contains(CryptoKeyName.Parse(encryptionInfo.KmsKeyVersionAsCryptoKeyVersionName.CryptoKeyId))); + } +} diff --git a/spanner/api/Spanner.Samples.Tests/CreateDatabaseWithMrCmekAsyncTest.cs b/spanner/api/Spanner.Samples.Tests/CreateDatabaseWithMrCmekAsyncTest.cs new file mode 100644 index 00000000000..690e9e42929 --- /dev/null +++ b/spanner/api/Spanner.Samples.Tests/CreateDatabaseWithMrCmekAsyncTest.cs @@ -0,0 +1,41 @@ +// Copyright 2024 Google Inc. +// +// 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. + +using System.Threading.Tasks; +using Xunit; +using Google.Cloud.Spanner.Admin.Database.V1; +using System.Linq; + +/// +/// Tests creating a databases using MR CMEK. +/// +[Collection(nameof(SpannerFixture))] +public class CreateDatabaseWithMrCmekAsyncTest +{ + private readonly SpannerFixture _fixture; + + public CreateDatabaseWithMrCmekAsyncTest(SpannerFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task TestCreateDatabaseWithMrCmekAsync() + { + // Create a database with custom encryption keys. + var sample = new CreateDatabaseWithMrCmekAsyncSample(); + var database = await sample.CreateDatabaseWithMrCmekAsync(_fixture.ProjectId, _fixture.InstanceId, _fixture.MrCmekDatabaseId, _fixture.KmsKeyNames); + Assert.All(database.EncryptionConfig.KmsKeyNames, keyName => _fixture.KmsKeyNames.Contains(CryptoKeyName.Parse(keyName))); + } +} diff --git a/spanner/api/Spanner.Samples.Tests/RestoreDatabaseWithMrCmekAsyncTest.cs b/spanner/api/Spanner.Samples.Tests/RestoreDatabaseWithMrCmekAsyncTest.cs new file mode 100644 index 00000000000..b50133a2901 --- /dev/null +++ b/spanner/api/Spanner.Samples.Tests/RestoreDatabaseWithMrCmekAsyncTest.cs @@ -0,0 +1,42 @@ +// Copyright 2024 Google Inc. +// +// 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. + +using Google.Cloud.Spanner.Admin.Database.V1; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +/// +/// Tests restoring a databases using customer managed encryption. +/// +[Collection(nameof(SpannerFixture))] +public class RestoreDatabaseWithMrCmekAsyncTest +{ + private readonly SpannerFixture _fixture; + + public RestoreDatabaseWithMrCmekAsyncTest(SpannerFixture fixture) + { + _fixture = fixture; + } + + [SkippableFact] + public async Task TestRestoreDatabaseWithMrCmekAsync() + { + Skip.If(!_fixture.RunCmekBackupSampleTests, SpannerFixture.SkipCmekBackupSamplesMessage); + var sample = new RestoreDatabaseWithMrCmekAsyncSample(); + var database = await sample.RestoreDatabaseWithMrCmekAsync(_fixture.ProjectId, _fixture.InstanceId, _fixture.MrCmekRestoreDatabaseId, _fixture.FixedMrCmekBackupId, _fixture.KmsKeyNames); + Assert.All(database.EncryptionConfig.KmsKeyNames, keyName => _fixture.KmsKeyNames.Contains(CryptoKeyName.Parse(keyName))); + } +} diff --git a/spanner/api/Spanner.Samples.Tests/SpannerFixture.cs b/spanner/api/Spanner.Samples.Tests/SpannerFixture.cs index 0ba576a897f..0c41b89783d 100644 --- a/spanner/api/Spanner.Samples.Tests/SpannerFixture.cs +++ b/spanner/api/Spanner.Samples.Tests/SpannerFixture.cs @@ -72,11 +72,21 @@ AlbumTitle STRING(MAX) public string FixedEncryptedDatabaseId { get; } = "fixed-enc-backup-db"; public string FixedEncryptedBackupId { get; } = "fixed-enc-backup"; + public string MrCmekDatabaseId { get; private set; } + public string MrCmekBackupId { get; } = GenerateId("my-mr-backup-"); + // 'restore' is abbreviated to prevent the name from becoming longer than 30 characters. + public string MrCmekRestoreDatabaseId { get; private set; } + + // These are intentionally kept on the instance to avoid the need to create a new encrypted database and backup for each run. + public string FixedMrCmekDatabaseId { get; } = "fixed-mr-backup-db"; + public string FixedMrCmekBackupId { get; } = "fixed-mr-backup"; + public CryptoKeyName KmsKeyName { get; } = new CryptoKeyName( Environment.GetEnvironmentVariable("spanner.test.key.project") ?? Environment.GetEnvironmentVariable("GOOGLE_PROJECT_ID"), Environment.GetEnvironmentVariable("spanner.test.key.location") ?? "us-central1", Environment.GetEnvironmentVariable("spanner.test.key.ring") ?? "spanner-test-keyring", Environment.GetEnvironmentVariable("spanner.test.key.name") ?? "spanner-test-key"); + public CryptoKeyName[] KmsKeyNames { get; private set; } public string InstanceIdWithProcessingUnits { get; } = GenerateId("my-ins-pu-"); public string InstanceIdWithMultiRegion { get; } = GenerateId("my-ins-mr-"); @@ -103,6 +113,8 @@ public async Task InitializeAsync() RestoredDatabaseId = GenerateTempDatabaseId("my-restore-db-"); EncryptedDatabaseId = GenerateTempDatabaseId("my-enc-db-"); EncryptedRestoreDatabaseId = GenerateTempDatabaseId("my-enc-r-db-"); + MrCmekDatabaseId = GenerateTempDatabaseId("my-mr-db-"); + MrCmekRestoreDatabaseId = GenerateTempDatabaseId("my-mr-r-db-"); DatabaseAdminClient = await DatabaseAdminClient.CreateAsync(); InstanceAdminClient = await InstanceAdminClient.CreateAsync(); @@ -124,6 +136,7 @@ public async Task InitializeAsync() if (RunCmekBackupSampleTests) { await InitializeEncryptedBackupAsync(); + await InitializeMrCmekBackupAsync(); } } @@ -137,7 +150,8 @@ public async Task DisposeAsync() DeleteInstanceAsync(InstanceIdWithProcessingUnits), DeleteInstanceAsync(InstanceIdWithInstancePartition), DeleteBackupAsync(ToBeCancelledBackupId), - DeleteBackupAsync(EncryptedBackupId) + DeleteBackupAsync(EncryptedBackupId), + DeleteBackupAsync(MrCmekBackupId), }; DeleteInstanceConfig(CreateCustomInstanceConfigId); @@ -367,6 +381,38 @@ private async Task InitializeEncryptedBackupAsync() } } + private async Task InitializeMrCmekBackupAsync() + { + // Sample backup for MR CMEK restore test. + try + { + CreateDatabaseWithMrCmekAsyncSample createDatabaseAsyncSample = new CreateDatabaseWithMrCmekAsyncSample(); + InsertDataAsyncSample insertDataAsyncSample = new InsertDataAsyncSample(); + await createDatabaseAsyncSample.CreateDatabaseWithMrCmekAsync(ProjectId, InstanceId, FixedMrCmekDatabaseId, KmsKeyNames); + await insertDataAsyncSample.InsertDataAsync(ProjectId, InstanceId, FixedMrCmekDatabaseId); + } + catch (Exception e) when (e.ToString().Contains("Database already exists")) + { + // We intentionally keep an existing database around to reduce + // the likelihood of test timeouts when creating a backup so + // it's ok to get an AlreadyExists error. + Console.WriteLine($"Database {FixedMrCmekDatabaseId} already exists."); + } + + try + { + CreateBackupWithMrCmekAsyncSample createBackupSample = new CreateBackupWithMrCmekAsyncSample(); + await createBackupSample.CreateBackupWithMrCmekAsync(ProjectId, InstanceId, FixedMrCmekDatabaseId, FixedMrCmekBackupId, KmsKeyNames); + } + catch (RpcException e) when (e.StatusCode == StatusCode.AlreadyExists) + { + // We intentionally keep an existing backup around to reduce + // the likelihood of test timeouts when creating a backup so + // it's ok to get an AlreadyExists error. + Console.WriteLine($"Backup {FixedMrCmekBackupId} already exists."); + } + } + private async Task DeleteInstanceAsync(string instanceId) { try diff --git a/spanner/api/Spanner.Samples/CopyBackupWithMrCmekAsync.cs b/spanner/api/Spanner.Samples/CopyBackupWithMrCmekAsync.cs new file mode 100644 index 00000000000..849a4fa694d --- /dev/null +++ b/spanner/api/Spanner.Samples/CopyBackupWithMrCmekAsync.cs @@ -0,0 +1,69 @@ +// Copyright 2024 Google Inc. +// +// 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. + +// [START spanner_copy_backup_with_MR_CMEK] + +using Google.Api.Gax; +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.Common.V1; +using Google.Protobuf.WellKnownTypes; +using System; +using System.Threading.Tasks; + +public class CopyBackupWithMrCmekAsyncSample +{ + public async Task CopyBackupWithMrCmekAsync(string sourceInstanceId, string sourceProjectId, string sourceBackupId, + string targetInstanceId, string targetProjectId, string targetBackupId, + DateTimeOffset expireTime, CryptoKeyName[] kmsKeyNames) + { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.Create(); + + var request = new CopyBackupRequest + { + SourceBackupAsBackupName = new BackupName(sourceProjectId, sourceInstanceId, sourceBackupId), + ParentAsInstanceName = new InstanceName(targetProjectId, targetInstanceId), + BackupId = targetBackupId, + ExpireTime = Timestamp.FromDateTimeOffset(expireTime), + EncryptionConfig = new CopyBackupEncryptionConfig + { + EncryptionType = CopyBackupEncryptionConfig.Types.EncryptionType.CustomerManagedEncryption, + KmsKeyNamesAsCryptoKeyNames = { kmsKeyNames }, + } + }; + + // Execute the CopyBackup request. + var operation = await databaseAdminClient.CopyBackupAsync(request); + + Console.WriteLine("Waiting for the operation to finish."); + + // Poll until the returned long-running operation is complete. + var completedResponse = await operation.PollUntilCompletedAsync(); + + if (completedResponse.IsFaulted) + { + Console.WriteLine($"Error while copying backup: {completedResponse.Exception}"); + throw completedResponse.Exception; + } + + Backup backup = completedResponse.Result; + + Console.WriteLine($"Backup copied successfully."); + Console.WriteLine($"Backup with Id {sourceBackupId} has been copied from {sourceProjectId}/{sourceInstanceId} to {targetProjectId}/{targetInstanceId} Backup {targetBackupId}"); + Console.WriteLine($"Backup {backup.Name} of size {backup.SizeBytes} bytes was created with encryption keys {string.Join(", ", (object[]) kmsKeyNames)} at {backup.CreateTime} from {backup.Database} and is in state {backup.State} and has version time {backup.VersionTime.ToDateTime()}"); + + return backup; + } +} + +// [END spanner_copy_backup_with_MR_CMEK] diff --git a/spanner/api/Spanner.Samples/CreateBackupWithMrCmekAsync.cs b/spanner/api/Spanner.Samples/CreateBackupWithMrCmekAsync.cs new file mode 100644 index 00000000000..64bf08b81be --- /dev/null +++ b/spanner/api/Spanner.Samples/CreateBackupWithMrCmekAsync.cs @@ -0,0 +1,64 @@ +// Copyright 2024 Google Inc. +// +// 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. + +// [START spanner_create_backup_with_MR_CMEK] + +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.Common.V1; +using Google.Protobuf.WellKnownTypes; +using System; +using System.Threading.Tasks; + +public class CreateBackupWithMrCmekAsyncSample +{ + public async Task CreateBackupWithMrCmekAsync(string projectId, string instanceId, string databaseId, string backupId, CryptoKeyName[] kmsKeyNames) + { + // Create a DatabaseAdminClient instance. + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.Create(); + + // Create the CreateBackupRequest with encryption configuration. + CreateBackupRequest request = new CreateBackupRequest + { + ParentAsInstanceName = InstanceName.FromProjectInstance(projectId, instanceId), + BackupId = backupId, + Backup = new Backup + { + DatabaseAsDatabaseName = DatabaseName.FromProjectInstanceDatabase(projectId, instanceId, databaseId), + ExpireTime = DateTime.UtcNow.AddDays(14).ToTimestamp(), + }, + EncryptionConfig = new CreateBackupEncryptionConfig + { + EncryptionType = CreateBackupEncryptionConfig.Types.EncryptionType.CustomerManagedEncryption, + KmsKeyNamesAsCryptoKeyNames = { kmsKeyNames }, + }, + }; + // Execute the CreateBackup request. + var operation = await databaseAdminClient.CreateBackupAsync(request); + + Console.WriteLine("Waiting for the operation to finish."); + + // Poll until the returned long-running operation is complete. + var completedResponse = await operation.PollUntilCompletedAsync(); + if (completedResponse.IsFaulted) + { + Console.WriteLine($"Error while creating backup: {completedResponse.Exception}"); + throw completedResponse.Exception; + } + + var backup = completedResponse.Result; + Console.WriteLine($"Backup {backup.Name} of size {backup.SizeBytes} bytes was created with encryption keys {string.Join(", ", (object[]) kmsKeyNames)} at {backup.CreateTime}"); + return backup; + } +} +// [END spanner_create_backup_with_MR_CMEK] diff --git a/spanner/api/Spanner.Samples/CreateDatabaseWithMrCmekAsync.cs b/spanner/api/Spanner.Samples/CreateDatabaseWithMrCmekAsync.cs new file mode 100644 index 00000000000..4f00238e017 --- /dev/null +++ b/spanner/api/Spanner.Samples/CreateDatabaseWithMrCmekAsync.cs @@ -0,0 +1,74 @@ +// Copyright 2024 Google Inc. +// +// 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. + +// [START spanner_create_database_with_MR_CMEK] + +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.Common.V1; +using System; +using System.Threading.Tasks; + +public class CreateDatabaseWithMrCmekAsyncSample +{ + public async Task CreateDatabaseWithMrCmekAsync(string projectId, string instanceId, string databaseId, CryptoKeyName[] kmsKeyNames) + { + // Create a DatabaseAdminClient instance that can be used to execute a + // CreateDatabaseRequest with custom encryption configuration options. + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.Create(); + // Define create table statement for table #1. + var createSingersTable = + @"CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + ComposerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)"; + // Define create table statement for table #2. + var createAlbumsTable = + @"CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE"; + + // Create the CreateDatabase request with encryption configuration and execute it. + var request = new CreateDatabaseRequest + { + ParentAsInstanceName = InstanceName.FromProjectInstance(projectId, instanceId), + CreateStatement = $"CREATE DATABASE `{databaseId}`", + ExtraStatements = { createSingersTable, createAlbumsTable }, + EncryptionConfig = new EncryptionConfig + { + KmsKeyNamesAsCryptoKeyNames = { kmsKeyNames }, + }, + }; + var operation = await databaseAdminClient.CreateDatabaseAsync(request); + + // Wait until the operation has finished. + Console.WriteLine("Waiting for the operation to finish."); + var completedResponse = await operation.PollUntilCompletedAsync(); + if (completedResponse.IsFaulted) + { + Console.WriteLine($"Error while creating database: {completedResponse.Exception}"); + throw completedResponse.Exception; + } + + var database = completedResponse.Result; + Console.WriteLine($"Database {database.Name} created with encryption keys {string.Join(", ", (object[]) kmsKeyNames)}"); + + return database; + } +} +// [END spanner_create_database_with_MR_CMEK] diff --git a/spanner/api/Spanner.Samples/RestoreDatabaseWithMrCmekAsync.cs b/spanner/api/Spanner.Samples/RestoreDatabaseWithMrCmekAsync.cs new file mode 100644 index 00000000000..889f4a5a8ed --- /dev/null +++ b/spanner/api/Spanner.Samples/RestoreDatabaseWithMrCmekAsync.cs @@ -0,0 +1,60 @@ +// Copyright 2024 Google Inc. +// +// 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. + +// [START spanner_restore_backup_with_MR_CMEK] + +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.Common.V1; +using System; +using System.Threading.Tasks; + +public class RestoreDatabaseWithMrCmekAsyncSample +{ + public async Task RestoreDatabaseWithMrCmekAsync(string projectId, string instanceId, string databaseId, string backupId, CryptoKeyName[] kmsKeyNames) + { + // Create a DatabaseAdminClient instance. + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.Create(); + + // Create the RestoreDatabaseRequest with encryption configuration. + RestoreDatabaseRequest request = new RestoreDatabaseRequest + { + ParentAsInstanceName = InstanceName.FromProjectInstance(projectId, instanceId), + DatabaseId = databaseId, + BackupAsBackupName = BackupName.FromProjectInstanceBackup(projectId, instanceId, backupId), + EncryptionConfig = new RestoreDatabaseEncryptionConfig + { + EncryptionType = RestoreDatabaseEncryptionConfig.Types.EncryptionType.CustomerManagedEncryption, + KmsKeyNamesAsCryptoKeyNames = { kmsKeyNames }, + } + }; + // Execute the RestoreDatabase request. + var operation = await databaseAdminClient.RestoreDatabaseAsync(request); + + Console.WriteLine("Waiting for the operation to finish."); + + // Poll until the returned long-running operation is complete. + var completedResponse = await operation.PollUntilCompletedAsync(); + if (completedResponse.IsFaulted) + { + Console.WriteLine($"Error while restoring database: {completedResponse.Exception}"); + throw completedResponse.Exception; + } + + var database = completedResponse.Result; + var restoreInfo = database.RestoreInfo; + Console.WriteLine($"Database {restoreInfo.BackupInfo.SourceDatabase} restored to {database.Name} from backup {restoreInfo.BackupInfo.Backup} using encryption keys {string.Join(", ", (object[]) kmsKeyNames)}"); + return database; + } +} +// [END spanner_restore_backup_with_MR_CMEK]