We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Docker and es::CreateElasticsearchDomain / elasticsearch endpoint
Hey all!

I'm using localstack for a project where I consume data from kinesis, process it and put it in elasticsearch.

In the later versions of localstack 'elasticsearch' was removed and is now lazily loaded after call to es::CreateElasticsearchDomain (at least that's how I understand it from #1881, #1865, #1870).

I'm running localstack in a docker using testcontainers (which gives me dynamic port mappings etc). My processing code is running on localhost.

The problem I'm facing now is that the response from es::CreateElasticsearchDomain returns an elasticsearch endpoint like: http://[docker container id]:port.
With my set up, this gives me two issues:
1. [docker container id] host is not (I believe) reachable from localhost unless I add it to /etc/hosts or something like that. I can surely "hack" my way around this by just replacing [docker container id] with localhost.
2. It says here: https://github.com/localstack/localstack/issues/1881#issuecomment-567495056 that we should not assume that elasticsearch will run on a static port in the future. When running localstack in a docker, how would I know which port to expose then in order to reach elasticsearch from localhost?

I currently have my tests running using 1.10.7 but there I just make a dummy call to es::CreateElasticsearchDomain and hardcode elasticsearch endpoint to http://localhost:4571. (It seems like it always chooses 4571 as of now?)

Thanks
Stefan

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `localstack/services/es/es_api.py`
Content:
```
1 import json
2 import time
3 import logging
4 from random import randint
5 from flask import Flask, jsonify, request, make_response
6 from localstack.utils import persistence
7 from localstack.services import generic_proxy, install
8 from localstack.utils.aws import aws_stack
9 from localstack.constants import TEST_AWS_ACCOUNT_ID, ELASTICSEARCH_DEFAULT_VERSION
10 from localstack.utils.common import to_str
11 from localstack.utils.tagging import TaggingService
12 from localstack.utils.analytics import event_publisher
13 from localstack.services.plugins import check_infra
14 
15 LOG = logging.getLogger(__name__)
16 
17 APP_NAME = 'es_api'
18 API_PREFIX = '/2015-01-01'
19 
20 DEFAULT_ES_VERSION = '7.1'
21 
22 ES_DOMAINS = {}
23 
24 TAGS = TaggingService()
25 
26 app = Flask(APP_NAME)
27 app.url_map.strict_slashes = False
28 
29 
30 def error_response(error_type, code=400, message='Unknown error.'):
31     if not message:
32         if error_type == 'ResourceNotFoundException':
33             message = 'Resource not found.'
34         elif error_type == 'ResourceAlreadyExistsException':
35             message = 'Resource already exists.'
36     response = make_response(jsonify({'error': message}))
37     response.headers['x-amzn-errortype'] = error_type
38     return response, code
39 
40 
41 def get_domain_config_status():
42     return {
43         'CreationDate': '%.2f' % time.time(),
44         'PendingDeletion': False,
45         'State': 'Active',
46         'UpdateDate': '%.2f' % time.time(),
47         'UpdateVersion': randint(1, 100)
48     }
49 
50 
51 def get_domain_config(domain_name):
52     config_status = get_domain_config_status()
53     return {
54         'DomainConfig': {
55             'AccessPolicies': {
56                 'Options': '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::%s:root"},"Action":"es:*","Resource":"arn:aws:es:%s:%s:domain/%s/*"}]}' % (TEST_AWS_ACCOUNT_ID, aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),  # noqa: E501
57                 'Status': config_status
58             },
59             'AdvancedOptions': {
60                 'Options': {
61                     'indices.fielddata.cache.size': '',
62                     'rest.action.multi.allow_explicit_index': 'true'
63                 },
64                 'Status': config_status
65             },
66             'EBSOptions': {
67                 'Options': {
68                     'EBSEnabled': True,
69                     'EncryptionEnabled': False,
70                     'Iops': 0,
71                     'VolumeSize': 10,
72                     'VolumeType': 'gp2'
73                 },
74                 'Status': config_status
75             },
76             'ElasticsearchClusterConfig': {
77                 'Options': {
78                     'DedicatedMasterCount': 1,
79                     'DedicatedMasterEnabled': True,
80                     'DedicatedMasterType': 'm3.medium.elasticsearch',
81                     'InstanceCount': 1,
82                     'InstanceType': 'm3.medium.elasticsearch',
83                     'ZoneAwarenessEnabled': False
84                 },
85                 'Status': config_status
86             },
87             'CognitoOptions': {
88                 'Enabled': False
89             },
90             'ElasticsearchVersion': {
91                 'Options': '5.3',
92                 'Status': config_status
93             },
94             'EncryptionAtRestOptions': {
95                 'Options': {
96                     'Enabled': False,
97                     'KmsKeyId': ''
98                 },
99                 'Status': config_status
100             },
101             'LogPublishingOptions': {
102                 'Options': {
103                     'INDEX_SLOW_LOGS': {
104                         'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501
105                         'Enabled': False
106                     },
107                     'SEARCH_SLOW_LOGS': {
108                         'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501
109                         'Enabled': False,
110                     }
111                 },
112                 'Status': config_status
113             },
114             'SnapshotOptions': {
115                 'Options': {
116                     'AutomatedSnapshotStartHour': randint(0, 23)
117                 },
118                 'Status': config_status
119             },
120             'VPCOptions': {
121                 'Options': {
122                     'AvailabilityZones': [
123                         'us-east-1b'
124                     ],
125                     'SecurityGroupIds': [
126                         'sg-12345678'
127                     ],
128                     'SubnetIds': [
129                         'subnet-12345678'
130                     ],
131                     'VPCId': 'vpc-12345678'
132                 },
133                 'Status': config_status
134             }
135         }
136     }
137 
138 
139 def get_domain_status(domain_name, deleted=False):
140     status = ES_DOMAINS.get(domain_name) or {}
141     return {
142         'DomainStatus': {
143             'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),
144             'Created': True,
145             'Deleted': deleted,
146             'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),
147             'DomainName': domain_name,
148             'ElasticsearchClusterConfig': {
149                 'DedicatedMasterCount': 1,
150                 'DedicatedMasterEnabled': True,
151                 'DedicatedMasterType': 'm3.medium.elasticsearch',
152                 'InstanceCount': 1,
153                 'InstanceType': 'm3.medium.elasticsearch',
154                 'ZoneAwarenessEnabled': False
155             },
156             'ElasticsearchVersion': status.get('ElasticsearchVersion') or DEFAULT_ES_VERSION,
157             'Endpoint': aws_stack.get_elasticsearch_endpoint(domain_name),
158             'Processing': False,
159             'EBSOptions': {
160                 'EBSEnabled': True,
161                 'VolumeType': 'gp2',
162                 'VolumeSize': 10,
163                 'Iops': 0
164             },
165             'CognitoOptions': {
166                 'Enabled': False
167             },
168         }
169     }
170 
171 
172 def get_install_version_for_api_version(version):
173     result = ELASTICSEARCH_DEFAULT_VERSION
174     if version.startswith('6.'):
175         result = '6.7.0'
176     elif version == '7.4':
177         result = '7.4.0'
178     if not result.startswith(result):
179         LOG.info('Elasticsearch version %s not yet supported, defaulting to %s' % (version, result))
180     return result
181 
182 
183 def start_elasticsearch_instance(version):
184     # Note: keep imports here to avoid circular dependencies
185     from localstack.services.es import es_starter
186 
187     # install ES version
188     install_version = get_install_version_for_api_version(version)
189     install.install_elasticsearch(install_version)
190 
191     t1 = es_starter.start_elasticsearch(asynchronous=True, version=install_version)
192     # sleep some time to give Elasticsearch enough time to come up
193     time.sleep(8)
194     # ensure that all infra components are up and running
195     check_infra(apis=[], additional_checks=[es_starter.check_elasticsearch])
196     return t1
197 
198 
199 def cleanup_elasticsearch_instance(status):
200     # Note: keep imports here to avoid circular dependencies
201     from localstack.services.es import es_starter
202     es_starter.stop_elasticsearch()
203 
204 
205 @app.route('%s/domain' % API_PREFIX, methods=['GET'])
206 def list_domain_names():
207     result = {
208         'DomainNames': [{'DomainName': name} for name in ES_DOMAINS.keys()]
209     }
210     return jsonify(result)
211 
212 
213 @app.route('%s/es/domain' % API_PREFIX, methods=['POST'])
214 def create_domain():
215     data = json.loads(to_str(request.data))
216     domain_name = data['DomainName']
217     if domain_name in ES_DOMAINS:
218         return error_response(error_type='ResourceAlreadyExistsException')
219     ES_DOMAINS[domain_name] = data
220     # start actual Elasticsearch instance
221     version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION
222     start_elasticsearch_instance(version=version)
223     result = get_domain_status(domain_name)
224 
225     # record event
226     event_publisher.fire_event(event_publisher.EVENT_ES_CREATE_DOMAIN,
227         payload={'n': event_publisher.get_hash(domain_name)})
228     persistence.record('es', request=request)
229 
230     return jsonify(result)
231 
232 
233 @app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['GET'])
234 def describe_domain(domain_name):
235     if domain_name not in ES_DOMAINS:
236         return error_response(error_type='ResourceNotFoundException')
237     result = get_domain_status(domain_name)
238     return jsonify(result)
239 
240 
241 @app.route('%s/es/domain-info' % API_PREFIX, methods=['POST'])
242 def describe_domains():
243     data = json.loads(to_str(request.data))
244     result = []
245     domain_names = data.get('DomainNames', [])
246     for domain_name in ES_DOMAINS:
247         if domain_name in domain_names:
248             status = get_domain_status(domain_name)
249             status = status.get('DomainStatus') or status
250             result.append(status)
251     result = {'DomainStatusList': result}
252     return jsonify(result)
253 
254 
255 @app.route('%s/es/domain/<domain_name>/config' % API_PREFIX, methods=['GET', 'POST'])
256 def domain_config(domain_name):
257     config = get_domain_config(domain_name)
258     return jsonify(config)
259 
260 
261 @app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['DELETE'])
262 def delete_domain(domain_name):
263     if domain_name not in ES_DOMAINS:
264         return error_response(error_type='ResourceNotFoundException')
265     result = get_domain_status(domain_name, deleted=True)
266     status = ES_DOMAINS.pop(domain_name)
267     if not ES_DOMAINS:
268         cleanup_elasticsearch_instance(status)
269 
270     # record event
271     event_publisher.fire_event(event_publisher.EVENT_ES_DELETE_DOMAIN,
272         payload={'n': event_publisher.get_hash(domain_name)})
273     persistence.record('es', request=request)
274 
275     return jsonify(result)
276 
277 
278 @app.route('%s/es/compatibleVersions' % API_PREFIX, methods=['GET'])
279 def get_compatible_versions():
280     result = [{
281         'SourceVersion': '6.5',
282         'TargetVersions': ['6.7', '6.8']
283     }, {
284         'SourceVersion': '6.7',
285         'TargetVersions': ['6.8']
286     }, {
287         'SourceVersion': '6.8',
288         'TargetVersions': ['7.1']
289     }]
290     return jsonify({'CompatibleElasticsearchVersions': result})
291 
292 
293 @app.route('%s/tags' % API_PREFIX, methods=['GET', 'POST'])
294 def add_list_tags():
295     if request.method == 'POST':
296         data = json.loads(to_str(request.data) or '{}')
297         arn = data.get('ARN')
298         TAGS.tag_resource(arn, data.get('TagList', []))
299     if request.method == 'GET' and request.args.get('arn'):
300         arn = request.args.get('arn')
301         tags = TAGS.list_tags_for_resource(arn)
302         response = {
303             'TagList': tags.get('Tags')
304         }
305         return jsonify(response)
306 
307     return jsonify({})
308 
309 
310 def serve(port, quiet=True):
311     generic_proxy.serve_flask_app(app=app, port=port, quiet=quiet)
312
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/localstack/services/es/es_api.py b/localstack/services/es/es_api.py
--- a/localstack/services/es/es_api.py
+++ b/localstack/services/es/es_api.py
@@ -3,11 +3,12 @@
 import logging
 from random import randint
 from flask import Flask, jsonify, request, make_response
+from localstack import config
 from localstack.utils import persistence
 from localstack.services import generic_proxy, install
 from localstack.utils.aws import aws_stack
 from localstack.constants import TEST_AWS_ACCOUNT_ID, ELASTICSEARCH_DEFAULT_VERSION
-from localstack.utils.common import to_str
+from localstack.utils.common import to_str, FuncThread, get_service_protocol
 from localstack.utils.tagging import TaggingService
 from localstack.utils.analytics import event_publisher
 from localstack.services.plugins import check_infra
@@ -138,10 +139,11 @@
 
 def get_domain_status(domain_name, deleted=False):
     status = ES_DOMAINS.get(domain_name) or {}
+    endpoint = '%s://%s:%s' % (get_service_protocol(), config.HOSTNAME_EXTERNAL, config.PORT_ELASTICSEARCH)
     return {
         'DomainStatus': {
             'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),
-            'Created': True,
+            'Created': status.get('Created', True),
             'Deleted': deleted,
             'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),
             'DomainName': domain_name,
@@ -154,7 +156,7 @@
                 'ZoneAwarenessEnabled': False
             },
             'ElasticsearchVersion': status.get('ElasticsearchVersion') or DEFAULT_ES_VERSION,
-            'Endpoint': aws_stack.get_elasticsearch_endpoint(domain_name),
+            'Endpoint': endpoint,
             'Processing': False,
             'EBSOptions': {
                 'EBSEnabled': True,
@@ -217,9 +219,18 @@
     if domain_name in ES_DOMAINS:
         return error_response(error_type='ResourceAlreadyExistsException')
     ES_DOMAINS[domain_name] = data
-    # start actual Elasticsearch instance
-    version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION
-    start_elasticsearch_instance(version=version)
+    data['Created'] = False
+
+    def do_start(*args):
+        # start actual Elasticsearch instance
+        version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION
+        start_elasticsearch_instance(version=version)
+        data['Created'] = True
+
+    # start ES instance in the background
+    FuncThread(do_start).start()
+    # sleep a short while, then return
+    time.sleep(5)
     result = get_domain_status(domain_name)
 
     # record event

{"golden_diff": "diff --git a/localstack/services/es/es_api.py b/localstack/services/es/es_api.py\n--- a/localstack/services/es/es_api.py\n+++ b/localstack/services/es/es_api.py\n@@ -3,11 +3,12 @@\n import logging\n from random import randint\n from flask import Flask, jsonify, request, make_response\n+from localstack import config\n from localstack.utils import persistence\n from localstack.services import generic_proxy, install\n from localstack.utils.aws import aws_stack\n from localstack.constants import TEST_AWS_ACCOUNT_ID, ELASTICSEARCH_DEFAULT_VERSION\n-from localstack.utils.common import to_str\n+from localstack.utils.common import to_str, FuncThread, get_service_protocol\n from localstack.utils.tagging import TaggingService\n from localstack.utils.analytics import event_publisher\n from localstack.services.plugins import check_infra\n@@ -138,10 +139,11 @@\n \n def get_domain_status(domain_name, deleted=False):\n     status = ES_DOMAINS.get(domain_name) or {}\n+    endpoint = '%s://%s:%s' % (get_service_protocol(), config.HOSTNAME_EXTERNAL, config.PORT_ELASTICSEARCH)\n     return {\n         'DomainStatus': {\n             'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),\n-            'Created': True,\n+            'Created': status.get('Created', True),\n             'Deleted': deleted,\n             'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),\n             'DomainName': domain_name,\n@@ -154,7 +156,7 @@\n                 'ZoneAwarenessEnabled': False\n             },\n             'ElasticsearchVersion': status.get('ElasticsearchVersion') or DEFAULT_ES_VERSION,\n-            'Endpoint': aws_stack.get_elasticsearch_endpoint(domain_name),\n+            'Endpoint': endpoint,\n             'Processing': False,\n             'EBSOptions': {\n                 'EBSEnabled': True,\n@@ -217,9 +219,18 @@\n     if domain_name in ES_DOMAINS:\n         return error_response(error_type='ResourceAlreadyExistsException')\n     ES_DOMAINS[domain_name] = data\n-    # start actual Elasticsearch instance\n-    version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION\n-    start_elasticsearch_instance(version=version)\n+    data['Created'] = False\n+\n+    def do_start(*args):\n+        # start actual Elasticsearch instance\n+        version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION\n+        start_elasticsearch_instance(version=version)\n+        data['Created'] = True\n+\n+    # start ES instance in the background\n+    FuncThread(do_start).start()\n+    # sleep a short while, then return\n+    time.sleep(5)\n     result = get_domain_status(domain_name)\n \n     # record event\n", "issue": "Docker and es::CreateElasticsearchDomain / elasticsearch endpoint\nHey all!\r\n\r\nI'm using localstack for a project where I consume data from kinesis, process it and put it in elasticsearch.\r\n\r\nIn the later versions of localstack 'elasticsearch' was removed and is now lazily loaded after call to es::CreateElasticsearchDomain (at least that's how I understand it from #1881, #1865, #1870).\r\n\r\nI'm running localstack in a docker using testcontainers (which gives me dynamic port mappings etc). My processing code is running on localhost.\r\n\r\nThe problem I'm facing now is that the response from es::CreateElasticsearchDomain returns an elasticsearch endpoint like: http://[docker container id]:port.\r\nWith my set up, this gives me two issues:\r\n1. [docker container id] host is not (I believe) reachable from localhost unless I add it to /etc/hosts or something like that. I can surely \"hack\" my way around this by just replacing [docker container id] with localhost.\r\n2. It says here: https://github.com/localstack/localstack/issues/1881#issuecomment-567495056 that we should not assume that elasticsearch will run on a static port in the future. When running localstack in a docker, how would I know which port to expose then in order to reach elasticsearch from localhost?\r\n\r\nI currently have my tests running using 1.10.7 but there I just make a dummy call to es::CreateElasticsearchDomain and hardcode elasticsearch endpoint to http://localhost:4571. (It seems like it always chooses 4571 as of now?)\r\n\r\nThanks\r\nStefan\n", "before_files": [{"content": "import json\nimport time\nimport logging\nfrom random import randint\nfrom flask import Flask, jsonify, request, make_response\nfrom localstack.utils import persistence\nfrom localstack.services import generic_proxy, install\nfrom localstack.utils.aws import aws_stack\nfrom localstack.constants import TEST_AWS_ACCOUNT_ID, ELASTICSEARCH_DEFAULT_VERSION\nfrom localstack.utils.common import to_str\nfrom localstack.utils.tagging import TaggingService\nfrom localstack.utils.analytics import event_publisher\nfrom localstack.services.plugins import check_infra\n\nLOG = logging.getLogger(__name__)\n\nAPP_NAME = 'es_api'\nAPI_PREFIX = '/2015-01-01'\n\nDEFAULT_ES_VERSION = '7.1'\n\nES_DOMAINS = {}\n\nTAGS = TaggingService()\n\napp = Flask(APP_NAME)\napp.url_map.strict_slashes = False\n\n\ndef error_response(error_type, code=400, message='Unknown error.'):\n    if not message:\n        if error_type == 'ResourceNotFoundException':\n            message = 'Resource not found.'\n        elif error_type == 'ResourceAlreadyExistsException':\n            message = 'Resource already exists.'\n    response = make_response(jsonify({'error': message}))\n    response.headers['x-amzn-errortype'] = error_type\n    return response, code\n\n\ndef get_domain_config_status():\n    return {\n        'CreationDate': '%.2f' % time.time(),\n        'PendingDeletion': False,\n        'State': 'Active',\n        'UpdateDate': '%.2f' % time.time(),\n        'UpdateVersion': randint(1, 100)\n    }\n\n\ndef get_domain_config(domain_name):\n    config_status = get_domain_config_status()\n    return {\n        'DomainConfig': {\n            'AccessPolicies': {\n                'Options': '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::%s:root\"},\"Action\":\"es:*\",\"Resource\":\"arn:aws:es:%s:%s:domain/%s/*\"}]}' % (TEST_AWS_ACCOUNT_ID, aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),  # noqa: E501\n                'Status': config_status\n            },\n            'AdvancedOptions': {\n                'Options': {\n                    'indices.fielddata.cache.size': '',\n                    'rest.action.multi.allow_explicit_index': 'true'\n                },\n                'Status': config_status\n            },\n            'EBSOptions': {\n                'Options': {\n                    'EBSEnabled': True,\n                    'EncryptionEnabled': False,\n                    'Iops': 0,\n                    'VolumeSize': 10,\n                    'VolumeType': 'gp2'\n                },\n                'Status': config_status\n            },\n            'ElasticsearchClusterConfig': {\n                'Options': {\n                    'DedicatedMasterCount': 1,\n                    'DedicatedMasterEnabled': True,\n                    'DedicatedMasterType': 'm3.medium.elasticsearch',\n                    'InstanceCount': 1,\n                    'InstanceType': 'm3.medium.elasticsearch',\n                    'ZoneAwarenessEnabled': False\n                },\n                'Status': config_status\n            },\n            'CognitoOptions': {\n                'Enabled': False\n            },\n            'ElasticsearchVersion': {\n                'Options': '5.3',\n                'Status': config_status\n            },\n            'EncryptionAtRestOptions': {\n                'Options': {\n                    'Enabled': False,\n                    'KmsKeyId': ''\n                },\n                'Status': config_status\n            },\n            'LogPublishingOptions': {\n                'Options': {\n                    'INDEX_SLOW_LOGS': {\n                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501\n                        'Enabled': False\n                    },\n                    'SEARCH_SLOW_LOGS': {\n                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501\n                        'Enabled': False,\n                    }\n                },\n                'Status': config_status\n            },\n            'SnapshotOptions': {\n                'Options': {\n                    'AutomatedSnapshotStartHour': randint(0, 23)\n                },\n                'Status': config_status\n            },\n            'VPCOptions': {\n                'Options': {\n                    'AvailabilityZones': [\n                        'us-east-1b'\n                    ],\n                    'SecurityGroupIds': [\n                        'sg-12345678'\n                    ],\n                    'SubnetIds': [\n                        'subnet-12345678'\n                    ],\n                    'VPCId': 'vpc-12345678'\n                },\n                'Status': config_status\n            }\n        }\n    }\n\n\ndef get_domain_status(domain_name, deleted=False):\n    status = ES_DOMAINS.get(domain_name) or {}\n    return {\n        'DomainStatus': {\n            'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),\n            'Created': True,\n            'Deleted': deleted,\n            'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),\n            'DomainName': domain_name,\n            'ElasticsearchClusterConfig': {\n                'DedicatedMasterCount': 1,\n                'DedicatedMasterEnabled': True,\n                'DedicatedMasterType': 'm3.medium.elasticsearch',\n                'InstanceCount': 1,\n                'InstanceType': 'm3.medium.elasticsearch',\n                'ZoneAwarenessEnabled': False\n            },\n            'ElasticsearchVersion': status.get('ElasticsearchVersion') or DEFAULT_ES_VERSION,\n            'Endpoint': aws_stack.get_elasticsearch_endpoint(domain_name),\n            'Processing': False,\n            'EBSOptions': {\n                'EBSEnabled': True,\n                'VolumeType': 'gp2',\n                'VolumeSize': 10,\n                'Iops': 0\n            },\n            'CognitoOptions': {\n                'Enabled': False\n            },\n        }\n    }\n\n\ndef get_install_version_for_api_version(version):\n    result = ELASTICSEARCH_DEFAULT_VERSION\n    if version.startswith('6.'):\n        result = '6.7.0'\n    elif version == '7.4':\n        result = '7.4.0'\n    if not result.startswith(result):\n        LOG.info('Elasticsearch version %s not yet supported, defaulting to %s' % (version, result))\n    return result\n\n\ndef start_elasticsearch_instance(version):\n    # Note: keep imports here to avoid circular dependencies\n    from localstack.services.es import es_starter\n\n    # install ES version\n    install_version = get_install_version_for_api_version(version)\n    install.install_elasticsearch(install_version)\n\n    t1 = es_starter.start_elasticsearch(asynchronous=True, version=install_version)\n    # sleep some time to give Elasticsearch enough time to come up\n    time.sleep(8)\n    # ensure that all infra components are up and running\n    check_infra(apis=[], additional_checks=[es_starter.check_elasticsearch])\n    return t1\n\n\ndef cleanup_elasticsearch_instance(status):\n    # Note: keep imports here to avoid circular dependencies\n    from localstack.services.es import es_starter\n    es_starter.stop_elasticsearch()\n\n\n@app.route('%s/domain' % API_PREFIX, methods=['GET'])\ndef list_domain_names():\n    result = {\n        'DomainNames': [{'DomainName': name} for name in ES_DOMAINS.keys()]\n    }\n    return jsonify(result)\n\n\n@app.route('%s/es/domain' % API_PREFIX, methods=['POST'])\ndef create_domain():\n    data = json.loads(to_str(request.data))\n    domain_name = data['DomainName']\n    if domain_name in ES_DOMAINS:\n        return error_response(error_type='ResourceAlreadyExistsException')\n    ES_DOMAINS[domain_name] = data\n    # start actual Elasticsearch instance\n    version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION\n    start_elasticsearch_instance(version=version)\n    result = get_domain_status(domain_name)\n\n    # record event\n    event_publisher.fire_event(event_publisher.EVENT_ES_CREATE_DOMAIN,\n        payload={'n': event_publisher.get_hash(domain_name)})\n    persistence.record('es', request=request)\n\n    return jsonify(result)\n\n\n@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['GET'])\ndef describe_domain(domain_name):\n    if domain_name not in ES_DOMAINS:\n        return error_response(error_type='ResourceNotFoundException')\n    result = get_domain_status(domain_name)\n    return jsonify(result)\n\n\n@app.route('%s/es/domain-info' % API_PREFIX, methods=['POST'])\ndef describe_domains():\n    data = json.loads(to_str(request.data))\n    result = []\n    domain_names = data.get('DomainNames', [])\n    for domain_name in ES_DOMAINS:\n        if domain_name in domain_names:\n            status = get_domain_status(domain_name)\n            status = status.get('DomainStatus') or status\n            result.append(status)\n    result = {'DomainStatusList': result}\n    return jsonify(result)\n\n\n@app.route('%s/es/domain/<domain_name>/config' % API_PREFIX, methods=['GET', 'POST'])\ndef domain_config(domain_name):\n    config = get_domain_config(domain_name)\n    return jsonify(config)\n\n\n@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['DELETE'])\ndef delete_domain(domain_name):\n    if domain_name not in ES_DOMAINS:\n        return error_response(error_type='ResourceNotFoundException')\n    result = get_domain_status(domain_name, deleted=True)\n    status = ES_DOMAINS.pop(domain_name)\n    if not ES_DOMAINS:\n        cleanup_elasticsearch_instance(status)\n\n    # record event\n    event_publisher.fire_event(event_publisher.EVENT_ES_DELETE_DOMAIN,\n        payload={'n': event_publisher.get_hash(domain_name)})\n    persistence.record('es', request=request)\n\n    return jsonify(result)\n\n\n@app.route('%s/es/compatibleVersions' % API_PREFIX, methods=['GET'])\ndef get_compatible_versions():\n    result = [{\n        'SourceVersion': '6.5',\n        'TargetVersions': ['6.7', '6.8']\n    }, {\n        'SourceVersion': '6.7',\n        'TargetVersions': ['6.8']\n    }, {\n        'SourceVersion': '6.8',\n        'TargetVersions': ['7.1']\n    }]\n    return jsonify({'CompatibleElasticsearchVersions': result})\n\n\n@app.route('%s/tags' % API_PREFIX, methods=['GET', 'POST'])\ndef add_list_tags():\n    if request.method == 'POST':\n        data = json.loads(to_str(request.data) or '{}')\n        arn = data.get('ARN')\n        TAGS.tag_resource(arn, data.get('TagList', []))\n    if request.method == 'GET' and request.args.get('arn'):\n        arn = request.args.get('arn')\n        tags = TAGS.list_tags_for_resource(arn)\n        response = {\n            'TagList': tags.get('Tags')\n        }\n        return jsonify(response)\n\n    return jsonify({})\n\n\ndef serve(port, quiet=True):\n    generic_proxy.serve_flask_app(app=app, port=port, quiet=quiet)\n", "path": "localstack/services/es/es_api.py"}], "after_files": [{"content": "import json\nimport time\nimport logging\nfrom random import randint\nfrom flask import Flask, jsonify, request, make_response\nfrom localstack import config\nfrom localstack.utils import persistence\nfrom localstack.services import generic_proxy, install\nfrom localstack.utils.aws import aws_stack\nfrom localstack.constants import TEST_AWS_ACCOUNT_ID, ELASTICSEARCH_DEFAULT_VERSION\nfrom localstack.utils.common import to_str, FuncThread, get_service_protocol\nfrom localstack.utils.tagging import TaggingService\nfrom localstack.utils.analytics import event_publisher\nfrom localstack.services.plugins import check_infra\n\nLOG = logging.getLogger(__name__)\n\nAPP_NAME = 'es_api'\nAPI_PREFIX = '/2015-01-01'\n\nDEFAULT_ES_VERSION = '7.1'\n\nES_DOMAINS = {}\n\nTAGS = TaggingService()\n\napp = Flask(APP_NAME)\napp.url_map.strict_slashes = False\n\n\ndef error_response(error_type, code=400, message='Unknown error.'):\n    if not message:\n        if error_type == 'ResourceNotFoundException':\n            message = 'Resource not found.'\n        elif error_type == 'ResourceAlreadyExistsException':\n            message = 'Resource already exists.'\n    response = make_response(jsonify({'error': message}))\n    response.headers['x-amzn-errortype'] = error_type\n    return response, code\n\n\ndef get_domain_config_status():\n    return {\n        'CreationDate': '%.2f' % time.time(),\n        'PendingDeletion': False,\n        'State': 'Active',\n        'UpdateDate': '%.2f' % time.time(),\n        'UpdateVersion': randint(1, 100)\n    }\n\n\ndef get_domain_config(domain_name):\n    config_status = get_domain_config_status()\n    return {\n        'DomainConfig': {\n            'AccessPolicies': {\n                'Options': '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::%s:root\"},\"Action\":\"es:*\",\"Resource\":\"arn:aws:es:%s:%s:domain/%s/*\"}]}' % (TEST_AWS_ACCOUNT_ID, aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),  # noqa: E501\n                'Status': config_status\n            },\n            'AdvancedOptions': {\n                'Options': {\n                    'indices.fielddata.cache.size': '',\n                    'rest.action.multi.allow_explicit_index': 'true'\n                },\n                'Status': config_status\n            },\n            'EBSOptions': {\n                'Options': {\n                    'EBSEnabled': True,\n                    'EncryptionEnabled': False,\n                    'Iops': 0,\n                    'VolumeSize': 10,\n                    'VolumeType': 'gp2'\n                },\n                'Status': config_status\n            },\n            'ElasticsearchClusterConfig': {\n                'Options': {\n                    'DedicatedMasterCount': 1,\n                    'DedicatedMasterEnabled': True,\n                    'DedicatedMasterType': 'm3.medium.elasticsearch',\n                    'InstanceCount': 1,\n                    'InstanceType': 'm3.medium.elasticsearch',\n                    'ZoneAwarenessEnabled': False\n                },\n                'Status': config_status\n            },\n            'CognitoOptions': {\n                'Enabled': False\n            },\n            'ElasticsearchVersion': {\n                'Options': '5.3',\n                'Status': config_status\n            },\n            'EncryptionAtRestOptions': {\n                'Options': {\n                    'Enabled': False,\n                    'KmsKeyId': ''\n                },\n                'Status': config_status\n            },\n            'LogPublishingOptions': {\n                'Options': {\n                    'INDEX_SLOW_LOGS': {\n                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501\n                        'Enabled': False\n                    },\n                    'SEARCH_SLOW_LOGS': {\n                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501\n                        'Enabled': False,\n                    }\n                },\n                'Status': config_status\n            },\n            'SnapshotOptions': {\n                'Options': {\n                    'AutomatedSnapshotStartHour': randint(0, 23)\n                },\n                'Status': config_status\n            },\n            'VPCOptions': {\n                'Options': {\n                    'AvailabilityZones': [\n                        'us-east-1b'\n                    ],\n                    'SecurityGroupIds': [\n                        'sg-12345678'\n                    ],\n                    'SubnetIds': [\n                        'subnet-12345678'\n                    ],\n                    'VPCId': 'vpc-12345678'\n                },\n                'Status': config_status\n            }\n        }\n    }\n\n\ndef get_domain_status(domain_name, deleted=False):\n    status = ES_DOMAINS.get(domain_name) or {}\n    endpoint = '%s://%s:%s' % (get_service_protocol(), config.HOSTNAME_EXTERNAL, config.PORT_ELASTICSEARCH)\n    return {\n        'DomainStatus': {\n            'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),\n            'Created': status.get('Created', True),\n            'Deleted': deleted,\n            'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),\n            'DomainName': domain_name,\n            'ElasticsearchClusterConfig': {\n                'DedicatedMasterCount': 1,\n                'DedicatedMasterEnabled': True,\n                'DedicatedMasterType': 'm3.medium.elasticsearch',\n                'InstanceCount': 1,\n                'InstanceType': 'm3.medium.elasticsearch',\n                'ZoneAwarenessEnabled': False\n            },\n            'ElasticsearchVersion': status.get('ElasticsearchVersion') or DEFAULT_ES_VERSION,\n            'Endpoint': endpoint,\n            'Processing': False,\n            'EBSOptions': {\n                'EBSEnabled': True,\n                'VolumeType': 'gp2',\n                'VolumeSize': 10,\n                'Iops': 0\n            },\n            'CognitoOptions': {\n                'Enabled': False\n            },\n        }\n    }\n\n\ndef get_install_version_for_api_version(version):\n    result = ELASTICSEARCH_DEFAULT_VERSION\n    if version.startswith('6.'):\n        result = '6.7.0'\n    elif version == '7.4':\n        result = '7.4.0'\n    if not result.startswith(result):\n        LOG.info('Elasticsearch version %s not yet supported, defaulting to %s' % (version, result))\n    return result\n\n\ndef start_elasticsearch_instance(version):\n    # Note: keep imports here to avoid circular dependencies\n    from localstack.services.es import es_starter\n\n    # install ES version\n    install_version = get_install_version_for_api_version(version)\n    install.install_elasticsearch(install_version)\n\n    t1 = es_starter.start_elasticsearch(asynchronous=True, version=install_version)\n    # sleep some time to give Elasticsearch enough time to come up\n    time.sleep(8)\n    # ensure that all infra components are up and running\n    check_infra(apis=[], additional_checks=[es_starter.check_elasticsearch])\n    return t1\n\n\ndef cleanup_elasticsearch_instance(status):\n    # Note: keep imports here to avoid circular dependencies\n    from localstack.services.es import es_starter\n    es_starter.stop_elasticsearch()\n\n\n@app.route('%s/domain' % API_PREFIX, methods=['GET'])\ndef list_domain_names():\n    result = {\n        'DomainNames': [{'DomainName': name} for name in ES_DOMAINS.keys()]\n    }\n    return jsonify(result)\n\n\n@app.route('%s/es/domain' % API_PREFIX, methods=['POST'])\ndef create_domain():\n    data = json.loads(to_str(request.data))\n    domain_name = data['DomainName']\n    if domain_name in ES_DOMAINS:\n        return error_response(error_type='ResourceAlreadyExistsException')\n    ES_DOMAINS[domain_name] = data\n    data['Created'] = False\n\n    def do_start(*args):\n        # start actual Elasticsearch instance\n        version = data.get('ElasticsearchVersion') or DEFAULT_ES_VERSION\n        start_elasticsearch_instance(version=version)\n        data['Created'] = True\n\n    # start ES instance in the background\n    FuncThread(do_start).start()\n    # sleep a short while, then return\n    time.sleep(5)\n    result = get_domain_status(domain_name)\n\n    # record event\n    event_publisher.fire_event(event_publisher.EVENT_ES_CREATE_DOMAIN,\n        payload={'n': event_publisher.get_hash(domain_name)})\n    persistence.record('es', request=request)\n\n    return jsonify(result)\n\n\n@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['GET'])\ndef describe_domain(domain_name):\n    if domain_name not in ES_DOMAINS:\n        return error_response(error_type='ResourceNotFoundException')\n    result = get_domain_status(domain_name)\n    return jsonify(result)\n\n\n@app.route('%s/es/domain-info' % API_PREFIX, methods=['POST'])\ndef describe_domains():\n    data = json.loads(to_str(request.data))\n    result = []\n    domain_names = data.get('DomainNames', [])\n    for domain_name in ES_DOMAINS:\n        if domain_name in domain_names:\n            status = get_domain_status(domain_name)\n            status = status.get('DomainStatus') or status\n            result.append(status)\n    result = {'DomainStatusList': result}\n    return jsonify(result)\n\n\n@app.route('%s/es/domain/<domain_name>/config' % API_PREFIX, methods=['GET', 'POST'])\ndef domain_config(domain_name):\n    config = get_domain_config(domain_name)\n    return jsonify(config)\n\n\n@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['DELETE'])\ndef delete_domain(domain_name):\n    if domain_name not in ES_DOMAINS:\n        return error_response(error_type='ResourceNotFoundException')\n    result = get_domain_status(domain_name, deleted=True)\n    status = ES_DOMAINS.pop(domain_name)\n    if not ES_DOMAINS:\n        cleanup_elasticsearch_instance(status)\n\n    # record event\n    event_publisher.fire_event(event_publisher.EVENT_ES_DELETE_DOMAIN,\n        payload={'n': event_publisher.get_hash(domain_name)})\n    persistence.record('es', request=request)\n\n    return jsonify(result)\n\n\n@app.route('%s/es/compatibleVersions' % API_PREFIX, methods=['GET'])\ndef get_compatible_versions():\n    result = [{\n        'SourceVersion': '6.5',\n        'TargetVersions': ['6.7', '6.8']\n    }, {\n        'SourceVersion': '6.7',\n        'TargetVersions': ['6.8']\n    }, {\n        'SourceVersion': '6.8',\n        'TargetVersions': ['7.1']\n    }]\n    return jsonify({'CompatibleElasticsearchVersions': result})\n\n\n@app.route('%s/tags' % API_PREFIX, methods=['GET', 'POST'])\ndef add_list_tags():\n    if request.method == 'POST':\n        data = json.loads(to_str(request.data) or '{}')\n        arn = data.get('ARN')\n        TAGS.tag_resource(arn, data.get('TagList', []))\n    if request.method == 'GET' and request.args.get('arn'):\n        arn = request.args.get('arn')\n        tags = TAGS.list_tags_for_resource(arn)\n        response = {\n            'TagList': tags.get('Tags')\n        }\n        return jsonify(response)\n\n    return jsonify({})\n\n\ndef serve(port, quiet=True):\n    generic_proxy.serve_flask_app(app=app, port=port, quiet=quiet)\n", "path": "localstack/services/es/es_api.py"}]}