From f5bafd3abcdcf271a2c189533539631bbc21bdf4 Mon Sep 17 00:00:00 2001 From: prenaissance Date: Sat, 17 May 2025 14:31:42 +0300 Subject: [PATCH 1/3] feat: add etcd package --- package-lock.json | 41 +++++++++++++++++ packages/modules/etcd/package.json | 38 +++++++++++++++ .../modules/etcd/src/etcd-container.test.ts | 21 +++++++++ packages/modules/etcd/src/etcd-container.ts | 46 +++++++++++++++++++ packages/modules/etcd/src/index.ts | 0 packages/modules/etcd/tsconfig.build.json | 12 +++++ packages/modules/etcd/tsconfig.json | 20 ++++++++ 7 files changed, 178 insertions(+) create mode 100644 packages/modules/etcd/package.json create mode 100644 packages/modules/etcd/src/etcd-container.test.ts create mode 100755 packages/modules/etcd/src/etcd-container.ts create mode 100644 packages/modules/etcd/src/index.ts create mode 100644 packages/modules/etcd/tsconfig.build.json create mode 100644 packages/modules/etcd/tsconfig.json diff --git a/package-lock.json b/package-lock.json index d3c2878b3..e2012f5d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5113,6 +5113,10 @@ "resolved": "packages/modules/elasticsearch", "link": true }, + "node_modules/@testcontainers/etcd": { + "resolved": "packages/modules/etcd", + "link": true + }, "node_modules/@testcontainers/eventstoredb": { "resolved": "packages/modules/eventstoredb", "link": true @@ -7699,6 +7703,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9118,6 +9132,22 @@ "node": ">=0.10.0" } }, + "node_modules/etcd3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/etcd3/-/etcd3-1.1.2.tgz", + "integrity": "sha512-YIampCz1/OmrVo/tR3QltAVUtYCQQOSFoqmHKKeoHbalm+WdXe3l4rhLIylklu8EzR/I3PBiOF4dC847dDskKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.8.20", + "@grpc/proto-loader": "^0.7.8", + "bignumber.js": "^9.1.1", + "cockatiel": "^3.1.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -19962,6 +19992,17 @@ "@elastic/elasticsearch": "^7.17.14" } }, + "packages/modules/etcd": { + "name": "@testcontainers/etcd", + "version": "10.21.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.26.0" + }, + "devDependencies": { + "etcd3": "^1.1.2" + } + }, "packages/modules/eventstoredb": { "name": "@testcontainers/eventstoredb", "version": "10.26.0", diff --git a/packages/modules/etcd/package.json b/packages/modules/etcd/package.json new file mode 100644 index 000000000..c4de497cb --- /dev/null +++ b/packages/modules/etcd/package.json @@ -0,0 +1,38 @@ +{ + "name": "@testcontainers/etcd", + "version": "10.21.0", + "license": "MIT", + "keywords": [ + "etcd", + "etcd3", + "testing", + "docker", + "testcontainers" + ], + "description": "Etcd 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": { + "prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .", + "build": "tsc --project tsconfig.build.json" + }, + "devDependencies": { + "etcd3": "^1.1.2" + }, + "dependencies": { + "testcontainers": "^10.26.0" + } +} diff --git a/packages/modules/etcd/src/etcd-container.test.ts b/packages/modules/etcd/src/etcd-container.test.ts new file mode 100644 index 000000000..7863737c1 --- /dev/null +++ b/packages/modules/etcd/src/etcd-container.test.ts @@ -0,0 +1,21 @@ +import { Etcd3 } from "etcd3"; +import { EtcdContainer } from "./etcd-container"; + +describe("etcd", () => { + it("should construct a container", async () => { + await new EtcdContainer().start(); + }); + + it("should connect and perform read/write operations", async () => { + const container = await new EtcdContainer().start(); + const client = new Etcd3({ + hosts: container.getClientEndpoint(), + }); + const key = "foo"; + const value = "bar"; + + await client.put(key).value(value); + const result = await client.get(key).string(); + expect(result).toEqual(value); + }); +}); diff --git a/packages/modules/etcd/src/etcd-container.ts b/packages/modules/etcd/src/etcd-container.ts new file mode 100755 index 000000000..937288778 --- /dev/null +++ b/packages/modules/etcd/src/etcd-container.ts @@ -0,0 +1,46 @@ +import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers"; + +const ETCD_CLIENT_PORT = 2379; +const ETCD_PEER_PORT = 2380; + +export class EtcdContainer extends GenericContainer { + constructor(image = "quay.io/coreos/etcd:v3.6.0", nodeName = "etcd-test") { + super(image); + this.withExposedPorts(ETCD_CLIENT_PORT, ETCD_PEER_PORT).withCommand([ + "etcd", + "--name", + nodeName, + "--initial-advertise-peer-urls", + `http://${nodeName}:${ETCD_PEER_PORT}`, + "--advertise-client-urls", + `http://${nodeName}:${ETCD_CLIENT_PORT}`, + "--listen-peer-urls", + `http://0.0.0.0:${ETCD_PEER_PORT}`, + "--listen-client-urls", + `http://0.0.0.0:${ETCD_CLIENT_PORT}`, + ]); + } + + public override async start(): Promise { + this.withWaitStrategy(Wait.forLogMessage(/"status":"SERVING"/)); + return new StartedEtcdContainer(await super.start()); + } +} + +export class StartedEtcdContainer extends AbstractStartedContainer { + public getClientPort(): number { + return this.startedTestContainer.getMappedPort(ETCD_CLIENT_PORT); + } + + public getPeerPort(): number { + return this.startedTestContainer.getMappedPort(ETCD_PEER_PORT); + } + + public getClientEndpoint(): string { + return `http://${this.getHost()}:${this.getClientPort()}`; + } + + public getPeerEndpoint(): string { + return `http://${this.getHost()}:${this.getPeerPort()}`; + } +} diff --git a/packages/modules/etcd/src/index.ts b/packages/modules/etcd/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/modules/etcd/tsconfig.build.json b/packages/modules/etcd/tsconfig.build.json new file mode 100644 index 000000000..ff7390b10 --- /dev/null +++ b/packages/modules/etcd/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/etcd/tsconfig.json b/packages/modules/etcd/tsconfig.json new file mode 100644 index 000000000..4d74c3e41 --- /dev/null +++ b/packages/modules/etcd/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file From 0120446cec707cd12856c72e1f87f19f46dcc169 Mon Sep 17 00:00:00 2001 From: prenaissance Date: Sun, 18 May 2025 20:53:25 +0300 Subject: [PATCH 2/3] docs: add etcd to mkdocs --- docs/modules/etcd.md | 19 +++++++++++ mkdocs.yml | 17 +++++----- .../modules/etcd/src/etcd-container.test.ts | 32 +++++++++++++++++-- packages/modules/etcd/src/etcd-container.ts | 4 +-- 4 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 docs/modules/etcd.md diff --git a/docs/modules/etcd.md b/docs/modules/etcd.md new file mode 100644 index 000000000..0088dba2e --- /dev/null +++ b/docs/modules/etcd.md @@ -0,0 +1,19 @@ +# Etcd Module + +[Etcd](https://etcd.io/) is a strongly consistent, distributed key-value store that provides a reliable way to store data that needs to be accessed by a distributed system or cluster of machines. + +## Install + +```bash +npm install @testcontainers/etcd --save-dev +``` + +## Examples + + +[Read and write key-value pairs:](../../packages/modules/etcd/src/etcd-container.test.ts) inside_block:readWrite + + + +[Subscribe to key changes:](../../packages/modules/etcd/src/etcd-container.test.ts) inside_block:subscribe + diff --git a/mkdocs.yml b/mkdocs.yml index 60b544f3b..b9d735cf7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,23 +1,23 @@ site_name: Testcontainers for NodeJS site_url: https://node.testcontainers.org -repo_name: 'testcontainers-node' -repo_url: 'https://github.com/testcontainers/testcontainers-node' +repo_name: "testcontainers-node" +repo_url: "https://github.com/testcontainers/testcontainers-node" edit_uri: edit/main/docs/ theme: - name: 'material' - custom_dir: 'docs/site/theme' + name: "material" + custom_dir: "docs/site/theme" palette: scheme: testcontainers font: text: Roboto code: Roboto Mono - logo: 'site/logo.svg' - favicon: 'site/favicon.ico' + logo: "site/logo.svg" + favicon: "site/favicon.ico" extra_css: - - 'site/css/extra.css' - - 'site/css/tc-header.css' + - "site/css/extra.css" + - "site/css/tc-header.css" plugins: - search @@ -56,6 +56,7 @@ nav: - Couchbase: modules/couchbase.md - CockroachDB: modules/cockroachdb.md - Elasticsearch: modules/elasticsearch.md + - Etcd: modules/etcd.md - EventStoreDB: modules/eventstoredb.md - GCloud: modules/gcloud.md - HiveMQ: modules/hivemq.md diff --git a/packages/modules/etcd/src/etcd-container.test.ts b/packages/modules/etcd/src/etcd-container.test.ts index 7863737c1..ec7b53a96 100644 --- a/packages/modules/etcd/src/etcd-container.test.ts +++ b/packages/modules/etcd/src/etcd-container.test.ts @@ -1,11 +1,17 @@ import { Etcd3 } from "etcd3"; -import { EtcdContainer } from "./etcd-container"; +import { promisify } from "node:util"; +import { EtcdContainer, StartedEtcdContainer } from "./etcd-container"; + +const sleep = promisify(setTimeout); describe("etcd", () => { it("should construct a container", async () => { - await new EtcdContainer().start(); + const container = await new EtcdContainer().start(); + expect(container).toBeInstanceOf(StartedEtcdContainer); + container.stop(); }); + // readWrite { it("should connect and perform read/write operations", async () => { const container = await new EtcdContainer().start(); const client = new Etcd3({ @@ -17,5 +23,27 @@ describe("etcd", () => { await client.put(key).value(value); const result = await client.get(key).string(); expect(result).toEqual(value); + + await container.stop(); + }); + // } + + // subscribe { + it("should subscribe to key changes", async () => { + const subscriber = vi.fn(); + const container = await new EtcdContainer().start(); + const client = new Etcd3({ + hosts: container.getClientEndpoint(), + }); + const key = "foo"; + const value = "bar"; + const watcher = await client.watch().key(key).create(); + watcher.on("put", subscriber); + await client.put(key).value(value); + await sleep(1000); + expect(subscriber).toHaveBeenCalled(); + await watcher.cancel(); + await container.stop(); }); + // } }); diff --git a/packages/modules/etcd/src/etcd-container.ts b/packages/modules/etcd/src/etcd-container.ts index 937288778..f8bad5d25 100755 --- a/packages/modules/etcd/src/etcd-container.ts +++ b/packages/modules/etcd/src/etcd-container.ts @@ -11,9 +11,9 @@ export class EtcdContainer extends GenericContainer { "--name", nodeName, "--initial-advertise-peer-urls", - `http://${nodeName}:${ETCD_PEER_PORT}`, + `http://0.0.0.0:${ETCD_PEER_PORT}`, "--advertise-client-urls", - `http://${nodeName}:${ETCD_CLIENT_PORT}`, + `http://0.0.0.0:${ETCD_CLIENT_PORT}`, "--listen-peer-urls", `http://0.0.0.0:${ETCD_PEER_PORT}`, "--listen-client-urls", From a9999c292fd9551478ce010f30c21a4e5f973c71 Mon Sep 17 00:00:00 2001 From: prenaissance Date: Mon, 19 May 2025 16:43:00 +0300 Subject: [PATCH 3/3] chore(etcd): increase test timeout, style fixes --- .../modules/etcd/src/etcd-container.test.ts | 8 ++--- packages/modules/etcd/src/etcd-container.ts | 29 ++++++++++--------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/modules/etcd/src/etcd-container.test.ts b/packages/modules/etcd/src/etcd-container.test.ts index ec7b53a96..b311288a3 100644 --- a/packages/modules/etcd/src/etcd-container.test.ts +++ b/packages/modules/etcd/src/etcd-container.test.ts @@ -1,11 +1,9 @@ import { Etcd3 } from "etcd3"; -import { promisify } from "node:util"; +import { setTimeout } from "node:timers/promises"; import { EtcdContainer, StartedEtcdContainer } from "./etcd-container"; -const sleep = promisify(setTimeout); - describe("etcd", () => { - it("should construct a container", async () => { + it("should construct a container", { timeout: 30_000 }, async () => { const container = await new EtcdContainer().start(); expect(container).toBeInstanceOf(StartedEtcdContainer); container.stop(); @@ -40,7 +38,7 @@ describe("etcd", () => { const watcher = await client.watch().key(key).create(); watcher.on("put", subscriber); await client.put(key).value(value); - await sleep(1000); + await setTimeout(1_000); expect(subscriber).toHaveBeenCalled(); await watcher.cancel(); await container.stop(); diff --git a/packages/modules/etcd/src/etcd-container.ts b/packages/modules/etcd/src/etcd-container.ts index f8bad5d25..d202c077f 100755 --- a/packages/modules/etcd/src/etcd-container.ts +++ b/packages/modules/etcd/src/etcd-container.ts @@ -6,23 +6,24 @@ const ETCD_PEER_PORT = 2380; export class EtcdContainer extends GenericContainer { constructor(image = "quay.io/coreos/etcd:v3.6.0", nodeName = "etcd-test") { super(image); - this.withExposedPorts(ETCD_CLIENT_PORT, ETCD_PEER_PORT).withCommand([ - "etcd", - "--name", - nodeName, - "--initial-advertise-peer-urls", - `http://0.0.0.0:${ETCD_PEER_PORT}`, - "--advertise-client-urls", - `http://0.0.0.0:${ETCD_CLIENT_PORT}`, - "--listen-peer-urls", - `http://0.0.0.0:${ETCD_PEER_PORT}`, - "--listen-client-urls", - `http://0.0.0.0:${ETCD_CLIENT_PORT}`, - ]); + this.withExposedPorts(ETCD_CLIENT_PORT, ETCD_PEER_PORT) + .withCommand([ + "etcd", + "--name", + nodeName, + "--initial-advertise-peer-urls", + `http://0.0.0.0:${ETCD_PEER_PORT}`, + "--advertise-client-urls", + `http://0.0.0.0:${ETCD_CLIENT_PORT}`, + "--listen-peer-urls", + `http://0.0.0.0:${ETCD_PEER_PORT}`, + "--listen-client-urls", + `http://0.0.0.0:${ETCD_CLIENT_PORT}`, + ]) + .withWaitStrategy(Wait.forLogMessage(/"status":"SERVING"/)); } public override async start(): Promise { - this.withWaitStrategy(Wait.forLogMessage(/"status":"SERVING"/)); return new StartedEtcdContainer(await super.start()); } }