Repository: localstack/localstack

Issue:
Create simple JUnit test to demonstrate integration with Java
We should provide a seamless integration of LocalStack with other languages apart from Python.

Let's start with Java - add an exemplary JUnit test class that spins up local cloud services as part of a `setup` method, runs some simple tests, and finally terminates the services on `teardown`.


Discussion:

Resolving patch:
diff --git a/localstack/mock/infra.py b/localstack/mock/infra.py
--- a/localstack/mock/infra.py
+++ b/localstack/mock/infra.py
@@ -4,6 +4,7 @@
 import re
 import sys
 import time
+import signal
 import traceback
 import logging
 import requests
@@ -23,8 +24,9 @@
 THIS_PATH = os.path.dirname(os.path.realpath(__file__))
 ROOT_PATH = os.path.realpath(os.path.join(THIS_PATH, '..'))
 
-# will be set to True if user hits CTRL-C
-KILLED = False
+# flag to indicate whether signal handlers have been set up already
+SIGNAL_HANDLERS_SETUP = False
+INFRA_STOPPED = False
 
 # cache table definitions - used for testing
 TABLE_DEFINITIONS = {}
@@ -46,7 +48,22 @@
 LOGGER = logging.getLogger(__name__)
 
 
+def register_signal_handlers():
+    global SIGNAL_HANDLERS_SETUP
+    if SIGNAL_HANDLERS_SETUP:
+        return
+
+    # register signal handlers
+    def signal_handler(signal, frame):
+        stop_infra()
+        os._exit(0)
+    signal.signal(signal.SIGTERM, signal_handler)
+    signal.signal(signal.SIGINT, signal_handler)
+    SIGNAL_HANDLERS_SETUP = True
+
+
 def do_run(cmd, async):
+    sys.stdout.flush()
     if async:
         t = ShellCommandThread(cmd)
         t.start()
@@ -193,6 +210,9 @@ def start_lambda(port=DEFAULT_PORT_LAMBDA, async=False):
 
 
 def stop_infra():
+    global INFRA_STOPPED
+    if INFRA_STOPPED:
+        return
     generic_proxy.QUIET = True
     common.cleanup(files=True, quiet=True)
     common.cleanup_resources()
@@ -200,6 +220,7 @@ def stop_infra():
     time.sleep(1)
     # TODO: optimize this (takes too long currently)
     # check_infra(retries=2, expect_shutdown=True)
+    INFRA_STOPPED = True
 
 
 def check_infra_kinesis(expect_shutdown=False):
@@ -306,6 +327,8 @@ def start_infra(async=False, dynamodb_update_listener=None, kinesis_update_liste
         # set environment
         os.environ['AWS_REGION'] = DEFAULT_REGION
         os.environ['ENV'] = ENV_DEV
+        # register signal handlers
+        register_signal_handlers()
         # make sure AWS credentials are configured, otherwise boto3 bails on us
         check_aws_credentials()
         # install libs if not present
@@ -338,8 +361,14 @@ def start_infra(async=False, dynamodb_update_listener=None, kinesis_update_liste
         time.sleep(3)
         # check that all infra components are up and running
         check_infra(apis=apis)
+        print('Ready.')
+        sys.stdout.flush()
         if not async and thread:
-            thread.join()
+            # this is a bit of an ugly hack, but we need to make sure that we
+            # stay in the execution context of the main thread, otherwise our
+            # signal handlers don't work
+            while True:
+                time.sleep(1)
         return thread
     except KeyboardInterrupt, e:
         print("Shutdown")
@@ -536,6 +565,8 @@ def dynamodb_extract_keys(item, table_name):
 
 if __name__ == '__main__':
     print('Starting local dev environment. CTRL-C to quit.')
+    # set up logging
     logging.basicConfig(level=logging.WARNING)
     logging.getLogger('elasticsearch').setLevel(logging.ERROR)
+    # fire it up!
     start_infra()
diff --git a/localstack/utils/aws/aws_stack.py b/localstack/utils/aws/aws_stack.py
--- a/localstack/utils/aws/aws_stack.py
+++ b/localstack/utils/aws/aws_stack.py
@@ -431,13 +431,10 @@ def assume_role(role_arn, update_global_session=True):
 
 # Using a timer to loop through the assume role function forever......
 def loop_assume_role(role_arn, timeout=DEFAULT_TIMER_LOOP_SECONDS):
+    # Do an initial assume role
+    assume_role(role_arn, True)
 
-    def do_assume_role():
-        # this will do it once (we ignore the return value as we default to update_global_session=True)
-        assume_role(role_arn, True)
-
-        # we call it again after it completes
-        loop_assume_role(role_arn, timeout)
-
-    t = Timer(timeout, do_assume_role)
+    # Create timer to loop through function
+    t = Timer(timeout, loop_assume_role, args=[role_arn, timeout])
+    t.daemon = True
     t.start()
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -76,8 +76,8 @@ def run(self):
 
     setup(
         name='localstack',
-        version='0.2.10',
-        description='Provides an easy-to-use test/mocking framework for developing Cloud applications',
+        version='0.3.0',
+        description='An easy-to-use test/mocking framework for developing Cloud applications',
         author='Waldemar Hummer (Atlassian)',
         author_email='waldemar.hummer@gmail.com',
         url='https://bitbucket.org/atlassian/localstack',


Test patch:
diff --git a/ext/java/src/test/java/com/atlassian/localstack/TestRunnerTest.java b/ext/java/src/test/java/com/atlassian/localstack/TestRunnerTest.java
new file mode 100644
--- /dev/null
+++ b/ext/java/src/test/java/com/atlassian/localstack/TestRunnerTest.java
@@ -0,0 +1,64 @@
+package com.atlassian.localstack;
+
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.kinesis.AmazonKinesis;
+import com.amazonaws.services.kinesis.AmazonKinesisClient;
+import com.amazonaws.services.kinesis.model.ListStreamsResult;
+import com.amazonaws.services.lambda.AWSLambda;
+import com.amazonaws.services.lambda.AWSLambdaClientBuilder;
+import com.amazonaws.services.lambda.model.ListFunctionsResult;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.services.s3.model.Bucket;
+
+@RunWith(LocalstackTestRunner.class)
+public class TestRunnerTest {
+
+	private static final String DEFAULT_REGION = "us-east-1";
+	private static final String TEST_ACCESS_KEY = "test";
+	private static final String TEST_SECRET_KEY = "test";
+	private static final AWSCredentials TEST_CREDENTIALS = new BasicAWSCredentials(TEST_ACCESS_KEY, TEST_SECRET_KEY);
+
+	static {
+		/* Need to disable CBOR protocol, see:
+		 * https://github.com/mhart/kinesalite/blob/master/README.md#cbor-protocol-issues-with-the-java-sdk
+		 */
+		TestUtils.setEnv("AWS_CBOR_DISABLE", "1");
+	}
+
+	@Test
+	public void testLocalKinesisAPI() {
+		AmazonKinesis kinesis = new AmazonKinesisClient(TEST_CREDENTIALS);
+		kinesis.setEndpoint(LocalstackTestRunner.getEndpointKinesis());
+		ListStreamsResult streams = kinesis.listStreams();
+		Assert.assertNotNull(streams.getStreamNames());
+	}
+
+	@Test
+	public void testLocalS3API() {
+		AmazonS3 s3 = new AmazonS3Client(TEST_CREDENTIALS);
+		s3.setEndpoint(LocalstackTestRunner.getEndpointS3());
+		List<Bucket> buckets = s3.listBuckets();
+		Assert.assertNotNull(buckets);
+	}
+
+	@Test
+	public void testLocalLambdaAPI() {
+		AWSLambda lambda = AWSLambdaClientBuilder.standard().withEndpointConfiguration(
+				new AwsClientBuilder.EndpointConfiguration(
+						LocalstackTestRunner.getEndpointLambda(), DEFAULT_REGION)).withCredentials(
+								new AWSStaticCredentialsProvider(TEST_CREDENTIALS)).build();
+		ListFunctionsResult functions = lambda.listFunctions();
+		Assert.assertNotNull(functions.getFunctions());
+	}
+
+}
diff --git a/ext/java/src/test/java/com/atlassian/localstack/TestUtils.java b/ext/java/src/test/java/com/atlassian/localstack/TestUtils.java
new file mode 100644
--- /dev/null
+++ b/ext/java/src/test/java/com/atlassian/localstack/TestUtils.java
@@ -0,0 +1,51 @@
+package com.atlassian.localstack;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@SuppressWarnings("all")
+public class TestUtils {
+
+	protected static void setEnv(String key, String value) {
+		Map<String, String> newEnv = new HashMap<String, String>();
+		newEnv.put(key, value);
+		setEnv(newEnv);
+	}
+
+	protected static void setEnv(Map<String, String> newEnv) {
+		try {
+			Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
+			Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
+			theEnvironmentField.setAccessible(true);
+			Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
+			env.putAll(newEnv);
+			Field theCaseInsensitiveEnvironmentField = processEnvironmentClass
+					.getDeclaredField("theCaseInsensitiveEnvironment");
+			theCaseInsensitiveEnvironmentField.setAccessible(true);
+			Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
+			cienv.putAll(newEnv);
+		} catch (NoSuchFieldException e) {
+			try {
+				Class[] classes = Collections.class.getDeclaredClasses();
+				Map<String, String> env = System.getenv();
+				for (Class cl : classes) {
+					if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
+						Field field = cl.getDeclaredField("m");
+						field.setAccessible(true);
+						Object obj = field.get(env);
+						Map<String, String> map = (Map<String, String>) obj;
+						map.clear();
+						map.putAll(newEnv);
+					}
+				}
+			} catch (Exception e2) {
+				e2.printStackTrace();
+			}
+		} catch (Exception e1) {
+			e1.printStackTrace();
+		}
+	}
+
+}