Skip to content

Commit

Permalink
Add support for LocalStack v2 (#6808)
Browse files Browse the repository at this point in the history
`HOSTNAME_EXTERNAL` env var is deprecated and will be replaced by
`LOCALSTACK_HOST` in the upcoming v2.

Fixes #6792
  • Loading branch information
eddumelendez committed Mar 30, 2023
1 parent 9c5c352 commit 1bd177f
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@
import java.util.stream.Collectors;

/**
* <p>Container for LocalStack, 'A fully functional local AWS cloud stack'.</p>
* <p>{@link LocalStackContainer#withServices(Service...)} should be used to select which services
* are to be launched. See {@link Service} for available choices.
* Testcontainers implementation for LocalStack.
*/
@Slf4j
public class LocalStackContainer extends GenericContainer<LocalStackContainer> {

static final int PORT = 4566;

@Deprecated
private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL";

private static final String LOCALSTACK_HOST_ENV_VAR = "LOCALSTACK_HOST";

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

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("localstack/localstack");
Expand Down Expand Up @@ -66,6 +67,8 @@ public class LocalStackContainer extends GenericContainer<LocalStackContainer> {
*/
private final boolean servicesEnvVarRequired;

private final boolean isVersion2;

/**
* @deprecated use {@link LocalStackContainer(DockerImageName)} instead
*/
Expand All @@ -92,18 +95,31 @@ public LocalStackContainer(final DockerImageName dockerImageName) {
/**
* @param dockerImageName image name to use for Localstack
* @param useLegacyMode if true, each AWS service is exposed on a different port
* @deprecated use {@link LocalStackContainer(DockerImageName)} instead
*/
@Deprecated
public LocalStackContainer(final DockerImageName dockerImageName, boolean useLegacyMode) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);

this.legacyMode = useLegacyMode;
this.servicesEnvVarRequired = isServicesEnvVarRequired(dockerImageName.getVersionPart());
String version = dockerImageName.getVersionPart();
this.servicesEnvVarRequired = isServicesEnvVarRequired(version);
this.isVersion2 = isVersion2(version);

withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock");
waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1));
}

private static boolean isVersion2(String version) {
if (version.equals("latest")) {
return true;
}

ComparableVersion comparableVersion = new ComparableVersion(version);
return comparableVersion.isGreaterThanOrEqualTo("2.0.0");
}

private static boolean isServicesEnvVarRequired(String version) {
if (version.equals("latest")) {
return false;
Expand Down Expand Up @@ -141,7 +157,7 @@ private static boolean shouldRunInLegacyMode(String version) {
protected void configure() {
super.configure();

if (servicesEnvVarRequired) {
if (this.servicesEnvVarRequired) {
Preconditions.check("services list must not be empty", !services.isEmpty());
}

Expand All @@ -152,26 +168,30 @@ protected void configure() {
}
}

if (this.isVersion2) {
resolveHostname(LOCALSTACK_HOST_ENV_VAR);
} else {
resolveHostname(HOSTNAME_EXTERNAL_ENV_VAR);
}

exposePorts();
}

private void resolveHostname(String envVar) {
String hostnameExternalReason;
if (getEnvMap().containsKey(HOSTNAME_EXTERNAL_ENV_VAR)) {
if (getEnvMap().containsKey(envVar)) {
// do nothing
hostnameExternalReason = "explicitly as environment variable";
} else if (getNetwork() != null && getNetworkAliases() != null && getNetworkAliases().size() >= 1) {
withEnv(HOSTNAME_EXTERNAL_ENV_VAR, getNetworkAliases().get(getNetworkAliases().size() - 1)); // use the last network alias set
withEnv(envVar, getNetworkAliases().get(getNetworkAliases().size() - 1)); // use the last network alias set
hostnameExternalReason = "to match last network alias on container with non-default network";
} else {
withEnv(HOSTNAME_EXTERNAL_ENV_VAR, getHost());
withEnv(envVar, getHost());
hostnameExternalReason = "to match host-routable address for container";
}
logger()
.info(
"{} environment variable set to {} ({})",
HOSTNAME_EXTERNAL_ENV_VAR,
getEnvMap().get(HOSTNAME_EXTERNAL_ENV_VAR),
hostnameExternalReason
);

exposePorts();
logger()
.info("{} environment variable set to {} ({})", envVar, getEnvMap().get(envVar), hostnameExternalReason);
}

private void exposePorts() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.localstack.LocalStackContainer.Service;
import org.testcontainers.utility.DockerImageName;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
Expand Down Expand Up @@ -265,6 +266,11 @@ public static class WithNetwork {
.withEnv("AWS_SECRET_ACCESS_KEY", "secretkey")
.withEnv("AWS_REGION", "eu-west-1");

@Test
public void localstackHostEnVarIsSet() {
assertThat(localstackInDockerNetwork.getEnvMap().get("HOSTNAME_EXTERNAL")).isEqualTo("localstack");
}

@Test
public void s3TestOverDockerNetwork() throws Exception {
runAwsCliAgainstDockerNetworkContainer(
Expand Down Expand Up @@ -357,17 +363,92 @@ public static class WithoutServices {

@Test
public void s3ServiceStartLazily() {
S3Client s3 = S3Client
.builder()
.endpointOverride(localstack.getEndpointOverride(Service.S3))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())
try (
S3Client s3 = S3Client
.builder()
.endpointOverride(localstack.getEndpointOverride(Service.S3))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())
)
)
.region(Region.of(localstack.getRegion()))
.build()
) {
assertThat(s3.listBuckets().buckets()).as("S3 Service is started lazily").isEmpty();
}
}
}

public static class WithVersion2 {

private static Network network = Network.newNetwork();

@ClassRule
public static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:2.0")
)
.withNetwork(network)
.withNetworkAliases("localstack");

@ClassRule
public static GenericContainer<?> awsCliInDockerNetwork = new GenericContainer<>(
LocalstackTestImages.AWS_CLI_IMAGE
)
.withNetwork(network)
.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("tail"))
.withCommand(" -f /dev/null")
.withEnv("AWS_ACCESS_KEY_ID", "accesskey")
.withEnv("AWS_SECRET_ACCESS_KEY", "secretkey")
.withEnv("AWS_REGION", "eu-west-1");

@Test
public void localstackHostEnVarIsSet() {
assertThat(localstack.getEnvMap().get("LOCALSTACK_HOST")).isEqualTo("localstack");
}

@Test
public void sqsTestOverDockerNetwork() throws Exception {
final String queueCreationResponse = runAwsCliAgainstDockerNetworkContainer(
"sqs create-queue --queue-name baz"
);

assertThat(queueCreationResponse)
.as("Created queue has external hostname URL")
.contains("http://localstack:" + LocalStackContainer.PORT);

runAwsCliAgainstDockerNetworkContainer(
String.format(
"sqs send-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz --message-body test",
LocalStackContainer.PORT,
LocalStackContainer.PORT
)
.region(Region.of(localstack.getRegion()))
.build();
assertThat(s3.listBuckets().buckets()).as("S3 Service is started lazily").isEmpty();
);
final String message = runAwsCliAgainstDockerNetworkContainer(
String.format(
"sqs receive-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz",
LocalStackContainer.PORT,
LocalStackContainer.PORT
)
);

assertThat(message).as("the sent message can be received").contains("\"Body\": \"test\"");
}

private String runAwsCliAgainstDockerNetworkContainer(String command) throws Exception {
final String[] commandParts = String
.format(
"/usr/local/bin/aws --region eu-west-1 %s --endpoint-url http://localstack:%d --no-verify-ssl",
command,
LocalStackContainer.PORT
)
.split(" ");
final Container.ExecResult execResult = awsCliInDockerNetwork.execInContainer(commandParts);
assertThat(execResult.getExitCode()).isEqualTo(0);

final String logs = execResult.getStdout() + execResult.getStderr();
log.info(logs);
return logs;
}
}
}

0 comments on commit 1bd177f

Please sign in to comment.