Skip to content

Commit 8c5a9af

Browse files
authored
Add Localstack module (#640)
1 parent 9941583 commit 8c5a9af

File tree

10 files changed

+2835
-1079
lines changed

10 files changed

+2835
-1079
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ jobs:
8484
- elasticsearch
8585
- hivemq
8686
- kafka
87+
- localstack
8788
- mongodb
8889
- mysql
8990
- nats

docs/modules/localstack.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Localstack Module
2+
3+
[Localstack](https://www.localstack.cloud/): Develop and test your AWS applications locally to reduce development time and increase product velocity
4+
5+
## Install
6+
7+
```bash
8+
npm install @testcontainers/localstack --save-dev
9+
```
10+
11+
## Examples
12+
13+
<!--codeinclude-->
14+
[Create a S3 bucket:](../../packages/modules/localstack/src/localstack-container.test.ts) inside_block:createS3Bucket
15+
<!--/codeinclude-->

package-lock.json

Lines changed: 2611 additions & 1079 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Config } from "jest";
2+
import * as path from "path";
3+
4+
const config: Config = {
5+
preset: "ts-jest",
6+
moduleNameMapper: {
7+
"^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"),
8+
},
9+
};
10+
11+
export default config;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@testcontainers/localstack",
3+
"version": "10.0.2",
4+
"license": "MIT",
5+
"keywords": [
6+
"localstack",
7+
"aws",
8+
"testing",
9+
"docker",
10+
"testcontainers"
11+
],
12+
"description": "LocalStack module for Testcontainers",
13+
"homepage": "https://github.com/testcontainers/testcontainers-node#readme",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/testcontainers/testcontainers-node"
17+
},
18+
"bugs": {
19+
"url": "https://github.com/testcontainers/testcontainers-node/issues"
20+
},
21+
"main": "build/index.js",
22+
"files": [
23+
"build"
24+
],
25+
"publishConfig": {
26+
"access": "public"
27+
},
28+
"scripts": {
29+
"build": "tsc --project tsconfig.build.json"
30+
},
31+
"dependencies": {
32+
"testcontainers": "^10.2.1"
33+
},
34+
"devDependencies": {
35+
"@aws-sdk/client-s3": "^3.468.0"
36+
}
37+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LocalstackContainer, StartedLocalStackContainer } from "./localstack-container";
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { LOCALSTACK_PORT, LocalstackContainer } from "./localstack-container";
2+
import { HeadBucketCommand, S3Client, CreateBucketCommand } from "@aws-sdk/client-s3";
3+
import { GenericContainer, log, Network, StartedTestContainer } from "testcontainers";
4+
5+
const runAwsCliAgainstDockerNetworkContainer = async (
6+
command: string,
7+
awsCliInDockerNetwork: StartedTestContainer
8+
): Promise<string> => {
9+
const commandParts = `/usr/local/bin/aws --region eu-west-1 ${command} --endpoint-url http://localstack:${LOCALSTACK_PORT} --no-verify-ssl`;
10+
const execResult = await awsCliInDockerNetwork.exec(commandParts);
11+
expect(execResult.exitCode).toEqual(0);
12+
log.info(execResult.output);
13+
return execResult.output;
14+
};
15+
16+
describe("LocalStackContainer", () => {
17+
jest.setTimeout(180_000);
18+
19+
// createS3Bucket {
20+
it("should create a S3 bucket", async () => {
21+
const container = await new LocalstackContainer().start();
22+
23+
const client = new S3Client({
24+
endpoint: container.getConnectionUri(),
25+
forcePathStyle: true,
26+
region: "us-east-1",
27+
credentials: {
28+
secretAccessKey: "test",
29+
accessKeyId: "test",
30+
},
31+
});
32+
const input = {
33+
Bucket: "testcontainers",
34+
};
35+
const command = new CreateBucketCommand(input);
36+
37+
const createBucketResponse = await client.send(command);
38+
expect(createBucketResponse.$metadata.httpStatusCode).toEqual(200);
39+
const headBucketResponse = await client.send(new HeadBucketCommand(input));
40+
expect(headBucketResponse.$metadata.httpStatusCode).toEqual(200);
41+
42+
await container.stop();
43+
});
44+
// }
45+
46+
it("should use custom network", async () => {
47+
const network = await new Network().start();
48+
const container = await new LocalstackContainer()
49+
.withNetwork(network)
50+
.withNetworkAliases("notthis", "localstack") // the last alias is used for HOSTNAME_EXTERNAL
51+
.start();
52+
53+
const awsCliInDockerNetwork = await new GenericContainer("amazon/aws-cli:2.7.27")
54+
.withNetwork(network)
55+
.withEntrypoint(["bash"])
56+
.withCommand(["-c", "echo 'START'; sleep infinity"])
57+
.withEnvironment({
58+
AWS_ACCESS_KEY_ID: "test",
59+
AWS_SECRET_ACCESS_KEY: "test",
60+
AWS_REGION: "us-east-1",
61+
})
62+
.start();
63+
64+
const response = await runAwsCliAgainstDockerNetworkContainer(
65+
"sqs create-queue --queue-name baz",
66+
awsCliInDockerNetwork
67+
);
68+
expect(response).toContain(`http://localstack:${LOCALSTACK_PORT}`);
69+
await container.stop();
70+
await awsCliInDockerNetwork.stop();
71+
});
72+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { AbstractStartedContainer, GenericContainer, log, StartedTestContainer, Wait } from "testcontainers";
2+
3+
export const LOCALSTACK_PORT = 4566;
4+
5+
export class LocalstackContainer extends GenericContainer {
6+
constructor(image = "localstack/localstack:2.2.0") {
7+
super(image);
8+
}
9+
10+
private resolveHostname(): void {
11+
const envVar = "LOCALSTACK_HOST";
12+
let hostnameExternalReason;
13+
if (this.environment[envVar]) {
14+
// do nothing
15+
hostnameExternalReason = "explicitly as environment variable";
16+
} else if (this.networkAliases && this.networkAliases.length > 0) {
17+
this.environment[envVar] = this.networkAliases.at(this.networkAliases.length - 1) || ""; // use the last network alias set
18+
hostnameExternalReason = "to match last network alias on container with non-default network";
19+
} else {
20+
this.withEnvironment({ LOCALSTACK_HOST: "localhost" });
21+
hostnameExternalReason = "to match host-routable address for container";
22+
}
23+
log.info(`${envVar} environment variable set to ${this.environment[envVar]} (${hostnameExternalReason})"`);
24+
}
25+
26+
protected override async beforeContainerCreated(): Promise<void> {
27+
this.resolveHostname();
28+
this.withExposedPorts(...(this.hasExposedPorts ? this.exposedPorts : [LOCALSTACK_PORT]))
29+
.withWaitStrategy(Wait.forLogMessage("Ready", 1))
30+
.withStartupTimeout(120_000);
31+
}
32+
33+
public override async start(): Promise<StartedLocalStackContainer> {
34+
return new StartedLocalStackContainer(await super.start());
35+
}
36+
}
37+
38+
export class StartedLocalStackContainer extends AbstractStartedContainer {
39+
constructor(startedTestContainer: StartedTestContainer) {
40+
super(startedTestContainer);
41+
}
42+
43+
public getPort(): number {
44+
return this.startedTestContainer.getMappedPort(LOCALSTACK_PORT);
45+
}
46+
47+
/**
48+
* @returns A connection URI in the form of `http://host:port`
49+
*/
50+
public getConnectionUri(): string {
51+
return `http://${this.getHost()}:${this.getPort().toString()}`;
52+
}
53+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"exclude": [
4+
"build",
5+
"jest.config.ts",
6+
"src/**/*.test.ts"
7+
],
8+
"references": [
9+
{
10+
"path": "../../testcontainers"
11+
}
12+
]
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"rootDir": "src",
5+
"outDir": "build",
6+
"paths": {
7+
"testcontainers": [
8+
"../../testcontainers/src"
9+
]
10+
}
11+
},
12+
"exclude": [
13+
"build",
14+
"jest.config.ts"
15+
],
16+
"references": [
17+
{
18+
"path": "../../testcontainers"
19+
}
20+
]
21+
}

0 commit comments

Comments
 (0)