Skip to content

Commit 3c11b70

Browse files
Merge branch 'main' into release-11.0.0
2 parents 8d72a34 + e7c499e commit 3c11b70

File tree

12 files changed

+223
-58
lines changed

12 files changed

+223
-58
lines changed

docs/features/containers.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,26 @@ const container = await new GenericContainer("alpine").start();
348348
await container.stop({ remove: false });
349349
```
350350

351+
Alternatively, you can disable automatic removal while configuring the container:
352+
353+
```javascript
354+
const container = await new GenericContainer("alpine")
355+
.withAutoRemove(false)
356+
.start();
357+
358+
await container.stop()
359+
```
360+
361+
The value specified to `.withAutoRemove()` can be overridden by `.stop()`:
362+
363+
```javascript
364+
const container = await new GenericContainer("alpine")
365+
.withAutoRemove(false)
366+
.start();
367+
368+
await container.stop({ remove: true }) // The container is stopped *AND* removed
369+
```
370+
351371
Volumes created by the container are removed when stopped. This is configurable:
352372

353373
```javascript

docs/modules/chromadb.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,37 @@
88
npm install @testcontainers/chromadb --save-dev
99
```
1010

11-
## Example
11+
## Resources
12+
13+
* [GitHub](https://github.com/chroma-core/chroma)
14+
* [Node.js Client](https://www.npmjs.com/package/chromadb)
15+
* [Docs](https://docs.trychroma.com)
16+
* [Discord](https://discord.gg/MMeYNTmh3x)
17+
* [Cookbook](https://cookbook.chromadb.dev)
18+
19+
## Examples
20+
21+
<!--codeinclude-->
22+
[Connect to Chroma:](../../packages/modules/chromadb/src/chromadb-container.test.ts)
23+
inside_block:simpleConnect
24+
<!--/codeinclude-->
25+
26+
<!--codeinclude-->
27+
[Create Collection:](../../packages/modules/chromadb/src/chromadb-container.test.ts)
28+
inside_block:createCollection
29+
<!--/codeinclude-->
30+
31+
<!--codeinclude-->
32+
[Query Collection with Embedding Function:](../../packages/modules/chromadb/src/chromadb-container.test.ts)
33+
inside_block:queryCollectionWithEmbeddingFunction
34+
<!--/codeinclude-->
35+
36+
<!--codeinclude-->
37+
[Work with persistent directory:](../../packages/modules/chromadb/src/chromadb-container.test.ts)
38+
inside_block:persistentData
39+
<!--/codeinclude-->
1240

1341
<!--codeinclude-->
14-
[](../../packages/modules/chromadb/src/chromadb-container.test.ts) inside_block:docs
42+
[Work with authentication:](../../packages/modules/chromadb/src/chromadb-container.test.ts) inside_block:auth
1543
<!--/codeinclude-->
1644

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/modules/chromadb/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"build": "tsc --project tsconfig.build.json"
3030
},
3131
"devDependencies": {
32-
"chromadb": "^2.0.1"
32+
"chromadb": "^2.0.1",
33+
"ollama": "^0.5.14"
3334
},
3435
"dependencies": {
3536
"testcontainers": "^10.21.0"
Lines changed: 114 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,127 @@
1-
import { AdminClient, ChromaClient } from "chromadb";
2-
import { ChromaDBContainer } from "./chromadb-container";
1+
import { AdminClient, ChromaClient, OllamaEmbeddingFunction } from "chromadb";
2+
import fs from "node:fs";
3+
import os from "node:os";
4+
import path from "node:path";
5+
import { GenericContainer } from "testcontainers";
6+
import { ChromaDBContainer, StartedChromaDBContainer } from "./chromadb-container";
37

4-
const IMAGE = "chromadb/chroma:0.4.24";
8+
const IMAGE = "chromadb/chroma:0.6.3";
59

610
describe("ChromaDB", { timeout: 360_000 }, () => {
7-
// docs {
8-
it("should connect and return a query result", async () => {
11+
// startContainer {
12+
it("should connect", async () => {
913
const container = await new ChromaDBContainer(IMAGE).start();
14+
const client = await connectTo(container);
15+
expect(await client.heartbeat()).toBeDefined();
16+
// Do something with the client
17+
await container.stop();
18+
});
19+
// }
20+
21+
// simpleConnect {
22+
async function connectTo(container: StartedChromaDBContainer) {
23+
const client = new ChromaClient({
24+
path: container.getHttpUrl(),
25+
});
26+
const hb = await client.heartbeat();
27+
expect(hb).toBeDefined();
28+
return client;
29+
}
30+
// }
31+
32+
// createCollection {
33+
it("should create collection and get data", async () => {
34+
const container = await new ChromaDBContainer(IMAGE).start();
35+
const client = await connectTo(container);
36+
const collection = await client.createCollection({ name: "test", metadata: { "hnsw:space": "cosine" } });
37+
expect(collection.name).toBe("test");
38+
expect(collection.metadata).toBeDefined();
39+
expect(collection.metadata?.["hnsw:space"]).toBe("cosine");
40+
await collection.add({ ids: ["1"], embeddings: [[1, 2, 3]], documents: ["my doc"], metadatas: [{ key: "value" }] });
41+
const getResults = await collection.get({ ids: ["1"] });
42+
expect(getResults.ids[0]).toBe("1");
43+
expect(getResults.documents[0]).toStrictEqual("my doc");
44+
expect(getResults.metadatas).toBeDefined();
45+
expect(getResults.metadatas?.[0]?.key).toStrictEqual("value");
46+
await container.stop();
47+
});
48+
// }
49+
50+
// queryCollectionWithEmbeddingFunction {
51+
it("should create collection and query", async () => {
52+
const container = await new ChromaDBContainer(IMAGE).start();
53+
const ollama = await new GenericContainer("ollama/ollama").withExposedPorts(11434).start();
54+
await ollama.exec(["ollama", "pull", "nomic-embed-text"]);
55+
const client = await connectTo(container);
56+
const embedder = new OllamaEmbeddingFunction({
57+
url: `http://${ollama.getHost()}:${ollama.getMappedPort(11434)}/api/embeddings`,
58+
model: "nomic-embed-text",
59+
});
60+
const collection = await client.createCollection({
61+
name: "test",
62+
metadata: { "hnsw:space": "cosine" },
63+
embeddingFunction: embedder,
64+
});
65+
expect(collection.name).toBe("test");
66+
await collection.add({
67+
ids: ["1", "2"],
68+
documents: [
69+
"This is a document about dogs. Dogs are awesome.",
70+
"This is a document about cats. Cats are awesome.",
71+
],
72+
});
73+
const results = await collection.query({ queryTexts: ["Tell me about dogs"], nResults: 1 });
74+
expect(results).toBeDefined();
75+
expect(results.ids[0]).toEqual(["1"]);
76+
expect(results.ids[0][0]).toBe("1");
77+
await container.stop();
78+
});
79+
80+
// persistentData {
81+
it("should reconnect with volume and persistence data", async () => {
82+
const sourcePath = fs.mkdtempSync(path.join(os.tmpdir(), "chroma-temp"));
83+
const container = await new ChromaDBContainer(IMAGE)
84+
.withBindMounts([{ source: sourcePath, target: "/chroma/chroma" }])
85+
.start();
86+
const client = await connectTo(container);
87+
const collection = await client.createCollection({ name: "test", metadata: { "hnsw:space": "cosine" } });
88+
expect(collection.name).toBe("test");
89+
expect(collection.metadata).toBeDefined();
90+
expect(collection.metadata?.["hnsw:space"]).toBe("cosine");
91+
await collection.add({ ids: ["1"], embeddings: [[1, 2, 3]], documents: ["my doc"] });
92+
const getResults = await collection.get({ ids: ["1"] });
93+
expect(getResults.ids[0]).toBe("1");
94+
expect(getResults.documents[0]).toStrictEqual("my doc");
95+
await container.stop();
96+
expect(fs.existsSync(`${sourcePath}/chroma.sqlite3`)).toBe(true);
97+
try {
98+
fs.rmSync(sourcePath, { force: true, recursive: true });
99+
} catch (e) {
100+
// Ignore clean up, when have no access on fs.
101+
console.log(e);
102+
}
103+
});
104+
// }
105+
106+
// auth {
107+
it("should use auth", async () => {
10108
const tenant = "test-tenant";
11109
const key = "test-key";
12110
const database = "test-db";
111+
const container = await new ChromaDBContainer(IMAGE)
112+
.withEnvironment({
113+
CHROMA_SERVER_AUTHN_CREDENTIALS: key,
114+
CHROMA_SERVER_AUTHN_PROVIDER: "chromadb.auth.token_authn.TokenAuthenticationServerProvider",
115+
CHROMA_AUTH_TOKEN_TRANSPORT_HEADER: "X-Chroma-Token",
116+
})
117+
.start();
118+
13119
const adminClient = new AdminClient({
14120
tenant: tenant,
15121
auth: {
16122
provider: "token",
17123
credentials: key,
18-
providerOptions: {
19-
headerType: "X_CHROMA_TOKEN",
20-
},
124+
tokenHeaderType: "X_CHROMA_TOKEN",
21125
},
22126
path: container.getHttpUrl(),
23127
});
@@ -30,52 +134,14 @@ describe("ChromaDB", { timeout: 360_000 }, () => {
30134
auth: {
31135
provider: "token",
32136
credentials: key,
33-
providerOptions: {
34-
headerType: "X_CHROMA_TOKEN",
35-
},
137+
tokenHeaderType: "X_CHROMA_TOKEN",
36138
},
37139
path: container.getHttpUrl(),
38140
database,
39141
});
40142

41143
const collection = await dbClient.createCollection({ name: "test-collection" });
42-
43-
await collection.add({
44-
ids: ["1", "2", "3"],
45-
documents: ["apple", "oranges", "pineapple"],
46-
embeddings: [
47-
[1, 2, 3],
48-
[4, 5, 6],
49-
[7, 8, 9],
50-
],
51-
});
52-
53-
const result = await collection.get({ ids: ["1", "2", "3"] });
54-
55-
expect(result).toMatchInlineSnapshot(`
56-
{
57-
"data": null,
58-
"documents": [
59-
"apple",
60-
"oranges",
61-
"pineapple",
62-
],
63-
"embeddings": null,
64-
"ids": [
65-
"1",
66-
"2",
67-
"3",
68-
],
69-
"metadatas": [
70-
null,
71-
null,
72-
null,
73-
],
74-
"uris": null,
75-
}
76-
`);
77-
78-
await container.stop();
144+
expect(collection.name).toBe("test-collection");
79145
});
80146
// }
81147
});

packages/modules/chromadb/src/chromadb-container.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export class ChromaDBContainer extends GenericContainer {
66
constructor(image: string) {
77
super(image);
88
this.withExposedPorts(CHROMADB_PORT)
9-
.withWaitStrategy(Wait.forHttp("/api/v1/heartbeat", CHROMADB_PORT))
9+
.withWaitStrategy(Wait.forHttp("/api/v2/heartbeat", CHROMADB_PORT))
1010
.withStartupTimeout(120_000);
1111
}
1212

packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ export class DockerComposeEnvironment {
168168
inspectResult,
169169
boundPorts,
170170
containerName,
171-
waitStrategy
171+
waitStrategy,
172+
true
172173
);
173174
})
174175
)

packages/testcontainers/src/generic-container/generic-container.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,34 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
518518
expect(await getRunningContainerNames()).not.toContain(container.getName());
519519
});
520520

521+
it("should stop but not remove the container", async () => {
522+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
523+
.withName(`container-${new RandomUuid().nextUuid()}`)
524+
.withAutoRemove(false)
525+
.start();
526+
527+
const stopped = await container.stop();
528+
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
529+
expect(stopped.getId()).toBeTruthy();
530+
const lowerLevelContainer = dockerode.getContainer(stopped.getId());
531+
expect((await lowerLevelContainer.inspect()).State.Status).toEqual("exited");
532+
});
533+
534+
it("should stop and override .withAutoRemove", async () => {
535+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
536+
.withName(`container-${new RandomUuid().nextUuid()}`)
537+
.withAutoRemove(false)
538+
.start();
539+
540+
await container.stop({ remove: true });
541+
542+
const stopped = await container.stop();
543+
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
544+
expect(stopped.getId()).toBeTruthy();
545+
const lowerLevelContainer = dockerode.getContainer(stopped.getId());
546+
await expect(lowerLevelContainer.inspect()).rejects.toThrow(/404/); // Error: (HTTP code 404) no such container
547+
});
548+
521549
it("should build a target stage", async () => {
522550
const context = path.resolve(fixtures, "docker-multi-stage");
523551
const firstContainer = await GenericContainer.fromDockerfile(context).withTarget("first").build();

packages/testcontainers/src/generic-container/generic-container.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class GenericContainer implements TestContainer {
5151
protected environment: Record<string, string> = {};
5252
protected exposedPorts: PortWithOptionalBinding[] = [];
5353
protected reuse = false;
54+
protected autoRemove = true;
5455
protected networkMode?: string;
5556
protected networkAliases: string[] = [];
5657
protected pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
@@ -160,7 +161,8 @@ export class GenericContainer implements TestContainer {
160161
inspectResult,
161162
boundPorts,
162163
inspectResult.Name,
163-
this.waitStrategy
164+
this.waitStrategy,
165+
this.autoRemove
164166
);
165167
}
166168

@@ -228,7 +230,8 @@ export class GenericContainer implements TestContainer {
228230
inspectResult,
229231
boundPorts,
230232
inspectResult.Name,
231-
this.waitStrategy
233+
this.waitStrategy,
234+
this.autoRemove
232235
);
233236

234237
if (this.containerStarted) {
@@ -439,6 +442,11 @@ export class GenericContainer implements TestContainer {
439442
return this;
440443
}
441444

445+
public withAutoRemove(autoRemove: boolean): this {
446+
this.autoRemove = autoRemove;
447+
return this;
448+
}
449+
442450
public withPullPolicy(pullPolicy: ImagePullPolicy): this {
443451
this.pullPolicy = pullPolicy;
444452
return this;

packages/testcontainers/src/generic-container/started-generic-container.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export class StartedGenericContainer implements StartedTestContainer {
2424
private inspectResult: ContainerInspectInfo,
2525
private boundPorts: BoundPorts,
2626
private readonly name: string,
27-
private readonly waitStrategy: WaitStrategy
27+
private readonly waitStrategy: WaitStrategy,
28+
private readonly autoRemove: boolean
2829
) {}
2930

3031
protected containerIsStopping?(): Promise<void>;
@@ -105,7 +106,7 @@ export class StartedGenericContainer implements StartedTestContainer {
105106
await this.containerIsStopping();
106107
}
107108

108-
const resolvedOptions: StopOptions = { remove: true, timeout: 0, removeVolumes: true, ...options };
109+
const resolvedOptions: StopOptions = { remove: this.autoRemove, timeout: 0, removeVolumes: true, ...options };
109110
await client.container.stop(this.container, { timeout: resolvedOptions.timeout });
110111
if (resolvedOptions.remove) {
111112
await client.container.remove(this.container, { removeVolumes: resolvedOptions.removeVolumes });

0 commit comments

Comments
 (0)