Container for Atlassian Labs Localstack, 'A fully functional local AWS cloud stack'.
@@ -30,13 +31,28 @@
* {@link LocalStackContainer#getDefaultCredentialsProvider()}
* be used to obtain compatible endpoint configuration and credentials, respectively.
*/
+@Slf4j
public class LocalStackContainer extends GenericContainer {
- public static final String VERSION = "0.10.8";
+ public static final String VERSION = "0.11.2";
+ static final int PORT = 4566;
private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL";
-
private final List services = new ArrayList<>();
+ /**
+ * Whether or to assume that all APIs run on different ports (when true) or are
+ * exposed on a single port (false). From the Localstack README:
+ *
+ *
Note: Starting with version 0.11.0, all APIs are exposed via a single edge
+ * service [...] The API-specific endpoints below are still left for backward-compatibility but
+ * may get removed in a future release - please reconfigure your client SDKs to start using the
+ * single edge endpoint URL!
+ *
+ * Testcontainers will use the tag of the docker image to infer whether or not the used version
+ * of Localstack supports this feature.
+ */
+ private final boolean legacyMode;
+
/**
* @deprecated use {@link LocalStackContainer(DockerImageName)} instead
*/
@@ -53,13 +69,41 @@ public LocalStackContainer(String version) {
this(DockerImageName.parse(TestcontainersConfiguration.getInstance().getLocalStackImage() + ":" + version));
}
+ /**
+ * @param dockerImageName image name to use for Localstack
+ */
public LocalStackContainer(final DockerImageName dockerImageName) {
+ this(dockerImageName, shouldRunInLegacyMode(dockerImageName.getVersionPart()));
+ }
+
+ /**
+ * @param dockerImageName image name to use for Localstack
+ * @param useLegacyMode if true, each AWS service is exposed on a different port
+ */
+ public LocalStackContainer(final DockerImageName dockerImageName, boolean useLegacyMode) {
super(dockerImageName);
+ this.legacyMode = useLegacyMode;
withFileSystemBind("//var/run/docker.sock", "/var/run/docker.sock");
waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1));
}
+ private static boolean shouldRunInLegacyMode(String version) {
+ if (version.equals("latest")) {
+ return false;
+ }
+
+ ComparableVersion comparableVersion = new ComparableVersion(version);
+ if (comparableVersion.isSemanticVersion()) {
+ boolean versionRequiresLegacyMode = comparableVersion.isLessThan("0.11");
+ return versionRequiresLegacyMode;
+ }
+
+ log.warn("Version {} is not a semantic version, LocalStack will run in legacy mode.", version);
+ log.warn("Consider using \"LocalStackContainer(DockerImageName dockerImageName, boolean legacyMode)\" constructor if you want to disable legacy mode.");
+ return true;
+ }
+
@Override
protected void configure() {
super.configure();
@@ -81,9 +125,14 @@ protected void configure() {
}
logger().info("{} environment variable set to {} ({})", HOSTNAME_EXTERNAL_ENV_VAR, getEnvMap().get(HOSTNAME_EXTERNAL_ENV_VAR), hostnameExternalReason);
- for (Service service : services) {
- addExposedPort(service.getPort());
- }
+ exposePorts();
+ }
+
+ private void exposePorts() {
+ services.stream()
+ .map(this::getServicePort)
+ .distinct()
+ .forEach(this::addExposedPort);
}
/**
@@ -154,12 +203,16 @@ public URI getEndpointOverride(Service service) {
return new URI("http://" +
ipAddress +
":" +
- getMappedPort(service.getPort()));
+ getMappedPort(getServicePort(service)));
} catch (UnknownHostException | URISyntaxException e) {
throw new IllegalStateException("Cannot obtain endpoint URL", e);
}
}
+ private int getServicePort(Service service) {
+ return legacyMode ? service.port : PORT;
+ }
+
/**
* Provides a {@link AWSCredentialsProvider} that is preconfigured to communicate with a given simulated service.
* The credentials provider should be set in the AWS Java SDK when building a client, e.g.:
@@ -271,5 +324,13 @@ public enum Service {
String localStackName;
int port;
+
+ @Deprecated
+ /*
+ Since version 0.11, LocalStack exposes all services on a single (4566) port.
+ */
+ public int getPort() {
+ return port;
+ }
}
}
diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java
new file mode 100644
index 00000000000..f50846b091f
--- /dev/null
+++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java
@@ -0,0 +1,128 @@
+package org.testcontainers.containers.localstack;
+
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
+import static org.rnorth.visibleassertions.VisibleAssertions.assertNotEquals;
+import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
+import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
+import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS;
+import static org.testcontainers.containers.localstack.LocalstackTestImages.LOCALSTACK_IMAGE;
+
+@RunWith(Enclosed.class)
+public class LegacyModeTest {
+
+ @RunWith(Parameterized.class)
+ @AllArgsConstructor
+ public static class Off {
+ private final String description;
+ private final LocalStackContainer localstack;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Iterable