Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 5 additions & 29 deletions docs/modules/localstack.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,21 @@ Testcontainers module for [LocalStack](http://localstack.cloud/), 'a fully funct

## Usage example

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

```java
DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:3.5.0");

@Rule
public LocalStackContainer localstack = new LocalStackContainer(localstackImage)
.withServices(S3);
```
<!--codeinclude-->
[Container creation](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:container
<!--/codeinclude-->

## Creating a client using AWS SDK

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

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

## `HOSTNAME_EXTERNAL` and hostname-sensitive services

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.

Testcontainers will inform Localstack of the best hostname automatically, using the `HOSTNAME_EXTERNAL` environment variable:

* 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).

<!--codeinclude-->
[Localstack container running without a custom network](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:without_network
<!--/codeinclude-->

* 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.

<!--codeinclude-->
[Localstack container running with a custom network](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:with_network
<!--/codeinclude-->

* 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.

## Adding this module to your project dependencies

Add the following dependency to your `pom.xml`/`build.gradle` file:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
* Supported images: {@code localstack/localstack}, {@code localstack/localstack-pro}
* <p>
* Exposed ports: 4566
*
* @deprecated use {@link org.testcontainers.localstack.LocalStackContainer} instead.
*/
@Slf4j
@Deprecated
public class LocalStackContainer extends GenericContainer<LocalStackContainer> {

static final int PORT = 4566;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package org.testcontainers.localstack;

import com.github.dockerjava.api.command.InspectContainerResponse;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.Transferable;
import org.testcontainers.utility.DockerImageName;

import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
* Testcontainers implementation for LocalStack.
* <p>
* Supported images: {@code localstack/localstack}, {@code localstack/localstack-pro}
* <p>
* Exposed ports: 4566
*/
@Slf4j
public class LocalStackContainer extends GenericContainer<LocalStackContainer> {

static final int PORT = 4566;

private final List<String> services = new ArrayList<>();

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("localstack/localstack");

private static final DockerImageName LOCALSTACK_PRO_IMAGE_NAME = DockerImageName.parse("localstack/localstack-pro");

private static final String DEFAULT_REGION = "us-east-1";

private static final String DEFAULT_AWS_ACCESS_KEY_ID = "test";

private static final String DEFAULT_AWS_SECRET_ACCESS_KEY = "test";

private static final String STARTER_SCRIPT = "/testcontainers_start.sh";

/**
* @param dockerImageName image name to use for Localstack
*/
public LocalStackContainer(final String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}

/**
* @param dockerImageName image name to use for Localstack
*/
public LocalStackContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, LOCALSTACK_PRO_IMAGE_NAME);

withExposedPorts(PORT);
withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock");
waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1));
withCreateContainerCmdModifier(cmd -> {
cmd.withEntrypoint(
"sh",
"-c",
"while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT
);
});
}

@Override
protected void configure() {
if (!services.isEmpty()) {
withEnv("SERVICES", String.join(",", this.services));
}
}

@Override
protected void containerIsStarting(InspectContainerResponse containerInfo) {
String command = "#!/bin/bash\n";
command += "export LAMBDA_DOCKER_FLAGS=" + configureServiceContainerLabels("LAMBDA_DOCKER_FLAGS") + "\n";
command += "export ECS_DOCKER_FLAGS=" + configureServiceContainerLabels("ECS_DOCKER_FLAGS") + "\n";
command += "export EC2_DOCKER_FLAGS=" + configureServiceContainerLabels("EC2_DOCKER_FLAGS") + "\n";
command += "export BATCH_DOCKER_FLAGS=" + configureServiceContainerLabels("BATCH_DOCKER_FLAGS") + "\n";
command += "/usr/local/bin/docker-entrypoint.sh\n";
copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT);
}

/**
* Configure the LocalStack container to include the default testcontainers labels on all spawned lambda containers
* Necessary to properly clean up lambda containers even if the LocalStack container is killed before it gets the
* chance.
* @return the lambda container labels as a string
*/
private String configureServiceContainerLabels(String existingEnvFlagKey) {
String internalMarkerFlags = internalMarkerLabels();
String existingFlags = getEnvMap().get(existingEnvFlagKey);
if (existingFlags != null) {
internalMarkerFlags = existingFlags + " " + internalMarkerFlags;
}
return "\"" + internalMarkerFlags + "\"";
}

/**
* Provides a docker argument string including all default labels set on testcontainers containers (excluding reuse labels)
* @return Argument string in the format `-l key1=value1 -l key2=value2`
*/
private String internalMarkerLabels() {
return getContainerInfo()
.getConfig()
.getLabels()
.entrySet()
.stream()
.filter(entry -> entry.getKey().startsWith(DockerClientFactory.TESTCONTAINERS_LABEL))
.filter(entry -> {
return (
!entry.getKey().equals("org.testcontainers.hash") &&
!entry.getKey().equals("org.testcontainers.copied_files.hash")
);
})
.map(entry -> String.format("-l %s=%s", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(" "));
}

/**
* Declare a set of simulated AWS services that should be launched by this container.
* @param services one or more service names
* @return this container object
*/
public LocalStackContainer withServices(String... services) {
this.services.addAll(Arrays.asList(services));
return self();
}

/**
* Provides an endpoint to communicate with LocalStack service.
* The provided endpoint should be set in the AWS Java SDK v2 when building a client, e.g.:
* <pre><code>S3Client s3 = S3Client
.builder()
.endpointOverride(localstack.getEndpoint())
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
localstack.getAccessKey(), localstack.getSecretKey()
)))
.region(Region.of(localstack.getRegion()))
.build()
</code></pre>
* <p><strong>Please note that this method is only intended to be used for configuring AWS SDK clients
* that are running on the test host. If other containers need to call this one, they should be configured
* specifically to do so using a Docker network and appropriate addressing.</strong></p>
*
* @return an {@link URI} endpoint
*/
public URI getEndpoint() {
try {
final String address = getHost();
// resolve IP address and use that as the endpoint so that path-style access is automatically used for S3
String ipAddress = InetAddress.getByName(address).getHostAddress();
return new URI("http://" + ipAddress + ":" + getMappedPort(PORT));
} catch (UnknownHostException | URISyntaxException e) {
throw new IllegalStateException("Cannot obtain endpoint URL", e);
}
}

/**
* Provides a default access key that is preconfigured to communicate with a given simulated service.
* <a href="https://github.com/localstack/localstack/blob/master/doc/interaction/README.md?plain=1#L32">AWS Access Key</a>
* The access key can be used to construct AWS SDK v2 clients:
* <pre><code>S3Client s3 = S3Client
.builder()
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
localstack.getAccessKey(), localstack.getSecretKey()
)))
.region(Region.of(localstack.getRegion()))
.build()
</code></pre>
* @return a default access key
*/
public String getAccessKey() {
return this.getEnvMap().getOrDefault("AWS_ACCESS_KEY_ID", DEFAULT_AWS_ACCESS_KEY_ID);
}

/**
* Provides a default secret key that is preconfigured to communicate with a given simulated service.
* <a href="https://github.com/localstack/localstack/blob/master/doc/interaction/README.md?plain=1#L32">AWS Secret Key</a>
* The secret key can be used to construct AWS SDK v2 clients:
* <pre><code>S3Client s3 = S3Client
.builder()
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
localstack.getAccessKey(), localstack.getSecretKey()
)))
.region(Region.of(localstack.getRegion()))
.build()
</code></pre>
* @return a default secret key
*/
public String getSecretKey() {
return this.getEnvMap().getOrDefault("AWS_SECRET_ACCESS_KEY", DEFAULT_AWS_SECRET_ACCESS_KEY);
}

/**
* Provides a default region that is preconfigured to communicate with a given simulated service.
* The region can be used to construct AWS SDK v2 clients:
* <pre><code>S3Client s3 = S3Client
.builder()
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
localstack.getAccessKey(), localstack.getSecretKey()
)))
.region(Region.of(localstack.getRegion()))
.build()
</code></pre>
* @return a default region
*/
public String getRegion() {
return this.getEnvMap().getOrDefault("DEFAULT_REGION", DEFAULT_REGION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.testcontainers.utility.DockerImageName;

public interface LocalstackTestImages {
DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:4.7.0");
DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:4.9.2");

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

Expand Down
Loading
Loading