Skip to content

Commit 39c4208

Browse files
Update to latest LocalStack container and support unified port mode (#2825)
Co-authored-by: Richard North <[email protected]>
1 parent c3f53b3 commit 39c4208

File tree

5 files changed

+263
-48
lines changed

5 files changed

+263
-48
lines changed

core/src/main/java/org/testcontainers/utility/ComparableVersion.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public int compareTo(@NotNull ComparableVersion other) {
3232
return 0;
3333
}
3434

35+
public boolean isSemanticVersion() {
36+
return parts.length > 0;
37+
}
38+
3539
public boolean isLessThan(String other) {
3640
return this.compareTo(new ComparableVersion(other)) < 0;
3741
}

modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@
44
import com.amazonaws.auth.AWSStaticCredentialsProvider;
55
import com.amazonaws.auth.BasicAWSCredentials;
66
import com.amazonaws.client.builder.AwsClientBuilder;
7-
import lombok.Getter;
8-
import lombok.RequiredArgsConstructor;
9-
import lombok.experimental.FieldDefaults;
10-
import org.rnorth.ducttape.Preconditions;
11-
import org.testcontainers.containers.GenericContainer;
12-
import org.testcontainers.containers.wait.strategy.Wait;
13-
import org.testcontainers.utility.DockerImageName;
14-
import org.testcontainers.utility.TestcontainersConfiguration;
15-
167
import java.net.InetAddress;
178
import java.net.URI;
189
import java.net.URISyntaxException;
@@ -21,6 +12,16 @@
2112
import java.util.Arrays;
2213
import java.util.List;
2314
import java.util.stream.Collectors;
15+
import lombok.Getter;
16+
import lombok.RequiredArgsConstructor;
17+
import lombok.experimental.FieldDefaults;
18+
import lombok.extern.slf4j.Slf4j;
19+
import org.rnorth.ducttape.Preconditions;
20+
import org.testcontainers.containers.GenericContainer;
21+
import org.testcontainers.containers.wait.strategy.Wait;
22+
import org.testcontainers.utility.ComparableVersion;
23+
import org.testcontainers.utility.DockerImageName;
24+
import org.testcontainers.utility.TestcontainersConfiguration;
2425

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

35-
public static final String VERSION = "0.10.8";
37+
public static final String VERSION = "0.11.2";
38+
static final int PORT = 4566;
3639
private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL";
37-
3840
private final List<Service> services = new ArrayList<>();
3941

42+
/**
43+
* Whether or to assume that all APIs run on different ports (when <code>true</code>) or are
44+
* exposed on a single port (<code>false</code>). From the Localstack README:
45+
*
46+
* <blockquote>Note: Starting with version 0.11.0, all APIs are exposed via a single edge
47+
* service [...] The API-specific endpoints below are still left for backward-compatibility but
48+
* may get removed in a future release - please reconfigure your client SDKs to start using the
49+
* single edge endpoint URL!</blockquote>
50+
* <p>
51+
* Testcontainers will use the tag of the docker image to infer whether or not the used version
52+
* of Localstack supports this feature.
53+
*/
54+
private final boolean legacyMode;
55+
4056
/**
4157
* @deprecated use {@link LocalStackContainer(DockerImageName)} instead
4258
*/
@@ -53,13 +69,41 @@ public LocalStackContainer(String version) {
5369
this(DockerImageName.parse(TestcontainersConfiguration.getInstance().getLocalStackImage() + ":" + version));
5470
}
5571

72+
/**
73+
* @param dockerImageName image name to use for Localstack
74+
*/
5675
public LocalStackContainer(final DockerImageName dockerImageName) {
76+
this(dockerImageName, shouldRunInLegacyMode(dockerImageName.getVersionPart()));
77+
}
78+
79+
/**
80+
* @param dockerImageName image name to use for Localstack
81+
* @param useLegacyMode if true, each AWS service is exposed on a different port
82+
*/
83+
public LocalStackContainer(final DockerImageName dockerImageName, boolean useLegacyMode) {
5784
super(dockerImageName);
85+
this.legacyMode = useLegacyMode;
5886

5987
withFileSystemBind("//var/run/docker.sock", "/var/run/docker.sock");
6088
waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1));
6189
}
6290

91+
private static boolean shouldRunInLegacyMode(String version) {
92+
if (version.equals("latest")) {
93+
return false;
94+
}
95+
96+
ComparableVersion comparableVersion = new ComparableVersion(version);
97+
if (comparableVersion.isSemanticVersion()) {
98+
boolean versionRequiresLegacyMode = comparableVersion.isLessThan("0.11");
99+
return versionRequiresLegacyMode;
100+
}
101+
102+
log.warn("Version {} is not a semantic version, LocalStack will run in legacy mode.", version);
103+
log.warn("Consider using \"LocalStackContainer(DockerImageName dockerImageName, boolean legacyMode)\" constructor if you want to disable legacy mode.");
104+
return true;
105+
}
106+
63107
@Override
64108
protected void configure() {
65109
super.configure();
@@ -81,9 +125,14 @@ protected void configure() {
81125
}
82126
logger().info("{} environment variable set to {} ({})", HOSTNAME_EXTERNAL_ENV_VAR, getEnvMap().get(HOSTNAME_EXTERNAL_ENV_VAR), hostnameExternalReason);
83127

84-
for (Service service : services) {
85-
addExposedPort(service.getPort());
86-
}
128+
exposePorts();
129+
}
130+
131+
private void exposePorts() {
132+
services.stream()
133+
.map(this::getServicePort)
134+
.distinct()
135+
.forEach(this::addExposedPort);
87136
}
88137

89138
/**
@@ -154,12 +203,16 @@ public URI getEndpointOverride(Service service) {
154203
return new URI("http://" +
155204
ipAddress +
156205
":" +
157-
getMappedPort(service.getPort()));
206+
getMappedPort(getServicePort(service)));
158207
} catch (UnknownHostException | URISyntaxException e) {
159208
throw new IllegalStateException("Cannot obtain endpoint URL", e);
160209
}
161210
}
162211

212+
private int getServicePort(Service service) {
213+
return legacyMode ? service.port : PORT;
214+
}
215+
163216
/**
164217
* Provides a {@link AWSCredentialsProvider} that is preconfigured to communicate with a given simulated service.
165218
* The credentials provider should be set in the AWS Java SDK when building a client, e.g.:
@@ -271,5 +324,13 @@ public enum Service {
271324
String localStackName;
272325

273326
int port;
327+
328+
@Deprecated
329+
/*
330+
Since version 0.11, LocalStack exposes all services on a single (4566) port.
331+
*/
332+
public int getPort() {
333+
return port;
334+
}
274335
}
275336
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package org.testcontainers.containers.localstack;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.SneakyThrows;
5+
import org.junit.After;
6+
import org.junit.BeforeClass;
7+
import org.junit.Test;
8+
import org.junit.experimental.runners.Enclosed;
9+
import org.junit.runner.RunWith;
10+
import org.junit.runners.Parameterized;
11+
12+
import java.io.BufferedReader;
13+
import java.io.IOException;
14+
import java.io.InputStream;
15+
import java.io.InputStreamReader;
16+
import java.util.Arrays;
17+
import java.util.function.Consumer;
18+
19+
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
20+
import static org.rnorth.visibleassertions.VisibleAssertions.assertNotEquals;
21+
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
22+
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
23+
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS;
24+
import static org.testcontainers.containers.localstack.LocalstackTestImages.LOCALSTACK_IMAGE;
25+
26+
@RunWith(Enclosed.class)
27+
public class LegacyModeTest {
28+
29+
@RunWith(Parameterized.class)
30+
@AllArgsConstructor
31+
public static class Off {
32+
private final String description;
33+
private final LocalStackContainer localstack;
34+
35+
@Parameterized.Parameters(name = "{0}")
36+
public static Iterable<Object[]> constructors() {
37+
return Arrays.asList(new Object[][]{
38+
{"default constructor", new LocalStackContainer(LOCALSTACK_IMAGE)},
39+
{"latest", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("latest"))},
40+
{"0.11.1", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.11.1"))},
41+
{"0.7.0 with legacy = off", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.7.0"), false)}
42+
});
43+
}
44+
45+
@Test
46+
public void samePortIsExposedForAllServices() {
47+
localstack.withServices(S3, SQS);
48+
localstack.start();
49+
50+
assertTrue("A single port is exposed", localstack.getExposedPorts().size() == 1);
51+
assertEquals(
52+
"Endpoint overrides are different",
53+
localstack.getEndpointOverride(S3).toString(),
54+
localstack.getEndpointOverride(SQS).toString());
55+
assertEquals(
56+
"Endpoint configuration have different endpoints",
57+
localstack.getEndpointConfiguration(S3).getServiceEndpoint(),
58+
localstack.getEndpointConfiguration(SQS).getServiceEndpoint());
59+
}
60+
61+
@After
62+
public void cleanup() {
63+
if (localstack != null) localstack.stop();
64+
}
65+
}
66+
67+
@RunWith(Parameterized.class)
68+
@AllArgsConstructor
69+
public static class On {
70+
private final String description;
71+
private final LocalStackContainer localstack;
72+
73+
@BeforeClass
74+
public static void createCustomTag() {
75+
run("docker pull localstack/localstack:latest");
76+
run("docker tag localstack/localstack:latest localstack/localstack:custom");
77+
}
78+
79+
@Parameterized.Parameters(name = "{0}")
80+
public static Iterable<Object[]> constructors() {
81+
return Arrays.asList(new Object[][]{
82+
{"0.10.7", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.10.7"))},
83+
{"custom", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("custom"))},
84+
{"0.11.1 with legacy = on", new LocalStackContainer(LOCALSTACK_IMAGE.withTag("0.11.1"), true)}
85+
});
86+
}
87+
88+
@Test
89+
public void differentPortsAreExposed() {
90+
localstack.withServices(S3, SQS);
91+
localstack.start();
92+
93+
assertTrue("Multiple ports are exposed", localstack.getExposedPorts().size() > 1);
94+
assertNotEquals(
95+
"Endpoint overrides are different",
96+
localstack.getEndpointOverride(S3).toString(),
97+
localstack.getEndpointOverride(SQS).toString());
98+
assertNotEquals(
99+
"Endpoint configuration have different endpoints",
100+
localstack.getEndpointConfiguration(S3).getServiceEndpoint(),
101+
localstack.getEndpointConfiguration(SQS).getServiceEndpoint());
102+
}
103+
104+
@After
105+
public void cleanup() {
106+
if (localstack != null) localstack.stop();
107+
}
108+
}
109+
110+
@SneakyThrows
111+
private static void run(String command) {
112+
Process process = Runtime.getRuntime().exec(command);
113+
join(process.getInputStream(), System.out::println);
114+
join(process.getErrorStream(), System.err::println);
115+
process.waitFor();
116+
if (process.exitValue() != 0)
117+
throw new RuntimeException("Failed to execute " + command);
118+
}
119+
120+
private static void join(InputStream stream, Consumer<String> logger) throws IOException {
121+
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream));
122+
String line;
123+
while ((line = bufferedReader.readLine()) != null) {
124+
logger.accept(line);
125+
}
126+
}
127+
128+
}

0 commit comments

Comments
 (0)