Skip to content

Commit 3d809a6

Browse files
committed
Implement HashiCorp Vault module
1 parent 8ed532e commit 3d809a6

File tree

8 files changed

+635
-0
lines changed

8 files changed

+635
-0
lines changed

package-lock.json

Lines changed: 384 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/modules/vault/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM hashicorp/vault:1.13.0
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@testcontainers/vault",
3+
"version": "11.2.1",
4+
"license": "MIT",
5+
"keywords": [
6+
"vault",
7+
"hashicorp",
8+
"testing",
9+
"docker",
10+
"testcontainers"
11+
],
12+
"description": "HashiCorp Vault 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+
"prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .",
30+
"build": "tsc --project tsconfig.build.json"
31+
},
32+
"devDependencies": {
33+
"node-vault": "^0.10.5"
34+
},
35+
"dependencies": {
36+
"testcontainers": "^11.2.1"
37+
}
38+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { StartedVaultContainer, VaultContainer } from "./vault-container";
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import vault from "node-vault";
2+
import { setTimeout } from "node:timers/promises";
3+
import { StartedVaultContainer, VaultContainer } from "./vault-container";
4+
5+
const VAULT_TOKEN = "my-root-token";
6+
7+
describe("vault", { timeout: 180_000 }, () => {
8+
let container: StartedVaultContainer;
9+
10+
afterEach(async () => {
11+
await container?.stop();
12+
});
13+
14+
it("should start Vault and allow reading/writing secrets", async () => {
15+
container = await new VaultContainer().withVaultToken(VAULT_TOKEN).start();
16+
17+
const client = vault({
18+
apiVersion: "v1",
19+
endpoint: container.getAddress(),
20+
token: container.getRootToken(),
21+
});
22+
23+
await client.write("secret/data/hello", {
24+
data: {
25+
message: "world",
26+
other: "vault",
27+
},
28+
});
29+
30+
await setTimeout(1000);
31+
32+
const result = await client.read("secret/data/hello");
33+
const data = result?.data?.data;
34+
35+
expect(data.message).toBe("world");
36+
expect(data.other).toBe("vault");
37+
});
38+
39+
it("should execute init commands using vault CLI", async () => {
40+
container = await new VaultContainer()
41+
.withVaultToken(VAULT_TOKEN)
42+
.withInitCommands("secrets enable transit", "write -f transit/keys/my-key")
43+
.start();
44+
45+
const result = await container.exec(["vault", "read", "-format=json", "transit/keys/my-key"]);
46+
47+
expect(result.exitCode).toBe(0);
48+
expect(result.output).toContain("my-key");
49+
});
50+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers";
2+
3+
const VAULT_PORT = 8200;
4+
5+
/**
6+
* Testcontainers module for HashiCorp Vault.
7+
*
8+
* Based on the upstream image `hashicorp/vault:1.13.0`, this container exposes Vault
9+
* on port 8200, sets up a wait strategy using the health check endpoint, and supports:
10+
* - Supplying a root token
11+
* - Executing post-start CLI init commands
12+
*/
13+
export class VaultContainer extends GenericContainer {
14+
private readonly initCommands: string[] = [];
15+
private token?: string;
16+
17+
/**
18+
* Constructs a VaultContainer with a default image and healthcheck strategy.
19+
*
20+
* - Sets VAULT_ADDR to internal container address
21+
* - Adds IPC_LOCK capability (required by Vault)
22+
* - Exposes Vault on port 8200
23+
* - Waits for HTTP 200 response from /v1/sys/health
24+
*
25+
* @param image Optional Docker image to use (default: hashicorp/vault:1.13.0)
26+
*/
27+
constructor(image: string = "hashicorp/vault:1.13.0") {
28+
super(image);
29+
30+
this.withExposedPorts(VAULT_PORT)
31+
.withEnvironment({ VAULT_ADDR: `http://0.0.0.0:${VAULT_PORT}` })
32+
.withAddedCapabilities("IPC_LOCK")
33+
.withWaitStrategy(Wait.forHttp("/v1/sys/health", VAULT_PORT).forStatusCode(200));
34+
}
35+
36+
/**
37+
* Sets a root token to be used with Vault, passed via environment variables.
38+
*
39+
* @param token Vault root token
40+
* @returns this
41+
*/
42+
public withVaultToken(token: string): this {
43+
this.token = token;
44+
this.withEnvironment({
45+
VAULT_DEV_ROOT_TOKEN_ID: token,
46+
VAULT_TOKEN: token,
47+
});
48+
return this;
49+
}
50+
51+
/**
52+
* Registers one or more Vault CLI init commands to be run after container starts.
53+
*
54+
* Example:
55+
* .withInitCommands("secrets enable transit", "kv put secret/foo bar=baz")
56+
*
57+
* @param commands Vault CLI commands (without `vault` prefix)
58+
* @returns this
59+
*/
60+
public withInitCommands(...commands: string[]): this {
61+
this.initCommands.push(...commands);
62+
return this;
63+
}
64+
65+
/**
66+
* Starts the Vault container and executes any registered init commands.
67+
*
68+
* Wraps the base container in a StartedVaultContainer with helper accessors.
69+
*/
70+
public override async start(): Promise<StartedVaultContainer> {
71+
const started = await super.start();
72+
const container = new StartedVaultContainer(started, this.token);
73+
74+
if (this.initCommands.length > 0) {
75+
await container.execVaultCommands(this.initCommands);
76+
}
77+
78+
return container;
79+
}
80+
}
81+
82+
/**
83+
* A running Vault container, with accessors for port, address, and exec helper.
84+
*/
85+
export class StartedVaultContainer extends AbstractStartedContainer {
86+
constructor(
87+
startedTestContainer: AbstractStartedContainer["startedTestContainer"],
88+
private readonly token?: string
89+
) {
90+
super(startedTestContainer);
91+
}
92+
93+
/**
94+
* Returns the mapped host port for Vault (default: 8200).
95+
*/
96+
public getVaultPort(): number {
97+
return this.getMappedPort(VAULT_PORT);
98+
}
99+
100+
/**
101+
* Returns the full Vault HTTP address (e.g., http://localhost:32768).
102+
*/
103+
public getAddress(): string {
104+
return `http://${this.getHost()}:${this.getVaultPort()}`;
105+
}
106+
107+
/**
108+
* Returns the root token set at container creation time, if any.
109+
*/
110+
public getRootToken(): string | undefined {
111+
return this.token;
112+
}
113+
114+
/**
115+
* Executes a list of Vault CLI commands inside the container after it has started.
116+
*
117+
* This is typically used to pre-configure secret engines or seed test data.
118+
*
119+
* @param commands Array of CLI commands (without `vault` prefix)
120+
*/
121+
public async execVaultCommands(commands: string[]): Promise<void> {
122+
const cmd = commands.map((c) => `vault ${c}`).join(" && ");
123+
const result = await this.exec(["/bin/sh", "-c", cmd]);
124+
125+
if (result.exitCode !== 0) {
126+
throw new Error(`Vault init commands failed: ${result.output}`);
127+
}
128+
}
129+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"exclude": [
4+
"build",
5+
"src/**/*.test.ts"
6+
],
7+
"references": [
8+
{
9+
"path": "../../testcontainers"
10+
}
11+
]
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
],
15+
"references": [
16+
{
17+
"path": "../../testcontainers"
18+
}
19+
]
20+
}

0 commit comments

Comments
 (0)