Skip to content

Commit 61a5e85

Browse files
Wolgojfmainville
authored andcommitted
add DeletionProtectionEnabled to dynamoDB (getmoto#7619)
1 parent 484c0b8 commit 61a5e85

7 files changed

Lines changed: 92 additions & 0 deletions

File tree

moto/dynamodb/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,9 @@ class UnknownKeyType(MockValidationException):
381381
def __init__(self, key_type: str, position: str):
382382
msg = f"1 validation error detected: Value '{key_type}' at '{position}' failed to satisfy constraint: Member must satisfy enum value set: [HASH, RANGE]"
383383
super().__init__(msg)
384+
385+
386+
class DeletionProtectedException(MockValidationException):
387+
def __init__(self, table_name: str):
388+
msg = f"1 validation error detected: Table '{table_name}' can't be deleted while DeletionProtectionEnabled is set to True"
389+
super().__init__(msg)

moto/dynamodb/models/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from moto.dynamodb.exceptions import (
1111
BackupNotFoundException,
1212
ConditionalCheckFailed,
13+
DeletionProtectedException,
1314
ItemSizeTooLarge,
1415
ItemSizeToUpdateTooLarge,
1516
MockValidationException,
@@ -71,6 +72,10 @@ def create_table(self, name: str, **params: Any) -> Table:
7172
def delete_table(self, name: str) -> Table:
7273
if name not in self.tables:
7374
raise ResourceNotFoundException
75+
table_for_deletion = self.tables.get(name)
76+
if isinstance(table_for_deletion, Table):
77+
if table_for_deletion.deletion_protection_enabled:
78+
raise DeletionProtectedException(name)
7479
return self.tables.pop(name)
7580

7681
def describe_endpoints(self) -> List[Dict[str, Union[int, str]]]:
@@ -137,6 +142,7 @@ def update_table(
137142
throughput: Dict[str, Any],
138143
billing_mode: str,
139144
stream_spec: Dict[str, Any],
145+
deletion_protection_enabled: bool,
140146
) -> Table:
141147
table = self.get_table(name)
142148
if attr_definitions:
@@ -149,6 +155,10 @@ def update_table(
149155
table = self.update_table_billing_mode(name, billing_mode)
150156
if stream_spec:
151157
table = self.update_table_streams(name, stream_spec)
158+
if deletion_protection_enabled:
159+
table = self.update_table_deletion_protection_enabled(
160+
name, deletion_protection_enabled
161+
)
152162
return table
153163

154164
def update_table_throughput(self, name: str, throughput: Dict[str, int]) -> Table:
@@ -161,6 +171,13 @@ def update_table_billing_mode(self, name: str, billing_mode: str) -> Table:
161171
table.billing_mode = billing_mode
162172
return table
163173

174+
def update_table_deletion_protection_enabled(
175+
self, name: str, deletion_protection_enabled: bool
176+
) -> Table:
177+
table = self.tables[name]
178+
table.deletion_protection_enabled = deletion_protection_enabled
179+
return table
180+
164181
def update_table_streams(
165182
self, name: str, stream_specification: Dict[str, Any]
166183
) -> Table:

moto/dynamodb/models/table.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def __init__(
243243
streams: Optional[Dict[str, Any]] = None,
244244
sse_specification: Optional[Dict[str, Any]] = None,
245245
tags: Optional[List[Dict[str, str]]] = None,
246+
deletion_protection_enabled: Optional[bool] = False,
246247
):
247248
self.name = table_name
248249
self.account_id = account_id
@@ -306,6 +307,7 @@ def __init__(
306307
self.sse_specification["KMSMasterKeyId"] = self._get_default_encryption_key(
307308
account_id, region
308309
)
310+
self.deletion_protection_enabled = deletion_protection_enabled
309311

310312
def _get_default_encryption_key(self, account_id: str, region: str) -> str:
311313
from moto.kms import kms_backends
@@ -443,6 +445,7 @@ def describe(self, base_key: str = "TableDescription") -> Dict[str, Any]:
443445
index.describe() for index in self.global_indexes
444446
],
445447
"LocalSecondaryIndexes": [index.describe() for index in self.indexes],
448+
"DeletionProtectionEnabled": self.deletion_protection_enabled,
446449
}
447450
}
448451
if self.latest_stream_label:

moto/dynamodb/responses.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ def create_table(self) -> str:
302302
streams = body.get("StreamSpecification")
303303
# Get any tags
304304
tags = body.get("Tags", [])
305+
deletion_protection_enabled = body.get("DeletionProtectionEnabled", False)
305306

306307
table = self.dynamodb_backend.create_table(
307308
table_name,
@@ -314,6 +315,7 @@ def create_table(self) -> str:
314315
billing_mode=billing_mode,
315316
sse_specification=sse_spec,
316317
tags=tags,
318+
deletion_protection_enabled=deletion_protection_enabled,
317319
)
318320
return dynamo_json_dump(table.describe())
319321

@@ -431,13 +433,15 @@ def update_table(self) -> str:
431433
throughput = self.body.get("ProvisionedThroughput", None)
432434
billing_mode = self.body.get("BillingMode", None)
433435
stream_spec = self.body.get("StreamSpecification", None)
436+
deletion_protection_enabled = self.body.get("DeletionProtectionEnabled")
434437
table = self.dynamodb_backend.update_table(
435438
name=name,
436439
attr_definitions=attr_definitions,
437440
global_index=global_index,
438441
throughput=throughput,
439442
billing_mode=billing_mode,
440443
stream_spec=stream_spec,
444+
deletion_protection_enabled=deletion_protection_enabled,
441445
)
442446
return dynamo_json_dump(table.describe())
443447

tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,31 @@ def test_cannot_scan_gsi_with_consistent_read():
13891389
"Code": "ValidationException",
13901390
"Message": "Consistent reads are not supported on global secondary indexes",
13911391
}
1392+
1393+
1394+
@mock_aws
1395+
def test_delete_table():
1396+
client = boto3.client("dynamodb", region_name="us-east-1")
1397+
1398+
# Create the DynamoDB table.
1399+
client.create_table(
1400+
TableName="test1",
1401+
AttributeDefinitions=[
1402+
{"AttributeName": "client", "AttributeType": "S"},
1403+
{"AttributeName": "app", "AttributeType": "S"},
1404+
],
1405+
KeySchema=[
1406+
{"AttributeName": "client", "KeyType": "HASH"},
1407+
{"AttributeName": "app", "KeyType": "RANGE"},
1408+
],
1409+
ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
1410+
DeletionProtectionEnabled=True,
1411+
)
1412+
1413+
with pytest.raises(ClientError) as err:
1414+
client.delete_table(TableName="test1")
1415+
assert err.value.response["Error"]["Code"] == "ValidationException"
1416+
assert (
1417+
err.value.response["Error"]["Message"]
1418+
== "1 validation error detected: Table 'test1' can't be deleted while DeletionProtectionEnabled is set to True"
1419+
)

tests/test_dynamodb/test_dynamodb_create_table.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def test_create_table_standard():
5050
{"AttributeName": "subject", "KeyType": "RANGE"},
5151
]
5252
assert actual["ItemCount"] == 0
53+
assert not actual["DeletionProtectionEnabled"]
5354

5455

5556
@mock_aws
@@ -233,6 +234,22 @@ def test_create_table_with_tags():
233234
assert resp["Tags"] == [{"Key": "tk", "Value": "tv"}]
234235

235236

237+
@mock_aws
238+
def test_create_table_with_deletion_protection_enabled():
239+
client = boto3.client("dynamodb", region_name="us-east-1")
240+
241+
client.create_table(
242+
TableName="test-deletion_protection",
243+
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
244+
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
245+
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
246+
DeletionProtectionEnabled=True,
247+
)
248+
249+
actual = client.describe_table(TableName="test-deletion_protection")["Table"]
250+
assert actual["DeletionProtectionEnabled"]
251+
252+
236253
@mock_aws
237254
def test_create_table_pay_per_request():
238255
client = boto3.client("dynamodb", region_name="us-east-1")

tests/test_dynamodb/test_dynamodb_update_table.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ def test_update_table_throughput():
5353
assert table.provisioned_throughput["WriteCapacityUnits"] == 6
5454

5555

56+
@mock_aws
57+
def test_update_table_deletion_protection_enabled():
58+
conn = boto3.resource("dynamodb", region_name="us-west-2")
59+
table = conn.create_table(
60+
TableName="messages",
61+
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
62+
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
63+
BillingMode="PAY_PER_REQUEST",
64+
DeletionProtectionEnabled=False,
65+
)
66+
assert not table.deletion_protection_enabled
67+
68+
table.update(DeletionProtectionEnabled=True)
69+
70+
assert table.deletion_protection_enabled
71+
72+
5673
@mock_aws
5774
def test_update_table__enable_stream():
5875
conn = boto3.client("dynamodb", region_name="us-east-1")

0 commit comments

Comments
 (0)