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": []})