Skip to content

Commit

Permalink
Add Localstack module (#640)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilopezluna committed Dec 10, 2023
1 parent 9941583 commit 8c5a9af
Show file tree
Hide file tree
Showing 10 changed files with 2,835 additions and 1,079 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
- elasticsearch
- hivemq
- kafka
- localstack
- mongodb
- mysql
- nats
Expand Down
15 changes: 15 additions & 0 deletions docs/modules/localstack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Localstack Module

[Localstack](https://www.localstack.cloud/): Develop and test your AWS applications locally to reduce development time and increase product velocity

## Install

```bash
npm install @testcontainers/localstack --save-dev
```

## Examples

<!--codeinclude-->
[Create a S3 bucket:](../../packages/modules/localstack/src/localstack-container.test.ts) inside_block:createS3Bucket
<!--/codeinclude-->
3,690 changes: 2,611 additions & 1,079 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/modules/localstack/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Config } from "jest";
import * as path from "path";

const config: Config = {
preset: "ts-jest",
moduleNameMapper: {
"^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"),
},
};

export default config;
37 changes: 37 additions & 0 deletions packages/modules/localstack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@testcontainers/localstack",
"version": "10.0.2",
"license": "MIT",
"keywords": [
"localstack",
"aws",
"testing",
"docker",
"testcontainers"
],
"description": "LocalStack module for Testcontainers",
"homepage": "https://github.com/testcontainers/testcontainers-node#readme",
"repository": {
"type": "git",
"url": "https://github.com/testcontainers/testcontainers-node"
},
"bugs": {
"url": "https://github.com/testcontainers/testcontainers-node/issues"
},
"main": "build/index.js",
"files": [
"build"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc --project tsconfig.build.json"
},
"dependencies": {
"testcontainers": "^10.2.1"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.468.0"
}
}
1 change: 1 addition & 0 deletions packages/modules/localstack/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LocalstackContainer, StartedLocalStackContainer } from "./localstack-container";
72 changes: 72 additions & 0 deletions packages/modules/localstack/src/localstack-container.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { LOCALSTACK_PORT, LocalstackContainer } from "./localstack-container";
import { HeadBucketCommand, S3Client, CreateBucketCommand } from "@aws-sdk/client-s3";
import { GenericContainer, log, Network, StartedTestContainer } from "testcontainers";

const runAwsCliAgainstDockerNetworkContainer = async (
command: string,
awsCliInDockerNetwork: StartedTestContainer
): Promise<string> => {
const commandParts = `/usr/local/bin/aws --region eu-west-1 ${command} --endpoint-url http://localstack:${LOCALSTACK_PORT} --no-verify-ssl`;
const execResult = await awsCliInDockerNetwork.exec(commandParts);
expect(execResult.exitCode).toEqual(0);
log.info(execResult.output);
return execResult.output;
};

describe("LocalStackContainer", () => {
jest.setTimeout(180_000);

// createS3Bucket {
it("should create a S3 bucket", async () => {
const container = await new LocalstackContainer().start();

const client = new S3Client({
endpoint: container.getConnectionUri(),
forcePathStyle: true,
region: "us-east-1",
credentials: {
secretAccessKey: "test",
accessKeyId: "test",
},
});
const input = {
Bucket: "testcontainers",
};
const command = new CreateBucketCommand(input);

const createBucketResponse = await client.send(command);
expect(createBucketResponse.$metadata.httpStatusCode).toEqual(200);
const headBucketResponse = await client.send(new HeadBucketCommand(input));
expect(headBucketResponse.$metadata.httpStatusCode).toEqual(200);

await container.stop();
});
// }

it("should use custom network", async () => {
const network = await new Network().start();
const container = await new LocalstackContainer()
.withNetwork(network)
.withNetworkAliases("notthis", "localstack") // the last alias is used for HOSTNAME_EXTERNAL
.start();

const awsCliInDockerNetwork = await new GenericContainer("amazon/aws-cli:2.7.27")
.withNetwork(network)
.withEntrypoint(["bash"])
.withCommand(["-c", "echo 'START'; sleep infinity"])
.withEnvironment({
AWS_ACCESS_KEY_ID: "test",
AWS_SECRET_ACCESS_KEY: "test",
AWS_REGION: "us-east-1",
})
.start();

const response = await runAwsCliAgainstDockerNetworkContainer(
"sqs create-queue --queue-name baz",
awsCliInDockerNetwork
);
expect(response).toContain(`http://localstack:${LOCALSTACK_PORT}`);
await container.stop();
await awsCliInDockerNetwork.stop();
});
});
53 changes: 53 additions & 0 deletions packages/modules/localstack/src/localstack-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { AbstractStartedContainer, GenericContainer, log, StartedTestContainer, Wait } from "testcontainers";

export const LOCALSTACK_PORT = 4566;

export class LocalstackContainer extends GenericContainer {
constructor(image = "localstack/localstack:2.2.0") {
super(image);
}

private resolveHostname(): void {
const envVar = "LOCALSTACK_HOST";
let hostnameExternalReason;
if (this.environment[envVar]) {
// do nothing
hostnameExternalReason = "explicitly as environment variable";
} else if (this.networkAliases && this.networkAliases.length > 0) {
this.environment[envVar] = this.networkAliases.at(this.networkAliases.length - 1) || ""; // use the last network alias set
hostnameExternalReason = "to match last network alias on container with non-default network";
} else {
this.withEnvironment({ LOCALSTACK_HOST: "localhost" });
hostnameExternalReason = "to match host-routable address for container";
}
log.info(`${envVar} environment variable set to ${this.environment[envVar]} (${hostnameExternalReason})"`);
}

protected override async beforeContainerCreated(): Promise<void> {
this.resolveHostname();
this.withExposedPorts(...(this.hasExposedPorts ? this.exposedPorts : [LOCALSTACK_PORT]))
.withWaitStrategy(Wait.forLogMessage("Ready", 1))
.withStartupTimeout(120_000);
}

public override async start(): Promise<StartedLocalStackContainer> {
return new StartedLocalStackContainer(await super.start());
}
}

export class StartedLocalStackContainer extends AbstractStartedContainer {
constructor(startedTestContainer: StartedTestContainer) {
super(startedTestContainer);
}

public getPort(): number {
return this.startedTestContainer.getMappedPort(LOCALSTACK_PORT);
}

/**
* @returns A connection URI in the form of `http://host:port`
*/
public getConnectionUri(): string {
return `http://${this.getHost()}:${this.getPort().toString()}`;
}
}
13 changes: 13 additions & 0 deletions packages/modules/localstack/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"exclude": [
"build",
"jest.config.ts",
"src/**/*.test.ts"
],
"references": [
{
"path": "../../testcontainers"
}
]
}
21 changes: 21 additions & 0 deletions packages/modules/localstack/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build",
"paths": {
"testcontainers": [
"../../testcontainers/src"
]
}
},
"exclude": [
"build",
"jest.config.ts"
],
"references": [
{
"path": "../../testcontainers"
}
]
}

0 comments on commit 8c5a9af

Please sign in to comment.