Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for LocalStack v2 #6808

Merged
merged 2 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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,32 @@ 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 imageTag = dockerImageName.getVersionPart();
String version = imageTag.startsWith("v") ? imageTag.substring(1) : imageTag;
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");
}

private static boolean isServicesEnvVarRequired(String version) {
if (version.equals("latest")) {
return false;
Expand Down Expand Up @@ -141,7 +158,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 +169,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:latest")
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved
)
.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;
}
}
}