Skip to content

Commit e4bc2a4

Browse files
committed
feat: add support for creating backups with CMEK
1 parent db040ed commit e4bc2a4

File tree

4 files changed

+126
-9
lines changed

4 files changed

+126
-9
lines changed

google/cloud/spanner_v1/backup.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from google.cloud.exceptions import NotFound
2020

2121
from google.cloud.spanner_admin_database_v1 import Backup as BackupPB
22+
from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig
23+
from google.cloud.spanner_admin_database_v1 import CreateBackupRequest
2224
from google.cloud.spanner_v1._helpers import _metadata_with_prefix
2325

2426
_BACKUP_NAME_RE = re.compile(
@@ -57,10 +59,18 @@ class Backup(object):
5759
the externally consistent copy of the database. If
5860
not present, it is the same as the `create_time` of
5961
the backup.
62+
63+
:type encryption_config:
64+
:class:`~google.cloud.spanner_admin_database_v1.types.CreateBackupEncryptionConfig`
65+
or :class:`dict`
66+
:param encryption_config:
67+
(Optional) Encryption configuration for the backup.
68+
If a dict is provided, it must be of the same form as the protobuf
69+
message :class:`~google.cloud.spanner_admin_database_v1.types.CreateBackupEncryptionConfig`
6070
"""
6171

6272
def __init__(
63-
self, backup_id, instance, database="", expire_time=None, version_time=None
73+
self, backup_id, instance, database="", expire_time=None, version_time=None, encryption_config=None
6474
):
6575
self.backup_id = backup_id
6676
self._instance = instance
@@ -72,6 +82,10 @@ def __init__(
7282
self._state = None
7383
self._referencing_databases = None
7484
self._encryption_info = None
85+
if type(encryption_config) == dict:
86+
self._encryption_config = CreateBackupEncryptionConfig(**encryption_config)
87+
else:
88+
self._encryption_config = encryption_config
7589

7690
@property
7791
def name(self):
@@ -165,6 +179,14 @@ def encryption_info(self):
165179
"""
166180
return self._encryption_info
167181

182+
@property
183+
def encryption_config(self):
184+
"""Encryption config for this database.
185+
:rtype: :class:`~google.cloud.spanner_admin_instance_v1.CreateBackupEncryptionConfig`
186+
:returns: an object representing the restore info for this database
187+
"""
188+
return self._encryption_config
189+
168190
@classmethod
169191
def from_pb(cls, backup_pb, instance):
170192
"""Create an instance of this class from a protobuf message.
@@ -224,10 +246,15 @@ def create(self):
224246
version_time=self.version_time,
225247
)
226248

227-
future = api.create_backup(
249+
request = CreateBackupRequest(
228250
parent=self._instance.name,
229251
backup_id=self.backup_id,
230252
backup=backup,
253+
encryption_config=self._encryption_config,
254+
)
255+
256+
future = api.create_backup(
257+
request=request,
231258
metadata=metadata,
232259
)
233260
return future

google/cloud/spanner_v1/instance.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ def list_databases(self, page_size=None):
424424
)
425425
return page_iter
426426

427-
def backup(self, backup_id, database="", expire_time=None, version_time=None):
427+
def backup(self, backup_id, database="", expire_time=None, version_time=None, encryption_config=None):
428428
"""Factory to create a backup within this instance.
429429
430430
:type backup_id: str
@@ -445,6 +445,14 @@ def backup(self, backup_id, database="", expire_time=None, version_time=None):
445445
Optional. The version time that will be used to create the externally
446446
consistent copy of the database. If not present, it is the same as
447447
the `create_time` of the backup.
448+
449+
:type encryption_config:
450+
:class:`~google.cloud.spanner_admin_database_v1.types.CreateBackupEncryptionConfig`
451+
or :class:`dict`
452+
:param encryption_config:
453+
(Optional) Encryption configuration for the backup.
454+
If a dict is provided, it must be of the same form as the protobuf
455+
message :class:`~google.cloud.spanner_admin_database_v1.types.CreateBackupEncryptionConfig`
448456
"""
449457
try:
450458
return Backup(
@@ -453,6 +461,7 @@ def backup(self, backup_id, database="", expire_time=None, version_time=None):
453461
database=database.name,
454462
expire_time=expire_time,
455463
version_time=version_time,
464+
encryption_config=encryption_config,
456465
)
457466
except AttributeError:
458467
return Backup(
@@ -461,6 +470,7 @@ def backup(self, backup_id, database="", expire_time=None, version_time=None):
461470
database=database,
462471
expire_time=expire_time,
463472
version_time=version_time,
473+
encryption_config=encryption_config
464474
)
465475

466476
def list_backups(self, filter_="", page_size=None):

tests/unit/test_backup.py

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,53 @@ def test_ctor_defaults(self):
6262
self.assertIsNone(backup._expire_time)
6363

6464
def test_ctor_non_defaults(self):
65+
from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig
6566
instance = _Instance(self.INSTANCE_NAME)
6667
timestamp = self._make_timestamp()
6768

69+
encryption_config = CreateBackupEncryptionConfig(
70+
encryption_type=CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION,
71+
kms_key_name="key_name"
72+
)
6873
backup = self._make_one(
69-
self.BACKUP_ID, instance, database=self.DATABASE_NAME, expire_time=timestamp
74+
self.BACKUP_ID,
75+
instance,
76+
database=self.DATABASE_NAME,
77+
expire_time=timestamp,
78+
encryption_config=encryption_config
79+
)
80+
81+
self.assertEqual(backup.backup_id, self.BACKUP_ID)
82+
self.assertIs(backup._instance, instance)
83+
self.assertEqual(backup._database, self.DATABASE_NAME)
84+
self.assertIsNotNone(backup._expire_time)
85+
self.assertIs(backup._expire_time, timestamp)
86+
self.assertEqual(backup.encryption_config, encryption_config)
87+
88+
def test_ctor_w_encryption_config_dict(self):
89+
from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig
90+
instance = _Instance(self.INSTANCE_NAME)
91+
timestamp = self._make_timestamp()
92+
93+
encryption_config = {
94+
"encryption_type": 3,
95+
"kms_key_name": "key_name"
96+
}
97+
backup = self._make_one(
98+
self.BACKUP_ID,
99+
instance,
100+
database=self.DATABASE_NAME,
101+
expire_time=timestamp,
102+
encryption_config=encryption_config
70103
)
104+
expected_encryption_config = CreateBackupEncryptionConfig(**encryption_config)
71105

72106
self.assertEqual(backup.backup_id, self.BACKUP_ID)
73107
self.assertIs(backup._instance, instance)
74108
self.assertEqual(backup._database, self.DATABASE_NAME)
75109
self.assertIsNotNone(backup._expire_time)
76110
self.assertIs(backup._expire_time, timestamp)
111+
self.assertEqual(backup.encryption_config, expected_encryption_config)
77112

78113
def test_from_pb_project_mismatch(self):
79114
from google.cloud.spanner_admin_database_v1 import Backup
@@ -180,10 +215,22 @@ def test_encrpytion_info_property(self):
180215
)
181216
self.assertEqual(backup.encryption_info, expected)
182217

218+
def test_encryption_config_property(self):
219+
from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig
220+
221+
instance = _Instance(self.INSTANCE_NAME)
222+
backup = self._make_one(self.BACKUP_ID, instance)
223+
expected = backup._encryption_config = CreateBackupEncryptionConfig(
224+
encryption_type=CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION,
225+
kms_key_name="kms_key_name"
226+
)
227+
self.assertEqual(backup.encryption_config, expected)
228+
183229
def test_create_grpc_error(self):
184230
from google.api_core.exceptions import GoogleAPICallError
185231
from google.api_core.exceptions import Unknown
186232
from google.cloud.spanner_admin_database_v1 import Backup
233+
from google.cloud.spanner_admin_database_v1 import CreateBackupRequest
187234

188235
client = _Client()
189236
api = client.database_admin_api = self._make_database_admin_api()
@@ -200,16 +247,21 @@ def test_create_grpc_error(self):
200247
with self.assertRaises(GoogleAPICallError):
201248
backup.create()
202249

203-
api.create_backup.assert_called_once_with(
250+
request = CreateBackupRequest(
204251
parent=self.INSTANCE_NAME,
205252
backup_id=self.BACKUP_ID,
206253
backup=backup_pb,
254+
)
255+
256+
api.create_backup.assert_called_once_with(
257+
request=request,
207258
metadata=[("google-cloud-resource-prefix", backup.name)],
208259
)
209260

210261
def test_create_already_exists(self):
211262
from google.cloud.exceptions import Conflict
212263
from google.cloud.spanner_admin_database_v1 import Backup
264+
from google.cloud.spanner_admin_database_v1 import CreateBackupRequest
213265

214266
client = _Client()
215267
api = client.database_admin_api = self._make_database_admin_api()
@@ -226,16 +278,21 @@ def test_create_already_exists(self):
226278
with self.assertRaises(Conflict):
227279
backup.create()
228280

229-
api.create_backup.assert_called_once_with(
281+
request = CreateBackupRequest(
230282
parent=self.INSTANCE_NAME,
231283
backup_id=self.BACKUP_ID,
232284
backup=backup_pb,
285+
)
286+
287+
api.create_backup.assert_called_once_with(
288+
request=request,
233289
metadata=[("google-cloud-resource-prefix", backup.name)],
234290
)
235291

236292
def test_create_instance_not_found(self):
237293
from google.cloud.exceptions import NotFound
238294
from google.cloud.spanner_admin_database_v1 import Backup
295+
from google.cloud.spanner_admin_database_v1 import CreateBackupRequest
239296

240297
client = _Client()
241298
api = client.database_admin_api = self._make_database_admin_api()
@@ -252,10 +309,14 @@ def test_create_instance_not_found(self):
252309
with self.assertRaises(NotFound):
253310
backup.create()
254311

255-
api.create_backup.assert_called_once_with(
312+
request = CreateBackupRequest(
256313
parent=self.INSTANCE_NAME,
257314
backup_id=self.BACKUP_ID,
258315
backup=backup_pb,
316+
)
317+
318+
api.create_backup.assert_called_once_with(
319+
request=request,
259320
metadata=[("google-cloud-resource-prefix", backup.name)],
260321
)
261322

@@ -276,6 +337,8 @@ def test_create_database_not_set(self):
276337

277338
def test_create_success(self):
278339
from google.cloud.spanner_admin_database_v1 import Backup
340+
from google.cloud.spanner_admin_database_v1 import CreateBackupRequest
341+
from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig
279342
from datetime import datetime
280343
from datetime import timedelta
281344
from pytz import UTC
@@ -289,12 +352,17 @@ def test_create_success(self):
289352
version_timestamp = datetime.utcnow() - timedelta(minutes=5)
290353
version_timestamp = version_timestamp.replace(tzinfo=UTC)
291354
expire_timestamp = self._make_timestamp()
355+
encryption_config = {
356+
"encryption_type": 3,
357+
"kms_key_name": "key_name"
358+
}
292359
backup = self._make_one(
293360
self.BACKUP_ID,
294361
instance,
295362
database=self.DATABASE_NAME,
296363
expire_time=expire_timestamp,
297364
version_time=version_timestamp,
365+
encryption_config=encryption_config
298366
)
299367

300368
backup_pb = Backup(
@@ -306,10 +374,16 @@ def test_create_success(self):
306374
future = backup.create()
307375
self.assertIs(future, op_future)
308376

309-
api.create_backup.assert_called_once_with(
377+
expected_encryption_config = CreateBackupEncryptionConfig(**encryption_config)
378+
request = CreateBackupRequest(
310379
parent=self.INSTANCE_NAME,
311380
backup_id=self.BACKUP_ID,
312381
backup=backup_pb,
382+
encryption_config=expected_encryption_config
383+
)
384+
385+
api.create_backup.assert_called_once_with(
386+
request=request,
313387
metadata=[("google-cloud-resource-prefix", backup.name)],
314388
)
315389

tests/unit/test_instance.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,22 +610,28 @@ def test_backup_factory_explicit(self):
610610
import datetime
611611
from google.cloud._helpers import UTC
612612
from google.cloud.spanner_v1.backup import Backup
613+
from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig
613614

614615
client = _Client(self.PROJECT)
615616
instance = self._make_one(self.INSTANCE_ID, client, self.CONFIG_NAME)
616617
BACKUP_ID = "backup-id"
617618
DATABASE_NAME = "database-name"
618619
timestamp = datetime.datetime.utcnow().replace(tzinfo=UTC)
620+
encryption_config = CreateBackupEncryptionConfig(
621+
encryption_type=CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION,
622+
kms_key_name="kms_key_name"
623+
)
619624

620625
backup = instance.backup(
621-
BACKUP_ID, database=DATABASE_NAME, expire_time=timestamp
626+
BACKUP_ID, database=DATABASE_NAME, expire_time=timestamp, encryption_config=encryption_config
622627
)
623628

624629
self.assertIsInstance(backup, Backup)
625630
self.assertEqual(backup.backup_id, BACKUP_ID)
626631
self.assertIs(backup._instance, instance)
627632
self.assertEqual(backup._database, DATABASE_NAME)
628633
self.assertIs(backup._expire_time, timestamp)
634+
self.assertEqual(backup._encryption_config, encryption_config)
629635

630636
def test_list_backups_defaults(self):
631637
from google.cloud.spanner_admin_database_v1 import Backup as BackupPB

0 commit comments

Comments
 (0)