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