From d78db35f41ebc435b842d07f7ca1759c6bd00afb Mon Sep 17 00:00:00 2001
From: Jean-Frederic Mainville <35815402+jfmainville@users.noreply.github.com>
Date: Sat, 2 Sep 2023 09:00:25 -0400
Subject: [PATCH] ElastiCache: Add the `create_cache_cluster`,
`delete_cache_cluster` and `describe_cache_clusters` methods support (#6754)
---
docs/docs/services/elasticache.rst | 10 +-
moto/__init__.py | 5 +-
moto/elasticache/exceptions.py | 23 +
moto/elasticache/models.py | 242 ++++++++++-
moto/elasticache/responses.py | 463 ++++++++++++++++++++-
tests/test_elasticache/test_elasticache.py | 214 +++++++++-
6 files changed, 928 insertions(+), 29 deletions(-)
diff --git a/docs/docs/services/elasticache.rst b/docs/docs/services/elasticache.rst
index 8ede5a9f94d0..2c5a64e3bd06 100644
--- a/docs/docs/services/elasticache.rst
+++ b/docs/docs/services/elasticache.rst
@@ -33,7 +33,7 @@ elasticache
- [ ] batch_stop_update_action
- [ ] complete_migration
- [ ] copy_snapshot
-- [ ] create_cache_cluster
+- [x] create_cache_cluster
- [ ] create_cache_parameter_group
- [ ] create_cache_security_group
- [ ] create_cache_subnet_group
@@ -44,7 +44,7 @@ elasticache
- [ ] create_user_group
- [ ] decrease_node_groups_in_global_replication_group
- [ ] decrease_replica_count
-- [ ] delete_cache_cluster
+- [x] delete_cache_cluster
- [ ] delete_cache_parameter_group
- [ ] delete_cache_security_group
- [ ] delete_cache_subnet_group
@@ -53,7 +53,7 @@ elasticache
- [ ] delete_snapshot
- [X] delete_user
- [ ] delete_user_group
-- [ ] describe_cache_clusters
+- [x] describe_cache_clusters
- [ ] describe_cache_engine_versions
- [ ] describe_cache_parameter_groups
- [ ] describe_cache_parameters
@@ -70,10 +70,10 @@ elasticache
- [ ] describe_update_actions
- [ ] describe_user_groups
- [X] describe_users
-
+
Only the `user_id` parameter is currently supported.
Pagination is not yet implemented.
-
+
- [ ] disassociate_global_replication_group
- [ ] failover_global_replication_group
diff --git a/moto/__init__.py b/moto/__init__.py
index cb1527a08901..29bae381b72e 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -1,9 +1,9 @@
import importlib
import sys
from contextlib import ContextDecorator
-from moto.core.models import BaseMockAWS
from typing import Any, Callable, List, Optional, TypeVar
+from moto.core.models import BaseMockAWS
TEST_METHOD = TypeVar("TEST_METHOD", bound=Callable[..., Any])
@@ -189,6 +189,9 @@ def f(*args: Any, **kwargs: Any) -> Any:
mock_xray_client = lazy_load(".xray", "mock_xray_client")
mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")
mock_textract = lazy_load(".textract", "mock_textract")
+mock_elasticache = lazy_load(
+ ".elasticache", "mock_elasticache", boto3_name="elasticache"
+)
class MockAll(ContextDecorator):
diff --git a/moto/elasticache/exceptions.py b/moto/elasticache/exceptions.py
index fe14cb05011f..f5e7f5b65413 100644
--- a/moto/elasticache/exceptions.py
+++ b/moto/elasticache/exceptions.py
@@ -1,4 +1,5 @@
from typing import Any
+
from moto.core.exceptions import RESTError
EXCEPTION_RESPONSE = """
@@ -58,3 +59,25 @@ class UserNotFound(ElastiCacheException):
def __init__(self, user_id: str):
super().__init__("UserNotFound", message=f"User {user_id} not found.")
+
+
+class CacheClusterAlreadyExists(ElastiCacheException):
+
+ code = 404
+
+ def __init__(self, cache_cluster_id: str):
+ super().__init__(
+ "CacheClusterAlreadyExists",
+ message=f"Cache cluster {cache_cluster_id} already exists.",
+ ),
+
+
+class CacheClusterNotFound(ElastiCacheException):
+
+ code = 404
+
+ def __init__(self, cache_cluster_id: str):
+ super().__init__(
+ "CacheClusterNotFound",
+ message=f"Cache cluster {cache_cluster_id} not found.",
+ )
diff --git a/moto/elasticache/models.py b/moto/elasticache/models.py
index 198d75dee8d7..25e37807c2b2 100644
--- a/moto/elasticache/models.py
+++ b/moto/elasticache/models.py
@@ -1,7 +1,15 @@
-from typing import List, Optional
+from datetime import datetime
+from typing import List, Optional, Dict, Any, Tuple
+
from moto.core import BaseBackend, BackendDict, BaseModel
-from .exceptions import UserAlreadyExists, UserNotFound
+from .exceptions import (
+ UserAlreadyExists,
+ UserNotFound,
+ CacheClusterAlreadyExists,
+ CacheClusterNotFound,
+)
+from ..moto_api._internal import mock_random
class User(BaseModel):
@@ -29,6 +37,93 @@ def __init__(
self.arn = f"arn:aws:elasticache:{self.region}:{account_id}:user:{self.id}"
+class CacheCluster(BaseModel):
+ def __init__(
+ self,
+ account_id: str,
+ region_name: str,
+ cache_cluster_id: str,
+ replication_group_id: Optional[str],
+ az_mode: Optional[str],
+ preferred_availability_zone: Optional[str],
+ num_cache_nodes: Optional[int],
+ cache_node_type: Optional[str],
+ engine: Optional[str],
+ engine_version: Optional[str],
+ cache_parameter_group_name: Optional[str],
+ cache_subnet_group_name: Optional[str],
+ transit_encryption_enabled: Optional[bool],
+ network_type: Optional[str],
+ ip_discovery: Optional[str],
+ snapshot_name: Optional[str],
+ preferred_maintenance_window: Optional[str],
+ port: Optional[int],
+ notification_topic_arn: Optional[str],
+ auto_minor_version_upgrade: Optional[bool],
+ snapshot_retention_limit: Optional[int],
+ snapshot_window: Optional[str],
+ auth_token: Optional[str],
+ outpost_mode: Optional[str],
+ preferred_outpost_arn: Optional[str],
+ preferred_availability_zones: Optional[List[str]],
+ cache_security_group_names: Optional[List[str]],
+ security_group_ids: Optional[List[str]],
+ tags: Optional[List[Dict[str, str]]],
+ snapshot_arns: Optional[List[str]],
+ preferred_outpost_arns: Optional[List[str]],
+ log_delivery_configurations: List[Dict[str, Any]],
+ cache_node_ids_to_remove: Optional[List[str]],
+ cache_node_ids_to_reboot: Optional[List[str]],
+ ):
+ if tags is None:
+ tags = []
+
+ self.cache_cluster_id = cache_cluster_id
+ self.az_mode = az_mode
+ self.preferred_availability_zone = preferred_availability_zone
+ self.preferred_availability_zones = preferred_availability_zones or []
+ self.engine = engine or "redis"
+ self.engine_version = engine_version
+ if engine == "redis":
+ self.num_cache_nodes = 1
+ self.replication_group_id = replication_group_id
+ self.snapshot_arns = snapshot_arns or []
+ self.snapshot_name = snapshot_name
+ self.snapshot_window = snapshot_window
+ if engine == "memcached":
+ if num_cache_nodes is None:
+ self.num_cache_nodes = 1
+ elif 1 <= num_cache_nodes <= 40:
+ self.num_cache_nodes = num_cache_nodes
+ self.cache_node_type = cache_node_type
+ self.cache_parameter_group_name = cache_parameter_group_name
+ self.cache_subnet_group_name = cache_subnet_group_name
+ self.cache_security_group_names = cache_security_group_names or []
+ self.security_group_ids = security_group_ids or []
+ self.tags = tags
+ self.preferred_maintenance_window = preferred_maintenance_window
+ self.port = port or 6379
+ self.notification_topic_arn = notification_topic_arn
+ self.auto_minor_version_upgrade = auto_minor_version_upgrade
+ self.snapshot_retention_limit = snapshot_retention_limit or 0
+ self.auth_token = auth_token
+ self.outpost_mode = outpost_mode
+ self.preferred_outpost_arn = preferred_outpost_arn
+ self.preferred_outpost_arns = preferred_outpost_arns or []
+ self.log_delivery_configurations = log_delivery_configurations or []
+ self.transit_encryption_enabled = transit_encryption_enabled
+ self.network_type = network_type
+ self.ip_discovery = ip_discovery
+ self.cache_node_ids_to_remove = cache_node_ids_to_remove
+ self.cache_node_ids_to_reboot = cache_node_ids_to_reboot
+
+ self.cache_cluster_create_time = datetime.utcnow()
+ self.auth_token_last_modified_date = datetime.utcnow()
+ self.cache_cluster_status = "available"
+ self.arn = f"arn:aws:elasticache:{region_name}:{account_id}:{cache_cluster_id}"
+ self.cache_node_id = str(mock_random.uuid4())
+
+
class ElastiCacheBackend(BaseBackend):
"""Implementation of ElastiCache APIs."""
@@ -45,6 +140,45 @@ def __init__(self, region_name: str, account_id: str):
no_password_required=True,
)
+ # Define the cache_clusters dictionary to detect duplicates
+ self.cache_clusters = dict()
+ self.cache_clusters["default"] = CacheCluster(
+ account_id=self.account_id,
+ region_name=self.region_name,
+ cache_cluster_id="default",
+ replication_group_id=None,
+ az_mode=None,
+ preferred_availability_zone=None,
+ num_cache_nodes=1,
+ cache_node_type=None,
+ engine="redis",
+ engine_version=None,
+ cache_parameter_group_name=None,
+ cache_subnet_group_name=None,
+ transit_encryption_enabled=True,
+ network_type=None,
+ ip_discovery=None,
+ snapshot_name=None,
+ preferred_maintenance_window=None,
+ port=6379,
+ notification_topic_arn=None,
+ auto_minor_version_upgrade=True,
+ snapshot_retention_limit=0,
+ snapshot_window=None,
+ auth_token=None,
+ outpost_mode=None,
+ preferred_outpost_arn=None,
+ preferred_availability_zones=[],
+ cache_security_group_names=[],
+ security_group_ids=[],
+ tags=[],
+ snapshot_arns=[],
+ preferred_outpost_arns=[],
+ log_delivery_configurations=[],
+ cache_node_ids_to_remove=[],
+ cache_node_ids_to_reboot=[],
+ )
+
def create_user(
self,
user_id: str,
@@ -92,5 +226,109 @@ def describe_users(self, user_id: Optional[str]) -> List[User]:
raise UserNotFound(user_id)
return list(self.users.values())
+ def create_cache_cluster(
+ self,
+ cache_cluster_id: str,
+ replication_group_id: str,
+ az_mode: str,
+ preferred_availability_zone: str,
+ num_cache_nodes: int,
+ cache_node_type: str,
+ engine: str,
+ engine_version: str,
+ cache_parameter_group_name: str,
+ cache_subnet_group_name: str,
+ transit_encryption_enabled: bool,
+ network_type: str,
+ ip_discovery: str,
+ snapshot_name: str,
+ preferred_maintenance_window: str,
+ port: int,
+ notification_topic_arn: str,
+ auto_minor_version_upgrade: bool,
+ snapshot_retention_limit: int,
+ snapshot_window: str,
+ auth_token: str,
+ outpost_mode: str,
+ preferred_outpost_arn: str,
+ preferred_availability_zones: List[str],
+ cache_security_group_names: List[str],
+ security_group_ids: List[str],
+ tags: List[Dict[str, str]],
+ snapshot_arns: List[str],
+ preferred_outpost_arns: List[str],
+ log_delivery_configurations: List[Dict[str, Any]],
+ cache_node_ids_to_remove: List[str],
+ cache_node_ids_to_reboot: List[str],
+ ) -> CacheCluster:
+ if cache_cluster_id in self.cache_clusters:
+ raise CacheClusterAlreadyExists(cache_cluster_id)
+ cache_cluster = CacheCluster(
+ account_id=self.account_id,
+ region_name=self.region_name,
+ cache_cluster_id=cache_cluster_id,
+ replication_group_id=replication_group_id,
+ az_mode=az_mode,
+ preferred_availability_zone=preferred_availability_zone,
+ preferred_availability_zones=preferred_availability_zones,
+ num_cache_nodes=num_cache_nodes,
+ cache_node_type=cache_node_type,
+ engine=engine,
+ engine_version=engine_version,
+ cache_parameter_group_name=cache_parameter_group_name,
+ cache_subnet_group_name=cache_subnet_group_name,
+ cache_security_group_names=cache_security_group_names,
+ security_group_ids=security_group_ids,
+ tags=tags,
+ snapshot_arns=snapshot_arns,
+ snapshot_name=snapshot_name,
+ preferred_maintenance_window=preferred_maintenance_window,
+ port=port,
+ notification_topic_arn=notification_topic_arn,
+ auto_minor_version_upgrade=auto_minor_version_upgrade,
+ snapshot_retention_limit=snapshot_retention_limit,
+ snapshot_window=snapshot_window,
+ auth_token=auth_token,
+ outpost_mode=outpost_mode,
+ preferred_outpost_arn=preferred_outpost_arn,
+ preferred_outpost_arns=preferred_outpost_arns,
+ log_delivery_configurations=log_delivery_configurations,
+ transit_encryption_enabled=transit_encryption_enabled,
+ network_type=network_type,
+ ip_discovery=ip_discovery,
+ cache_node_ids_to_remove=cache_node_ids_to_remove,
+ cache_node_ids_to_reboot=cache_node_ids_to_reboot,
+ )
+ self.cache_clusters[cache_cluster_id] = cache_cluster
+ return cache_cluster
+
+ def describe_cache_clusters(
+ self,
+ cache_cluster_id: str,
+ max_records: int,
+ marker: str,
+ ) -> Tuple[str, List[CacheCluster]]:
+ if marker is None:
+ marker = str(mock_random.uuid4())
+ if max_records is None:
+ max_records = 100
+ if cache_cluster_id:
+ if cache_cluster_id in self.cache_clusters:
+ cache_cluster = self.cache_clusters[cache_cluster_id]
+ return marker, [cache_cluster]
+ else:
+ raise CacheClusterNotFound(cache_cluster_id)
+ cache_clusters = list(self.cache_clusters.values())[:max_records]
+
+ return marker, cache_clusters
+
+ def delete_cache_cluster(self, cache_cluster_id: str) -> CacheCluster:
+ if cache_cluster_id:
+ if cache_cluster_id in self.cache_clusters:
+ cache_cluster = self.cache_clusters[cache_cluster_id]
+ cache_cluster.cache_cluster_status = "deleting"
+ return cache_cluster
+ raise CacheClusterNotFound(cache_cluster_id)
+
elasticache_backends = BackendDict(ElastiCacheBackend, "elasticache")
diff --git a/moto/elasticache/responses.py b/moto/elasticache/responses.py
index 273fa002d7b5..66a78a8e69bc 100644
--- a/moto/elasticache/responses.py
+++ b/moto/elasticache/responses.py
@@ -1,4 +1,5 @@
from moto.core.responses import BaseResponse
+
from .exceptions import PasswordTooShort, PasswordRequired
from .models import elasticache_backends, ElastiCacheBackend
@@ -52,6 +53,97 @@ def describe_users(self) -> str:
template = self.response_template(DESCRIBE_USERS_TEMPLATE)
return template.render(users=users)
+ def create_cache_cluster(self) -> str:
+ cache_cluster_id = self._get_param("CacheClusterId")
+ replication_group_id = self._get_param("ReplicationGroupId")
+ az_mode = self._get_param("AZMode")
+ preferred_availability_zone = self._get_param("PreferredAvailabilityZone")
+ preferred_availability_zones = self._get_param("PreferredAvailabilityZones")
+ num_cache_nodes = self._get_int_param("NumCacheNodes")
+ cache_node_type = self._get_param("CacheNodeType")
+ engine = self._get_param("Engine")
+ engine_version = self._get_param("EngineVersion")
+ cache_parameter_group_name = self._get_param("CacheParameterGroupName")
+ 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")
+ snapshot_arns = self._get_param("SnapshotArns")
+ snapshot_name = self._get_param("SnapshotName")
+ preferred_maintenance_window = self._get_param("PreferredMaintenanceWindow")
+ port = self._get_param("Port")
+ notification_topic_arn = self._get_param("NotificationTopicArn")
+ auto_minor_version_upgrade = self._get_bool_param("AutoMinorVersionUpgrade")
+ snapshot_retention_limit = self._get_int_param("SnapshotRetentionLimit")
+ snapshot_window = self._get_param("SnapshotWindow")
+ auth_token = self._get_param("AuthToken")
+ outpost_mode = self._get_param("OutpostMode")
+ preferred_outpost_arn = self._get_param("PreferredOutpostArn")
+ preferred_outpost_arns = self._get_param("PreferredOutpostArns")
+ log_delivery_configurations = self._get_param("LogDeliveryConfigurations")
+ transit_encryption_enabled = self._get_bool_param("TransitEncryptionEnabled")
+ network_type = self._get_param("NetworkType")
+ ip_discovery = self._get_param("IpDiscovery")
+ # Define the following attributes as they're included in the response even during creation of a cache cluster
+ cache_node_ids_to_remove = self._get_param("CacheNodeIdsToRemove", [])
+ cache_node_ids_to_reboot = self._get_param("CacheNodeIdsToReboot", [])
+ cache_cluster = self.elasticache_backend.create_cache_cluster(
+ cache_cluster_id=cache_cluster_id,
+ replication_group_id=replication_group_id,
+ az_mode=az_mode,
+ preferred_availability_zone=preferred_availability_zone,
+ preferred_availability_zones=preferred_availability_zones,
+ num_cache_nodes=num_cache_nodes,
+ cache_node_type=cache_node_type,
+ engine=engine,
+ engine_version=engine_version,
+ cache_parameter_group_name=cache_parameter_group_name,
+ cache_subnet_group_name=cache_subnet_group_name,
+ cache_security_group_names=cache_security_group_names,
+ security_group_ids=security_group_ids,
+ tags=tags,
+ snapshot_arns=snapshot_arns,
+ snapshot_name=snapshot_name,
+ preferred_maintenance_window=preferred_maintenance_window,
+ port=port,
+ notification_topic_arn=notification_topic_arn,
+ auto_minor_version_upgrade=auto_minor_version_upgrade,
+ snapshot_retention_limit=snapshot_retention_limit,
+ snapshot_window=snapshot_window,
+ auth_token=auth_token,
+ outpost_mode=outpost_mode,
+ preferred_outpost_arn=preferred_outpost_arn,
+ preferred_outpost_arns=preferred_outpost_arns,
+ log_delivery_configurations=log_delivery_configurations,
+ transit_encryption_enabled=transit_encryption_enabled,
+ network_type=network_type,
+ ip_discovery=ip_discovery,
+ cache_node_ids_to_remove=cache_node_ids_to_remove,
+ cache_node_ids_to_reboot=cache_node_ids_to_reboot,
+ )
+ template = self.response_template(CREATE_CACHE_CLUSTER_TEMPLATE)
+ return template.render(cache_cluster=cache_cluster)
+
+ def describe_cache_clusters(self) -> str:
+ cache_cluster_id = self._get_param("CacheClusterId")
+ max_records = self._get_int_param("MaxRecords")
+ marker = self._get_param("Marker")
+ marker, cache_clusters = self.elasticache_backend.describe_cache_clusters(
+ cache_cluster_id=cache_cluster_id,
+ marker=marker,
+ max_records=max_records,
+ )
+ template = self.response_template(DESCRIBE_CACHE_CLUSTERS_TEMPLATE)
+ return template.render(marker=marker, cache_clusters=cache_clusters)
+
+ def delete_cache_cluster(self) -> str:
+ cache_cluster_id = self._get_param("CacheClusterId")
+ cache_cluster = self.elasticache_backend.delete_cache_cluster(
+ cache_cluster_id=cache_cluster_id,
+ )
+ template = self.response_template(DELETE_CACHE_CLUSTER_TEMPLATE)
+ return template.render(cache_cluster=cache_cluster)
+
USER_TEMPLATE = """{{ user.id }}
{{ user.name }}
@@ -74,14 +166,13 @@ def describe_users(self) -> str:
{{ user.arn }}"""
-
CREATE_USER_TEMPLATE = (
"""
-
- 1549581b-12b7-11e3-895e-1334aEXAMPLE
-
-
- """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+ """
+ USER_TEMPLATE
+ """
@@ -90,11 +181,11 @@ def describe_users(self) -> str:
DELETE_USER_TEMPLATE = (
"""
-
- 1549581b-12b7-11e3-895e-1334aEXAMPLE
-
-
- """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+ """
+ USER_TEMPLATE
+ """
@@ -103,14 +194,14 @@ def describe_users(self) -> str:
DESCRIBE_USERS_TEMPLATE = (
"""
-
- 1549581b-12b7-11e3-895e-1334aEXAMPLE
-
-
-
-{% for user in users %}
-
- """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+
+ {% for user in users %}
+
+ """
+ USER_TEMPLATE
+ """
@@ -120,3 +211,337 @@ def describe_users(self) -> str:
"""
)
+
+CREATE_CACHE_CLUSTER_TEMPLATE = """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+
+ {{ cache_cluster.cache_cluster_id }}
+
+ example.cache.amazonaws.com
+ {{ cache_cluster.port }}
+
+
+ {{ cache_cluster.cache_node_type }}
+ {{ cache_cluster.engine }}
+ {{ cache_cluster.engine_version }}
+ available
+ {{ cache_cluster.num_cache_nodes }}
+ {{ cache_cluster.preferred_availability_zone }}
+ {{ cache_cluster.preferred_outpost_arn }}
+ {{ cache_cluster.cache_cluster_create_time }}
+ {{ cache_cluster.preferred_maintenance_window }}
+ {% if cache_cluster.cache_node_ids_to_remove != [] %}
+
+ {{ cache_cluster.num_cache_nodes }}
+ {% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %}
+ {{ cache_node_id_to_remove }}
+ {% endfor %}
+ {{ cache_cluster.engine_version }}
+ {{ cache_cluster.cache_node_type }}
+ SETTING
+
+ {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+ {{ log_delivery_configuration.LogType }}
+ {{ log_delivery_configuration.DestinationType }}
+
+
+ {{ log_delivery_configuration.LogGroup }}
+
+
+ {{ log_delivery_configuration.DeliveryStream }}
+
+
+ {{ log_delivery_configuration.LogFormat }}
+ {% endfor %}
+
+ {{ cache_cluster.transit_encryption_enabled }}
+ preferred
+
+ {% endif %}
+
+ {{ cache_cluster.notification_topic_arn }}
+ active
+
+
+ {% for cache_security_group_name in cache_cluster.cache_security_group_names %}
+ {{ cache_security_group_name }}
+ {% endfor %}
+ active
+
+
+ {{ cache_cluster.cache_parameter_group_name }}
+ active
+ {% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %}
+
+ {{ cache_node_id_to_reboot }}
+
+ {% endfor %}
+
+ {{ cache_cluster.cache_subnet_group_name }}
+
+ {{ cache_cluster.cache_node_id }}
+ {{ cache_cluster.cache_node_status }}
+ {{ cache_cluster.cache_cluster_create_time }}
+
+ {{ cache_cluster.address }}
+ {{ cache_cluster.port }}
+
+ active
+ {{ cache_cluster.cache_node_id }}
+ {{ cache_cluster.preferred_availability_zone }}
+ {{ cache_cluster.preferred_output_arn }}
+
+ {{ cache_cluster.auto_minor_version_upgrade }}
+
+ {% for security_group_id in cache_cluster.security_group_ids %}
+ {{ security_group_id }}
+ active
+ {% endfor %}
+
+ {% if cache_cluster.engine == "redis" %}
+ {{ cache_cluster.replication_group_id }}
+ {{ cache_cluster.snapshot_retention_limit }}
+ {{ cache_cluster.snapshot_window }}
+ {% endif %}
+ true
+ {{ cache_cluster.cache_cluster_create_time }}
+ {{ cache_cluster.transit_encryption_enabled }}
+ true
+ {{ cache_cluster.arn }}
+ true
+
+ {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+ {{ log_delivery_configuration.LogType }}
+ {{ log_delivery_configuration.DestinationType }}
+
+
+ {{ log_delivery_configuration.LogGroup }}
+
+
+ {{ log_delivery_configuration.DeliveryStream }}
+
+
+ {{ log_delivery_configuration.LogFormat }}
+ active
+
+ {% endfor %}
+
+ {{ cache_cluster.network_type }}
+ {{ cache_cluster.ip_discovery }}
+ preferred
+
+
+"""
+
+DESCRIBE_CACHE_CLUSTERS_TEMPLATE = """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+ {{ marker }}
+
+{% for cache_cluster in cache_clusters %}
+
+ {{ cache_cluster.cache_cluster_id }}
+ {{ cache_cluster.configuration_endpoint }}
+ {{ cache_cluster.client_download_landing_page }}
+ {{ cache_cluster.cache_node_type }}
+ {{ cache_cluster.engine }}
+ {{ cache_cluster.engine_version }}
+ {{ cache_cluster.cache_cluster_status }}
+ {{ cache_cluster.num_cache_nodes }}
+ {{ cache_cluster.preferred_availability_zone }}
+ {{ cache_cluster.preferred_outpost_arn }}
+ {{ cache_cluster.cache_cluster_create_time }}
+ {{ cache_cluster.preferred_maintenance_window }}
+ {{ cache_cluster.pending_modified_values }}
+ {{ cache_cluster.notification_configuration }}
+
+{% for cache_security_group in cache_cluster.cache_security_groups %}
+
+ {{ cache_security_group.cache_security_group_name }}
+ {{ cache_security_group.status }}
+
+{% endfor %}
+
+ {{ cache_cluster.cache_parameter_group }}
+ {{ cache_cluster.cache_subnet_group_name }}
+
+{% for cache_node in cache_cluster.cache_nodes %}
+
+ {{ cache_node.cache_node_id }}
+ {{ cache_node.cache_node_status }}
+ {{ cache_node.cache_node_create_time }}
+ {{ cache_node.endpoint }}
+ {{ cache_node.parameter_group_status }}
+ {{ cache_node.source_cache_node_id }}
+ {{ cache_node.customer_availability_zone }}
+ {{ cache_node.customer_outpost_arn }}
+
+{% endfor %}
+
+ {{ cache_cluster.auto_minor_version_upgrade }}
+
+{% for security_group in cache_cluster.security_groups %}
+
+ {{ security_group.security_group_id }}
+ {{ security_group.status }}
+
+{% endfor %}
+
+ {{ cache_cluster.replication_group_id }}
+ {{ cache_cluster.snapshot_retention_limit }}
+ {{ cache_cluster.snapshot_window }}
+ {{ cache_cluster.auth_token_enabled }}
+ {{ cache_cluster.auth_token_last_modified_date }}
+ {{ cache_cluster.transit_encryption_enabled }}
+ {{ cache_cluster.at_rest_encryption_enabled }}
+ {{ cache_cluster.arn }}
+ {{ cache_cluster.replication_group_log_delivery_enabled }}
+
+{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+
+ {{ log_delivery_configuration.log_type }}
+ {{ log_delivery_configuration.destination_type }}
+ {{ log_delivery_configuration.destination_details }}
+ {{ log_delivery_configuration.log_format }}
+ {{ log_delivery_configuration.status }}
+ {{ log_delivery_configuration.message }}
+
+{% endfor %}
+
+ {{ cache_cluster.network_type }}
+ {{ cache_cluster.ip_discovery }}
+ {{ cache_cluster.transit_encryption_mode }}
+
+{% endfor %}
+
+
+"""
+
+DELETE_CACHE_CLUSTER_TEMPLATE = """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+
+ {{ cache_cluster.cache_cluster_id }}
+
+ example.cache.amazonaws.com
+ {{ cache_cluster.port }}
+
+
+ {{ cache_cluster.cache_node_type }}
+ {{ cache_cluster.engine }}
+ {{ cache_cluster.engine_version }}
+ available
+ {{ cache_cluster.num_cache_nodes }}
+ {{ cache_cluster.preferred_availability_zone }}
+ {{ cache_cluster.preferred_outpost_arn }}
+ {{ cache_cluster.cache_cluster_create_time }}
+ {{ cache_cluster.preferred_maintenance_window }}
+ {% if cache_cluster.cache_node_ids_to_remove != [] %}
+
+ {{ cache_cluster.num_cache_nodes }}
+ {% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %}
+ {{ cache_node_id_to_remove }}
+ {% endfor %}
+ {{ cache_cluster.engine_version }}
+ {{ cache_cluster.cache_node_type }}
+ SETTING
+
+ {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+ {{ log_delivery_configuration.LogType }}
+ {{ log_delivery_configuration.DestinationType }}
+
+
+ {{ log_delivery_configuration.LogGroup }}
+
+
+ {{ log_delivery_configuration.DeliveryStream }}
+
+
+ {{ log_delivery_configuration.LogFormat }}
+ {% endfor %}
+
+ {{ cache_cluster.transit_encryption_enabled }}
+ preferred
+
+ {% endif %}
+
+ {{ cache_cluster.notification_topic_arn }}
+ active
+
+
+ {% for cache_security_group_name in cache_cluster.cache_security_group_names %}
+ {{ cache_security_group_name }}
+ {% endfor %}
+ active
+
+
+ {{ cache_cluster.cache_parameter_group_name }}
+ active
+ {% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %}
+
+ {{ cache_node_id_to_reboot }}
+
+ {% endfor %}
+
+ {{ cache_cluster.cache_subnet_group_name }}
+
+ {{ cache_cluster.cache_node_id }}
+ {{ cache_cluster.cache_node_status }}
+ {{ cache_cluster.cache_cluster_create_time }}
+
+ {{ cache_cluster.address }}
+ {{ cache_cluster.port }}
+
+ active
+ {{ cache_cluster.cache_node_id }}
+ {{ cache_cluster.preferred_availability_zone }}
+ {{ cache_cluster.preferred_output_arn }}
+
+ {{ cache_cluster.auto_minor_version_upgrade }}
+
+ {% for security_group_id in cache_cluster.security_group_ids %}
+ {{ security_group_id }}
+ active
+ {% endfor %}
+
+ {% if cache_cluster.engine == "redis" %}
+ {{ cache_cluster.replication_group_id }}
+ {{ cache_cluster.snapshot_retention_limit }}
+ {{ cache_cluster.snapshot_window }}
+ {% endif %}
+ true
+ {{ cache_cluster.cache_cluster_create_time }}
+ {{ cache_cluster.transit_encryption_enabled }}
+ true
+ {{ cache_cluster.arn }}
+ true
+
+ {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+ {{ log_delivery_configuration.LogType }}
+ {{ log_delivery_configuration.DestinationType }}
+
+
+ {{ log_delivery_configuration.LogGroup }}
+
+
+ {{ log_delivery_configuration.DeliveryStream }}
+
+
+ {{ log_delivery_configuration.LogFormat }}
+ active
+
+ {% endfor %}
+
+ {{ cache_cluster.network_type }}
+ {{ cache_cluster.ip_discovery }}
+ preferred
+
+
+"""
diff --git a/tests/test_elasticache/test_elasticache.py b/tests/test_elasticache/test_elasticache.py
index ecc18679abe8..e119fe1a4329 100644
--- a/tests/test_elasticache/test_elasticache.py
+++ b/tests/test_elasticache/test_elasticache.py
@@ -1,10 +1,11 @@
import boto3
import pytest
-
from botocore.exceptions import ClientError
-from moto import mock_elasticache
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto import mock_elasticache
+
+
# See our Development Tips on writing tests for hints on how to write good tests:
# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
@@ -208,3 +209,212 @@ def test_describe_users_unknown_userid():
err = exc.value.response["Error"]
assert err["Code"] == "UserNotFound"
assert err["Message"] == "User unknown not found."
+
+
+@mock_elasticache
+def test_create_redis_cache_cluster():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ test_redis_cache_cluster_exist = False
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_engine = "redis"
+ cache_cluster_num_cache_nodes = 5
+
+ resp = client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+ cache_cluster = resp["CacheCluster"]
+
+ if cache_cluster["CacheClusterId"] == cache_cluster_id:
+ if cache_cluster["Engine"] == cache_cluster_engine:
+ if cache_cluster["NumCacheNodes"] == 1:
+ test_redis_cache_cluster_exist = True
+
+ assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+ assert test_redis_cache_cluster_exist
+
+
+@mock_elasticache
+def test_create_memcached_cache_cluster():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ test_memcached_cache_cluster_exist = False
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ resp = client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+ cache_cluster = resp["CacheCluster"]
+
+ if cache_cluster["CacheClusterId"] == cache_cluster_id:
+ if cache_cluster["Engine"] == cache_cluster_engine:
+ if cache_cluster["NumCacheNodes"] == 5:
+ test_memcached_cache_cluster_exist = True
+
+ assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+ assert test_memcached_cache_cluster_exist
+
+
+@mock_elasticache
+def test_create_duplicate_cache_cluster():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ with pytest.raises(ClientError) as exc:
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+ err = exc.value.response["Error"]
+ assert err["Code"] == "CacheClusterAlreadyExists"
+ assert err["Message"] == f"Cache cluster {cache_cluster_id} already exists."
+
+
+@mock_elasticache
+def test_describe_all_cache_clusters():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ test_memcached_cache_cluster_exist = False
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ resp = client.describe_cache_clusters()
+
+ cache_clusters = resp["CacheClusters"]
+
+ for cache_cluster in cache_clusters:
+ if cache_cluster["CacheClusterId"] == cache_cluster_id:
+ test_memcached_cache_cluster_exist = True
+
+ assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+ assert test_memcached_cache_cluster_exist
+
+
+@mock_elasticache
+def test_describe_specific_cache_clusters():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ test_memcached_cache_cluster_exist = False
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ client.create_cache_cluster(
+ CacheClusterId="test-cache-cluster-2",
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ resp = client.describe_cache_clusters(CacheClusterId=cache_cluster_id)
+
+ cache_clusters = resp["CacheClusters"]
+
+ for cache_cluster in cache_clusters:
+ if cache_cluster["CacheClusterId"] == cache_cluster_id:
+ test_memcached_cache_cluster_exist = True
+
+ assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+ assert test_memcached_cache_cluster_exist
+
+
+@mock_elasticache
+def test_describe_unknown_cache_cluster():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_id_unknown = "unknown-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ with pytest.raises(ClientError) as exc:
+ client.describe_cache_clusters(
+ CacheClusterId=cache_cluster_id_unknown,
+ )
+ err = exc.value.response["Error"]
+ assert err["Code"] == "CacheClusterNotFound"
+ assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found."
+
+
+@mock_elasticache
+def test_delete_cache_cluster():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ client.delete_cache_cluster(CacheClusterId=cache_cluster_id)
+
+ resp = client.describe_cache_clusters(CacheClusterId=cache_cluster_id)
+
+ assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+ assert resp["CacheClusters"][0]["CacheClusterStatus"] == "deleting"
+
+
+@mock_elasticache
+def test_delete_unknown_cache_cluster():
+ client = boto3.client("elasticache", region_name="us-east-2")
+
+ cache_cluster_id = "test-cache-cluster"
+ cache_cluster_id_unknown = "unknown-cache-cluster"
+ cache_cluster_engine = "memcached"
+ cache_cluster_num_cache_nodes = 5
+
+ client.create_cache_cluster(
+ CacheClusterId=cache_cluster_id,
+ Engine=cache_cluster_engine,
+ NumCacheNodes=cache_cluster_num_cache_nodes,
+ )
+
+ with pytest.raises(ClientError) as exc:
+ client.delete_cache_cluster(
+ CacheClusterId=cache_cluster_id_unknown,
+ )
+ err = exc.value.response["Error"]
+ assert err["Code"] == "CacheClusterNotFound"
+ assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found."