Skip to content

Commit 1bd177f

Browse files
authored
Add support for LocalStack v2 (#6808)
`HOSTNAME_EXTERNAL` env var is deprecated and will be replaced by `LOCALSTACK_HOST` in the upcoming v2. Fixes #6792
1 parent 9c5c352 commit 1bd177f

File tree

2 files changed

+126
-25
lines changed

2 files changed

+126
-25
lines changed

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

+36-16
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@
2121
import java.util.stream.Collectors;
2222

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

3129
static final int PORT = 4566;
3230

31+
@Deprecated
3332
private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL";
3433

34+
private static final String LOCALSTACK_HOST_ENV_VAR = "LOCALSTACK_HOST";
35+
3536
private final List<EnabledService> services = new ArrayList<>();
3637

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

70+
private final boolean isVersion2;
71+
6972
/**
7073
* @deprecated use {@link LocalStackContainer(DockerImageName)} instead
7174
*/
@@ -92,18 +95,31 @@ public LocalStackContainer(final DockerImageName dockerImageName) {
9295
/**
9396
* @param dockerImageName image name to use for Localstack
9497
* @param useLegacyMode if true, each AWS service is exposed on a different port
98+
* @deprecated use {@link LocalStackContainer(DockerImageName)} instead
9599
*/
100+
@Deprecated
96101
public LocalStackContainer(final DockerImageName dockerImageName, boolean useLegacyMode) {
97102
super(dockerImageName);
98103
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
99104

100105
this.legacyMode = useLegacyMode;
101-
this.servicesEnvVarRequired = isServicesEnvVarRequired(dockerImageName.getVersionPart());
106+
String version = dockerImageName.getVersionPart();
107+
this.servicesEnvVarRequired = isServicesEnvVarRequired(version);
108+
this.isVersion2 = isVersion2(version);
102109

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

114+
private static boolean isVersion2(String version) {
115+
if (version.equals("latest")) {
116+
return true;
117+
}
118+
119+
ComparableVersion comparableVersion = new ComparableVersion(version);
120+
return comparableVersion.isGreaterThanOrEqualTo("2.0.0");
121+
}
122+
107123
private static boolean isServicesEnvVarRequired(String version) {
108124
if (version.equals("latest")) {
109125
return false;
@@ -141,7 +157,7 @@ private static boolean shouldRunInLegacyMode(String version) {
141157
protected void configure() {
142158
super.configure();
143159

144-
if (servicesEnvVarRequired) {
160+
if (this.servicesEnvVarRequired) {
145161
Preconditions.check("services list must not be empty", !services.isEmpty());
146162
}
147163

@@ -152,26 +168,30 @@ protected void configure() {
152168
}
153169
}
154170

171+
if (this.isVersion2) {
172+
resolveHostname(LOCALSTACK_HOST_ENV_VAR);
173+
} else {
174+
resolveHostname(HOSTNAME_EXTERNAL_ENV_VAR);
175+
}
176+
177+
exposePorts();
178+
}
179+
180+
private void resolveHostname(String envVar) {
155181
String hostnameExternalReason;
156-
if (getEnvMap().containsKey(HOSTNAME_EXTERNAL_ENV_VAR)) {
182+
if (getEnvMap().containsKey(envVar)) {
157183
// do nothing
158184
hostnameExternalReason = "explicitly as environment variable";
159185
} else if (getNetwork() != null && getNetworkAliases() != null && getNetworkAliases().size() >= 1) {
160-
withEnv(HOSTNAME_EXTERNAL_ENV_VAR, getNetworkAliases().get(getNetworkAliases().size() - 1)); // use the last network alias set
186+
withEnv(envVar, getNetworkAliases().get(getNetworkAliases().size() - 1)); // use the last network alias set
161187
hostnameExternalReason = "to match last network alias on container with non-default network";
162188
} else {
163-
withEnv(HOSTNAME_EXTERNAL_ENV_VAR, getHost());
189+
withEnv(envVar, getHost());
164190
hostnameExternalReason = "to match host-routable address for container";
165191
}
166-
logger()
167-
.info(
168-
"{} environment variable set to {} ({})",
169-
HOSTNAME_EXTERNAL_ENV_VAR,
170-
getEnvMap().get(HOSTNAME_EXTERNAL_ENV_VAR),
171-
hostnameExternalReason
172-
);
173192

174-
exposePorts();
193+
logger()
194+
.info("{} environment variable set to {} ({})", envVar, getEnvMap().get(envVar), hostnameExternalReason);
175195
}
176196

177197
private void exposePorts() {

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

+90-9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.testcontainers.containers.GenericContainer;
3131
import org.testcontainers.containers.Network;
3232
import org.testcontainers.containers.localstack.LocalStackContainer.Service;
33+
import org.testcontainers.utility.DockerImageName;
3334
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
3435
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
3536
import software.amazon.awssdk.regions.Region;
@@ -265,6 +266,11 @@ public static class WithNetwork {
265266
.withEnv("AWS_SECRET_ACCESS_KEY", "secretkey")
266267
.withEnv("AWS_REGION", "eu-west-1");
267268

269+
@Test
270+
public void localstackHostEnVarIsSet() {
271+
assertThat(localstackInDockerNetwork.getEnvMap().get("HOSTNAME_EXTERNAL")).isEqualTo("localstack");
272+
}
273+
268274
@Test
269275
public void s3TestOverDockerNetwork() throws Exception {
270276
runAwsCliAgainstDockerNetworkContainer(
@@ -357,17 +363,92 @@ public static class WithoutServices {
357363

358364
@Test
359365
public void s3ServiceStartLazily() {
360-
S3Client s3 = S3Client
361-
.builder()
362-
.endpointOverride(localstack.getEndpointOverride(Service.S3))
363-
.credentialsProvider(
364-
StaticCredentialsProvider.create(
365-
AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())
366+
try (
367+
S3Client s3 = S3Client
368+
.builder()
369+
.endpointOverride(localstack.getEndpointOverride(Service.S3))
370+
.credentialsProvider(
371+
StaticCredentialsProvider.create(
372+
AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())
373+
)
366374
)
375+
.region(Region.of(localstack.getRegion()))
376+
.build()
377+
) {
378+
assertThat(s3.listBuckets().buckets()).as("S3 Service is started lazily").isEmpty();
379+
}
380+
}
381+
}
382+
383+
public static class WithVersion2 {
384+
385+
private static Network network = Network.newNetwork();
386+
387+
@ClassRule
388+
public static LocalStackContainer localstack = new LocalStackContainer(
389+
DockerImageName.parse("localstack/localstack:2.0")
390+
)
391+
.withNetwork(network)
392+
.withNetworkAliases("localstack");
393+
394+
@ClassRule
395+
public static GenericContainer<?> awsCliInDockerNetwork = new GenericContainer<>(
396+
LocalstackTestImages.AWS_CLI_IMAGE
397+
)
398+
.withNetwork(network)
399+
.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("tail"))
400+
.withCommand(" -f /dev/null")
401+
.withEnv("AWS_ACCESS_KEY_ID", "accesskey")
402+
.withEnv("AWS_SECRET_ACCESS_KEY", "secretkey")
403+
.withEnv("AWS_REGION", "eu-west-1");
404+
405+
@Test
406+
public void localstackHostEnVarIsSet() {
407+
assertThat(localstack.getEnvMap().get("LOCALSTACK_HOST")).isEqualTo("localstack");
408+
}
409+
410+
@Test
411+
public void sqsTestOverDockerNetwork() throws Exception {
412+
final String queueCreationResponse = runAwsCliAgainstDockerNetworkContainer(
413+
"sqs create-queue --queue-name baz"
414+
);
415+
416+
assertThat(queueCreationResponse)
417+
.as("Created queue has external hostname URL")
418+
.contains("http://localstack:" + LocalStackContainer.PORT);
419+
420+
runAwsCliAgainstDockerNetworkContainer(
421+
String.format(
422+
"sqs send-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz --message-body test",
423+
LocalStackContainer.PORT,
424+
LocalStackContainer.PORT
367425
)
368-
.region(Region.of(localstack.getRegion()))
369-
.build();
370-
assertThat(s3.listBuckets().buckets()).as("S3 Service is started lazily").isEmpty();
426+
);
427+
final String message = runAwsCliAgainstDockerNetworkContainer(
428+
String.format(
429+
"sqs receive-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz",
430+
LocalStackContainer.PORT,
431+
LocalStackContainer.PORT
432+
)
433+
);
434+
435+
assertThat(message).as("the sent message can be received").contains("\"Body\": \"test\"");
436+
}
437+
438+
private String runAwsCliAgainstDockerNetworkContainer(String command) throws Exception {
439+
final String[] commandParts = String
440+
.format(
441+
"/usr/local/bin/aws --region eu-west-1 %s --endpoint-url http://localstack:%d --no-verify-ssl",
442+
command,
443+
LocalStackContainer.PORT
444+
)
445+
.split(" ");
446+
final Container.ExecResult execResult = awsCliInDockerNetwork.execInContainer(commandParts);
447+
assertThat(execResult.getExitCode()).isEqualTo(0);
448+
449+
final String logs = execResult.getStdout() + execResult.getStderr();
450+
log.info(logs);
451+
return logs;
371452
}
372453
}
373454
}

0 commit comments

Comments
 (0)