"""Tests for database logic.""" from botocore.stub import Stubber import pytest import config from common.connectors.boto_clients import kms_client from common.connectors.boto_clients import rds_client from common.logic.database import _poll_for_pending_modifications from common.logic.database import associate_roles_with_cluster from common.logic.database import clone_database from common.logic.database import configure_event_subscriptions from common.logic.database import configure_snapshot_sharing_settings from common.logic.database import copy_snapshot from common.logic.database import create_snapshot from common.logic.database import DatabaseNotFound from common.logic.database import DatabaseSnapshotNotFound from common.logic.database import delete_database from common.logic.database import delete_database_if_exists from common.logic.database import delete_snapshot from common.logic.database import encrypted_with_default_key from common.logic.database import find_database_identifier from common.logic.database import find_database_snapshot_identifier from common.logic.database import get_config from common.logic.database import rename_database from common.logic.database import restore_database from common.logic.database import UnsupportedDatabaseSnapshotType from common.logic.database import UnsupportedDatabaseType from common.logic.database import wait_for_database_availability config.ENVIRONMENT = 'test' config.SERVICE_NAME = 'unittest' @pytest.fixture def rds_stubber(): """Return stubber for the RDS client.""" with Stubber(rds_client) as stubber: yield stubber @pytest.fixture def kms_stubber(): """Return stubber for the KMS client.""" with Stubber(kms_client) as stubber: yield stubber def test_copy_snapshot_cluster(rds_stubber): """Test copying a cluster snapshot.""" rds_stubber.add_response('copy_db_cluster_snapshot', { 'DBClusterSnapshot': { 'DBClusterSnapshotIdentifier': 'dest-snapshot-id' } }, { 'SourceDBClusterSnapshotIdentifier': 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'TargetDBClusterSnapshotIdentifier': 'dest-snapshot-id', 'KmsKeyId': 'alias/kms/test', 'SourceRegion': 'eu-west-1', 'Tags': [ {'Key': 'environment', 'Value': 'test'}, {'Key': 'service_name', 'Value': 'unittest'}, {'Key': 'snapshot_type', 'Value': 'shared'} ] }) _mock_cluster_snapshot_available(rds_stubber, 'dest-snapshot-id') snapshot_id = copy_snapshot( 'src', '111111111111', 'cluster', 'dest-snapshot-id', 'shared', 'alias/kms/test', 'eu-west-1' ) assert snapshot_id == 'dest-snapshot-id' rds_stubber.assert_no_pending_responses() def test_copy_snapshot_standalone(rds_stubber): """Test copying a standalone snapshot.""" rds_stubber.add_response('copy_db_snapshot', { 'DBSnapshot': { 'DBSnapshotIdentifier': 'dest-snapshot-id' } }, { 'SourceDBSnapshotIdentifier': 'arn:aws:rds:eu-west-1:111111111111:snapshot:src', 'TargetDBSnapshotIdentifier': 'dest-snapshot-id', 'KmsKeyId': 'alias/kms/test', 'SourceRegion': 'eu-west-1', 'Tags': [ {'Key': 'environment', 'Value': 'test'}, {'Key': 'service_name', 'Value': 'unittest'}, {'Key': 'snapshot_type', 'Value': 'shared'} ] }) rds_stubber.add_response('describe_db_snapshots', { 'DBSnapshots': [ { 'DBSnapshotIdentifier': 'dest-snapshot-id', 'Status': 'available' } ] }) snapshot_id = copy_snapshot( 'src', '111111111111', 'standalone', 'dest-snapshot-id', 'shared', 'alias/kms/test', 'eu-west-1' ) assert snapshot_id == 'dest-snapshot-id' rds_stubber.assert_no_pending_responses() def test_copy_snapshot_unsupported_type(): """Test copy_snapshot throws an exception with an unsupported type.""" with pytest.raises(UnsupportedDatabaseType): copy_snapshot( 'src', '111111111111', 'unsupported', 'dest-snapshot-id', 'shared', 'alias/kms/test', 'eu-west-1' ) def test_restore_database_cluster(rds_stubber): """Test restoring a database cluster.""" db_config = { 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } rds_stubber.add_response('restore_db_cluster_from_snapshot', {}, { 'DBClusterIdentifier': 'my-db', 'SnapshotIdentifier': 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}] }) rds_stubber.add_response('create_db_instance', { 'DBInstance': { 'DBInstanceIdentifier': 'my-db-0' } }, { 'DBInstanceIdentifier': 'my-db-0', 'DBClusterIdentifier': 'my-db', 'DBInstanceClass': 'db.r5.large', 'Engine': 'aurora-mysql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroupName': 'my-subnet-group', 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnablePerformanceInsights': True, 'PubliclyAccessible': False, 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'PromotionTier': 0, }) _mock_instance_available(rds_stubber, 'my-db-0') _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('add_role_to_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', }) new_instance = restore_database( 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'my-db', 'cluster', db_config ) assert new_instance == 'my-db-0' rds_stubber.assert_no_pending_responses() def test_restore_database_cluster_multiple_instances(rds_stubber): """Test restoring a cluster recreates every instance, writer first.""" db_config = { 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'ClusterInstances': [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': [], }, { 'PromotionTier': 1, 'DBInstanceClass': 'db.r5.xlarge', 'DBParameterGroupName': 'my-reader-param-group', 'EventSubscriptions': [], }, ], 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } rds_stubber.add_response('restore_db_cluster_from_snapshot', {}, { 'DBClusterIdentifier': 'my-db', 'SnapshotIdentifier': 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}] }) # Writer (-0) is created and awaited before the reader (-1). _mock_create_cluster_instance(rds_stubber, 'my-db-0', 'my-db', 0) _mock_instance_available(rds_stubber, 'my-db-0') _mock_create_cluster_instance( rds_stubber, 'my-db-1', 'my-db', 1, instance_class='db.r5.xlarge', parameter_group='my-reader-param-group') _mock_instance_available(rds_stubber, 'my-db-1') _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('add_role_to_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', }) new_instance = restore_database( 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'my-db', 'cluster', db_config ) assert new_instance == 'my-db-0' rds_stubber.assert_no_pending_responses() def test_restore_database_cluster_postgresql_omits_backtrack(rds_stubber): """Restoring an Aurora PostgreSQL cluster must not pass BacktrackWindow. Backtrack is only supported by Aurora MySQL; passing the parameter for aurora-postgresql is rejected by RDS with InvalidParameterValue. """ db_config = { 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-postgresql', 'EngineVersion': '15.4', 'BacktrackWindow': 0, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 5432, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['postgresql'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } # No BacktrackWindow in the expected params: the stubber would raise # StubAssertionError if the call passed it. rds_stubber.add_response('restore_db_cluster_from_snapshot', {}, { 'DBClusterIdentifier': 'my-db', 'SnapshotIdentifier': 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-postgresql', 'EngineVersion': '15.4', 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 5432, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['postgresql'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}] }) rds_stubber.add_response('create_db_instance', { 'DBInstance': { 'DBInstanceIdentifier': 'my-db-0' } }, { 'DBInstanceIdentifier': 'my-db-0', 'DBClusterIdentifier': 'my-db', 'DBInstanceClass': 'db.r5.large', 'Engine': 'aurora-postgresql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroupName': 'my-subnet-group', 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnablePerformanceInsights': True, 'PubliclyAccessible': False, 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'PromotionTier': 0, }) _mock_instance_available(rds_stubber, 'my-db-0') _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('add_role_to_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', }) new_instance = restore_database( 'arn:aws:rds:eu-west-1:111111111111:cluster-snapshot:src', 'my-db', 'cluster', db_config ) assert new_instance == 'my-db-0' rds_stubber.assert_no_pending_responses() def test_restore_database_standalone(rds_stubber): """Test restoring a standalone database.""" db_config = { 'DBInstanceClass': 'db.r5.large', 'Engine': 'aurora-mysql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroupName': 'my-subnet-group', 'DBParameterGroupName': 'my-param-group', 'PubliclyAccessible': False, 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' } rds_stubber.add_response('restore_db_instance_from_db_snapshot', { 'DBInstance': { 'DBInstanceIdentifier': 'my-db' } }, { 'DBInstanceIdentifier': 'my-db', 'DBSnapshotIdentifier': 'arn:aws:rds:eu-west-1:111111111111:snapshot:src', 'DBInstanceClass': 'db.r5.large', 'Engine': 'aurora-mysql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroupName': 'my-subnet-group', 'DBParameterGroupName': 'my-param-group', 'PubliclyAccessible': False, 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'] }) _mock_instance_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_instance', {}, { 'DBInstanceIdentifier': 'my-db', 'ApplyImmediately': True, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' }) _mock_instance_available(rds_stubber, 'my-db') new_instance = restore_database( 'arn:aws:rds:eu-west-1:111111111111:snapshot:src', 'my-db', 'standalone', db_config ) assert new_instance == 'my-db' def test_restore_database_unsupported_type(): """Test restore_database throws an exception with an unsupported type.""" with pytest.raises(UnsupportedDatabaseType): restore_database( 'arn:aws:rds:eu-west-1:111111111111:snapshot:src', 'my-db', 'unsupported', {} ) def test_clone_database(rds_stubber): """Test cloning a database.""" db_config = { 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } rds_stubber.add_response('restore_db_cluster_to_point_in_time', {}, { 'SourceDBClusterIdentifier': 'my-db-src', 'DBClusterIdentifier': 'my-db', 'UseLatestRestorableTime': True, 'RestoreType': 'copy-on-write', 'KmsKeyId': 'alias/kms/test', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}] }) rds_stubber.add_response('create_db_instance', { 'DBInstance': { 'DBInstanceIdentifier': 'my-db-0' } }, { 'DBInstanceIdentifier': 'my-db-0', 'DBClusterIdentifier': 'my-db', 'DBInstanceClass': 'db.r5.large', 'Engine': 'aurora-mysql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroupName': 'my-subnet-group', 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnablePerformanceInsights': True, 'PubliclyAccessible': False, 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'PromotionTier': 0, }) _mock_instance_available(rds_stubber, 'my-db-0') _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('add_role_to_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', }) new_instance = clone_database( 'my-db-src', 'my-db', 'cluster', db_config ) assert new_instance == 'my-db-0' def test_clone_database_multiple_instances(rds_stubber): """Test cloning a cluster recreates every instance, writer first.""" db_config = { 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'ClusterInstances': [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': [], }, { 'PromotionTier': 1, 'DBInstanceClass': 'db.r5.xlarge', 'DBParameterGroupName': 'my-reader-param-group', 'EventSubscriptions': [], }, ], 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } rds_stubber.add_response('restore_db_cluster_to_point_in_time', {}, { 'SourceDBClusterIdentifier': 'my-db-src', 'DBClusterIdentifier': 'my-db', 'UseLatestRestorableTime': True, 'RestoreType': 'copy-on-write', 'KmsKeyId': 'alias/kms/test', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}] }) _mock_create_cluster_instance(rds_stubber, 'my-db-0', 'my-db', 0) _mock_instance_available(rds_stubber, 'my-db-0') _mock_create_cluster_instance( rds_stubber, 'my-db-1', 'my-db', 1, instance_class='db.r5.xlarge', parameter_group='my-reader-param-group') _mock_instance_available(rds_stubber, 'my-db-1') _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47' }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('add_role_to_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', }) new_instance = clone_database( 'my-db-src', 'my-db', 'cluster', db_config ) assert new_instance == 'my-db-0' rds_stubber.assert_no_pending_responses() def test_create_snapshot_cluster(rds_stubber): """Test creating a cluster snapshot.""" rds_stubber.add_response('create_db_cluster_snapshot', { 'DBClusterSnapshot': { 'DBClusterSnapshotIdentifier': 'my-snapshot' } }, { 'DBClusterSnapshotIdentifier': 'my-snapshot', 'DBClusterIdentifier': 'my-db', 'Tags': [ {'Key': 'environment', 'Value': 'test'}, {'Key': 'service_name', 'Value': 'unittest'}, {'Key': 'snapshot_type', 'Value': 'intermediate'} ] }) _mock_cluster_snapshot_available(rds_stubber, 'my-snapshot') snapshot_id = create_snapshot('my-snapshot', 'my-db', 'cluster') assert snapshot_id == 'my-snapshot' rds_stubber.assert_no_pending_responses() def test_create_snapshot_standalone(rds_stubber): """Test creating a standalone snapshot.""" rds_stubber.add_response('create_db_snapshot', { 'DBSnapshot': { 'DBSnapshotIdentifier': 'my-snapshot' } }, { 'DBSnapshotIdentifier': 'my-snapshot', 'DBInstanceIdentifier': 'my-db', 'Tags': [ {'Key': 'environment', 'Value': 'test'}, {'Key': 'service_name', 'Value': 'unittest'}, {'Key': 'snapshot_type', 'Value': 'intermediate'} ] }) _mock_instance_snapshot_available(rds_stubber, 'my-snapshot') snapshot_id = create_snapshot('my-snapshot', 'my-db', 'standalone') assert snapshot_id == 'my-snapshot' rds_stubber.assert_no_pending_responses() def test_create_snapshot_unsupported_type(): """Test create_snapshot throws an exception with an unsupported type.""" with pytest.raises(UnsupportedDatabaseType): create_snapshot('my-snapshot', 'my-db', 'unsupported') def test_configure_shared_snapshot_settings_cluster(rds_stubber): """Test configuring shared snapshot settings for a cluster.""" rds_stubber.add_response('modify_db_cluster_snapshot_attribute', { 'DBClusterSnapshotAttributesResult': { 'DBClusterSnapshotIdentifier': 'my-snapshot' } }, { 'DBClusterSnapshotIdentifier': 'my-snapshot', 'AttributeName': 'restore', 'ValuesToAdd': [ '111111111111' ] }) snapshot_id = configure_snapshot_sharing_settings( 'my-snapshot', '111111111111', 'cluster' ) assert snapshot_id == 'my-snapshot' rds_stubber.assert_no_pending_responses() def test_configure_shared_snapshot_settings_standalone(rds_stubber): """Test configuring shared snapshot settings for a standalone instance.""" rds_stubber.add_response('modify_db_snapshot_attribute', { 'DBSnapshotAttributesResult': { 'DBSnapshotIdentifier': 'my-snapshot' } }, { 'DBSnapshotIdentifier': 'my-snapshot', 'AttributeName': 'restore', 'ValuesToAdd': [ '111111111111' ], 'ValuesToRemove': [ 'all' ] }) snapshot_id = configure_snapshot_sharing_settings( 'my-snapshot', '111111111111', 'standalone' ) assert snapshot_id == 'my-snapshot' rds_stubber.assert_no_pending_responses() def test_configure_shared_snapshot_settings_unsupported_type(): """Test exception is thrown for unsupported type.""" with pytest.raises(UnsupportedDatabaseSnapshotType): configure_snapshot_sharing_settings( 'my-snapshot', '111111111111', 'unsupported') def test_delete_database_cluster(rds_stubber): """Test deletion of a database cluster.""" _mock_cluster_members(rds_stubber, 'my-db', ['my-db-0'], writer='my-db-0') rds_stubber.add_response('delete_db_instance', {}, { 'DBInstanceIdentifier': 'my-db-0', 'SkipFinalSnapshot': True }) rds_stubber.add_response('delete_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'SkipFinalSnapshot': True }) _mock_cluster_deleted(rds_stubber) delete_database('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_delete_database_cluster_multiple_instances(rds_stubber): """Test deletion of a cluster with more than one instance. The reader must be deleted before the writer: deleting the writer while a reader is still available triggers an Aurora failover, and the promoting reader would then reject its own delete. """ _mock_cluster_members( rds_stubber, 'my-db', ['my-db-0', 'my-db-1'], writer='my-db-0') rds_stubber.add_response('delete_db_instance', {}, { 'DBInstanceIdentifier': 'my-db-1', 'SkipFinalSnapshot': True }) rds_stubber.add_response('delete_db_instance', {}, { 'DBInstanceIdentifier': 'my-db-0', 'SkipFinalSnapshot': True }) rds_stubber.add_response('delete_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'SkipFinalSnapshot': True }) _mock_cluster_deleted(rds_stubber) delete_database('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_delete_database_standalone(rds_stubber): """Test deletion of a standalone database.""" rds_stubber.add_response('delete_db_instance', {}, { 'DBInstanceIdentifier': 'my-db', 'SkipFinalSnapshot': True }) _mock_instance_deleted(rds_stubber) delete_database('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_delete_database_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): delete_database('my-db', 'unsupported') def test_delete_database_if_exists_cluster_exists(rds_stubber): """Test delete_database_if_exists for a cluster that exists.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': 'my-db' } ] }) _mock_cluster_members(rds_stubber, 'my-db', ['my-db-0']) rds_stubber.add_response('delete_db_instance', {}, { 'DBInstanceIdentifier': 'my-db-0', 'SkipFinalSnapshot': True }) rds_stubber.add_response('delete_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'SkipFinalSnapshot': True }) _mock_cluster_deleted(rds_stubber) delete_database_if_exists('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_delete_database_if_exists_cluster_does_not_exist(rds_stubber): """Test delete_database_if_exists for a cluster that does not exist.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [] }) delete_database_if_exists('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_delete_database_if_exists_standalone_exists(rds_stubber): """Test delete_database_if_exists for a standalone instance that exists.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db' } ] }) rds_stubber.add_response('delete_db_instance', {}, { 'DBInstanceIdentifier': 'my-db', 'SkipFinalSnapshot': True }) _mock_instance_deleted(rds_stubber) delete_database_if_exists('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_delete_database_if_exists_standalone_does_not_exist(rds_stubber): """Test delete_database_if_exists for standalone db that does not exist.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }) delete_database_if_exists('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_delete_database_if_exists_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): delete_database_if_exists('my-db', 'unsupported') def test_delete_snapshot_cluster(rds_stubber): """Test deletion of a database cluster snapshot.""" rds_stubber.add_response('delete_db_cluster_snapshot', {}, { 'DBClusterSnapshotIdentifier': 'my-snapshot' }) _mock_cluster_snapshot_deleted(rds_stubber) delete_snapshot('my-snapshot', 'cluster') rds_stubber.assert_no_pending_responses() def test_delete_snapshot_standalone(rds_stubber): """Test deletion of a standalone database snapshot.""" rds_stubber.add_response('delete_db_snapshot', {}, { 'DBSnapshotIdentifier': 'my-snapshot' }) _mock_instance_snapshot_deleted(rds_stubber) delete_snapshot('my-snapshot', 'standalone') rds_stubber.assert_no_pending_responses() def test_delete_snapshot_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseSnapshotType): delete_snapshot('my-snapshot', 'unsupported') def test_find_database_identifier_cluster(rds_stubber): """Test finding database identifier for a cluster.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-0' } ] }, { 'Filters': [ { 'Name': 'db-cluster-id', 'Values': ['my-db'] } ] }) identifier, db_type = find_database_identifier('my-db') assert identifier == 'my-db' assert db_type == 'cluster' rds_stubber.assert_no_pending_responses() def test_find_database_identifier_standalone(rds_stubber): """Test finding database identifier for a standalone instance.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }, { 'Filters': [ { 'Name': 'db-cluster-id', 'Values': ['my-db'] } ] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db' } ] }, { 'Filters': [ { 'Name': 'db-instance-id', 'Values': ['my-db'] } ] }) identifier, db_type = find_database_identifier('my-db') assert identifier == 'my-db' assert db_type == 'standalone' rds_stubber.assert_no_pending_responses() def test_find_database_identifier_not_found(rds_stubber): """Test an exception is thrown if database not found.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }, { 'Filters': [ { 'Name': 'db-cluster-id', 'Values': ['my-db'] } ] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }, { 'Filters': [ { 'Name': 'db-instance-id', 'Values': ['my-db'] } ] }) with pytest.raises(DatabaseNotFound): find_database_identifier('my-db') def test_find_database_snapshot_identifier_cluster(rds_stubber): """Test finding database identifier for a cluster snapshot.""" rds_stubber.add_response('describe_db_cluster_snapshots', { 'DBClusterSnapshots': [ { 'DBClusterSnapshotIdentifier': 'my-snapshot' } ] }, { 'Filters': [ { 'Name': 'db-cluster-snapshot-id', 'Values': ['my-snapshot'] } ], 'IncludeShared': True }) identifier, db_type = find_database_snapshot_identifier('my-snapshot') assert identifier == 'my-snapshot' assert db_type == 'cluster' rds_stubber.assert_no_pending_responses() def test_find_database_snapshot_identifier_standalone(rds_stubber): """Test finding database identifier for a standalone snapshot.""" rds_stubber.add_response('describe_db_cluster_snapshots', { 'DBClusterSnapshots': [] }) rds_stubber.add_response('describe_db_snapshots', { 'DBSnapshots': [ { 'DBSnapshotIdentifier': 'my-snapshot' } ] }, { 'Filters': [ { 'Name': 'db-snapshot-id', 'Values': ['my-snapshot'] } ], 'IncludeShared': True }) identifier, db_type = find_database_snapshot_identifier('my-snapshot') assert identifier == 'my-snapshot' assert db_type == 'standalone' rds_stubber.assert_no_pending_responses() def test_find_database_snapshot_identifier_not_found(rds_stubber): """Test an exception is thrown if database snapshot not found.""" rds_stubber.add_response('describe_db_cluster_snapshots', { 'DBClusterSnapshots': [] }) rds_stubber.add_response('describe_db_snapshots', { 'DBSnapshots': [] }) with pytest.raises(DatabaseSnapshotNotFound): find_database_snapshot_identifier('my-snapshot') def test_poll_for_pending_modifications_cluster_pending_changes(rds_stubber): """Test polling where there are pending modifications to a cluster.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': 'my-db', 'PendingModifiedValues': { 'IAMDatabaseAuthenticationEnabled': True } } ] }) assert _poll_for_pending_modifications('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_cluster_no_pending_changes( rds_stubber): """Test polling where there are no pending modifications to a cluster.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': 'my-db' } ] }) assert not _poll_for_pending_modifications('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_standalone_pending_changes( rds_stubber): """Test poll where there are pending changes to a standalone db.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db', 'PendingModifiedValues': { 'IAMDatabaseAuthenticationEnabled': True } } ] }) assert _poll_for_pending_modifications('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_standalone_no_pending_changes( rds_stubber): """Test poll where there are no pending changes to a standalone db.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db', 'PendingModifiedValues': {} } ] }) assert not _poll_for_pending_modifications('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): _poll_for_pending_modifications('my-db', 'unsupported') def test_get_config_cluster(rds_stubber): """Test retrieving config for a cluster.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroup': 'my-cluster-param-group', 'DBSubnetGroup': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroups': [ {'VpcSecurityGroupId': 'my-vpc-1'}, {'VpcSecurityGroupId': 'my-vpc-2'} ], 'CopyTagsToSnapshot': False, 'EnabledCloudwatchLogsExports': ['audit'], 'TagList': [{'Key': 'my-key', 'Value': 'my-value'}], 'KmsKeyId': 'alias/kms/test', 'BackupRetentionPeriod': 30, 'PerformanceInsightsEnabled': True, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'CrossAccountClone': False, 'CloneGroupId': 'my-clone-group-id', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ], 'DBClusterMembers': [ { 'DBInstanceIdentifier': 'my-db-0', 'IsClusterWriter': True } ] } ] }, { 'DBClusterIdentifier': 'my-db' }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [ { 'CustSubscriptionId': 'my-cluster-subscription', 'SourceIdsList': ['my-db'] } ] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-0', 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'my-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 0 } ] }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [ { 'CustSubscriptionId': 'my-instance-subscription', 'SourceIdsList': ['my-db-0'] } ] }) db_config = get_config('my-db', 'cluster') assert db_config == { 'KmsKeyId': 'alias/kms/test', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'ClusterInstances': [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': ['my-instance-subscription'], } ], 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'ClusterEventSubscriptions': ['my-cluster-subscription'], 'InstanceEventSubscriptions': ['my-instance-subscription'], 'CrossAccountClone': False, 'CloneGroupId': 'my-clone-group-id', 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } rds_stubber.assert_no_pending_responses() def test_get_config_cluster_multiple_instances(rds_stubber): """Promotion tiers capture every instance's tier, writer first.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroup': 'my-cluster-param-group', 'DBSubnetGroup': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroups': [ {'VpcSecurityGroupId': 'my-vpc-1'} ], 'CopyTagsToSnapshot': False, 'EnabledCloudwatchLogsExports': ['audit'], 'TagList': [{'Key': 'my-key', 'Value': 'my-value'}], 'KmsKeyId': 'alias/kms/test', 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'CrossAccountClone': False, 'AssociatedRoles': [], # Reader listed first to prove the writer is ordered first. 'DBClusterMembers': [ { 'DBInstanceIdentifier': 'my-db-1', 'IsClusterWriter': False }, { 'DBInstanceIdentifier': 'my-db-0', 'IsClusterWriter': True } ] } ] }, { 'DBClusterIdentifier': 'my-db' }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-1', 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'my-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 1 }, { 'DBInstanceIdentifier': 'my-db-0', 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'my-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 0 } ] }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [] }) db_config = get_config('my-db', 'cluster') # Writer (-0, tier 0) first, then reader (-1, tier 1). assert db_config['ClusterInstances'] == [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': [], }, { 'PromotionTier': 1, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': [], }, ] assert db_config['PromotionTier'] == 0 rds_stubber.assert_no_pending_responses() def test_get_config_cluster_writer_not_zero_suffixed(rds_stubber): """Writer-first ordering does not depend on the instance name suffix. AWS does not guarantee the writer is the ``-0`` instance (e.g. after a failover the writer can be ``my-db-1``). get_config must still order the writer's tier first and read the uniform instance settings from the writer, regardless of its identifier suffix. """ rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroup': 'my-cluster-param-group', 'DBSubnetGroup': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroups': [ {'VpcSecurityGroupId': 'my-vpc-1'} ], 'CopyTagsToSnapshot': False, 'EnabledCloudwatchLogsExports': ['audit'], 'TagList': [{'Key': 'my-key', 'Value': 'my-value'}], 'KmsKeyId': 'alias/kms/test', 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'CrossAccountClone': False, 'AssociatedRoles': [], # Writer is the -1 instance; reader is -0. 'DBClusterMembers': [ { 'DBInstanceIdentifier': 'my-db-0', 'IsClusterWriter': False }, { 'DBInstanceIdentifier': 'my-db-1', 'IsClusterWriter': True } ] } ] }, { 'DBClusterIdentifier': 'my-db' }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-0', 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'reader-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 1 }, { 'DBInstanceIdentifier': 'my-db-1', 'DBInstanceClass': 'db.r5.xlarge', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'writer-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 0 } ] }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [] }) db_config = get_config('my-db', 'cluster') # Writer (my-db-1, tier 0) first even though it is not the -0 instance, # with each member keeping its own class and parameter group. assert db_config['ClusterInstances'] == [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.xlarge', 'DBParameterGroupName': 'writer-param-group', 'EventSubscriptions': [], }, { 'PromotionTier': 1, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'reader-param-group', 'EventSubscriptions': [], }, ] assert db_config['PromotionTier'] == 0 # Top-level instance settings are read from the writer, not the -0 # instance. assert db_config['DBInstanceClass'] == 'db.r5.xlarge' assert db_config['DBParameterGroupName'] == 'writer-param-group' rds_stubber.assert_no_pending_responses() def test_get_config_cluster_member_without_instance_detail(rds_stubber): """A member missing from describe_db_instances is dropped, not fatal. describe_db_clusters and describe_db_instances are independent, eventually-consistent calls; a member can be listed by the cluster before its instance detail is available. get_config must skip such members rather than raising a KeyError on the per-instance lookup. """ rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroup': 'my-cluster-param-group', 'DBSubnetGroup': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroups': [ {'VpcSecurityGroupId': 'my-vpc-1'} ], 'CopyTagsToSnapshot': False, 'EnabledCloudwatchLogsExports': ['audit'], 'TagList': [{'Key': 'my-key', 'Value': 'my-value'}], 'KmsKeyId': 'alias/kms/test', 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'CrossAccountClone': False, 'AssociatedRoles': [], # my-db-1 is listed but has no instance detail yet. 'DBClusterMembers': [ { 'DBInstanceIdentifier': 'my-db-0', 'IsClusterWriter': True }, { 'DBInstanceIdentifier': 'my-db-1', 'IsClusterWriter': False } ] } ] }, { 'DBClusterIdentifier': 'my-db' }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-0', 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'my-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 0 } ] }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [] }) db_config = get_config('my-db', 'cluster') # The member without instance detail is dropped; only my-db-0 remains. assert db_config['ClusterInstances'] == [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': [], } ] assert db_config['PromotionTier'] == 0 rds_stubber.assert_no_pending_responses() def test_get_config_standalone(rds_stubber): """Test retrieving config for a standalone db.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db', 'DBInstanceClass': 'db.r5.large', 'Engine': 'aurora-mysql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroup': { 'DBSubnetGroupName': 'my-subnet-group' }, 'DBParameterGroups': [ {'DBParameterGroupName': 'my-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'TagList': [{'Key': 'my-key', 'Value': 'my-value'}], 'Endpoint': { 'Port': 3306 }, 'VpcSecurityGroups': [ {'VpcSecurityGroupId': 'my-vpc-1'}, {'VpcSecurityGroupId': 'my-vpc-2'} ], 'CopyTagsToSnapshot': False, 'EnabledCloudwatchLogsExports': ['audit'], 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', } ] }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [ { 'CustSubscriptionId': 'my-instance-subscription', 'SourceIdsList': ['my-db'] }, { 'CustSubscriptionId': 'some-other-subscription', 'SourceIdsList': ['some-other-db'] }, { 'CustSubscriptionId': 'subscription-with-no-dbs-associated' } ] }) db_config = get_config('my-db', 'standalone') assert db_config == { 'Engine': 'aurora-mysql', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'PubliclyAccessible': False, 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'InstanceEventSubscriptions': ['my-instance-subscription'], 'EnableInstancePerformanceInsights': True } rds_stubber.assert_no_pending_responses() def test_get_config_cluster_with_custom_kms_key(rds_stubber, kms_stubber): """Test retrieving config for a cluster with a custom KMS key.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroup': 'my-cluster-param-group', 'DBSubnetGroup': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroups': [ {'VpcSecurityGroupId': 'my-vpc-1'}, {'VpcSecurityGroupId': 'my-vpc-2'} ], 'CopyTagsToSnapshot': False, 'EnabledCloudwatchLogsExports': ['audit'], 'TagList': [{'Key': 'my-key', 'Value': 'my-value'}], 'KmsKeyId': 'arn:aws:kms:region:account-id:key/custom-key-id', 'BackupRetentionPeriod': 30, 'PerformanceInsightsEnabled': True, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'CrossAccountClone': False, 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ], 'DBClusterMembers': [ { 'DBInstanceIdentifier': 'my-db-0', 'IsClusterWriter': True } ] } ] }, { 'DBClusterIdentifier': 'my-db' }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [ { 'CustSubscriptionId': 'my-cluster-subscription', 'SourceIdsList': ['my-db'] } ] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-0', 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroups': [ {'DBParameterGroupName': 'my-param-group'} ], 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'PerformanceInsightsEnabled': True, 'PubliclyAccessible': False, 'PromotionTier': 0 } ] }) rds_stubber.add_response('describe_event_subscriptions', { 'EventSubscriptionsList': [ { 'CustSubscriptionId': 'my-instance-subscription', 'SourceIdsList': ['my-db-0'] } ] }) kms_stubber.add_response('describe_key', { 'KeyMetadata': { 'Arn': 'arn:aws:kms:region:account-id:key/default-key-id', 'KeyId': 'default-key-id' } }, { 'KeyId': 'alias/aws/rds' }) db_config = get_config('my-db', 'cluster', kms_client=kms_client) assert db_config == { 'KmsKeyId': 'arn:aws:kms:region:account-id:key/custom-key-id', 'Engine': 'aurora-mysql', 'EngineVersion': '5.7.mysql_aurora.2.10.2', 'BacktrackWindow': 1, 'DatabaseName': 'my-db-name', 'DBClusterParameterGroupName': 'my-cluster-param-group', 'DBSubnetGroupName': 'my-subnet-group', 'Port': 3306, 'VpcSecurityGroupIds': ['my-vpc-1', 'my-vpc-2'], 'CopyTagsToSnapshot': False, 'EnableCloudwatchLogsExports': ['audit'], 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'DBInstanceClass': 'db.r5.large', 'AutoMinorVersionUpgrade': True, 'DBParameterGroupName': 'my-param-group', 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnableInstancePerformanceInsights': True, 'PubliclyAccessible': False, 'PromotionTier': 0, 'ClusterInstances': [ { 'PromotionTier': 0, 'DBInstanceClass': 'db.r5.large', 'DBParameterGroupName': 'my-param-group', 'EventSubscriptions': ['my-instance-subscription'], } ], 'BackupRetentionPeriod': 30, 'PreferredBackupWindow': '07:00-09:00', 'PreferredMaintenanceWindow': 'thu:08:17-thu:08:47', 'ClusterEventSubscriptions': ['my-cluster-subscription'], 'InstanceEventSubscriptions': ['my-instance-subscription'], 'CrossAccountClone': False, 'CloneGroupId': None, 'IsEncryptedWithDefaultKey': False, 'AssociatedRoles': [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', 'Status': 'ACTIVE' } ] } rds_stubber.assert_no_pending_responses() kms_stubber.assert_no_pending_responses() def test_get_config_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): get_config('my-db', 'unsupported') def test_rename_database_cluster(rds_stubber): """Test renaming a database cluster.""" _mock_cluster_members(rds_stubber, 'my-old-db', ['my-old-db-0']) _mock_cluster_available(rds_stubber, 'my-old-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-old-db', 'NewDBClusterIdentifier': 'my-new-db', 'ApplyImmediately': True }) _mock_instance_available(rds_stubber, 'my-old-db-0') rds_stubber.add_response('modify_db_instance', {}, { 'DBInstanceIdentifier': 'my-old-db-0', 'NewDBInstanceIdentifier': 'my-new-db-0', 'ApplyImmediately': True }) rds_stubber.add_response('describe_db_instances', {}, { 'DBInstanceIdentifier': 'my-new-db-0' }) _mock_instance_available(rds_stubber, 'my-new-db-0') _mock_cluster_available(rds_stubber, 'my-new-db') rename_database('my-old-db', 'my-new-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_rename_database_cluster_multiple_instances(rds_stubber): """Test renaming a cluster with more than one instance.""" _mock_cluster_members( rds_stubber, 'my-old-db', ['my-old-db-0', 'my-old-db-1']) _mock_cluster_available(rds_stubber, 'my-old-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-old-db', 'NewDBClusterIdentifier': 'my-new-db', 'ApplyImmediately': True }) # Every instance is awaited available first, then all renames are issued # together so they run concurrently. _mock_instance_available(rds_stubber, 'my-old-db-0') _mock_instance_available(rds_stubber, 'my-old-db-1') rds_stubber.add_response('modify_db_instance', {}, { 'DBInstanceIdentifier': 'my-old-db-0', 'NewDBInstanceIdentifier': 'my-new-db-0', 'ApplyImmediately': True }) rds_stubber.add_response('modify_db_instance', {}, { 'DBInstanceIdentifier': 'my-old-db-1', 'NewDBInstanceIdentifier': 'my-new-db-1', 'ApplyImmediately': True }) # Every renamed instance is awaited - the rename is not complete until the # readers have landed under their new names too, not just the writer. rds_stubber.add_response('describe_db_instances', {}, { 'DBInstanceIdentifier': 'my-new-db-0' }) _mock_instance_available(rds_stubber, 'my-new-db-0') rds_stubber.add_response('describe_db_instances', {}, { 'DBInstanceIdentifier': 'my-new-db-1' }) _mock_instance_available(rds_stubber, 'my-new-db-1') _mock_cluster_available(rds_stubber, 'my-new-db') rename_database('my-old-db', 'my-new-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_rename_database_cluster_collision_aborts_before_rename(rds_stubber): """A bad instance-name mapping aborts before anything is renamed. An instance whose identifier does not track the cluster name cannot be mapped onto the new cluster name, so the rename must fail fast before the cluster or any instance has been modified. """ _mock_cluster_members( rds_stubber, 'my-old-db', ['my-old-db-0', 'unrelated-name']) with pytest.raises(ValueError): rename_database('my-old-db', 'my-new-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_rename_database_standalone(rds_stubber): """Test renaming a standalone db.""" _mock_instance_available(rds_stubber, 'my-old-db') rds_stubber.add_response('modify_db_instance', {}, { 'DBInstanceIdentifier': 'my-old-db', 'NewDBInstanceIdentifier': 'my-new-db', 'ApplyImmediately': True }) rds_stubber.add_response('describe_db_instances', {}, { 'DBInstanceIdentifier': 'my-new-db' }) _mock_instance_available(rds_stubber, 'my-new-db') rename_database('my-old-db', 'my-new-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_rename_database_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): rename_database('my-old-db', 'my-new-db', 'unsupported') def test_configure_event_subscriptions_cluster(rds_stubber): """Test configuring event subscriptions for a cluster.""" rds_stubber.add_response('add_source_identifier_to_subscription', {}, { 'SubscriptionName': 'my-cluster-subscription', 'SourceIdentifier': 'my-db' }) rds_stubber.add_response('add_source_identifier_to_subscription', {}, { 'SubscriptionName': 'my-instance-subscription', 'SourceIdentifier': 'my-db-0' }) configure_event_subscriptions('my-db', 'cluster', { 'ClusterEventSubscriptions': ['my-cluster-subscription'], 'InstanceEventSubscriptions': ['my-instance-subscription'] }) rds_stubber.assert_no_pending_responses() def test_configure_event_subscriptions_cluster_multiple_instances(rds_stubber): """Each member's subscriptions are re-attached to its restored instance.""" rds_stubber.add_response('add_source_identifier_to_subscription', {}, { 'SubscriptionName': 'my-cluster-subscription', 'SourceIdentifier': 'my-db' }) rds_stubber.add_response('add_source_identifier_to_subscription', {}, { 'SubscriptionName': 'my-writer-subscription', 'SourceIdentifier': 'my-db-0' }) rds_stubber.add_response('add_source_identifier_to_subscription', {}, { 'SubscriptionName': 'my-reader-subscription', 'SourceIdentifier': 'my-db-1' }) configure_event_subscriptions('my-db', 'cluster', { 'ClusterEventSubscriptions': ['my-cluster-subscription'], 'InstanceEventSubscriptions': ['my-writer-subscription'], 'ClusterInstances': [ {'EventSubscriptions': ['my-writer-subscription']}, {'EventSubscriptions': ['my-reader-subscription']}, ] }) rds_stubber.assert_no_pending_responses() def test_configure_event_subscriptions_standalone(rds_stubber): """Test configuring event subscriptions for a standalone db.""" rds_stubber.add_response('add_source_identifier_to_subscription', {}, { 'SubscriptionName': 'my-instance-subscription', 'SourceIdentifier': 'my-db' }) configure_event_subscriptions('my-db', 'standalone', { 'InstanceEventSubscriptions': ['my-instance-subscription'] }) rds_stubber.assert_no_pending_responses() def test_configure_event_subscriptions_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): configure_event_subscriptions('my-db', 'unsupported', {}) def test_wait_for_database_availability_cluster(rds_stubber): """Test waiting for cluster to be available.""" _mock_cluster_available(rds_stubber, 'my-db') wait_for_database_availability('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_wait_for_database_availability_standalone(rds_stubber): """Test waiting for cluster to be available.""" _mock_instance_available(rds_stubber, 'my-db') wait_for_database_availability('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_wait_for_database_availability_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): wait_for_database_availability('my-db', 'unsupported') def test_encrypted_with_default_key_default_key(kms_stubber): """Test when the KMS key is the default key.""" kms_stubber.add_response('describe_key', { 'KeyMetadata': { 'Arn': 'arn:aws:kms:region:account-id:key/default-key-id', 'KeyId': 'default-key-id' } }, { 'KeyId': 'alias/aws/rds' }) result = encrypted_with_default_key( 'arn:aws:kms:region:account-id:key/default-key-id', kms_client ) assert result is True kms_stubber.assert_no_pending_responses() def test_encrypted_with_default_key_custom_key(kms_stubber): """Test when the KMS key is a custom key.""" kms_stubber.add_response('describe_key', { 'KeyMetadata': { 'Arn': 'arn:aws:kms:region:account-id:key/default-key-id', 'KeyId': 'default-key-id' } }, { 'KeyId': 'alias/aws/rds' }) result = encrypted_with_default_key( 'arn:aws:kms:region:account-id:key/custom-key-id', kms_client ) assert result is False kms_stubber.assert_no_pending_responses() def test_associate_roles_with_cluster(rds_stubber): """Test associating roles with a cluster.""" rds_stubber.add_response('add_role_to_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', }) associate_roles_with_cluster('my-db', [ { 'RoleArn': 'arn:aws:iam::111111111111:role/my-role', } ]) rds_stubber.assert_no_pending_responses() def _mock_cluster_snapshot_available(rds_stubber, snapshot_id): rds_stubber.add_response('describe_db_cluster_snapshots', { 'DBClusterSnapshots': [ { 'DBClusterSnapshotIdentifier': snapshot_id, 'Status': 'available' } ] }) def _mock_instance_snapshot_available(rds_stubber, snapshot_id): rds_stubber.add_response('describe_db_snapshots', { 'DBSnapshots': [ { 'DBSnapshotIdentifier': snapshot_id, 'Status': 'available' } ] }) def _mock_create_cluster_instance( rds_stubber, instance_id, cluster_id, tier, instance_class='db.r5.large', parameter_group='my-param-group'): """Stub a create_db_instance call for an Aurora cluster member.""" rds_stubber.add_response('create_db_instance', { 'DBInstance': { 'DBInstanceIdentifier': instance_id } }, { 'DBInstanceIdentifier': instance_id, 'DBClusterIdentifier': cluster_id, 'DBInstanceClass': instance_class, 'Engine': 'aurora-mysql', 'AutoMinorVersionUpgrade': True, 'DBSubnetGroupName': 'my-subnet-group', 'DBParameterGroupName': parameter_group, 'MonitoringInterval': 5, 'MonitoringRoleArn': 'arn:aws:iam::111111111111:role/my-monitoring-role', 'EnablePerformanceInsights': True, 'PubliclyAccessible': False, 'Tags': [{'Key': 'my-key', 'Value': 'my-value'}], 'PromotionTier': tier, }) def _mock_cluster_members(rds_stubber, cluster_id, instance_ids, writer=None): rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': cluster_id, 'DBClusterMembers': [ { 'DBInstanceIdentifier': instance_id, 'IsClusterWriter': instance_id == writer } for instance_id in instance_ids ] } ] }, {'DBClusterIdentifier': cluster_id}) def _mock_cluster_available(rds_stubber, cluster_id): rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': cluster_id, 'Status': 'available' } ] }) def _mock_instance_available(rds_stubber, instance_id): rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': instance_id, 'DBInstanceStatus': 'available', 'PendingModifiedValues': {} } ] }) def _mock_cluster_deleted(rds_stubber): rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [] }) def _mock_instance_deleted(rds_stubber): rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }) def _mock_cluster_snapshot_deleted(rds_stubber): rds_stubber.add_response('describe_db_cluster_snapshots', { 'DBClusterSnapshots': [] }) def _mock_instance_snapshot_deleted(rds_stubber): rds_stubber.add_response('describe_db_snapshots', { 'DBSnapshots': [] })