Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
858ec54
:arrow_up: latest default LocalStack container
artamonovkirill May 31, 2020
b310e3e
:recycle: rely on ComparableVersion for version comparison. added Loc…
artamonovkirill Jun 6, 2020
df1d96e
:white_check_mark: wait for pull and tag commands to finish
artamonovkirill Jun 7, 2020
68ee3bc
WIP
rnorth Jun 25, 2020
46c7e1b
WIP
rnorth Jun 25, 2020
054b637
WIP
rnorth Jun 25, 2020
196f1a9
WIP
rnorth Jun 29, 2020
f3fe807
Tidy up test image references
rnorth Jul 2, 2020
c523b38
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 2, 2020
296f9f7
Update docs re contrainer creation and specifying versions
rnorth Jul 2, 2020
aa94289
Update Javadocs with deprecation warnings.
rnorth Jul 2, 2020
77aa418
Add `asCanonicalNameString()` method on `DockerImageName`
rnorth Jul 3, 2020
592f08a
Use static factory method instead of constructor
rnorth Jul 3, 2020
1edc307
Use constant for docker registry image name
rnorth Jul 4, 2020
113f3b3
Simplify image usage in ReusabilityUnitTests
rnorth Jul 4, 2020
fc7f625
Use `DockerImageName` instead of `String` for image name
rnorth Jul 4, 2020
cd7e13b
Inline Mongo image name parsing
rnorth Jul 4, 2020
a35b3c4
Fix constructor delegation
rnorth Jul 4, 2020
868f962
Undo deprecation of SocatContainer no-args constructor
rnorth Jul 4, 2020
faf1217
Update references to DockerImageName.parse in example code
rnorth Jul 4, 2020
ee45003
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 4, 2020
8960ceb
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 6, 2020
f4e314d
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 16, 2020
f0371ec
Merge branch 'deprecate-ambiguous-constructors' into chore/latest-loc…
rnorth Jul 17, 2020
3cf1a17
Integrate changes
rnorth Jul 17, 2020
b38d73c
Merge master into chore/latest-localstack
rnorth Jul 19, 2020
d3b4d43
Clarify meaning of `legacyMode` in a Javadoc comment
rnorth Jul 19, 2020
71838fb
Inline image name references used in docs
rnorth Jul 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public int compareTo(@NotNull ComparableVersion other) {
return 0;
}

public boolean isSemanticVersion() {
return parts.length > 0;
}

public boolean isLessThan(String other) {
return this.compareTo(new ComparableVersion(other)) < 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.rnorth.ducttape.Preconditions;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
Expand All @@ -21,6 +12,16 @@
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.rnorth.ducttape.Preconditions;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.TestcontainersConfiguration;

/**
* <p>Container for Atlassian Labs Localstack, 'A fully functional local AWS cloud stack'.</p>
Expand All @@ -30,13 +31,28 @@
* {@link LocalStackContainer#getDefaultCredentialsProvider()}
* be used to obtain compatible endpoint configuration and credentials, respectively.</p>
*/
@Slf4j
public class LocalStackContainer extends GenericContainer<LocalStackContainer> {

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<Service> services = new ArrayList<>();

/**
* Whether or to assume that all APIs run on different ports (when <code>true</code>) or are
* exposed on a single port (<code>false</code>). From the Localstack README:
*
* <blockquote>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!</blockquote>
* <p>
* 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
*/
Expand All @@ -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();
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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.:
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Object[]> constructors() {
return Arrays.asList(new Object[][]{
{"default constructor", new LocalStackContainer(LOCALSTACK_IMAGE)},
{"latest", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("latest"))},
{"0.11.1", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.11.1"))},
{"0.7.0 with legacy = off", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.7.0"), false)}
});
}

@Test
public void samePortIsExposedForAllServices() {
localstack.withServices(S3, SQS);
localstack.start();

assertTrue("A single port is exposed", localstack.getExposedPorts().size() == 1);
assertEquals(
"Endpoint overrides are different",
localstack.getEndpointOverride(S3).toString(),
localstack.getEndpointOverride(SQS).toString());
assertEquals(
"Endpoint configuration have different endpoints",
localstack.getEndpointConfiguration(S3).getServiceEndpoint(),
localstack.getEndpointConfiguration(SQS).getServiceEndpoint());
}

@After
public void cleanup() {
if (localstack != null) localstack.stop();
}
}

@RunWith(Parameterized.class)
@AllArgsConstructor
public static class On {
private final String description;
private final LocalStackContainer localstack;

@BeforeClass
public static void createCustomTag() {
run("docker pull localstack/localstack:latest");
run("docker tag localstack/localstack:latest localstack/localstack:custom");
}

@Parameterized.Parameters(name = "{0}")
public static Iterable<Object[]> constructors() {
return Arrays.asList(new Object[][]{
{"0.10.7", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.10.7"))},
{"custom", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("custom"))},
{"0.11.1 with legacy = on", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.11.1"), true)}
});
}

@Test
public void differentPortsAreExposed() {
localstack.withServices(S3, SQS);
localstack.start();

assertTrue("Multiple ports are exposed", localstack.getExposedPorts().size() > 1);
assertNotEquals(
"Endpoint overrides are different",
localstack.getEndpointOverride(S3).toString(),
localstack.getEndpointOverride(SQS).toString());
assertNotEquals(
"Endpoint configuration have different endpoints",
localstack.getEndpointConfiguration(S3).getServiceEndpoint(),
localstack.getEndpointConfiguration(SQS).getServiceEndpoint());
}

@After
public void cleanup() {
if (localstack != null) localstack.stop();
}
}

@SneakyThrows
private static void run(String command) {
Process process = Runtime.getRuntime().exec(command);
join(process.getInputStream(), System.out::println);
join(process.getErrorStream(), System.err::println);
process.waitFor();
if (process.exitValue() != 0)
throw new RuntimeException("Failed to execute " + command);
}

private static void join(InputStream stream, Consumer<String> logger) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = bufferedReader.readLine()) != null) {
logger.accept(line);
}
}

}
Loading