From cb60761510bbad4fb1c213a42eceaedcf8894383 Mon Sep 17 00:00:00 2001 From: Dmytro Kazanzhy Date: Sat, 29 Jan 2022 14:24:28 +0200 Subject: [PATCH] Added event subscriptions: create_event_subscription, delete_event_subscription, describe_event_subscriptions (#4807) --- moto/rds2/exceptions.py | 16 ++ moto/rds2/models.py | 98 ++++++++++++- moto/rds2/responses.py | 65 +++++++++ .../test_rds2_event_subscriptions.py | 138 ++++++++++++++++++ 4 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 tests/test_rds2/test_rds2_event_subscriptions.py diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index f5e72f228..d22fb0e23 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -177,3 +177,19 @@ class InvalidExportSourceStateError(RDSClientError): status ), ) + + +class SubscriptionAlreadyExistError(RDSClientError): + def __init__(self, subscription_name): + super().__init__( + "SubscriptionAlreadyExistFault", + "Subscription {} already exists.".format(subscription_name), + ) + + +class SubscriptionNotFoundError(RDSClientError): + def __init__(self, subscription_name): + super().__init__( + "SubscriptionNotFoundFault", + "Subscription {} not found.".format(subscription_name), + ) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 2c2706ca9..66c3dd534 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -33,6 +33,8 @@ from .exceptions import ( ExportTaskNotFoundError, ExportTaskAlreadyExistsError, InvalidExportSourceStateError, + SubscriptionNotFoundError, + SubscriptionAlreadyExistError, ) from .utils import FilterDef, apply_filter, merge_filters, validate_filters @@ -957,6 +959,72 @@ class ExportTask(BaseModel): return template.render(task=self, snapshot=self.snapshot) +class EventSubscription(BaseModel): + def __init__(self, kwargs): + self.subscription_name = kwargs.get("subscription_name") + self.sns_topic_arn = kwargs.get("sns_topic_arn") + self.source_type = kwargs.get("source_type") + self.event_categories = kwargs.get("event_categories", []) + self.source_ids = kwargs.get("source_ids", []) + self.enabled = kwargs.get("enabled", True) + self.tags = kwargs.get("tags", True) + + self.region = "" + self.customer_aws_id = copy.copy(ACCOUNT_ID) + self.status = "available" + self.created_at = iso_8601_datetime_with_milliseconds(datetime.datetime.now()) + + @property + def es_arn(self): + return "arn:aws:rds:{0}:{1}:es:{2}".format( + self.region, ACCOUNT_ID, self.subscription_name + ) + + def to_xml(self): + template = Template( + """ + + {{ subscription.customer_aws_id }} + {{ subscription.subscription_name }} + {{ subscription.sns_topic_arn }} + {{ subscription.created_at }} + {{ subscription.source_type }} + + {%- for source_id in subscription.source_ids -%} + {{ source_id }} + {%- endfor -%} + + + {%- for category in subscription.event_categories -%} + {{ category }} + {%- endfor -%} + + {{ subscription.status }} + {{ subscription.enabled }} + {{ subscription.es_arn }} + + {%- for tag in subscription.tags -%} + {{ tag['Key'] }}{{ tag['Value'] }} + {%- endfor -%} + + + """ + ) + return template.render(subscription=self) + + def get_tags(self): + return self.tags + + def add_tags(self, tags): + new_keys = [tag_set["Key"] for tag_set in tags] + self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys] + self.tags.extend(tags) + return self.tags + + def remove_tags(self, tag_keys): + self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys] + + class SecurityGroup(CloudFormationModel): def __init__(self, group_name, description, tags): self.group_name = group_name @@ -1183,6 +1251,7 @@ class RDS2Backend(BaseBackend): self.database_snapshots = OrderedDict() self.cluster_snapshots = OrderedDict() self.export_tasks = OrderedDict() + self.event_subscriptions = OrderedDict() self.db_parameter_groups = {} self.option_groups = {} self.security_groups = {} @@ -1844,6 +1913,30 @@ class RDS2Backend(BaseBackend): raise ExportTaskNotFoundError(export_task_identifier) return self.export_tasks.values() + def create_event_subscription(self, kwargs): + subscription_name = kwargs["subscription_name"] + + if subscription_name in self.event_subscriptions: + raise SubscriptionAlreadyExistError(subscription_name) + + subscription = EventSubscription(kwargs) + self.event_subscriptions[subscription_name] = subscription + + return subscription + + def delete_event_subscription(self, subscription_name): + if subscription_name in self.event_subscriptions: + return self.event_subscriptions.pop(subscription_name) + raise SubscriptionNotFoundError(subscription_name) + + def describe_event_subscriptions(self, subscription_name): + if subscription_name: + if subscription_name in self.event_subscriptions: + return [self.event_subscriptions[subscription_name]] + else: + raise SubscriptionNotFoundError(subscription_name) + return self.event_subscriptions.values() + def list_tags_for_resource(self, arn): if self.arn_regex.match(arn): arn_breakdown = arn.split(":") @@ -1856,9 +1949,8 @@ class RDS2Backend(BaseBackend): if resource_name in self.clusters: return self.clusters[resource_name].get_tags() elif resource_type == "es": # Event Subscription - # TODO: Complete call to tags on resource type Event - # Subscription - return [] + if resource_name in self.event_subscriptions: + return self.event_subscriptions[resource_name].get_tags() elif resource_type == "og": # Option Group if resource_name in self.option_groups: return self.option_groups[resource_name].get_tags() diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index e9a73ead7..34133dfdc 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -122,6 +122,19 @@ class RDS2Response(BaseResponse): "export_only": self.unpack_list_params("ExportOnly.member"), } + def _get_event_subscription_kwargs(self): + return { + "subscription_name": self._get_param("SubscriptionName"), + "sns_topic_arn": self._get_param("SnsTopicArn"), + "source_type": self._get_param("SourceType"), + "event_categories": self.unpack_list_params( + "EventCategories.EventCategory" + ), + "source_ids": self.unpack_list_params("SourceIds.SourceId"), + "enabled": self._get_param("Enabled"), + "tags": self.unpack_complex_list_params("Tags.Tag", ("Key", "Value")), + } + def unpack_complex_list_params(self, label, names): unpacked_list = list() count = 1 @@ -577,6 +590,24 @@ class RDS2Response(BaseResponse): template = self.response_template(DESCRIBE_EXPORT_TASKS_TEMPLATE) return template.render(tasks=tasks) + def create_event_subscription(self): + kwargs = self._get_event_subscription_kwargs() + subscription = self.backend.create_event_subscription(kwargs) + template = self.response_template(CREATE_EVENT_SUBSCRIPTION_TEMPLATE) + return template.render(subscription=subscription) + + def delete_event_subscription(self): + subscription_name = self._get_param("SubscriptionName") + subscription = self.backend.delete_event_subscription(subscription_name) + template = self.response_template(DELETE_EVENT_SUBSCRIPTION_TEMPLATE) + return template.render(subscription=subscription) + + def describe_event_subscriptions(self): + subscription_name = self._get_param("SubscriptionName") + subscriptions = self.backend.describe_event_subscriptions(subscription_name) + template = self.response_template(DESCRIBE_EVENT_SUBSCRIPTIONS_TEMPLATE) + return template.render(subscriptions=subscriptions) + CREATE_DATABASE_TEMPLATE = """ @@ -1063,3 +1094,37 @@ DESCRIBE_EXPORT_TASKS_TEMPLATE = """ + + {{ subscription.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + + +""" + +DELETE_EVENT_SUBSCRIPTION_TEMPLATE = """ + + {{ subscription.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + + +""" + +DESCRIBE_EVENT_SUBSCRIPTIONS_TEMPLATE = """ + + + {%- for subscription in subscriptions -%} + {{ subscription.to_xml() }} + {%- endfor -%} + + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + + +""" diff --git a/tests/test_rds2/test_rds2_event_subscriptions.py b/tests/test_rds2/test_rds2_event_subscriptions.py new file mode 100644 index 000000000..1e780fcf9 --- /dev/null +++ b/tests/test_rds2/test_rds2_event_subscriptions.py @@ -0,0 +1,138 @@ +import boto3 +import pytest +import sure # noqa # pylint: disable=unused-import + +from botocore.exceptions import ClientError +from moto import mock_rds2 +from moto.core import ACCOUNT_ID + +DB_INSTANCE_IDENTIFIER = "db-primary-1" + + +def _prepare_db_instance(client): + resp = client.create_db_instance( + DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER, + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + return resp["DBInstance"]["DBInstanceIdentifier"] + + +@mock_rds2 +def test_create_event_subscription(): + client = boto3.client("rds", region_name="us-west-2") + db_identifier = _prepare_db_instance(client) + + es = client.create_event_subscription( + SubscriptionName=f"{db_identifier}-events", + SnsTopicArn=f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic", + SourceType="db-instance", + EventCategories=[ + "Backup", + "Creation", + "Deletion", + "Failure", + "Recovery", + "Restoration", + ], + SourceIds=[db_identifier], + ).get("EventSubscription") + + es["CustSubscriptionId"].should.equal(f"{db_identifier}-events") + es["SnsTopicArn"].should.equal( + f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic" + ) + es["SourceType"].should.equal("db-instance") + es["EventCategoriesList"].should.equal( + ["Backup", "Creation", "Deletion", "Failure", "Recovery", "Restoration"] + ) + es["SourceIdsList"].should.equal([db_identifier]) + es["Enabled"].should.equal(False) + + +@mock_rds2 +def test_create_event_fail_already_exists(): + client = boto3.client("rds", region_name="us-west-2") + db_identifier = _prepare_db_instance(client) + + client.create_event_subscription( + SubscriptionName=f"{db_identifier}-events", + SnsTopicArn=f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic", + ) + + with pytest.raises(ClientError) as ex: + client.create_event_subscription( + SubscriptionName=f"{db_identifier}-events", + SnsTopicArn=f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic", + Enabled=True, + ) + + err = ex.value.response["Error"] + + err["Code"].should.equal("SubscriptionAlreadyExistFault") + err["Message"].should.equal("Subscription db-primary-1-events already exists.") + + +@mock_rds2 +def test_delete_event_subscription_fails_unknown_subscription(): + client = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + client.delete_event_subscription(SubscriptionName="my-db-events") + + err = ex.value.response["Error"] + err["Code"].should.equal("SubscriptionNotFoundFault") + err["Message"].should.equal("Subscription my-db-events not found.") + + +@mock_rds2 +def test_delete_event_subscription(): + client = boto3.client("rds", region_name="us-west-2") + db_identifier = _prepare_db_instance(client) + + client.create_event_subscription( + SubscriptionName=f"{db_identifier}-events", + SnsTopicArn=f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic", + ) + + es = client.delete_event_subscription( + SubscriptionName=f"{db_identifier}-events", + ).get("EventSubscription") + + es["CustSubscriptionId"].should.equal(f"{db_identifier}-events") + es["SnsTopicArn"].should.equal( + f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic" + ) + + +@mock_rds2 +def test_describe_event_subscriptions(): + client = boto3.client("rds", region_name="us-west-2") + db_identifier = _prepare_db_instance(client) + + client.create_event_subscription( + SubscriptionName=f"{db_identifier}-events", + SnsTopicArn=f"arn:aws:sns::{ACCOUNT_ID}:{db_identifier}-events-topic", + ) + + subscriptions = client.describe_event_subscriptions().get("EventSubscriptionsList") + + subscriptions.should.have.length_of(1) + subscriptions[0]["CustSubscriptionId"].should.equal(f"{db_identifier}-events") + + +@mock_rds2 +def test_describe_event_subscriptions_fails_unknown_subscription(): + client = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + client.describe_event_subscriptions(SubscriptionName="my-db-events") + + err = ex.value.response["Error"] + + err["Code"].should.equal("SubscriptionNotFoundFault") + err["Message"].should.equal("Subscription my-db-events not found.")