diff --git a/moto/elasticache/exceptions.py b/moto/elasticache/exceptions.py index 0d63efb88..703db9749 100644 --- a/moto/elasticache/exceptions.py +++ b/moto/elasticache/exceptions.py @@ -82,3 +82,14 @@ class CacheClusterNotFound(ElastiCacheException): "CacheClusterNotFound", message=f"Cache cluster {cache_cluster_id} not found.", ) + + +class InvalidARNFault(ElastiCacheException): + + code = 400 + + def __init__(self, arn: str): + super().__init__( + "InvalidARNFault", + message=f"ARN {arn} is invalid.", + ) diff --git a/moto/elasticache/models.py b/moto/elasticache/models.py index b2cd0790f..ae3bcc0c7 100644 --- a/moto/elasticache/models.py +++ b/moto/elasticache/models.py @@ -1,3 +1,4 @@ +from re import compile as re_compile from typing import Any, Dict, List, Optional, Tuple from moto.core.base_backend import BackendDict, BaseBackend @@ -8,6 +9,7 @@ from ..moto_api._internal import mock_random from .exceptions import ( CacheClusterAlreadyExists, CacheClusterNotFound, + InvalidARNFault, UserAlreadyExists, UserNotFound, ) @@ -78,7 +80,6 @@ class CacheCluster(BaseModel): ): if tags is None: tags = [] - self.cache_cluster_id = cache_cluster_id self.az_mode = az_mode self.preferred_availability_zone = preferred_availability_zone @@ -121,15 +122,23 @@ class CacheCluster(BaseModel): self.cache_cluster_create_time = utcnow() self.auth_token_last_modified_date = utcnow() self.cache_cluster_status = "available" - self.arn = f"arn:aws:elasticache:{region_name}:{account_id}:{cache_cluster_id}" + self.arn = ( + f"arn:aws:elasticache:{region_name}:{account_id}:cluster:{cache_cluster_id}" + ) self.cache_node_id = str(mock_random.uuid4()) + def get_tags(self) -> List[Dict[str, str]]: + return self.tags + class ElastiCacheBackend(BaseBackend): """Implementation of ElastiCache APIs.""" def __init__(self, region_name: str, account_id: str): super().__init__(region_name, account_id) + self.arn_regex = re_compile( + r"^arn:aws:elasticache:.*:[0-9]*:(cluster|snapshot):.*$" + ) self.users = dict() self.users["default"] = User( account_id=self.account_id, @@ -331,5 +340,19 @@ class ElastiCacheBackend(BaseBackend): return cache_cluster raise CacheClusterNotFound(cache_cluster_id) + def list_tags_for_resource(self, arn: str) -> List[Dict[str, str]]: + if self.arn_regex.match(arn): + arn_breakdown = arn.split(":") + resource_type = arn_breakdown[len(arn_breakdown) - 2] + resource_name = arn_breakdown[len(arn_breakdown) - 1] + if resource_type == "cluster": + if resource_name in self.cache_clusters: + return self.cache_clusters[resource_name].get_tags() + else: + return [] + else: + raise InvalidARNFault(arn) + return [] + elasticache_backends = BackendDict(ElastiCacheBackend, "elasticache") diff --git a/moto/elasticache/responses.py b/moto/elasticache/responses.py index 19fdbf515..71f7bdc8a 100644 --- a/moto/elasticache/responses.py +++ b/moto/elasticache/responses.py @@ -67,7 +67,7 @@ class ElastiCacheResponse(BaseResponse): cache_subnet_group_name = self._get_param("CacheSubnetGroupName") cache_security_group_names = self._get_param("CacheSecurityGroupNames") security_group_ids = self._get_param("SecurityGroupIds") - tags = self._get_param("Tags") + tags = (self._get_multi_param_dict("Tags") or {}).get("Tag", []) snapshot_arns = self._get_param("SnapshotArns") snapshot_name = self._get_param("SnapshotName") preferred_maintenance_window = self._get_param("PreferredMaintenanceWindow") @@ -144,6 +144,12 @@ class ElastiCacheResponse(BaseResponse): template = self.response_template(DELETE_CACHE_CLUSTER_TEMPLATE) return template.render(cache_cluster=cache_cluster) + def list_tags_for_resource(self) -> str: + arn = self._get_param("ResourceName") + template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE) + tags = self.elasticache_backend.list_tags_for_resource(arn) + return template.render(tags=tags) + USER_TEMPLATE = """{{ user.id }} {{ user.name }} @@ -545,3 +551,19 @@ DELETE_CACHE_CLUSTER_TEMPLATE = """ + + + {%- for tag in tags -%} + + {{ tag['Key'] }} + {{ tag['Value'] }} + + {%- endfor -%} + + + + 8c21ba39-a598-11e4-b688-194eaf8658fa + +""" diff --git a/tests/test_elasticache/test_elasticache.py b/tests/test_elasticache/test_elasticache.py index 5f77e7191..7d7962f5d 100644 --- a/tests/test_elasticache/test_elasticache.py +++ b/tests/test_elasticache/test_elasticache.py @@ -417,3 +417,26 @@ def test_delete_unknown_cache_cluster(): err = exc.value.response["Error"] assert err["Code"] == "CacheClusterNotFound" assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found." + + +@mock_aws +def test_list_tags_cache_cluster(): + conn = boto3.client("elasticache", region_name="ap-southeast-1") + result = conn.list_tags_for_resource( + ResourceName="arn:aws:elasticache:us-west-2:1234567890:cluster:foo" + ) + assert result["TagList"] == [] + test_instance = conn.create_cache_cluster( + CacheClusterId="test-cache-cluster", + Engine="memcached", + NumCacheNodes=2, + Tags=[{"Key": "foo", "Value": "bar"}, {"Key": "foo1", "Value": "bar1"}], + SecurityGroupIds=["sg-1234"], + ) + post_create_result = conn.list_tags_for_resource( + ResourceName=test_instance["CacheCluster"]["ARN"], + ) + assert post_create_result["TagList"] == [ + {"Value": "bar", "Key": "foo"}, + {"Value": "bar1", "Key": "foo1"}, + ]