diff --git a/moto/route53/models.py b/moto/route53/models.py index a20867b54..c15862770 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from collections import defaultdict + import uuid from jinja2 import Template @@ -226,6 +228,7 @@ class Route53Backend(BaseBackend): def __init__(self): self.zones = {} self.health_checks = {} + self.resource_tags = defaultdict(dict) def create_hosted_zone(self, name, private_zone, comment=None): new_id = get_random_hex() @@ -233,6 +236,25 @@ class Route53Backend(BaseBackend): self.zones[new_id] = new_zone return new_zone + def change_tags_for_resource(self, resource_id, tags): + if 'Tag' in tags: + for key, tag in tags.items(): + for t in tag: + self.resource_tags[resource_id][t['Key']] = t['Value'] + + else: + for _, keys in tags.items(): + if isinstance(keys, list): + for key in keys: + del(self.resource_tags[resource_id][key]) + else: + del(self.resource_tags[resource_id][keys]) + + + def list_tags_for_resource(self, resource_id): + if resource_id in self.resource_tags: + return self.resource_tags[resource_id] + def get_all_hosted_zones(self): return self.zones.values() diff --git a/moto/route53/responses.py b/moto/route53/responses.py index db520df5a..0e171a52c 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -15,7 +15,7 @@ def list_or_create_hostzone_response(request, full_url, headers): # in boto3, this field is set directly in the xml private_zone = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["PrivateZone"] except KeyError: - # if a VPC subsection is only included in xmls params when private_zone=True, + # if a VPC subsection is only included in xmls params when private_zone=True, # see boto: boto/route53/connection.py private_zone = 'VPC' in elements["CreateHostedZoneRequest"] else: @@ -138,6 +138,52 @@ def not_implemented_response(request, full_url, headers): raise NotImplementedError("The action for {0} has not been implemented for route 53".format(action)) +def list_or_change_tags_for_resource_request(request, full_url, headers): + parsed_url = urlparse(full_url) + id_ = parsed_url.path.split("/")[-1] + type_ = parsed_url.path.split("/")[-2] + + if request.method == "GET": + tags = route53_backend.list_tags_for_resource(id_) + template = Template(LIST_TAGS_FOR_RESOURCE_RESPONSE) + return 200, headers, template.render( + resource_type=type_, resource_id=id_, tags=tags) + + if request.method == "POST": + tags = xmltodict.parse( + request.body)['ChangeTagsForResourceRequest'] + + if 'AddTags' in tags: + tags = tags['AddTags'] + elif 'RemoveTagKeys' in tags: + tags = tags['RemoveTagKeys'] + + route53_backend.change_tags_for_resource(id_, tags) + template = Template(CHANGE_TAGS_FOR_RESOURCE_RESPONSE) + + return 200, headers, template.render() + +LIST_TAGS_FOR_RESOURCE_RESPONSE = """ + + + {{resource_type}} + {{resource_id}} + + {% for key, value in tags.items() %} + + {{key}} + {{value}} + + {% endfor %} + + + +""" + +CHANGE_TAGS_FOR_RESOURCE_RESPONSE = """ + +""" + LIST_RRSET_REPONSE = """ {% for record_set in record_sets %} diff --git a/moto/route53/urls.py b/moto/route53/urls.py index b1bc346f5..361c96317 100644 --- a/moto/route53/urls.py +++ b/moto/route53/urls.py @@ -10,5 +10,6 @@ url_paths = { '{0}hostedzone/[^/]+$': responses.get_or_delete_hostzone_response, '{0}hostedzone/[^/]+/rrset/?$': responses.rrset_response, '{0}healthcheck': responses.health_check_response, - '{0}tags|trafficpolicyinstances/*': responses.not_implemented_response, + '{0}tags/(healthcheck|hostedzone)/*': responses.list_or_change_tags_for_resource_request, + '{0}trafficpolicyinstances/*': responses.not_implemented_response } diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index c6c36ce35..b3bed8272 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -7,6 +7,8 @@ from boto.route53.record import ResourceRecordSets import sure # noqa +import uuid + from moto import mock_route53 @@ -328,3 +330,66 @@ def test_hosted_zone_private_zone_preserved_boto3(): # zone = conn.list_hosted_zones_by_name(DNSName="testdns.aws.com.") # zone.config["PrivateZone"].should.equal(True) + +@mock_route53 +def test_list_or_change_tags_for_resource_request(): + conn = boto3.client('route53') + healthcheck_id = str(uuid.uuid4()) + + tag1 = {"Key": "Deploy", "Value": "True"} + tag2 = {"Key": "Name", "Value": "UnitTest"} + + # Test adding a tag for a resource id + conn.change_tags_for_resource( + ResourceType='healthcheck', + ResourceId=healthcheck_id, + AddTags=[tag1, tag2] + ) + + # Check to make sure that the response has the 'ResourceTagSet' key + response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response.should.contain('ResourceTagSet') + + # Validate that each key was added + response['ResourceTagSet']['Tags'].should.contain(tag1) + response['ResourceTagSet']['Tags'].should.contain(tag2) + + # Try to remove the tags + conn.change_tags_for_resource( + ResourceType='healthcheck', + ResourceId=healthcheck_id, + RemoveTagKeys=[tag1['Key']] + ) + + # Check to make sure that the response has the 'ResourceTagSet' key + response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response.should.contain('ResourceTagSet') + response['ResourceTagSet']['Tags'].should_not.contain(tag1) + response['ResourceTagSet']['Tags'].should.contain(tag2) + + # Remove the second tag + conn.change_tags_for_resource( + ResourceType='healthcheck', + ResourceId=healthcheck_id, + RemoveTagKeys=[tag2['Key']] + ) + + response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response['ResourceTagSet']['Tags'].should_not.contain(tag2) + + # Re-add the tags + conn.change_tags_for_resource( + ResourceType='healthcheck', + ResourceId=healthcheck_id, + AddTags=[tag1, tag2] + ) + + # Remove both + conn.change_tags_for_resource( + ResourceType='healthcheck', + ResourceId=healthcheck_id, + RemoveTagKeys=[tag1['Key'], tag2['Key']] + ) + + response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response['ResourceTagSet']['Tags'].should.be.empty