diff --git a/moto/rds/models.py b/moto/rds/models.py index 0bfc1b259..1ac46d05a 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -229,6 +229,15 @@ class Cluster: self.replication_source_identifier = kwargs.get("replication_source_identifier") self.read_replica_identifiers: List[str] = list() self.is_writer: bool = False + self.storage_encrypted = kwargs.get("storage_encrypted", False) + if self.storage_encrypted: + self.kms_key_id = kwargs.get("kms_key_id", "default_kms_key_id") + else: + self.kms_key_id = kwargs.get("kms_key_id") + if self.engine == "aurora-mysql" or self.engine == "aurora-postgresql": + self.global_write_forwarding_requested = kwargs.get( + "enable_global_write_forwarding" + ) @property def is_multi_az(self) -> bool: @@ -361,7 +370,8 @@ class Cluster: {% endfor %} {{ cluster.hosted_zone_id }} - false + {{ 'true' if cluster.storage_encrypted else 'false' }} + {{ cluster.global_write_forwarding_requested }} {{ cluster.resource_id }} {{ cluster.db_cluster_arn }} @@ -485,6 +495,7 @@ class ClusterSnapshot(BaseModel): self.tags = tags self.status = "available" self.created_at = iso_8601_datetime_with_milliseconds() + self.attributes: List[Dict[str, Any]] = [] @property def arn(self) -> str: @@ -1122,6 +1133,7 @@ class DatabaseSnapshot(BaseModel): self.tags = tags self.status = "available" self.created_at = iso_8601_datetime_with_milliseconds() + self.attributes: List[Dict[str, Any]] = [] @property def arn(self) -> str: @@ -2803,6 +2815,92 @@ class RDSBackend(BaseBackend): pass return None + def describe_db_snapshot_attributes( + self, db_snapshot_identifier: str + ) -> List[Dict[str, Any]]: + snapshot = self.describe_db_snapshots( + db_instance_identifier=None, db_snapshot_identifier=db_snapshot_identifier + )[0] + return snapshot.attributes + + def modify_db_snapshot_attribute( + self, + db_snapshot_identifier: str, + attribute_name: str, + values_to_add: Optional[Dict[str, Dict[str, str]]] = None, + values_to_remove: Optional[Dict[str, Dict[str, str]]] = None, + ) -> List[Dict[str, Any]]: + snapshot = self.describe_db_snapshots( + db_instance_identifier=None, db_snapshot_identifier=db_snapshot_identifier + )[0] + attribute_present = False + for attribute in snapshot.attributes: + if attribute["AttributeName"] == attribute_name: + attribute_present = True + if values_to_add: + attribute["AttributeValues"] = ( + values_to_add["AttributeValue"].values() + + attribute["AttributeValues"] + ) + if values_to_remove: + attribute["AttributeValues"] = [ + i + for i in attribute["AttributeValues"] + if i not in values_to_remove["AttributeValue"].values() + ] + if not attribute_present and values_to_add: + snapshot.attributes.append( + { + "AttributeName": attribute_name, + "AttributeValues": values_to_add["AttributeValue"].values(), + } + ) + return snapshot.attributes + + def describe_db_cluster_snapshot_attributes( + self, db_cluster_snapshot_identifier: str + ) -> List[Dict[str, Any]]: + snapshot = self.describe_db_cluster_snapshots( + db_cluster_identifier=None, + db_snapshot_identifier=db_cluster_snapshot_identifier, + )[0] + return snapshot.attributes + + def modify_db_cluster_snapshot_attribute( + self, + db_cluster_snapshot_identifier: str, + attribute_name: str, + values_to_add: Optional[Dict[str, Dict[str, str]]] = None, + values_to_remove: Optional[Dict[str, Dict[str, str]]] = None, + ) -> List[Dict[str, Any]]: + snapshot = self.describe_db_cluster_snapshots( + db_cluster_identifier=None, + db_snapshot_identifier=db_cluster_snapshot_identifier, + )[0] + attribute_present = False + for attribute in snapshot.attributes: + if attribute["AttributeName"] == attribute_name: + attribute_present = True + if values_to_add: + attribute["AttributeValues"] = ( + values_to_add["AttributeValue"].values() + + attribute["AttributeValues"] + ) + if values_to_remove: + attribute["AttributeValues"] = [ + i + for i in attribute["AttributeValues"] + if i not in values_to_remove["AttributeValue"].values() + ] + if not attribute_present and values_to_add: + snapshot.attributes.append( + { + "AttributeName": attribute_name, + "AttributeValues": values_to_add["AttributeValue"].values(), + } + ) + return snapshot.attributes + class OptionGroup: def __init__( diff --git a/moto/rds/responses.py b/moto/rds/responses.py index a68484f57..e3dbbe1cc 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -195,6 +195,10 @@ class RDSResponse(BaseResponse): "allocated_storage": self._get_param("AllocatedStorage"), "global_cluster_identifier": self._get_param("GlobalClusterIdentifier"), "iops": self._get_param("Iops"), + "storage_encrypted": self._get_param("StorageEncrypted"), + "enable_global_write_forwarding": self._get_param( + "EnableGlobalWriteForwarding" + ), "storage_type": self._get_param("StorageType"), "kms_key_id": self._get_param("KmsKeyId"), "master_username": self._get_param("MasterUsername"), @@ -818,6 +822,66 @@ class RDSResponse(BaseResponse): template = self.response_template(PROMOTE_READ_REPLICA_DB_CLUSTER_TEMPLATE) return template.render(cluster=cluster) + def describe_db_snapshot_attributes(self) -> str: + params = self._get_params() + db_snapshot_identifier = params["DBSnapshotIdentifier"] + db_snapshot_attributes_result = self.backend.describe_db_snapshot_attributes( + db_snapshot_identifier=db_snapshot_identifier, + ) + template = self.response_template(DESCRIBE_DB_SNAPSHOT_ATTRIBUTES_TEMPLATE) + return template.render( + db_snapshot_attributes_result=db_snapshot_attributes_result, + db_snapshot_identifier=db_snapshot_identifier, + ) + + def modify_db_snapshot_attribute(self) -> str: + params = self._get_params() + db_snapshot_identifier = params["DBSnapshotIdentifier"] + db_snapshot_attributes_result = self.backend.modify_db_snapshot_attribute( + db_snapshot_identifier=db_snapshot_identifier, + attribute_name=params["AttributeName"], + values_to_add=params.get("ValuesToAdd"), + values_to_remove=params.get("ValuesToRemove"), + ) + template = self.response_template(MODIFY_DB_SNAPSHOT_ATTRIBUTE_TEMPLATE) + return template.render( + db_snapshot_attributes_result=db_snapshot_attributes_result, + db_snapshot_identifier=db_snapshot_identifier, + ) + + def describe_db_cluster_snapshot_attributes(self) -> str: + params = self._get_params() + db_cluster_snapshot_identifier = params["DBClusterSnapshotIdentifier"] + db_cluster_snapshot_attributes_result = ( + self.backend.describe_db_cluster_snapshot_attributes( + db_cluster_snapshot_identifier=db_cluster_snapshot_identifier, + ) + ) + template = self.response_template( + DESCRIBE_DB_CLUSTER_SNAPSHOT_ATTRIBUTES_TEMPLATE + ) + return template.render( + db_cluster_snapshot_attributes_result=db_cluster_snapshot_attributes_result, + db_cluster_snapshot_identifier=db_cluster_snapshot_identifier, + ) + + def modify_db_cluster_snapshot_attribute(self) -> str: + params = self._get_params() + db_cluster_snapshot_identifier = params["DBClusterSnapshotIdentifier"] + db_cluster_snapshot_attributes_result = ( + self.backend.modify_db_cluster_snapshot_attribute( + db_cluster_snapshot_identifier=db_cluster_snapshot_identifier, + attribute_name=params["AttributeName"], + values_to_add=params.get("ValuesToAdd"), + values_to_remove=params.get("ValuesToRemove"), + ) + ) + template = self.response_template(MODIFY_DB_CLUSTER_SNAPSHOT_ATTRIBUTE_TEMPLATE) + return template.render( + db_cluster_snapshot_attributes_result=db_cluster_snapshot_attributes_result, + db_cluster_snapshot_identifier=db_cluster_snapshot_identifier, + ) + CREATE_DATABASE_TEMPLATE = """ @@ -1474,3 +1538,95 @@ PROMOTE_READ_REPLICA_DB_CLUSTER_TEMPLATE = """7369556f-b70d-11c3-faca-6ba18376ea1b """ + +DESCRIBE_DB_SNAPSHOT_ATTRIBUTES_TEMPLATE = """ + + + + {%- for attribute in db_snapshot_attributes_result -%} + + {{ attribute["AttributeName"] }} + + {%- for value in attribute["AttributeValues"] -%} + {{ value }} + {%- endfor -%} + + + {%- endfor -%} + + {{ db_snapshot_identifier }} + + + + 1549581b-12b7-11e3-895e-1334a + +""" + +MODIFY_DB_SNAPSHOT_ATTRIBUTE_TEMPLATE = """ + + + + {%- for attribute in db_snapshot_attributes_result -%} + + {{ attribute["AttributeName"] }} + + {%- for value in attribute["AttributeValues"] -%} + {{ value }} + {%- endfor -%} + + + {%- endfor -%} + + {{ db_snapshot_identifier }} + + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +MODIFY_DB_CLUSTER_SNAPSHOT_ATTRIBUTE_TEMPLATE = """ + + + + {%- for attribute in db_cluster_snapshot_attributes_result -%} + + {{ attribute["AttributeName"] }} + + {%- for value in attribute["AttributeValues"] -%} + {{ value }} + {%- endfor -%} + + + {%- endfor -%} + + {{ db_cluster_snapshot_identifier }} + + + + 1549581b-12b7-11e3-895e-1334a + +""" + +DESCRIBE_DB_CLUSTER_SNAPSHOT_ATTRIBUTES_TEMPLATE = """ + + + + {%- for attribute in db_cluster_snapshot_attributes_result -%} + + {{ attribute["AttributeName"] }} + + {%- for value in attribute["AttributeValues"] -%} + {{ value }} + {%- endfor -%} + + + {%- endfor -%} + + {{ db_cluster_snapshot_identifier }} + + + + 1549581b-12b7-11e3-895e-1334a + +""" diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index 5b3d75ef0..fd2551550 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -2694,6 +2694,106 @@ def test_createdb_instance_engine_with_invalid_value(): ) +@mock_rds +def test_describe_db_snapshot_attributes_default(): + client = boto3.client("rds", region_name="us-east-2") + client.create_db_instance( + DBInstanceIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + + client.create_db_snapshot( + DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1" + ).get("DBSnapshot") + + resp = client.describe_db_snapshot_attributes(DBSnapshotIdentifier="snapshot-1") + + assert resp["DBSnapshotAttributesResult"]["DBSnapshotIdentifier"] == "snapshot-1" + assert resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"] == [] + + +@mock_rds +def test_describe_db_snapshot_attributes(): + client = boto3.client("rds", region_name="us-east-2") + client.create_db_instance( + DBInstanceIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + + client.create_db_snapshot( + DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1" + ).get("DBSnapshot") + + resp = client.modify_db_snapshot_attribute( + DBSnapshotIdentifier="snapshot-1", + AttributeName="restore", + ValuesToAdd=["Test", "Test2"], + ) + + resp = client.describe_db_snapshot_attributes(DBSnapshotIdentifier="snapshot-1") + + assert ( + resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"][0]["AttributeName"] + == "restore" + ) + assert resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"][0][ + "AttributeValues" + ] == ["Test", "Test2"] + + +@mock_rds +def test_modify_db_snapshot_attribute(): + client = boto3.client("rds", region_name="us-east-2") + client.create_db_instance( + DBInstanceIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + + client.create_db_snapshot( + DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1" + ).get("DBSnapshot") + + resp = client.modify_db_snapshot_attribute( + DBSnapshotIdentifier="snapshot-1", + AttributeName="restore", + ValuesToAdd=["Test", "Test2"], + ) + resp = client.modify_db_snapshot_attribute( + DBSnapshotIdentifier="snapshot-1", + AttributeName="restore", + ValuesToRemove=["Test"], + ) + + assert ( + resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"][0]["AttributeName"] + == "restore" + ) + assert resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"][0][ + "AttributeValues" + ] == ["Test2"] + + def validation_helper(exc): err = exc.value.response["Error"] assert err["Code"] == "InvalidParameterValue" diff --git a/tests/test_rds/test_rds_clusters.py b/tests/test_rds/test_rds_clusters.py index d1f6a702c..14dde421d 100644 --- a/tests/test_rds/test_rds_clusters.py +++ b/tests/test_rds/test_rds_clusters.py @@ -215,6 +215,8 @@ def test_create_db_cluster__verify_default_properties(): assert cluster["TagList"] == [] assert "ClusterCreateTime" in cluster assert cluster["EarliestRestorableTime"] >= cluster["ClusterCreateTime"] + assert cluster["StorageEncrypted"] is False + assert cluster["GlobalWriteForwardingRequested"] is False @mock_rds @@ -236,6 +238,8 @@ def test_create_db_cluster_additional_parameters(): KmsKeyId="some:kms:arn", NetworkType="IPV4", DBSubnetGroupName="subnetgroupname", + StorageEncrypted=True, + EnableGlobalWriteForwarding=True, ScalingConfiguration={ "MinCapacity": 5, "AutoPause": True, @@ -260,6 +264,8 @@ def test_create_db_cluster_additional_parameters(): assert cluster["KmsKeyId"] == "some:kms:arn" assert cluster["NetworkType"] == "IPV4" assert cluster["DBSubnetGroup"] == "subnetgroupname" + assert cluster["StorageEncrypted"] is True + assert cluster["GlobalWriteForwardingRequested"] is True assert cluster["ScalingConfigurationInfo"] == {"MinCapacity": 5, "AutoPause": True} assert cluster["ServerlessV2ScalingConfiguration"] == { "MaxCapacity": 4.0, @@ -977,3 +983,120 @@ def test_createdb_instance_engine_mismatch_fail(): == "The engine name requested for your DB instance (mysql) doesn't match " "the engine name of your DB cluster (aurora-postgresql)." ) + + +@mock_rds +def test_describe_db_cluster_snapshot_attributes_default(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_cluster( + DBClusterIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DatabaseName="staging-postgres", + DBClusterInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2000", + Port=1234, + ) + + conn.create_db_cluster_snapshot( + DBClusterIdentifier="db-primary-1", DBClusterSnapshotIdentifier="g-1" + ).get("DBClusterSnapshot") + + resp = conn.describe_db_cluster_snapshot_attributes( + DBClusterSnapshotIdentifier="g-1" + ) + + assert ( + resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotIdentifier"] + == "g-1" + ) + assert ( + resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"] == [] + ) + + +@mock_rds +def test_describe_db_cluster_snapshot_attributes(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_cluster( + DBClusterIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DatabaseName="staging-postgres", + DBClusterInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2000", + Port=1234, + ) + + conn.create_db_cluster_snapshot( + DBClusterIdentifier="db-primary-1", DBClusterSnapshotIdentifier="g-1" + ).get("DBClusterSnapshot") + + conn.modify_db_cluster_snapshot_attribute( + DBClusterSnapshotIdentifier="g-1", + AttributeName="restore", + ValuesToAdd=["test", "test2"], + ) + + resp = conn.describe_db_cluster_snapshot_attributes( + DBClusterSnapshotIdentifier="g-1" + ) + + assert ( + resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotIdentifier"] + == "g-1" + ) + assert ( + resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"][0][ + "AttributeName" + ] + == "restore" + ) + assert resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"][0][ + "AttributeValues" + ] == ["test", "test2"] + + +@mock_rds +def test_modify_db_cluster_snapshot_attribute(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_cluster( + DBClusterIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DatabaseName="staging-postgres", + DBClusterInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2000", + Port=1234, + ) + + conn.create_db_cluster_snapshot( + DBClusterIdentifier="db-primary-1", DBClusterSnapshotIdentifier="g-1" + ).get("DBClusterSnapshot") + + resp = conn.modify_db_cluster_snapshot_attribute( + DBClusterSnapshotIdentifier="g-1", + AttributeName="restore", + ValuesToAdd=["test", "test2"], + ) + resp = conn.modify_db_cluster_snapshot_attribute( + DBClusterSnapshotIdentifier="g-1", + AttributeName="restore", + ValuesToRemove=["test"], + ) + assert ( + resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotIdentifier"] + == "g-1" + ) + assert ( + resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"][0][ + "AttributeName" + ] + == "restore" + ) + assert resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"][0][ + "AttributeValues" + ] == ["test2"]