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"]