Add support for DynamoDB Backup/Restore (#3995)

* Add support for DynamoDB Backup/Restore

Basic support for the following endpoints has been implemented with full test coverage:
- create_backup
- delete_backup
- describe_backup
- list_backups
- restore_table_from_backup

Behavior and error messages verified against a real AWS backend.

* Refactor test based on PR feedback
This commit is contained in:
Brian Pandola 2021-06-09 23:05:07 -07:00 committed by GitHub
commit c1b38be02d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 510 additions and 7 deletions

View file

@ -1,5 +1,6 @@
from __future__ import unicode_literals, print_function
import uuid
from datetime import datetime
from decimal import Decimal
@ -6008,3 +6009,303 @@ def test_get_item_for_non_existent_table_raises_error():
client.get_item(TableName="non-existent", Key={"site-id": {"S": "foo"}})
ex.value.response["Error"]["Code"].should.equal("ResourceNotFoundException")
ex.value.response["Error"]["Message"].should.equal("Requested resource not found")
@mock_dynamodb2
def test_create_backup_for_non_existent_table_raises_error():
client = boto3.client("dynamodb", "us-east-1")
with pytest.raises(ClientError) as ex:
client.create_backup(TableName="non-existent", BackupName="backup")
error = ex.value.response["Error"]
error["Code"].should.equal("TableNotFoundException")
error["Message"].should.equal("Table not found: non-existent")
@mock_dynamodb2
def test_create_backup():
client = boto3.client("dynamodb", "us-east-1")
table_name = "test-table"
client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
backup_name = "backup-test-table"
resp = client.create_backup(TableName=table_name, BackupName=backup_name)
details = resp.get("BackupDetails")
details.should.have.key("BackupArn").should.contain(table_name)
details.should.have.key("BackupName").should.equal(backup_name)
details.should.have.key("BackupSizeBytes").should.be.a(int)
details.should.have.key("BackupStatus")
details.should.have.key("BackupType").should.equal("USER")
details.should.have.key("BackupCreationDateTime").should.be.a(datetime)
@mock_dynamodb2
def test_create_multiple_backups_with_same_name():
client = boto3.client("dynamodb", "us-east-1")
table_name = "test-table"
client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
backup_name = "backup-test-table"
backup_arns = []
for i in range(4):
backup = client.create_backup(TableName=table_name, BackupName=backup_name).get(
"BackupDetails"
)
backup["BackupName"].should.equal(backup_name)
backup_arns.should_not.contain(backup["BackupArn"])
backup_arns.append(backup["BackupArn"])
@mock_dynamodb2
def test_describe_backup_for_non_existent_backup_raises_error():
client = boto3.client("dynamodb", "us-east-1")
non_existent_arn = "arn:aws:dynamodb:us-east-1:123456789012:table/table-name/backup/01623095754481-2cfcd6f9"
with pytest.raises(ClientError) as ex:
client.describe_backup(BackupArn=non_existent_arn)
error = ex.value.response["Error"]
error["Code"].should.equal("BackupNotFoundException")
error["Message"].should.equal("Backup not found: {}".format(non_existent_arn))
@mock_dynamodb2
def test_describe_backup():
client = boto3.client("dynamodb", "us-east-1")
table_name = "test-table"
table = client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
).get("TableDescription")
backup_name = "backup-test-table"
backup_arn = (
client.create_backup(TableName=table_name, BackupName=backup_name)
.get("BackupDetails")
.get("BackupArn")
)
resp = client.describe_backup(BackupArn=backup_arn)
description = resp.get("BackupDescription")
details = description.get("BackupDetails")
details.should.have.key("BackupArn").should.contain(table_name)
details.should.have.key("BackupName").should.equal(backup_name)
details.should.have.key("BackupSizeBytes").should.be.a(int)
details.should.have.key("BackupStatus")
details.should.have.key("BackupType").should.equal("USER")
details.should.have.key("BackupCreationDateTime").should.be.a(datetime)
source = description.get("SourceTableDetails")
source.should.have.key("TableName").should.equal(table_name)
source.should.have.key("TableArn").should.equal(table["TableArn"])
source.should.have.key("TableSizeBytes").should.be.a(int)
source.should.have.key("KeySchema").should.equal(table["KeySchema"])
source.should.have.key("TableCreationDateTime").should.equal(
table["CreationDateTime"]
)
source.should.have.key("ProvisionedThroughput").should.be.a(dict)
source.should.have.key("ItemCount").should.equal(table["ItemCount"])
@mock_dynamodb2
def test_list_backups_for_non_existent_table():
client = boto3.client("dynamodb", "us-east-1")
resp = client.list_backups(TableName="non-existent")
resp["BackupSummaries"].should.have.length_of(0)
@mock_dynamodb2
def test_list_backups():
client = boto3.client("dynamodb", "us-east-1")
table_names = ["test-table-1", "test-table-2"]
backup_names = ["backup-1", "backup-2"]
for table_name in table_names:
client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
for backup_name in backup_names:
client.create_backup(TableName=table_name, BackupName=backup_name)
resp = client.list_backups(BackupType="USER")
resp["BackupSummaries"].should.have.length_of(4)
for table_name in table_names:
resp = client.list_backups(TableName=table_name)
resp["BackupSummaries"].should.have.length_of(2)
for summary in resp["BackupSummaries"]:
summary.should.have.key("TableName").should.equal(table_name)
summary.should.have.key("TableArn").should.contain(table_name)
summary.should.have.key("BackupName").should.be.within(backup_names)
summary.should.have.key("BackupArn")
summary.should.have.key("BackupCreationDateTime").should.be.a(datetime)
summary.should.have.key("BackupStatus")
summary.should.have.key("BackupType").should.be.within(["USER", "SYSTEM"])
summary.should.have.key("BackupSizeBytes").should.be.a(int)
@mock_dynamodb2
def test_restore_table_from_non_existent_backup_raises_error():
client = boto3.client("dynamodb", "us-east-1")
non_existent_arn = "arn:aws:dynamodb:us-east-1:123456789012:table/table-name/backup/01623095754481-2cfcd6f9"
with pytest.raises(ClientError) as ex:
client.restore_table_from_backup(
TargetTableName="from-backup", BackupArn=non_existent_arn
)
error = ex.value.response["Error"]
error["Code"].should.equal("BackupNotFoundException")
error["Message"].should.equal("Backup not found: {}".format(non_existent_arn))
@mock_dynamodb2
def test_restore_table_from_backup_raises_error_when_table_already_exists():
client = boto3.client("dynamodb", "us-east-1")
table_name = "test-table"
client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
resp = client.create_backup(TableName=table_name, BackupName="backup")
backup = resp.get("BackupDetails")
with pytest.raises(ClientError) as ex:
client.restore_table_from_backup(
TargetTableName=table_name, BackupArn=backup["BackupArn"]
)
error = ex.value.response["Error"]
error["Code"].should.equal("TableAlreadyExistsException")
error["Message"].should.equal("Table already exists: {}".format(table_name))
@mock_dynamodb2
def test_restore_table_from_backup():
client = boto3.client("dynamodb", "us-east-1")
table_name = "test-table"
resp = client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
table = resp.get("TableDescription")
for i in range(5):
client.put_item(TableName=table_name, Item={"id": {"S": "item %d" % i}})
backup_arn = (
client.create_backup(TableName=table_name, BackupName="backup")
.get("BackupDetails")
.get("BackupArn")
)
restored_table_name = "restored-from-backup"
restored = client.restore_table_from_backup(
TargetTableName=restored_table_name, BackupArn=backup_arn
).get("TableDescription")
restored.should.have.key("AttributeDefinitions").should.equal(
table["AttributeDefinitions"]
)
restored.should.have.key("TableName").should.equal(restored_table_name)
restored.should.have.key("KeySchema").should.equal(table["KeySchema"])
restored.should.have.key("TableStatus")
restored.should.have.key("ItemCount").should.equal(5)
restored.should.have.key("TableArn").should.contain(restored_table_name)
restored.should.have.key("RestoreSummary").should.be.a(dict)
summary = restored.get("RestoreSummary")
summary.should.have.key("SourceBackupArn").should.equal(backup_arn)
summary.should.have.key("SourceTableArn").should.equal(table["TableArn"])
summary.should.have.key("RestoreDateTime").should.be.a(datetime)
summary.should.have.key("RestoreInProgress").should.equal(False)
@mock_dynamodb2
def test_delete_non_existent_backup_raises_error():
client = boto3.client("dynamodb", "us-east-1")
non_existent_arn = "arn:aws:dynamodb:us-east-1:123456789012:table/table-name/backup/01623095754481-2cfcd6f9"
with pytest.raises(ClientError) as ex:
client.delete_backup(BackupArn=non_existent_arn)
error = ex.value.response["Error"]
error["Code"].should.equal("BackupNotFoundException")
error["Message"].should.equal("Backup not found: {}".format(non_existent_arn))
@mock_dynamodb2
def test_delete_backup():
client = boto3.client("dynamodb", "us-east-1")
table_name = "test-table-1"
backup_names = ["backup-1", "backup-2"]
client.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
for backup_name in backup_names:
client.create_backup(TableName=table_name, BackupName=backup_name)
resp = client.list_backups(TableName=table_name, BackupType="USER")
resp["BackupSummaries"].should.have.length_of(2)
backup_to_delete = resp["BackupSummaries"][0]["BackupArn"]
backup_deleted = client.delete_backup(BackupArn=backup_to_delete).get(
"BackupDescription"
)
backup_deleted.should.have.key("SourceTableDetails")
backup_deleted.should.have.key("BackupDetails")
details = backup_deleted["BackupDetails"]
details.should.have.key("BackupArn").should.equal(backup_to_delete)
details.should.have.key("BackupName").should.be.within(backup_names)
details.should.have.key("BackupStatus").should.equal("DELETED")
resp = client.list_backups(TableName=table_name, BackupType="USER")
resp["BackupSummaries"].should.have.length_of(1)
@mock_dynamodb2
def test_source_and_restored_table_items_are_not_linked():
client = boto3.client("dynamodb", "us-east-1")
def add_guids_to_table(table, num_items):
guids = []
for i in range(num_items):
guid = str(uuid.uuid4())
client.put_item(TableName=table, Item={"id": {"S": guid}})
guids.append(guid)
return guids
source_table_name = "source-table"
client.create_table(
TableName=source_table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
guids_original = add_guids_to_table(source_table_name, 5)
backup_arn = (
client.create_backup(TableName=source_table_name, BackupName="backup")
.get("BackupDetails")
.get("BackupArn")
)
guids_added_after_backup = add_guids_to_table(source_table_name, 5)
restored_table_name = "restored-from-backup"
client.restore_table_from_backup(
TargetTableName=restored_table_name, BackupArn=backup_arn
)
guids_added_after_restore = add_guids_to_table(restored_table_name, 5)
source_table_items = client.scan(TableName=source_table_name)
source_table_items.should.have.key("Count").should.equal(10)
source_table_guids = [x["id"]["S"] for x in source_table_items["Items"]]
set(source_table_guids).should.equal(
set(guids_original) | set(guids_added_after_backup)
)
restored_table_items = client.scan(TableName=restored_table_name)
restored_table_items.should.have.key("Count").should.equal(10)
restored_table_guids = [x["id"]["S"] for x in restored_table_items["Items"]]
set(restored_table_guids).should.equal(
set(guids_original) | set(guids_added_after_restore)
)