diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index bb75c88f6..6fabaae1b 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -2191,6 +2191,52 @@
- [ ] untag_resource
+## es
+
+10% implemented
+
+- [ ] accept_inbound_cross_cluster_search_connection
+- [ ] add_tags
+- [ ] associate_package
+- [ ] cancel_elasticsearch_service_software_update
+- [X] create_elasticsearch_domain
+- [ ] create_outbound_cross_cluster_search_connection
+- [ ] create_package
+- [X] delete_elasticsearch_domain
+- [ ] delete_elasticsearch_service_role
+- [ ] delete_inbound_cross_cluster_search_connection
+- [ ] delete_outbound_cross_cluster_search_connection
+- [ ] delete_package
+- [ ] describe_domain_auto_tunes
+- [X] describe_elasticsearch_domain
+- [ ] describe_elasticsearch_domain_config
+- [ ] describe_elasticsearch_domains
+- [ ] describe_elasticsearch_instance_type_limits
+- [ ] describe_inbound_cross_cluster_search_connections
+- [ ] describe_outbound_cross_cluster_search_connections
+- [ ] describe_packages
+- [ ] describe_reserved_elasticsearch_instance_offerings
+- [ ] describe_reserved_elasticsearch_instances
+- [ ] dissociate_package
+- [ ] get_compatible_elasticsearch_versions
+- [ ] get_package_version_history
+- [ ] get_upgrade_history
+- [ ] get_upgrade_status
+- [X] list_domain_names
+- [ ] list_domains_for_package
+- [ ] list_elasticsearch_instance_types
+- [ ] list_elasticsearch_versions
+- [ ] list_packages_for_domain
+- [ ] list_tags
+- [ ] purchase_reserved_elasticsearch_instance_offering
+- [ ] reject_inbound_cross_cluster_search_connection
+- [ ] remove_tags
+- [ ] start_elasticsearch_service_software_update
+- [ ] update_elasticsearch_domain_config
+- [ ] update_package
+- [ ] upgrade_elasticsearch_domain
+
+
## events
78% implemented
@@ -5054,7 +5100,6 @@
- ebs
- ecr-public
- elastic-inference
-- es
- evidently
- finspace
- finspace-data
diff --git a/docs/docs/services/es.rst b/docs/docs/services/es.rst
new file mode 100644
index 000000000..f7b5b19b1
--- /dev/null
+++ b/docs/docs/services/es.rst
@@ -0,0 +1,75 @@
+.. _implementedservice_es:
+
+.. |start-h3| raw:: html
+
+
+
+.. |end-h3| raw:: html
+
+
+
+==
+es
+==
+
+.. autoclass:: moto.es.models.ElasticsearchServiceBackend
+
+|start-h3| Example usage |end-h3|
+
+.. sourcecode:: python
+
+ @mock_es
+ def test_es_behaviour:
+ boto3.client("es")
+ ...
+
+
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] accept_inbound_cross_cluster_search_connection
+- [ ] add_tags
+- [ ] associate_package
+- [ ] cancel_elasticsearch_service_software_update
+- [X] create_elasticsearch_domain
+- [ ] create_outbound_cross_cluster_search_connection
+- [ ] create_package
+- [X] delete_elasticsearch_domain
+- [ ] delete_elasticsearch_service_role
+- [ ] delete_inbound_cross_cluster_search_connection
+- [ ] delete_outbound_cross_cluster_search_connection
+- [ ] delete_package
+- [ ] describe_domain_auto_tunes
+- [X] describe_elasticsearch_domain
+- [ ] describe_elasticsearch_domain_config
+- [ ] describe_elasticsearch_domains
+- [ ] describe_elasticsearch_instance_type_limits
+- [ ] describe_inbound_cross_cluster_search_connections
+- [ ] describe_outbound_cross_cluster_search_connections
+- [ ] describe_packages
+- [ ] describe_reserved_elasticsearch_instance_offerings
+- [ ] describe_reserved_elasticsearch_instances
+- [ ] dissociate_package
+- [ ] get_compatible_elasticsearch_versions
+- [ ] get_package_version_history
+- [ ] get_upgrade_history
+- [ ] get_upgrade_status
+- [X] list_domain_names
+
+ The engine-type parameter is not yet supported.
+ Pagination is not yet implemented.
+
+
+- [ ] list_domains_for_package
+- [ ] list_elasticsearch_instance_types
+- [ ] list_elasticsearch_versions
+- [ ] list_packages_for_domain
+- [ ] list_tags
+- [ ] purchase_reserved_elasticsearch_instance_offering
+- [ ] reject_inbound_cross_cluster_search_connection
+- [ ] remove_tags
+- [ ] start_elasticsearch_service_software_update
+- [ ] update_elasticsearch_domain_config
+- [ ] update_package
+- [ ] upgrade_elasticsearch_domain
+
diff --git a/moto/__init__.py b/moto/__init__.py
index f91e4f631..d3020477b 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -89,6 +89,7 @@ mock_emr_deprecated = lazy_load(".emr", "mock_emr_deprecated")
mock_emrcontainers = lazy_load(
".emrcontainers", "mock_emrcontainers", boto3_name="emr-containers"
)
+mock_es = lazy_load(".es", "mock_es")
mock_events = lazy_load(".events", "mock_events")
mock_firehose = lazy_load(".firehose", "mock_firehose")
mock_forecast = lazy_load(".forecast", "mock_forecast")
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 3fed47bfb..71b294174 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -61,6 +61,7 @@ backend_url_patterns = [
("emr", re.compile("https?://(.+)\\.elasticmapreduce\\.amazonaws.com")),
("emr", re.compile("https?://elasticmapreduce\\.(.+)\\.amazonaws.com")),
("emr-containers", re.compile("https?://emr-containers\\.(.+)\\.amazonaws\\.com")),
+ ("es", re.compile("https?://es\\.(.+)\\.amazonaws\\.com")),
("events", re.compile("https?://events\\.(.+)\\.amazonaws\\.com")),
("firehose", re.compile("https?://firehose\\.(.+)\\.amazonaws\\.com")),
("forecast", re.compile("https?://forecast\\.(.+)\\.amazonaws\\.com")),
diff --git a/moto/es/__init__.py b/moto/es/__init__.py
new file mode 100644
index 000000000..c015d07da
--- /dev/null
+++ b/moto/es/__init__.py
@@ -0,0 +1,5 @@
+"""es module initialization; sets value for base decorator."""
+from .models import es_backends
+from ..core.models import base_decorator
+
+mock_es = base_decorator(es_backends)
diff --git a/moto/es/exceptions.py b/moto/es/exceptions.py
new file mode 100644
index 000000000..31fb77c9e
--- /dev/null
+++ b/moto/es/exceptions.py
@@ -0,0 +1,25 @@
+"""Exceptions raised by the ElasticSearch service."""
+from moto.core.exceptions import JsonRESTError
+
+
+class ElasticSearchError(JsonRESTError):
+ code = 400
+
+
+class ResourceNotFound(ElasticSearchError):
+ code = 409
+
+ def __init__(self, resource_type, resource_name):
+ msg = f"{resource_type} not found: {resource_name}"
+ super().__init__("ResourceNotFoundException", msg)
+
+
+class InvalidDomainName(ElasticSearchError):
+ def __init__(self, domain_name):
+ msg = f"1 validation error detected: Value '{domain_name}' at 'domainName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-z][a-z0-9\\-]+"
+ super().__init__("ValidationException", msg)
+
+
+class DomainNotFound(ResourceNotFound):
+ def __init__(self, domain_name):
+ super().__init__("Domain", domain_name)
diff --git a/moto/es/models.py b/moto/es/models.py
new file mode 100644
index 000000000..8a8ed2b4e
--- /dev/null
+++ b/moto/es/models.py
@@ -0,0 +1,160 @@
+from boto3 import Session
+
+from moto.core import BaseBackend, BaseModel
+from moto.core.utils import get_random_hex
+from .exceptions import DomainNotFound
+
+
+class Domain(BaseModel):
+ def __init__(
+ self,
+ region_name,
+ domain_name,
+ es_version,
+ elasticsearch_cluster_config,
+ ebs_options,
+ access_policies,
+ snapshot_options,
+ vpc_options,
+ cognito_options,
+ encryption_at_rest_options,
+ node_to_node_encryption_options,
+ advanced_options,
+ log_publishing_options,
+ domain_endpoint_options,
+ advanced_security_options,
+ auto_tune_options,
+ tag_list,
+ ):
+ self.domain_id = get_random_hex(8)
+ self.region_name = region_name
+ self.domain_name = domain_name
+ self.es_version = es_version
+ self.elasticsearch_cluster_config = elasticsearch_cluster_config
+ self.ebs_options = ebs_options
+ self.access_policies = access_policies
+ self.snapshot_options = snapshot_options
+ self.vpc_options = vpc_options
+ self.cognito_options = cognito_options
+ self.encryption_at_rest_options = encryption_at_rest_options
+ self.node_to_node_encryption_options = node_to_node_encryption_options
+ self.advanced_options = advanced_options
+ self.log_publishing_options = log_publishing_options
+ self.domain_endpoint_options = domain_endpoint_options
+ self.advanced_security_options = advanced_security_options
+ self.auto_tune_options = auto_tune_options
+ if self.auto_tune_options:
+ self.auto_tune_options["State"] = "ENABLED"
+
+ @property
+ def arn(self):
+ return f"arn:aws:es:{self.region_name}:domain/{self.domain_id}"
+
+ def to_json(self):
+ return {
+ "DomainId": self.domain_id,
+ "DomainName": self.domain_name,
+ "ARN": self.arn,
+ "Created": True,
+ "Deleted": False,
+ "Processing": False,
+ "UpgradeProcessing": False,
+ "ElasticsearchVersion": self.es_version,
+ "ElasticsearchClusterConfig": self.elasticsearch_cluster_config,
+ "EBSOptions": self.ebs_options,
+ "AccessPolicies": self.access_policies,
+ "SnapshotOptions": self.snapshot_options,
+ "VPCOptions": self.vpc_options,
+ "CognitoOptions": self.cognito_options,
+ "EncryptionAtRestOptions": self.encryption_at_rest_options,
+ "NodeToNodeEncryptionOptions": self.node_to_node_encryption_options,
+ "AdvancedOptions": self.advanced_options,
+ "LogPublishingOptions": self.log_publishing_options,
+ "DomainEndpointOptions": self.domain_endpoint_options,
+ "AdvancedSecurityOptions": self.advanced_security_options,
+ "AutoTuneOptions": self.auto_tune_options,
+ }
+
+
+class ElasticsearchServiceBackend(BaseBackend):
+ """Implementation of ElasticsearchService APIs."""
+
+ def __init__(self, region_name=None):
+ self.region_name = region_name
+ self.domains = dict()
+
+ def reset(self):
+ """Re-initialize all attributes for this instance."""
+ region_name = self.region_name
+ self.__dict__ = {}
+ self.__init__(region_name)
+
+ def create_elasticsearch_domain(
+ self,
+ domain_name,
+ elasticsearch_version,
+ elasticsearch_cluster_config,
+ ebs_options,
+ access_policies,
+ snapshot_options,
+ vpc_options,
+ cognito_options,
+ encryption_at_rest_options,
+ node_to_node_encryption_options,
+ advanced_options,
+ log_publishing_options,
+ domain_endpoint_options,
+ advanced_security_options,
+ auto_tune_options,
+ tag_list,
+ ):
+ # TODO: Persist/Return other attributes
+ new_domain = Domain(
+ region_name=self.region_name,
+ domain_name=domain_name,
+ es_version=elasticsearch_version,
+ elasticsearch_cluster_config=elasticsearch_cluster_config,
+ ebs_options=ebs_options,
+ access_policies=access_policies,
+ snapshot_options=snapshot_options,
+ vpc_options=vpc_options,
+ cognito_options=cognito_options,
+ encryption_at_rest_options=encryption_at_rest_options,
+ node_to_node_encryption_options=node_to_node_encryption_options,
+ advanced_options=advanced_options,
+ log_publishing_options=log_publishing_options,
+ domain_endpoint_options=domain_endpoint_options,
+ advanced_security_options=advanced_security_options,
+ auto_tune_options=auto_tune_options,
+ tag_list=tag_list,
+ )
+ self.domains[domain_name] = new_domain
+ return new_domain.to_json()
+
+ def delete_elasticsearch_domain(self, domain_name):
+ if domain_name not in self.domains:
+ raise DomainNotFound(domain_name)
+ del self.domains[domain_name]
+
+ def describe_elasticsearch_domain(self, domain_name):
+ if domain_name not in self.domains:
+ raise DomainNotFound(domain_name)
+ return self.domains[domain_name].to_json()
+
+ def list_domain_names(self, engine_type):
+ """
+ The engine-type parameter is not yet supported.
+ Pagination is not yet implemented.
+ """
+ return [{"DomainName": domain.domain_name} for domain in self.domains.values()]
+
+
+es_backends = {}
+for available_region in Session().get_available_regions("es"):
+ es_backends[available_region] = ElasticsearchServiceBackend(available_region)
+for available_region in Session().get_available_regions(
+ "es", partition_name="aws-us-gov"
+):
+ es_backends[available_region] = ElasticsearchServiceBackend(available_region)
+for available_region in Session().get_available_regions("es", partition_name="aws-cn"):
+ es_backends[available_region] = ElasticsearchServiceBackend(available_region)
diff --git a/moto/es/responses.py b/moto/es/responses.py
new file mode 100644
index 000000000..e93427c33
--- /dev/null
+++ b/moto/es/responses.py
@@ -0,0 +1,113 @@
+import json
+import re
+
+from functools import wraps
+from moto.core.responses import BaseResponse
+from .exceptions import ElasticSearchError, InvalidDomainName
+from .models import es_backends
+
+
+def error_handler(f):
+ @wraps(f)
+ def _wrapper(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except ElasticSearchError as e:
+ return e.code, e.get_headers(), e.get_body()
+
+ return _wrapper
+
+
+class ElasticsearchServiceResponse(BaseResponse):
+ """Handler for ElasticsearchService requests and responses."""
+
+ @property
+ def es_backend(self):
+ """Return backend instance specific for this region."""
+ return es_backends[self.region]
+
+ @classmethod
+ @error_handler
+ def list_domains(cls, request, full_url, headers):
+ response = ElasticsearchServiceResponse()
+ response.setup_class(request, full_url, headers)
+ if request.method == "GET":
+ return response.list_domain_names()
+
+ @classmethod
+ @error_handler
+ def domains(cls, request, full_url, headers):
+ response = ElasticsearchServiceResponse()
+ response.setup_class(request, full_url, headers)
+ if request.method == "POST":
+ return response.create_elasticsearch_domain()
+
+ @classmethod
+ @error_handler
+ def domain(cls, request, full_url, headers):
+ response = ElasticsearchServiceResponse()
+ response.setup_class(request, full_url, headers)
+ if request.method == "DELETE":
+ return response.delete_elasticsearch_domain()
+ if request.method == "GET":
+ return response.describe_elasticsearch_domain()
+
+ def create_elasticsearch_domain(self):
+ params = json.loads(self.body)
+ domain_name = params.get("DomainName")
+ if not re.match(r"^[a-z][a-z0-9\-]+$", domain_name):
+ raise InvalidDomainName(domain_name)
+ elasticsearch_version = params.get("ElasticsearchVersion")
+ elasticsearch_cluster_config = params.get("ElasticsearchClusterConfig")
+ ebs_options = params.get("EBSOptions")
+ access_policies = params.get("AccessPolicies")
+ snapshot_options = params.get("SnapshotOptions")
+ vpc_options = params.get("VPCOptions")
+ cognito_options = params.get("CognitoOptions")
+ encryption_at_rest_options = params.get("EncryptionAtRestOptions")
+ node_to_node_encryption_options = params.get("NodeToNodeEncryptionOptions")
+ advanced_options = params.get("AdvancedOptions")
+ log_publishing_options = params.get("LogPublishingOptions")
+ domain_endpoint_options = params.get("DomainEndpointOptions")
+ advanced_security_options = params.get("AdvancedSecurityOptions")
+ auto_tune_options = params.get("AutoTuneOptions")
+ tag_list = params.get("TagList")
+ domain_status = self.es_backend.create_elasticsearch_domain(
+ domain_name=domain_name,
+ elasticsearch_version=elasticsearch_version,
+ elasticsearch_cluster_config=elasticsearch_cluster_config,
+ ebs_options=ebs_options,
+ access_policies=access_policies,
+ snapshot_options=snapshot_options,
+ vpc_options=vpc_options,
+ cognito_options=cognito_options,
+ encryption_at_rest_options=encryption_at_rest_options,
+ node_to_node_encryption_options=node_to_node_encryption_options,
+ advanced_options=advanced_options,
+ log_publishing_options=log_publishing_options,
+ domain_endpoint_options=domain_endpoint_options,
+ advanced_security_options=advanced_security_options,
+ auto_tune_options=auto_tune_options,
+ tag_list=tag_list,
+ )
+ return 200, {}, json.dumps({"DomainStatus": domain_status})
+
+ def delete_elasticsearch_domain(self):
+ domain_name = self.path.split("/")[-1]
+ self.es_backend.delete_elasticsearch_domain(domain_name=domain_name,)
+ return 200, {}, json.dumps(dict())
+
+ def describe_elasticsearch_domain(self):
+ domain_name = self.path.split("/")[-1]
+ if not re.match(r"^[a-z][a-z0-9\-]+$", domain_name):
+ raise InvalidDomainName(domain_name)
+ domain_status = self.es_backend.describe_elasticsearch_domain(
+ domain_name=domain_name,
+ )
+ return 200, {}, json.dumps({"DomainStatus": domain_status})
+
+ def list_domain_names(self):
+ params = self._get_params()
+ engine_type = params.get("EngineType")
+ domain_names = self.es_backend.list_domain_names(engine_type=engine_type,)
+ return 200, {}, json.dumps({"DomainNames": domain_names})
diff --git a/moto/es/urls.py b/moto/es/urls.py
new file mode 100644
index 000000000..4c4675f29
--- /dev/null
+++ b/moto/es/urls.py
@@ -0,0 +1,12 @@
+from .responses import ElasticsearchServiceResponse
+
+url_bases = [
+ r"https?://es\.(.+)\.amazonaws\.com",
+]
+
+
+url_paths = {
+ "{0}/2015-01-01/domain$": ElasticsearchServiceResponse.list_domains,
+ "{0}/2015-01-01/es/domain$": ElasticsearchServiceResponse.domains,
+ "{0}/2015-01-01/es/domain/(?P[^/]+)": ElasticsearchServiceResponse.domain,
+}
diff --git a/tests/test_es/__init__.py b/tests/test_es/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_es/test_es.py b/tests/test_es/test_es.py
new file mode 100644
index 000000000..88255a89e
--- /dev/null
+++ b/tests/test_es/test_es.py
@@ -0,0 +1,236 @@
+"""Unit tests for es-supported APIs."""
+import boto3
+import pytest
+import sure # noqa # pylint: disable=unused-import
+from botocore.exceptions import ClientError
+from moto import mock_es
+
+# 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
+
+
+@pytest.mark.parametrize(
+ "name", ["getmoto.org", "search-is-$$$", "dev_or_test", "dev/test", "1love", "DEV"]
+)
+@mock_es
+def test_create_domain_invalid_name(name):
+ client = boto3.client("es", region_name="us-east-2")
+ with pytest.raises(ClientError) as exc:
+ client.create_elasticsearch_domain(DomainName=name)
+ err = exc.value.response["Error"]
+ err["Message"].should.equal(
+ f"1 validation error detected: Value '{name}' at 'domainName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-z][a-z0-9\\-]+"
+ )
+ err["Code"].should.equal("ValidationException")
+
+
+@mock_es
+def test_create_elasticsearch_domain_minimal():
+ client = boto3.client("es", region_name="us-east-2")
+ resp = client.create_elasticsearch_domain(DomainName="motosearch")
+
+ resp.should.have.key("DomainStatus")
+ domain = resp["DomainStatus"]
+ domain.should.have.key("DomainName").equals("motosearch")
+ domain.should.have.key("DomainId")
+ domain.should.have.key("ARN").equals(
+ f"arn:aws:es:us-east-2:domain/{domain['DomainId']}"
+ )
+ domain.should.have.key("Created").equals(True)
+ domain.should.have.key("Deleted").equals(False)
+ domain.should.have.key("Processing").equals(False)
+ domain.should.have.key("UpgradeProcessing").equals(False)
+ domain.shouldnt.have.key("ElasticsearchVersion")
+
+
+@mock_es
+def test_create_elasticsearch_domain():
+ client = boto3.client("es", region_name="us-east-2")
+ resp = client.create_elasticsearch_domain(
+ DomainName="motosearch",
+ ElasticsearchVersion="7.10",
+ ElasticsearchClusterConfig={
+ "InstanceType": "m3.large.elasticsearch",
+ "InstanceCount": 1,
+ "DedicatedMasterEnabled": True,
+ "DedicatedMasterType": "m3.large.elasticsearch",
+ "DedicatedMasterCount": 1,
+ "ZoneAwarenessEnabled": False,
+ "WarmEnabled": False,
+ "ColdStorageOptions": {"Enabled": False},
+ },
+ EBSOptions={
+ "EBSEnabled": True,
+ "VolumeType": "io2",
+ "VolumeSize": 10,
+ "Iops": 1,
+ },
+ AccessPolicies="some unvalidated accesspolicy",
+ SnapshotOptions={"AutomatedSnapshotStartHour": 1,},
+ VPCOptions={"SubnetIds": ["s1"], "SecurityGroupIds": ["sg1"]},
+ CognitoOptions={"Enabled": False},
+ EncryptionAtRestOptions={"Enabled": False},
+ NodeToNodeEncryptionOptions={"Enabled": False},
+ AdvancedOptions={"option": "value"},
+ LogPublishingOptions={"log1": {"Enabled": False}},
+ DomainEndpointOptions={"EnforceHTTPS": True, "CustomEndpointEnabled": False,},
+ AdvancedSecurityOptions={"Enabled": False},
+ AutoTuneOptions={"DesiredState": "ENABLED"},
+ )
+
+ domain = resp["DomainStatus"]
+ domain.should.have.key("DomainId")
+ domain.should.have.key("Created").equals(True)
+ domain.should.have.key("ElasticsearchVersion").equals("7.10")
+
+ domain.should.have.key("ElasticsearchClusterConfig")
+ cluster_config = domain["ElasticsearchClusterConfig"]
+ cluster_config.should.have.key("ColdStorageOptions").equals({"Enabled": False})
+ cluster_config.should.have.key("DedicatedMasterCount").equals(1)
+ cluster_config.should.have.key("DedicatedMasterType").equals(
+ "m3.large.elasticsearch"
+ )
+ cluster_config.should.have.key("WarmEnabled").equals(False)
+
+ domain.should.have.key("EBSOptions")
+ ebs = domain["EBSOptions"]
+ ebs.should.have.key("EBSEnabled").equals(True)
+ ebs.should.have.key("Iops").equals(1)
+ ebs.should.have.key("VolumeSize").equals(10)
+ ebs.should.have.key("VolumeType").equals("io2")
+
+ domain.should.have.key("AccessPolicies").equals("some unvalidated accesspolicy")
+
+ domain.should.have.key("SnapshotOptions")
+ snapshots = domain["SnapshotOptions"]
+ snapshots.should.have.key("AutomatedSnapshotStartHour").equals(1)
+
+ domain.should.have.key("VPCOptions")
+ vpcs = domain["VPCOptions"]
+ vpcs.should.have.key("SubnetIds").equals(["s1"])
+ vpcs.should.have.key("SecurityGroupIds").equals(["sg1"])
+
+ domain.should.have.key("CognitoOptions")
+ cognito = domain["CognitoOptions"]
+ cognito.should.have.key("Enabled").equals(False)
+
+ domain.should.have.key("EncryptionAtRestOptions")
+ encryption_at_rest = domain["EncryptionAtRestOptions"]
+ encryption_at_rest.should.have.key("Enabled").equals(False)
+
+ domain.should.have.key("NodeToNodeEncryptionOptions")
+ encryption = domain["NodeToNodeEncryptionOptions"]
+ encryption.should.have.key("Enabled").equals(False)
+
+ domain.should.have.key("AdvancedOptions")
+ advanced = domain["AdvancedOptions"]
+ advanced.should.have.key("option").equals("value")
+
+ domain.should.have.key("LogPublishingOptions")
+ advanced = domain["LogPublishingOptions"]
+ advanced.should.have.key("log1").equals({"Enabled": False})
+
+ domain.should.have.key("DomainEndpointOptions")
+ endpoint = domain["DomainEndpointOptions"]
+ endpoint.should.have.key("EnforceHTTPS").equals(True)
+ endpoint.should.have.key("CustomEndpointEnabled").equals(False)
+
+ domain.should.have.key("AdvancedSecurityOptions")
+ advanced_security = domain["AdvancedSecurityOptions"]
+ advanced_security.should.have.key("Enabled").equals(False)
+
+ domain.should.have.key("AutoTuneOptions")
+ auto_tune = domain["AutoTuneOptions"]
+ auto_tune.should.have.key("State").equals("ENABLED")
+
+
+@mock_es
+def test_delete_elasticsearch_domain():
+ client = boto3.client("es", region_name="ap-southeast-1")
+ client.create_elasticsearch_domain(DomainName="motosearch")
+ client.delete_elasticsearch_domain(DomainName="motosearch")
+
+ client.list_domain_names()["DomainNames"].should.equal([])
+
+
+@mock_es
+def test_missing_delete_elasticsearch_domain():
+ client = boto3.client("es", region_name="ap-southeast-1")
+ with pytest.raises(ClientError) as exc:
+ client.delete_elasticsearch_domain(DomainName="unknown")
+
+ meta = exc.value.response["ResponseMetadata"]
+ meta["HTTPStatusCode"].should.equal(409)
+
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("ResourceNotFoundException")
+ err["Message"].should.equal("Domain not found: unknown")
+
+
+@mock_es
+def test_describe_invalid_domain():
+ client = boto3.client("es", region_name="us-east-2")
+ with pytest.raises(ClientError) as exc:
+ client.describe_elasticsearch_domain(DomainName="moto.org")
+ meta = exc.value.response["ResponseMetadata"]
+ meta["HTTPStatusCode"].should.equal(400)
+ err = exc.value.response["Error"]
+ err["Message"].should.equal(
+ f"1 validation error detected: Value 'moto.org' at 'domainName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-z][a-z0-9\\-]+"
+ )
+ err["Code"].should.equal("ValidationException")
+
+
+@mock_es
+def test_describe_unknown_domain():
+ client = boto3.client("es", region_name="ap-southeast-1")
+ with pytest.raises(ClientError) as exc:
+ client.describe_elasticsearch_domain(DomainName="unknown")
+
+ meta = exc.value.response["ResponseMetadata"]
+ meta["HTTPStatusCode"].should.equal(409)
+
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("ResourceNotFoundException")
+ err["Message"].should.equal("Domain not found: unknown")
+
+
+@mock_es
+def test_describe_elasticsearch_domain():
+ client = boto3.client("es", region_name="ap-southeast-1")
+ client.create_elasticsearch_domain(DomainName="motosearch")
+ resp = client.describe_elasticsearch_domain(DomainName="motosearch")
+
+ resp.should.have.key("DomainStatus")
+ domain = resp["DomainStatus"]
+ domain.should.have.key("DomainName").equals("motosearch")
+ domain.should.have.key("DomainId")
+ domain.should.have.key("ARN").equals(
+ f"arn:aws:es:ap-southeast-1:domain/{domain['DomainId']}"
+ )
+ domain.should.have.key("Created").equals(True)
+ domain.should.have.key("Deleted").equals(False)
+ domain.should.have.key("Processing").equals(False)
+ domain.should.have.key("UpgradeProcessing").equals(False)
+ domain.shouldnt.have.key("ElasticsearchVersion")
+
+
+@mock_es
+def test_list_domain_names_initial():
+ client = boto3.client("es", region_name="eu-west-1")
+ resp = client.list_domain_names()
+
+ resp.should.have.key("DomainNames").equals([])
+
+
+@mock_es
+def test_list_domain_names_with_multiple_domains():
+ client = boto3.client("es", region_name="eu-west-1")
+ domain_names = [f"env{i}" for i in range(1, 5)]
+ for name in domain_names:
+ client.create_elasticsearch_domain(DomainName=name)
+ resp = client.list_domain_names()
+
+ resp.should.have.key("DomainNames").length_of(4)
+ for name in domain_names:
+ resp["DomainNames"].should.contain({"DomainName": name})
diff --git a/tests/test_es/test_server.py b/tests/test_es/test_server.py
new file mode 100644
index 000000000..83299280d
--- /dev/null
+++ b/tests/test_es/test_server.py
@@ -0,0 +1,13 @@
+import json
+import sure # noqa # pylint: disable=unused-import
+
+import moto.server as server
+
+
+def test_es_list():
+ backend = server.create_backend_app("es")
+ test_client = backend.test_client()
+
+ resp = test_client.get("/2015-01-01/domain")
+ resp.status_code.should.equal(200)
+ json.loads(resp.data).should.equals({"DomainNames": []})