From af977a9a823fe2f468a39df42b2b1e74eac0f7c3 Mon Sep 17 00:00:00 2001 From: Leo Urbina Date: Fri, 14 Feb 2025 00:28:15 +0100 Subject: [PATCH 1/4] feat: Adds Cockroachdb container --- docs/modules/cockroachdb.md | 28 +++++ mkdocs.yml | 1 + package-lock.json | 16 +++ packages/modules/cockroachdb/jest.config.ts | 11 ++ packages/modules/cockroachdb/package.json | 39 +++++++ .../src/cockroachdb-container.test.ts | 108 ++++++++++++++++++ .../cockroachdb/src/cockroachdb-container.ts | 75 ++++++++++++ packages/modules/cockroachdb/src/index.ts | 1 + .../modules/cockroachdb/tsconfig.build.json | 13 +++ packages/modules/cockroachdb/tsconfig.json | 21 ++++ 10 files changed, 313 insertions(+) create mode 100644 docs/modules/cockroachdb.md create mode 100644 packages/modules/cockroachdb/jest.config.ts create mode 100644 packages/modules/cockroachdb/package.json create mode 100644 packages/modules/cockroachdb/src/cockroachdb-container.test.ts create mode 100755 packages/modules/cockroachdb/src/cockroachdb-container.ts create mode 100644 packages/modules/cockroachdb/src/index.ts create mode 100644 packages/modules/cockroachdb/tsconfig.build.json create mode 100644 packages/modules/cockroachdb/tsconfig.json diff --git a/docs/modules/cockroachdb.md b/docs/modules/cockroachdb.md new file mode 100644 index 000000000..07bf4c73d --- /dev/null +++ b/docs/modules/cockroachdb.md @@ -0,0 +1,28 @@ +# CockroachDB Module + +[CockroachDB](https://github.com/cockroachdb/cockroach) is a cloud-native, postgresql compatible, distributed SQL database designed to build, scale, and manage modern, data-intensive applications. + + +## Install + +```bash +npm install @testcontainers/cockroachdb --save-dev +``` + +## Examples + + +[Connect and execute query:](../../packages/modules/cockroachdb/src/cockroachdb-container.test.ts) inside_block:connect + + + +[Connect and execute query using URI:](../../packages/modules/cockroachdb/src/cockroachdb-container.test.ts) inside_block:uriConnect + + + +[Set database:](../../packages/modules/cockroachdb/src/cockroachdb-container.test.ts) inside_block:setDatabase + + + +[Set username:](../../packages/modules/cockroachdb/src/cockroachdb-container.test.ts) inside_block:setUsername + diff --git a/mkdocs.yml b/mkdocs.yml index d9372ea17..1254db9f2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - Cassandra: modules/cassandra.md - ChromaDB: modules/chromadb.md - Couchbase: modules/couchbase.md + - CockroachDB: modules/cockroachdb.md - Elasticsearch: modules/elasticsearch.md - EventStoreDB: modules/eventstoredb.md - GCloud: modules/gcloud.md diff --git a/package-lock.json b/package-lock.json index 5fc443da8..5f06c2dfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5754,6 +5754,10 @@ "resolved": "packages/modules/chromadb", "link": true }, + "node_modules/@testcontainers/cockroachdb": { + "resolved": "packages/modules/cockroachdb", + "link": true + }, "node_modules/@testcontainers/couchbase": { "resolved": "packages/modules/couchbase", "link": true @@ -21194,6 +21198,18 @@ } } }, + "packages/modules/cockroachdb": { + "name": "@testcontainers/cockroachdb", + "version": "10.18.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.18.0" + }, + "devDependencies": { + "@types/pg": "^8.11.6", + "pg": "^8.12.0" + } + }, "packages/modules/couchbase": { "name": "@testcontainers/couchbase", "version": "10.18.0", diff --git a/packages/modules/cockroachdb/jest.config.ts b/packages/modules/cockroachdb/jest.config.ts new file mode 100644 index 000000000..1f677baaf --- /dev/null +++ b/packages/modules/cockroachdb/jest.config.ts @@ -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; diff --git a/packages/modules/cockroachdb/package.json b/packages/modules/cockroachdb/package.json new file mode 100644 index 000000000..77e8e3a02 --- /dev/null +++ b/packages/modules/cockroachdb/package.json @@ -0,0 +1,39 @@ +{ + "name": "@testcontainers/cockroachdb", + "version": "10.18.0", + "license": "MIT", + "keywords": [ + "cockroachdb", + "crdb", + "testing", + "docker", + "testcontainers" + ], + "description": "CockroachDB 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": { + "@types/pg": "^8.11.6", + "pg": "^8.12.0" + }, + "dependencies": { + "testcontainers": "^10.18.0" + } +} diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts new file mode 100644 index 000000000..a13e47f48 --- /dev/null +++ b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts @@ -0,0 +1,108 @@ +import { Client } from "pg"; +import { CockroachDbContainer } from "./cockroachdb-container"; + +describe("CockroachDbContainer", () => { + jest.setTimeout(180_000); + + // connect { + it("should connect and return a query result", async () => { + const container = await new CockroachDbContainer().start(); + + console.log(container.getDatabase(), container.getHost(), container.getPort()); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + ssl: false, + }); + + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": "1" }); + + await client.end(); + await container.stop(); + }); + // } + + // uriConnect { + it("should work with database URI", async () => { + const container = await new CockroachDbContainer().start(); + + const client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": "1" }); + + await client.end(); + await container.stop(); + }); + // } + + // setDatabase { + it("should set database", async () => { + const container = await new CockroachDbContainer().withDatabase("custom_database").start(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + }); + await client.connect(); + + const result = await client.query("SELECT current_database()"); + expect(result.rows[0]).toEqual({ current_database: "custom_database" }); + + await client.end(); + await container.stop(); + }); + // } + + // setUsername { + it("should set username", async () => { + const container = await new CockroachDbContainer().withUsername("custom_username").start(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + }); + await client.connect(); + + const result = await client.query("SELECT current_user"); + expect(result.rows[0]).toEqual({ current_user: "custom_username" }); + + await client.end(); + await container.stop(); + }); + // } + + it("should work with restarted container", async () => { + const container = await new CockroachDbContainer().start(); + const port = container.getFirstMappedPort(); + await container.restart(); + expect(port).not.toEqual(container.getFirstMappedPort()); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": "1" }); + + await client.end(); + await container.stop(); + }); +}); diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.ts b/packages/modules/cockroachdb/src/cockroachdb-container.ts new file mode 100755 index 000000000..f540feed5 --- /dev/null +++ b/packages/modules/cockroachdb/src/cockroachdb-container.ts @@ -0,0 +1,75 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const COCKROACH_PORT = 26257; +const COCKROACH_HTTP_PORT = 26258; + +export class CockroachDbContainer extends GenericContainer { + private database = "test"; + private username = "test"; + + constructor(image = "cockroachdb/cockroach:v24.3.5") { + super(image); + this.withExposedPorts(COCKROACH_PORT, COCKROACH_HTTP_PORT) + .withCommand(["start-single-node", "--insecure", "--http-addr=0.0.0.0:" + COCKROACH_HTTP_PORT]) + .withHealthCheck({ + test: ["CMD-SHELL", "curl -f http://localhost:" + COCKROACH_HTTP_PORT + " || exit 1"], + interval: 1000, + timeout: 3000, + retries: 5, + startPeriod: 1000, + }) + .withWaitStrategy(Wait.forHealthCheck()); + } + + public withDatabase(database: string): this { + this.database = database; + return this; + } + + public withUsername(username: string): this { + this.username = username; + return this; + } + + public override async start(): Promise { + this.withEnvironment({ + COCKROACH_DATABASE: this.database, + COCKROACH_USER: this.username, + }); + return new StartedCockroachDbContainer(await super.start(), this.database, this.username); + } +} + +export class StartedCockroachDbContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly database: string, + private readonly username: string + ) { + super(startedTestContainer); + } + + public getPort(): number { + return this.startedTestContainer.getMappedPort(COCKROACH_PORT); + } + + public getDatabase(): string { + return this.database; + } + + public getUsername(): string { + return this.username; + } + + /** + * @returns A connection URI in the form of `postgres[ql]://[username[:password]@][host[:port],]/database` + */ + public getConnectionUri(): string { + const url = new URL("", "postgres://"); + url.hostname = this.getHost(); + url.port = this.getPort().toString(); + url.pathname = this.getDatabase(); + url.username = this.getUsername(); + return url.toString(); + } +} diff --git a/packages/modules/cockroachdb/src/index.ts b/packages/modules/cockroachdb/src/index.ts new file mode 100644 index 000000000..27dbcc8a2 --- /dev/null +++ b/packages/modules/cockroachdb/src/index.ts @@ -0,0 +1 @@ +export { CockroachDbContainer, StartedCockroachDbContainer } from "./cockroachdb-container"; diff --git a/packages/modules/cockroachdb/tsconfig.build.json b/packages/modules/cockroachdb/tsconfig.build.json new file mode 100644 index 000000000..0222f6ff1 --- /dev/null +++ b/packages/modules/cockroachdb/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "jest.config.ts", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/cockroachdb/tsconfig.json b/packages/modules/cockroachdb/tsconfig.json new file mode 100644 index 000000000..39b165817 --- /dev/null +++ b/packages/modules/cockroachdb/tsconfig.json @@ -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" + } + ] +} \ No newline at end of file From 8ca9e3d92bc955618fccd5ea3b937d89731e79a5 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Sun, 2 Mar 2025 11:53:11 +0000 Subject: [PATCH 2/4] Update healthcheck command --- .../src/cockroachdb-container.test.ts | 2 -- .../cockroachdb/src/cockroachdb-container.ts | 22 ++++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts index a13e47f48..92ee1f7f8 100644 --- a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts +++ b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts @@ -8,8 +8,6 @@ describe("CockroachDbContainer", () => { it("should connect and return a query result", async () => { const container = await new CockroachDbContainer().start(); - console.log(container.getDatabase(), container.getHost(), container.getPort()); - const client = new Client({ host: container.getHost(), port: container.getPort(), diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.ts b/packages/modules/cockroachdb/src/cockroachdb-container.ts index f540feed5..9a220f751 100755 --- a/packages/modules/cockroachdb/src/cockroachdb-container.ts +++ b/packages/modules/cockroachdb/src/cockroachdb-container.ts @@ -9,16 +9,11 @@ export class CockroachDbContainer extends GenericContainer { constructor(image = "cockroachdb/cockroach:v24.3.5") { super(image); - this.withExposedPorts(COCKROACH_PORT, COCKROACH_HTTP_PORT) - .withCommand(["start-single-node", "--insecure", "--http-addr=0.0.0.0:" + COCKROACH_HTTP_PORT]) - .withHealthCheck({ - test: ["CMD-SHELL", "curl -f http://localhost:" + COCKROACH_HTTP_PORT + " || exit 1"], - interval: 1000, - timeout: 3000, - retries: 5, - startPeriod: 1000, - }) - .withWaitStrategy(Wait.forHealthCheck()); + this.withExposedPorts(COCKROACH_PORT, COCKROACH_HTTP_PORT).withCommand([ + "start-single-node", + "--insecure", + `--http-addr=0.0.0.0:${COCKROACH_HTTP_PORT}`, + ]); } public withDatabase(database: string): this { @@ -36,6 +31,13 @@ export class CockroachDbContainer extends GenericContainer { COCKROACH_DATABASE: this.database, COCKROACH_USER: this.username, }); + this.withHealthCheck({ + test: ["CMD-SHELL", `cockroach sql --insecure -u ${this.username} -d ${this.database} -e "SELECT 1;"`], + interval: 250, + timeout: 1000, + retries: 1000, + }); + this.withWaitStrategy(Wait.forHealthCheck()); return new StartedCockroachDbContainer(await super.start(), this.database, this.username); } } From 0742fd1a36832c4e860bbb7a301927c1b9e34119 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 3 Mar 2025 08:21:08 +0000 Subject: [PATCH 3/4] Port is not guaranteed to be different on restart --- packages/modules/cockroachdb/src/cockroachdb-container.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts index 92ee1f7f8..e4880225b 100644 --- a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts +++ b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts @@ -85,9 +85,7 @@ describe("CockroachDbContainer", () => { it("should work with restarted container", async () => { const container = await new CockroachDbContainer().start(); - const port = container.getFirstMappedPort(); await container.restart(); - expect(port).not.toEqual(container.getFirstMappedPort()); const client = new Client({ host: container.getHost(), From 075173a9d53ca3a13b58e9a655c666fc7d09b69a Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 3 Mar 2025 16:09:41 +0000 Subject: [PATCH 4/4] Add support for custom health checks in CockroachDbContainer --- .../src/cockroachdb-container.test.ts | 11 +++++++++ .../cockroachdb/src/cockroachdb-container.ts | 23 +++++++++---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts index e4880225b..1b0d7fff2 100644 --- a/packages/modules/cockroachdb/src/cockroachdb-container.test.ts +++ b/packages/modules/cockroachdb/src/cockroachdb-container.test.ts @@ -101,4 +101,15 @@ describe("CockroachDbContainer", () => { await client.end(); await container.stop(); }); + + it("should allow custom healthcheck", async () => { + const container = new CockroachDbContainer().withHealthCheck({ + test: ["CMD-SHELL", "exit 1"], + interval: 100, + retries: 0, + timeout: 0, + }); + + await expect(() => container.start()).rejects.toThrow(); + }); }); diff --git a/packages/modules/cockroachdb/src/cockroachdb-container.ts b/packages/modules/cockroachdb/src/cockroachdb-container.ts index 9a220f751..eef0008d9 100755 --- a/packages/modules/cockroachdb/src/cockroachdb-container.ts +++ b/packages/modules/cockroachdb/src/cockroachdb-container.ts @@ -9,11 +9,9 @@ export class CockroachDbContainer extends GenericContainer { constructor(image = "cockroachdb/cockroach:v24.3.5") { super(image); - this.withExposedPorts(COCKROACH_PORT, COCKROACH_HTTP_PORT).withCommand([ - "start-single-node", - "--insecure", - `--http-addr=0.0.0.0:${COCKROACH_HTTP_PORT}`, - ]); + this.withExposedPorts(COCKROACH_PORT, COCKROACH_HTTP_PORT) + .withCommand(["start-single-node", "--insecure", `--http-addr=0.0.0.0:${COCKROACH_HTTP_PORT}`]) + .withWaitStrategy(Wait.forHealthCheck()); } public withDatabase(database: string): this { @@ -31,13 +29,14 @@ export class CockroachDbContainer extends GenericContainer { COCKROACH_DATABASE: this.database, COCKROACH_USER: this.username, }); - this.withHealthCheck({ - test: ["CMD-SHELL", `cockroach sql --insecure -u ${this.username} -d ${this.database} -e "SELECT 1;"`], - interval: 250, - timeout: 1000, - retries: 1000, - }); - this.withWaitStrategy(Wait.forHealthCheck()); + if (!this.healthCheck) { + this.withHealthCheck({ + test: ["CMD-SHELL", `cockroach sql --insecure -u ${this.username} -d ${this.database} -e "SELECT 1;"`], + interval: 250, + timeout: 1000, + retries: 1000, + }); + } return new StartedCockroachDbContainer(await super.start(), this.database, this.username); } }