Repository: localstack/localstack

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


Discussion:

Resolving patch:
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_config(domain_name):
 
 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 @@ def get_domain_status(domain_name, deleted=False):
                 '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 @@ def create_domain():
     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


Test patch:
diff --git a/tests/integration/test_elasticsearch.py b/tests/integration/test_elasticsearch.py
--- a/tests/integration/test_elasticsearch.py
+++ b/tests/integration/test_elasticsearch.py
@@ -4,7 +4,7 @@
 from nose.tools import assert_equal, assert_in, assert_not_in
 from botocore.exceptions import ClientError
 from localstack.utils.aws import aws_stack
-from localstack.utils.common import short_uid, safe_requests as requests
+from localstack.utils.common import short_uid, retry, safe_requests as requests
 from localstack.services.es.es_api import DEFAULT_ES_VERSION
 
 TEST_INDEX = 'megacorp'
@@ -130,3 +130,9 @@ def _create_domain(cls, name=None, version=None):
             kwargs['ElasticsearchVersion'] = version
         es_client.create_elasticsearch_domain(DomainName=name, **kwargs)
         assert_in(name, [d['DomainName'] for d in es_client.list_domain_names()['DomainNames']])
+
+        # wait for completion status
+        def check_cluster_ready(*args):
+            status = es_client.describe_elasticsearch_domain(DomainName=name)
+            assert status['DomainStatus']['Created']
+        retry(check_cluster_ready, sleep=6, retries=10)