Skip to content

Commit c90c854

Browse files
committed
Add LocalStack container implementation under org.testcontainers.localstack
1 parent 06ad992 commit c90c854

File tree

5 files changed

+246
-133
lines changed

5 files changed

+246
-133
lines changed

docs/modules/localstack.md

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,21 @@ Testcontainers module for [LocalStack](http://localstack.cloud/), 'a fully funct
44

55
## Usage example
66

7-
Running LocalStack as a stand-in for AWS S3 during a test:
7+
You can start a LocalStack container instance from any Java application by using:
88

9-
```java
10-
DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:3.5.0");
11-
12-
@Rule
13-
public LocalStackContainer localstack = new LocalStackContainer(localstackImage)
14-
.withServices(S3);
15-
```
9+
<!--codeinclude-->
10+
[Container creation](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:container
11+
<!--/codeinclude-->
1612

1713
## Creating a client using AWS SDK
1814

1915
<!--codeinclude-->
20-
[AWS SDK V2](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:with_aws_sdk_v2
16+
[AWS SDK V2](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:with_aws_sdk_v2
2117
<!--/codeinclude-->
2218

2319
Environment variables listed in [Localstack's README](https://github.com/localstack/localstack#configurations) may be used to customize Localstack's configuration.
2420
Use the `.withEnv(key, value)` method on `LocalStackContainer` to apply configuration settings.
2521

26-
## `HOSTNAME_EXTERNAL` and hostname-sensitive services
27-
28-
Some Localstack APIs, such as SQS, require the container to be aware of the hostname that it is accessible on - for example, for construction of queue URLs in responses.
29-
30-
Testcontainers will inform Localstack of the best hostname automatically, using the `HOSTNAME_EXTERNAL` environment variable:
31-
32-
* when running the Localstack container directly without a custom network defined, it is expected that all calls to the container will be from the test host. As such, the container address will be used (typically localhost or the address where the Docker daemon is running).
33-
34-
<!--codeinclude-->
35-
[Localstack container running without a custom network](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:without_network
36-
<!--/codeinclude-->
37-
38-
* when running the Localstack container [with a custom network defined](/features/networking/#advanced-networking), it is expected that all calls to the container will be **from other containers on that network**. `HOSTNAME_EXTERNAL` will be set to the *last* network alias that has been configured for the Localstack container.
39-
40-
<!--codeinclude-->
41-
[Localstack container running with a custom network](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:with_network
42-
<!--/codeinclude-->
43-
44-
* Other usage scenarios, such as where the Localstack container is used from both the test host and containers on a custom network are not automatically supported. If you have this use case, you should set `HOSTNAME_EXTERNAL` manually.
45-
4622
## Adding this module to your project dependencies
4723

4824
Add the following dependency to your `pom.xml`/`build.gradle` file:

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
* Supported images: {@code localstack/localstack}, {@code localstack/localstack-pro}
2929
* <p>
3030
* Exposed ports: 4566
31+
*
32+
* @deprecated use {@link org.testcontainers.localstack.LocalStackContainer} instead.
3133
*/
3234
@Slf4j
35+
@Deprecated
3336
public class LocalStackContainer extends GenericContainer<LocalStackContainer> {
3437

3538
static final int PORT = 4566;
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package org.testcontainers.localstack;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.testcontainers.DockerClientFactory;
6+
import org.testcontainers.containers.GenericContainer;
7+
import org.testcontainers.containers.wait.strategy.Wait;
8+
import org.testcontainers.images.builder.Transferable;
9+
import org.testcontainers.utility.DockerImageName;
10+
11+
import java.net.InetAddress;
12+
import java.net.URI;
13+
import java.net.URISyntaxException;
14+
import java.net.UnknownHostException;
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import java.util.stream.Collectors;
19+
20+
/**
21+
* Testcontainers implementation for LocalStack.
22+
* <p>
23+
* Supported images: {@code localstack/localstack}, {@code localstack/localstack-pro}
24+
* <p>
25+
* Exposed ports: 4566
26+
*/
27+
@Slf4j
28+
public class LocalStackContainer extends GenericContainer<LocalStackContainer> {
29+
30+
static final int PORT = 4566;
31+
32+
private final List<String> services = new ArrayList<>();
33+
34+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("localstack/localstack");
35+
36+
private static final DockerImageName LOCALSTACK_PRO_IMAGE_NAME = DockerImageName.parse("localstack/localstack-pro");
37+
38+
private static final String DEFAULT_REGION = "us-east-1";
39+
40+
private static final String DEFAULT_AWS_ACCESS_KEY_ID = "test";
41+
42+
private static final String DEFAULT_AWS_SECRET_ACCESS_KEY = "test";
43+
44+
private static final String STARTER_SCRIPT = "/testcontainers_start.sh";
45+
46+
/**
47+
* @param dockerImageName image name to use for Localstack
48+
*/
49+
public LocalStackContainer(final String dockerImageName) {
50+
this(DockerImageName.parse(dockerImageName));
51+
}
52+
53+
/**
54+
* @param dockerImageName image name to use for Localstack
55+
*/
56+
public LocalStackContainer(final DockerImageName dockerImageName) {
57+
super(dockerImageName);
58+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, LOCALSTACK_PRO_IMAGE_NAME);
59+
60+
withExposedPorts(PORT);
61+
withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock");
62+
waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1));
63+
withCreateContainerCmdModifier(cmd -> {
64+
cmd.withEntrypoint(
65+
"sh",
66+
"-c",
67+
"while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT
68+
);
69+
});
70+
}
71+
72+
@Override
73+
protected void configure() {
74+
if (!services.isEmpty()) {
75+
withEnv("SERVICES", String.join(",", this.services));
76+
}
77+
}
78+
79+
@Override
80+
protected void containerIsStarting(InspectContainerResponse containerInfo) {
81+
String command = "#!/bin/bash\n";
82+
command += "export LAMBDA_DOCKER_FLAGS=" + configureServiceContainerLabels("LAMBDA_DOCKER_FLAGS") + "\n";
83+
command += "export ECS_DOCKER_FLAGS=" + configureServiceContainerLabels("ECS_DOCKER_FLAGS") + "\n";
84+
command += "export EC2_DOCKER_FLAGS=" + configureServiceContainerLabels("EC2_DOCKER_FLAGS") + "\n";
85+
command += "export BATCH_DOCKER_FLAGS=" + configureServiceContainerLabels("BATCH_DOCKER_FLAGS") + "\n";
86+
command += "/usr/local/bin/docker-entrypoint.sh\n";
87+
copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT);
88+
}
89+
90+
/**
91+
* Configure the LocalStack container to include the default testcontainers labels on all spawned lambda containers
92+
* Necessary to properly clean up lambda containers even if the LocalStack container is killed before it gets the
93+
* chance.
94+
* @return the lambda container labels as a string
95+
*/
96+
private String configureServiceContainerLabels(String existingEnvFlagKey) {
97+
String internalMarkerFlags = internalMarkerLabels();
98+
String existingFlags = getEnvMap().get(existingEnvFlagKey);
99+
if (existingFlags != null) {
100+
internalMarkerFlags = existingFlags + " " + internalMarkerFlags;
101+
}
102+
return "\"" + internalMarkerFlags + "\"";
103+
}
104+
105+
/**
106+
* Provides a docker argument string including all default labels set on testcontainers containers (excluding reuse labels)
107+
* @return Argument string in the format `-l key1=value1 -l key2=value2`
108+
*/
109+
private String internalMarkerLabels() {
110+
return getContainerInfo()
111+
.getConfig()
112+
.getLabels()
113+
.entrySet()
114+
.stream()
115+
.filter(entry -> entry.getKey().startsWith(DockerClientFactory.TESTCONTAINERS_LABEL))
116+
.filter(entry -> {
117+
return (
118+
!entry.getKey().equals("org.testcontainers.hash") &&
119+
!entry.getKey().equals("org.testcontainers.copied_files.hash")
120+
);
121+
})
122+
.map(entry -> String.format("-l %s=%s", entry.getKey(), entry.getValue()))
123+
.collect(Collectors.joining(" "));
124+
}
125+
126+
/**
127+
* Declare a set of simulated AWS services that should be launched by this container.
128+
* @param services one or more service names
129+
* @return this container object
130+
*/
131+
public LocalStackContainer withServices(String... services) {
132+
this.services.addAll(Arrays.asList(services));
133+
return self();
134+
}
135+
136+
/**
137+
* Provides an endpoint to communicate with LocalStack service.
138+
* The provided endpoint should be set in the AWS Java SDK v2 when building a client, e.g.:
139+
* <pre><code>S3Client s3 = S3Client
140+
.builder()
141+
.endpointOverride(localstack.getEndpoint())
142+
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
143+
localstack.getAccessKey(), localstack.getSecretKey()
144+
)))
145+
.region(Region.of(localstack.getRegion()))
146+
.build()
147+
</code></pre>
148+
* <p><strong>Please note that this method is only intended to be used for configuring AWS SDK clients
149+
* that are running on the test host. If other containers need to call this one, they should be configured
150+
* specifically to do so using a Docker network and appropriate addressing.</strong></p>
151+
*
152+
* @return an {@link URI} endpoint
153+
*/
154+
public URI getEndpoint() {
155+
try {
156+
final String address = getHost();
157+
// resolve IP address and use that as the endpoint so that path-style access is automatically used for S3
158+
String ipAddress = InetAddress.getByName(address).getHostAddress();
159+
return new URI("http://" + ipAddress + ":" + getMappedPort(PORT));
160+
} catch (UnknownHostException | URISyntaxException e) {
161+
throw new IllegalStateException("Cannot obtain endpoint URL", e);
162+
}
163+
}
164+
165+
/**
166+
* Provides a default access key that is preconfigured to communicate with a given simulated service.
167+
* <a href="https://github.com/localstack/localstack/blob/master/doc/interaction/README.md?plain=1#L32">AWS Access Key</a>
168+
* The access key can be used to construct AWS SDK v2 clients:
169+
* <pre><code>S3Client s3 = S3Client
170+
.builder()
171+
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
172+
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
173+
localstack.getAccessKey(), localstack.getSecretKey()
174+
)))
175+
.region(Region.of(localstack.getRegion()))
176+
.build()
177+
</code></pre>
178+
* @return a default access key
179+
*/
180+
public String getAccessKey() {
181+
return this.getEnvMap().getOrDefault("AWS_ACCESS_KEY_ID", DEFAULT_AWS_ACCESS_KEY_ID);
182+
}
183+
184+
/**
185+
* Provides a default secret key that is preconfigured to communicate with a given simulated service.
186+
* <a href="https://github.com/localstack/localstack/blob/master/doc/interaction/README.md?plain=1#L32">AWS Secret Key</a>
187+
* The secret key can be used to construct AWS SDK v2 clients:
188+
* <pre><code>S3Client s3 = S3Client
189+
.builder()
190+
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
191+
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
192+
localstack.getAccessKey(), localstack.getSecretKey()
193+
)))
194+
.region(Region.of(localstack.getRegion()))
195+
.build()
196+
</code></pre>
197+
* @return a default secret key
198+
*/
199+
public String getSecretKey() {
200+
return this.getEnvMap().getOrDefault("AWS_SECRET_ACCESS_KEY", DEFAULT_AWS_SECRET_ACCESS_KEY);
201+
}
202+
203+
/**
204+
* Provides a default region that is preconfigured to communicate with a given simulated service.
205+
* The region can be used to construct AWS SDK v2 clients:
206+
* <pre><code>S3Client s3 = S3Client
207+
.builder()
208+
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
209+
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
210+
localstack.getAccessKey(), localstack.getSecretKey()
211+
)))
212+
.region(Region.of(localstack.getRegion()))
213+
.build()
214+
</code></pre>
215+
* @return a default region
216+
*/
217+
public String getRegion() {
218+
return this.getEnvMap().getOrDefault("DEFAULT_REGION", DEFAULT_REGION);
219+
}
220+
}

modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import org.testcontainers.utility.DockerImageName;
44

55
public interface LocalstackTestImages {
6-
DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:4.7.0");
6+
DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:4.9.2");
77

88
DockerImageName LOCALSTACK_0_10_IMAGE = LOCALSTACK_IMAGE.withTag("0.10.7");
99

0 commit comments

Comments
 (0)