From eaa8c8db6e77b93f12ee90f2aa0fed483ce45d30 Mon Sep 17 00:00:00 2001 From: Brady Date: Thu, 16 Jan 2020 21:00:24 -0500 Subject: [PATCH 1/6] add tagging support to events --- moto/events/models.py | 28 ++++++++ moto/events/responses.py | 23 ++++++ moto/utilities/__init__.py | 0 moto/utilities/tagging_service.py | 56 +++++++++++++++ tests/test_events/test_events.py | 74 ++++++++++++++++---- tests/test_utilities/test_tagging_service.py | 59 ++++++++++++++++ 6 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 moto/utilities/__init__.py create mode 100644 moto/utilities/tagging_service.py create mode 100644 tests/test_utilities/test_tagging_service.py diff --git a/moto/events/models.py b/moto/events/models.py index 548d41393..695cfb17a 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -6,6 +6,7 @@ from boto3 import Session from moto.core.exceptions import JsonRESTError from moto.core import BaseBackend, BaseModel from moto.sts.models import ACCOUNT_ID +from moto.utilities.tagging_service import TaggingService class Rule(BaseModel): @@ -104,6 +105,7 @@ class EventsBackend(BaseBackend): self.region_name = region_name self.event_buses = {} self.event_sources = {} + self.tagger = TaggingService() self._add_default_event_bus() @@ -361,6 +363,32 @@ class EventsBackend(BaseBackend): self.event_buses.pop(name, None) + def list_tags_for_resource(self, arn): + name = arn.split("/")[-1] + if name in self.rules: + return self.tagger.list_tags_for_resource(self.rules[name].arn) + raise JsonRESTError( + "ResourceNotFoundException", "An entity that you specified does not exist." + ) + + def tag_resource(self, arn, tags): + name = arn.split("/")[-1] + if name in self.rules: + self.tagger.tag_resource(self.rules[name].arn, tags) + return {} + raise JsonRESTError( + "ResourceNotFoundException", "An entity that you specified does not exist." + ) + + def untag_resource(self, arn, tag_names): + name = arn.split("/")[-1] + if name in self.rules: + self.tagger.untag_resource_using_names(self.rules[name].arn, tag_names) + return {} + raise JsonRESTError( + "ResourceNotFoundException", "An entity that you specified does not exist." + ) + events_backends = {} for region in Session().get_available_regions("events"): diff --git a/moto/events/responses.py b/moto/events/responses.py index b415564f8..68c2114a6 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -297,3 +297,26 @@ class EventsHandler(BaseResponse): self.events_backend.delete_event_bus(name) return "", self.response_headers + + def list_tags_for_resource(self): + arn = self._get_param("ResourceARN") + + result = self.events_backend.list_tags_for_resource(arn) + + return json.dumps(result), self.response_headers + + def tag_resource(self): + arn = self._get_param("ResourceARN") + tags = self._get_param("Tags") + + result = self.events_backend.tag_resource(arn, tags) + + return json.dumps(result), self.response_headers + + def untag_resource(self): + arn = self._get_param("ResourceARN") + tags = self._get_param("TagKeys") + + result = self.events_backend.untag_resource(arn, tags) + + return json.dumps(result), self.response_headers diff --git a/moto/utilities/__init__.py b/moto/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/moto/utilities/tagging_service.py b/moto/utilities/tagging_service.py new file mode 100644 index 000000000..8c7a86f1d --- /dev/null +++ b/moto/utilities/tagging_service.py @@ -0,0 +1,56 @@ +class TaggingService: + def __init__(self, tagName="Tags", keyName="Key", valueName="Value"): + self.tagName = tagName + self.keyName = keyName + self.valueName = valueName + self.tags = {} + + def list_tags_for_resource(self, arn): + result = [] + if arn in self.tags: + for k, v in self.tags[arn].items(): + result.append({self.keyName: k, self.valueName: v}) + return {self.tagName: result} + + def tag_resource(self, arn, tags): + if arn not in self.tags: + self.tags[arn] = {} + for t in tags: + if self.valueName in t: + self.tags[arn][t[self.keyName]] = t[self.valueName] + else: + self.tags[arn][t[self.keyName]] = None + + def untag_resource_using_names(self, arn, tag_names): + for name in tag_names: + if name in self.tags.get(arn, {}): + del self.tags[arn][name] + + def untag_resource_using_tags(self, arn, tags): + m = self.tags.get(arn, {}) + for t in tags: + if self.keyName in t: + if t[self.keyName] in m: + if self.valueName in t: + if m[t[self.keyName]] != t[self.valueName]: + continue + # If both key and value are provided, match both before deletion + del m[t[self.keyName]] + + def extract_tag_names(self, tags): + results = [] + if len(tags) == 0: + return results + for tag in tags: + if self.keyName in tag: + results.append(tag[self.keyName]) + return results + + def flatten_tag_list(self, tags): + result = {} + for t in tags: + if self.valueName in t: + result[t[self.keyName]] = t[self.valueName] + else: + result[t[self.keyName]] = None + return result diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 14d872806..d276a1705 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -1,12 +1,15 @@ -import random -import boto3 import json -import sure # noqa +import random +import unittest -from moto.events import mock_events +import boto3 from botocore.exceptions import ClientError from nose.tools import assert_raises + from moto.core import ACCOUNT_ID +from moto.core.exceptions import JsonRESTError +from moto.events import mock_events +from moto.events.models import EventsBackend RULES = [ {"Name": "test1", "ScheduleExpression": "rate(5 minutes)"}, @@ -136,14 +139,6 @@ def test_list_rule_names_by_target(): assert rule in test_2_target["Rules"] -@mock_events -def test_list_rules(): - client = generate_environment() - - rules = client.list_rules() - assert len(rules["Rules"]) == len(RULES) - - @mock_events def test_delete_rule(): client = generate_environment() @@ -461,3 +456,58 @@ def test_delete_event_bus_errors(): client.delete_event_bus.when.called_with(Name="default").should.throw( ClientError, "Cannot delete event bus default." ) + + +@mock_events +def test_rule_tagging_happy(): + client = generate_environment() + rule_name = get_random_rule()["Name"] + rule_arn = client.describe_rule(Name=rule_name).get("Arn") + + tags = [{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}] + client.tag_resource(ResourceARN=rule_arn, Tags=tags) + + actual = client.list_tags_for_resource(ResourceARN=rule_arn).get("Tags") + tc = unittest.TestCase("__init__") + expected = [{"Value": "value1", "Key": "key1"}, {"Value": "value2", "Key": "key2"}] + tc.assertTrue( + (expected[0] == actual[0] and expected[1] == actual[1]) + or (expected[1] == actual[0] and expected[0] == actual[1]) + ) + + client.untag_resource(ResourceARN=rule_arn, TagKeys=["key1"]) + + actual = client.list_tags_for_resource(ResourceARN=rule_arn).get("Tags") + expected = [{"Key": "key2", "Value": "value2"}] + assert expected == actual + + +def freeze_dict(obj): + if isinstance(obj, dict): + dict_items = list(obj.items()) + dict_items.append(("__frozen__", True)) + return tuple([(k, freeze_dict(v)) for k, v in dict_items]) + return obj + + +@mock_events +def test_rule_tagging_sad(): + b = EventsBackend("us-west-2") + + try: + b.tag_resource("unknown", []) + raise "tag_resource should fail if ResourceARN is not known" + except JsonRESTError: + pass + + try: + b.untag_resource("unknown", []) + raise "untag_resource should fail if ResourceARN is not known" + except JsonRESTError: + pass + + try: + b.list_tags_for_resource("unknown") + raise "list_tags_for_resource should fail if ResourceARN is not known" + except JsonRESTError: + pass diff --git a/tests/test_utilities/test_tagging_service.py b/tests/test_utilities/test_tagging_service.py new file mode 100644 index 000000000..1cd820a19 --- /dev/null +++ b/tests/test_utilities/test_tagging_service.py @@ -0,0 +1,59 @@ +import unittest + +from moto.utilities.tagging_service import TaggingService + + +class TestTaggingService(unittest.TestCase): + def test_list_empty(self): + svc = TaggingService() + result = svc.list_tags_for_resource("test") + self.assertEqual(result, {"Tags": []}) + + def test_create_tag(self): + svc = TaggingService("TheTags", "TagKey", "TagValue") + tags = [{"TagKey": "key_key", "TagValue": "value_value"}] + svc.tag_resource("arn", tags) + actual = svc.list_tags_for_resource("arn") + expected = {"TheTags": [{"TagKey": "key_key", "TagValue": "value_value"}]} + self.assertDictEqual(expected, actual) + + def test_create_tag_without_value(self): + svc = TaggingService() + tags = [{"Key": "key_key"}] + svc.tag_resource("arn", tags) + actual = svc.list_tags_for_resource("arn") + expected = {"Tags": [{"Key": "key_key", "Value": None}]} + self.assertDictEqual(expected, actual) + + def test_delete_tag_using_names(self): + svc = TaggingService() + tags = [{"Key": "key_key", "Value": "value_value"}] + svc.tag_resource("arn", tags) + svc.untag_resource_using_names("arn", ["key_key"]) + result = svc.list_tags_for_resource("arn") + self.assertEqual(result, {"Tags": []}) + + def test_list_empty_delete(self): + svc = TaggingService() + svc.untag_resource_using_names("arn", ["key_key"]) + result = svc.list_tags_for_resource("arn") + self.assertEqual(result, {"Tags": []}) + + def test_delete_tag_using_tags(self): + svc = TaggingService() + tags = [{"Key": "key_key", "Value": "value_value"}] + svc.tag_resource("arn", tags) + svc.untag_resource_using_tags("arn", tags) + result = svc.list_tags_for_resource("arn") + self.assertEqual(result, {"Tags": []}) + + def test_extract_tag_names(self): + svc = TaggingService() + tags = [{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}] + actual = svc.extract_tag_names(tags) + expected = ["key1", "key2"] + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() From 1e851fb1d8b328b18a54702d31a44423138b1e83 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 17 Jan 2020 10:12:58 -0500 Subject: [PATCH 2/6] remove dead code --- tests/test_events/test_events.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index d276a1705..4fb3b4029 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -482,14 +482,6 @@ def test_rule_tagging_happy(): assert expected == actual -def freeze_dict(obj): - if isinstance(obj, dict): - dict_items = list(obj.items()) - dict_items.append(("__frozen__", True)) - return tuple([(k, freeze_dict(v)) for k, v in dict_items]) - return obj - - @mock_events def test_rule_tagging_sad(): b = EventsBackend("us-west-2") From 414f8086b0210ca6522183c14a3ab6a188689766 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 5 Feb 2020 10:30:59 -0500 Subject: [PATCH 3/6] use sure for unit test assertions --- tests/test_utilities/test_tagging_service.py | 109 ++++++++++--------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/tests/test_utilities/test_tagging_service.py b/tests/test_utilities/test_tagging_service.py index 1cd820a19..0d7db3e25 100644 --- a/tests/test_utilities/test_tagging_service.py +++ b/tests/test_utilities/test_tagging_service.py @@ -1,59 +1,64 @@ -import unittest +import sure from moto.utilities.tagging_service import TaggingService -class TestTaggingService(unittest.TestCase): - def test_list_empty(self): - svc = TaggingService() - result = svc.list_tags_for_resource("test") - self.assertEqual(result, {"Tags": []}) +def test_list_empty(): + svc = TaggingService() + result = svc.list_tags_for_resource("test") - def test_create_tag(self): - svc = TaggingService("TheTags", "TagKey", "TagValue") - tags = [{"TagKey": "key_key", "TagValue": "value_value"}] - svc.tag_resource("arn", tags) - actual = svc.list_tags_for_resource("arn") - expected = {"TheTags": [{"TagKey": "key_key", "TagValue": "value_value"}]} - self.assertDictEqual(expected, actual) - - def test_create_tag_without_value(self): - svc = TaggingService() - tags = [{"Key": "key_key"}] - svc.tag_resource("arn", tags) - actual = svc.list_tags_for_resource("arn") - expected = {"Tags": [{"Key": "key_key", "Value": None}]} - self.assertDictEqual(expected, actual) - - def test_delete_tag_using_names(self): - svc = TaggingService() - tags = [{"Key": "key_key", "Value": "value_value"}] - svc.tag_resource("arn", tags) - svc.untag_resource_using_names("arn", ["key_key"]) - result = svc.list_tags_for_resource("arn") - self.assertEqual(result, {"Tags": []}) - - def test_list_empty_delete(self): - svc = TaggingService() - svc.untag_resource_using_names("arn", ["key_key"]) - result = svc.list_tags_for_resource("arn") - self.assertEqual(result, {"Tags": []}) - - def test_delete_tag_using_tags(self): - svc = TaggingService() - tags = [{"Key": "key_key", "Value": "value_value"}] - svc.tag_resource("arn", tags) - svc.untag_resource_using_tags("arn", tags) - result = svc.list_tags_for_resource("arn") - self.assertEqual(result, {"Tags": []}) - - def test_extract_tag_names(self): - svc = TaggingService() - tags = [{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}] - actual = svc.extract_tag_names(tags) - expected = ["key1", "key2"] - self.assertEqual(expected, actual) + {"Tags": []}.should.be.equal(result) -if __name__ == "__main__": - unittest.main() +def test_create_tag(): + svc = TaggingService("TheTags", "TagKey", "TagValue") + tags = [{"TagKey": "key_key", "TagValue": "value_value"}] + svc.tag_resource("arn", tags) + actual = svc.list_tags_for_resource("arn") + expected = {"TheTags": [{"TagKey": "key_key", "TagValue": "value_value"}]} + + expected.should.be.equal(actual) + +def test_create_tag_without_value(): + svc = TaggingService() + tags = [{"Key": "key_key"}] + svc.tag_resource("arn", tags) + actual = svc.list_tags_for_resource("arn") + expected = {"Tags": [{"Key": "key_key", "Value": None}]} + + expected.should.be.equal(actual) + +def test_delete_tag_using_names(): + svc = TaggingService() + tags = [{"Key": "key_key", "Value": "value_value"}] + svc.tag_resource("arn", tags) + svc.untag_resource_using_names("arn", ["key_key"]) + result = svc.list_tags_for_resource("arn") + + {"Tags": []}.should.be.equal(result) + +def test_list_empty_delete(): + svc = TaggingService() + svc.untag_resource_using_names("arn", ["key_key"]) + result = svc.list_tags_for_resource("arn") + + {"Tags": []}.should.be.equal(result) + +def test_delete_tag_using_tags(): + svc = TaggingService() + tags = [{"Key": "key_key", "Value": "value_value"}] + svc.tag_resource("arn", tags) + svc.untag_resource_using_tags("arn", tags) + result = svc.list_tags_for_resource("arn") + + {"Tags": []}.should.be.equal(result) + + +def test_extract_tag_names(): + svc = TaggingService() + tags = [{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}] + actual = svc.extract_tag_names(tags) + expected = ["key1", "key2"] + + expected.should.be.equal(actual) + From c95254a2843fac342e702f7708cce63274a053d0 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 5 Feb 2020 11:58:52 -0500 Subject: [PATCH 4/6] delete tags when their resource is deleted --- moto/events/models.py | 2 ++ moto/utilities/tagging_service.py | 3 +++ tests/test_utilities/test_tagging_service.py | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/moto/events/models.py b/moto/events/models.py index 695cfb17a..82723ac6c 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -143,6 +143,8 @@ class EventsBackend(BaseBackend): def delete_rule(self, name): self.rules_order.pop(self.rules_order.index(name)) + arn = self.rules.get(name).arn + self.tagger.delete_all_tags_for_resource(arn) return self.rules.pop(name) is not None def describe_rule(self, name): diff --git a/moto/utilities/tagging_service.py b/moto/utilities/tagging_service.py index 8c7a86f1d..c56fd2306 100644 --- a/moto/utilities/tagging_service.py +++ b/moto/utilities/tagging_service.py @@ -12,6 +12,9 @@ class TaggingService: result.append({self.keyName: k, self.valueName: v}) return {self.tagName: result} + def delete_all_tags_for_resource(self, arn): + del self.tags[arn] + def tag_resource(self, arn, tags): if arn not in self.tags: self.tags[arn] = {} diff --git a/tests/test_utilities/test_tagging_service.py b/tests/test_utilities/test_tagging_service.py index 0d7db3e25..249e903fe 100644 --- a/tests/test_utilities/test_tagging_service.py +++ b/tests/test_utilities/test_tagging_service.py @@ -19,6 +19,7 @@ def test_create_tag(): expected.should.be.equal(actual) + def test_create_tag_without_value(): svc = TaggingService() tags = [{"Key": "key_key"}] @@ -28,6 +29,7 @@ def test_create_tag_without_value(): expected.should.be.equal(actual) + def test_delete_tag_using_names(): svc = TaggingService() tags = [{"Key": "key_key", "Value": "value_value"}] @@ -37,6 +39,19 @@ def test_delete_tag_using_names(): {"Tags": []}.should.be.equal(result) + +def test_delete_all_tags_for_resource(): + svc = TaggingService() + tags = [{"Key": "key_key", "Value": "value_value"}] + tags2 = [{"Key": "key_key2", "Value": "value_value2"}] + svc.tag_resource("arn", tags) + svc.tag_resource("arn", tags2) + svc.delete_all_tags_for_resource("arn") + result = svc.list_tags_for_resource("arn") + + {"Tags": []}.should.be.equal(result) + + def test_list_empty_delete(): svc = TaggingService() svc.untag_resource_using_names("arn", ["key_key"]) @@ -44,6 +59,7 @@ def test_list_empty_delete(): {"Tags": []}.should.be.equal(result) + def test_delete_tag_using_tags(): svc = TaggingService() tags = [{"Key": "key_key", "Value": "value_value"}] @@ -61,4 +77,3 @@ def test_extract_tag_names(): expected = ["key1", "key2"] expected.should.be.equal(actual) - From 5b5510218156ada78990432bf3d07157c68e611d Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 5 Feb 2020 15:30:34 -0500 Subject: [PATCH 5/6] fix test case --- moto/events/models.py | 3 ++- moto/utilities/tagging_service.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/moto/events/models.py b/moto/events/models.py index 82723ac6c..a80b86daa 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -144,7 +144,8 @@ class EventsBackend(BaseBackend): def delete_rule(self, name): self.rules_order.pop(self.rules_order.index(name)) arn = self.rules.get(name).arn - self.tagger.delete_all_tags_for_resource(arn) + if self.tagger.has_tags(arn): + self.tagger.delete_all_tags_for_resource(arn) return self.rules.pop(name) is not None def describe_rule(self, name): diff --git a/moto/utilities/tagging_service.py b/moto/utilities/tagging_service.py index c56fd2306..89b857277 100644 --- a/moto/utilities/tagging_service.py +++ b/moto/utilities/tagging_service.py @@ -15,6 +15,9 @@ class TaggingService: def delete_all_tags_for_resource(self, arn): del self.tags[arn] + def has_tags(self, arn): + return arn in self.tags + def tag_resource(self, arn, tags): if arn not in self.tags: self.tags[arn] = {} From ecdedf30c87fdd321d910374972ec1808bc1b7a1 Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 5 Feb 2020 16:31:33 -0500 Subject: [PATCH 6/6] force build... --- tests/test_events/test_events.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 4fb3b4029..4ecb2d882 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -484,22 +484,22 @@ def test_rule_tagging_happy(): @mock_events def test_rule_tagging_sad(): - b = EventsBackend("us-west-2") + back_end = EventsBackend("us-west-2") try: - b.tag_resource("unknown", []) + back_end.tag_resource("unknown", []) raise "tag_resource should fail if ResourceARN is not known" except JsonRESTError: pass try: - b.untag_resource("unknown", []) + back_end.untag_resource("unknown", []) raise "untag_resource should fail if ResourceARN is not known" except JsonRESTError: pass try: - b.list_tags_for_resource("unknown") + back_end.list_tags_for_resource("unknown") raise "list_tags_for_resource should fail if ResourceARN is not known" except JsonRESTError: pass