Skip to content

Commit 83dc753

Browse files
committed
working prototype
0 parents  commit 83dc753

10 files changed

Lines changed: 246 additions & 0 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.venv
2+
__pycache__

Makefile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
VENV_DIR ?= .venv
2+
TEST_PATH ?= ./tests
3+
PIP_CMD ?= pip
4+
NOSE_LOG_LEVEL ?= WARNING
5+
6+
ifeq ($(OS), Windows_NT)
7+
VENV_RUN = . $(VENV_DIR)/Scripts/activate
8+
else
9+
VENV_RUN = . $(VENV_DIR)/bin/activate
10+
endif
11+
12+
setup-venv:
13+
(test `which virtualenv` || $(PIP_CMD) install --user virtualenv) && \
14+
(test -e $(VENV_DIR) || virtualenv $(VENV_OPTS) $(VENV_DIR))
15+
16+
install-venv:
17+
make setup-venv && \
18+
test ! -e requirements.txt || ($(VENV_RUN); $(PIP_CMD) -q install -r requirements.txt)
19+
20+
test:
21+
($(VENV_RUN); DEBUG=$(DEBUG) PYTHONPATH=`pwd` nosetests $(NOSE_ARGS) --with-timer --with-coverage --logging-level=$(NOSE_LOG_LEVEL) --nocapture --no-skip --exe --cover-erase --cover-tests --cover-inclusive --cover-package=localstack --with-xunit --exclude='$(VENV_DIR).*' --ignore-files='lambda_python3.py' $(TEST_PATH))

common_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

localstack.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import re
2+
import sys
3+
import docker
4+
import logging
5+
from localstack_docker.container import Container
6+
from localstack_docker.localstack_docker_configuration import LocalstackDockerConfiguration
7+
from localstack_docker.localstack_logger import LocalstackLogger
8+
9+
ENV_CONFIG_USE_SSL = "USE_SSL"
10+
ENV_CONFIG_EDGE_PORT = "EDGE_PORT"
11+
INIT_SCRIPTS_PATH = "/docker-entrypoint-initaws.d"
12+
TMP_PATH = "/tmp/localstack"
13+
READY_TOKEN = re.compile("Ready\\.")
14+
DEFAULT_EDGE_PORT = 4566
15+
PORT_CONFIG_FILENAME = "/opt/code/localstack/.venv/lib/python3.8/site-packages/localstack_client/config.py"
16+
# DEFAULT_PORT_PATTERN = re.compile("'(\\w+)'\\Q: '{proto}://{host}:\\E(\\d+)'")
17+
18+
localstack_instance = None
19+
LOG = logging.getLogger(__name__);
20+
21+
22+
class Localstack:
23+
24+
localstack_container = None
25+
service_to_map = {}
26+
locked = False
27+
28+
@staticmethod
29+
def INSTANCE():
30+
return Localstack()
31+
32+
external_hostName = ''
33+
34+
def startup(self, docker_configuration) :
35+
if self.locked:
36+
raise Exception("A docker instance is starting or already started.")
37+
self.locked = True
38+
self.external_hostname = docker_configuration.external_hostname
39+
40+
try:
41+
self.localstack_container = Container.create_localstack_container(
42+
docker_configuration.external_hostname,
43+
docker_configuration.pull_new_image,
44+
docker_configuration.randomize_ports,
45+
docker_configuration.image_name,
46+
docker_configuration.image_tag,
47+
docker_configuration.port_edge,
48+
docker_configuration.port_elastic_search,
49+
docker_configuration.environment_variables,
50+
docker_configuration.port_mappings,
51+
docker_configuration.bind_mounts,
52+
docker_configuration.platform
53+
)
54+
55+
self.setup_logger()
56+
57+
Container.waitForReady(self.localstack_container, READY_TOKEN)
58+
59+
except docker.errors.APIError as error:
60+
if not docker_configuration.ignore_docker_runerrors:
61+
raise "Unable to start docker"
62+
63+
except:
64+
error = sys.exc_info()
65+
print(error)
66+
67+
def stop(self):
68+
try:
69+
self.localstack_container.stop()
70+
except:
71+
error = sys.exc_info()
72+
print(error)
73+
74+
def setup_logger(self):
75+
localstack_logger = LocalstackLogger(self.localstack_container)
76+
localstack_logger.start()
77+
78+
# @param services
79+
def startup_localstack(port=4566, services=[], ignore_docker_errors = False):
80+
global localstack_instance
81+
localstack_instance = Localstack.INSTANCE()
82+
config = LocalstackDockerConfiguration()
83+
84+
if len(services):
85+
config.envirement_variables.update({'SERVICES': ','.join(services)})
86+
if port != 4566:
87+
config.port_edge = str(port)
88+
89+
config.ignore_docker_runerrors = ignore_docker_errors
90+
91+
localstack_instance.startup(config)
92+
93+
def stop_localstack():
94+
if localstack_instance:
95+
localstack_instance.stop()

localstack_docker/container.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
import re
3+
import docker
4+
from time import sleep
5+
6+
LOCALSTACK_NAME = "localstack/localstack";
7+
LOCALSTACK_TAG = "latest";
8+
LOCALSTACK_PORT_EDGE = "4566";
9+
LOCALSTACK_PORT_ELASTICSEARCH = "4571";
10+
11+
MAX_PORT_CONNECTION_ATTEMPTS = 10;
12+
MAX_LOG_COLLECTION_ATTEMPTS = 120;
13+
POLL_INTERVAL = 1;
14+
NUM_LOG_LINES = 10;
15+
16+
ENV_DEBUG = "DEBUG";
17+
ENV_USE_SSL = "USE_SSL";
18+
ENV_DEBUG_DEFAULT = "1";
19+
LOCALSTACK_EXTERNAL_HOSTNAME = "HOSTNAME_EXTERNAL"
20+
DEFAULT_CONTAINER_ID = "localstack_main"
21+
22+
DOCKER_CLIENT = docker.from_env()
23+
LOG = logging.getLogger(__name__);
24+
25+
class Container:
26+
27+
@staticmethod
28+
def create_localstack_container(external_hostname, pull_new_image, randomize_ports, image_name, image_tag, port_edge, port_elasticsearch, environment_variables, port_mappings, bind_mounts, platform):
29+
30+
environment_variables = {} if environment_variables == None else environment_variables
31+
bind_mounts = {} if bind_mounts == None else bind_mounts
32+
port_mappings = {} if port_mappings == None else port_mappings
33+
image_name_or_default = LOCALSTACK_NAME if image_name == None else image_name
34+
image_exists = True if len(DOCKER_CLIENT.images.list(name=image_name_or_default)) else False
35+
36+
fullPortEdge = {(LOCALSTACK_PORT_EDGE if port_edge == None else port_edge) : (LOCALSTACK_PORT_EDGE)}
37+
38+
if pull_new_image or not image_exists:
39+
LOG.info("Pulling latest image")
40+
DOCKER_CLIENT.images.pull(image_name_or_default, image_tag)
41+
42+
return DOCKER_CLIENT.containers.run(image_name_or_default, ports=fullPortEdge, environment=environment_variables, detach=True)
43+
44+
@staticmethod
45+
def waitForReady(container, pattern):
46+
attemps = 0
47+
48+
while True:
49+
logs = container.logs(tail=NUM_LOG_LINES).decode('utf-8')
50+
if re.search(pattern, logs):
51+
return;
52+
53+
sleep(POLL_INTERVAL)
54+
attemps += 1
55+
56+
if attemps >= MAX_LOG_COLLECTION_ATTEMPTS:
57+
raise "Could not find token: " +pattern.toString()+ "in logs"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class LocalstackDockerConfiguration:
2+
pull_new_image = False
3+
randomize_ports = False
4+
image_name = None
5+
image_tag = None
6+
platform = None
7+
8+
port_edge = '4566'
9+
port_elastic_search = '4571'
10+
external_hostname = 'localhost'
11+
environment_variables = {}
12+
port_mappings = {}
13+
bind_mounts = {}
14+
15+
initialization_token = None
16+
use_dingle_docker_container = False
17+
ignore_docker_runerrors = False
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import threading
2+
import logging
3+
4+
LOG = logging.getLogger(__name__);
5+
6+
class LocalstackLogger():
7+
stream = None
8+
def __init__(self, localsack_container) -> None:
9+
stream = localsack_container.logs(stream=True, follow=True)
10+
11+
12+
def start(self):
13+
log_thread = threading.Thread(target=log_stream,args=[self.stream])
14+
15+
def log_stream(stream):
16+
try:
17+
print("starting logs")
18+
while True:
19+
line = next(stream).decode("utf-8")
20+
LOG.warning(line)
21+
except StopIteration:
22+
print("LOGS ENDED")

nosetests.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="1" errors="0" failures="0" skip="0"><testcase classname="kinesis_test.kinesis_test" name="test_create_stream" time="35.622"><system-out><![CDATA[(<class 'KeyboardInterrupt'>, KeyboardInterrupt(), <traceback object at 0x7fa229b1ae80>)
2+
]]></system-out></testcase></testsuite>

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
docker>=5.0.0
2+
nose>=1.3.7
3+
nose-timer>=0.7.5
4+
boto3>=1.17.70

tests/kinesis_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import time
2+
import boto3
3+
import unittest
4+
from localstack import startup_localstack, stop_localstack
5+
class kinesis_test(unittest.TestCase):
6+
def setUp(self):
7+
startup_localstack()
8+
session = boto3.session.Session()
9+
10+
def tearDown(self):
11+
stop_localstack()
12+
return super().tearDown()
13+
14+
def test_create_stream(self):
15+
kinesis = boto3.client(
16+
service_name='kinesis',
17+
aws_access_key_id='test',
18+
aws_secret_access_key='test',
19+
endpoint_url='http://localhost:4566')
20+
21+
kinesis.create_stream(StreamName='test', ShardCount=1)
22+
time.sleep(10)
23+
24+
response = kinesis.list_streams()
25+
self.assertGreater(len(response.get('StreamNames', [])),0)

0 commit comments

Comments
 (0)