Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { log } from "../common";
import { StartedTestContainer } from "../test-container";

export class ContainerManagerMaster{
private static instance: ContainerManagerMaster | null = null;
private containers: Map<string, StartedTestContainer> = new Map();
private isShuttingDown = false;

protected constructor() { }

public static getInstance(): ContainerManagerMaster {
if (!ContainerManagerMaster.instance) {
ContainerManagerMaster.instance = new ContainerManagerMaster();
}
return ContainerManagerMaster.instance;
}
public async addContainer(container: StartedTestContainer): Promise<void> {
log.debug(`Adding container ${container.getId()}...`);
this.containers.set(container.getId(), container);
}
public removeContainer(container: StartedTestContainer): boolean {
log.debug(`Removing container ${container.getId()}...`);
return this.containers.delete(container.getId());
}
public async destroyAllContainers(): Promise<void> {
const containerPromises = Array.from(this.containers.values()).map(async (container) => {
try {
const containerId = container.getId();
log.debug(`Stopping container ${containerId}...`);
await container.stop();
log.debug(`Container ${containerId} stopped successfully`);
} catch (error) {
log.error(`Error stopping container ${container.getId()}:${error}`);
}
});
await Promise.allSettled(containerPromises);
this.destroyInstance();
}
public getContainerCount(): number {
log.debug(`containers size is ${this.containers.size}`);
return this.containers.size;
}

public destroyInstance(): void {
ContainerManagerMaster.instance = null;
}
}
16 changes: 16 additions & 0 deletions packages/testcontainers/src/container-manager/destory-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { StartedTestContainer } from "../test-container";
import { ContainerManagerMaster } from "./container-manager-master";

export class ContainerManager{


public static async addContainer(container: StartedTestContainer): Promise<void> {
await ContainerManagerMaster.getInstance().addContainer(container);
}
public static async destroyAllContainers(): Promise<void> {
await ContainerManagerMaster.getInstance().destroyAllContainers();
}
public static async removeContainer(container: StartedTestContainer): Promise<boolean> {
return ContainerManagerMaster.getInstance().removeContainer(container);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Readable } from "stream";
import { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container";
import { CommitOptions, ContentToCopy, DirectoryToCopy, ExecOptions, ExecResult, FileToCopy, Labels } from "../types";
import { ContainerManager } from "../container-manager/destory-manager";

export class AbstractStartedContainer implements StartedTestContainer {
constructor(protected readonly startedTestContainer: StartedTestContainer) {}
Expand All @@ -17,6 +18,9 @@ export class AbstractStartedContainer implements StartedTestContainer {
if (this.containerStopped) {
await this.containerStopped();
}
if (this.startedTestContainer.getLabels()["testcontainers.bulkDelete"] === "true") {
ContainerManager.removeContainer(this.startedTestContainer);
}

return stoppedContainer;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ContainerManagerMaster } from "../container-manager/container-manager-master";
import { ContainerManager } from "../container-manager/destory-manager";
import { getRunningContainerNames } from "../utils/test-helper";
import { GenericContainer } from "./generic-container";


describe("GenericContainer manager", { timeout: 180_000 }, () => {
it("should add container to manager", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).withSignal().start();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(1);
await container.stop();
ContainerManagerMaster.getInstance().destroyInstance();

});
it("should remove container from manager", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).withSignal().start();
expect(await ContainerManager.removeContainer(container)).toBe(true);
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(0);
ContainerManagerMaster.getInstance().destroyInstance();
});
it("should destroy all containers when destory is called", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).withSignal().start();
expect(container.getId()).toBeDefined();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(1);
await ContainerManagerMaster.getInstance().destroyAllContainers();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(0);
expect(await getRunningContainerNames()).not.toContain(container.getName());
ContainerManagerMaster.getInstance().destroyInstance();
});
it("should destroy all containers created ", async () => {
const containersName = [];
for (let i = 0; i < 10; i++) {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).withSignal().start();
expect(container.getId()).toBeDefined();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(i + 1);
containersName.push(container.getName());
}
await ContainerManager.destroyAllContainers();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(0);
ContainerManagerMaster.getInstance().destroyInstance();
});
it("if stops called remove from the manager", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).withSignal().start();
expect(container.getId()).toBeDefined();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(1);
await container.stop();
expect(ContainerManagerMaster.getInstance().getContainerCount()).toBe(0);
expect(await getRunningContainerNames()).not.toContain(container.getName());
await expect(ContainerManager.destroyAllContainers()).resolves.not.toThrow();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AsyncLock from "async-lock";
import { Container, ContainerCreateOptions, HostConfig } from "dockerode";
import { Readable } from "stream";
import { containerLog, hash, log, toNanos } from "../common";
import { ContainerManager } from "../container-manager/destory-manager";
import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime";
import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types";
import { StartedNetwork } from "../network/network";
Expand Down Expand Up @@ -68,6 +69,11 @@ export class GenericContainer implements TestContainer {
this.createOpts = { Image: this.imageName.string };
this.hostConfig = { AutoRemove: this.imageName.string === REAPER_IMAGE };
}
public withSignal(): this {
this.hostConfig.BulkDelete = true;
this.createOpts.Labels = { ...this.createOpts.Labels, "testcontainers.bulkDelete": "true" };
return this;
}

private isHelperContainer() {
return this.isReaper() || this.imageName.string === SSHD_IMAGE;
Expand Down Expand Up @@ -246,6 +252,9 @@ export class GenericContainer implements TestContainer {
if (this.containerStarted) {
await this.containerStarted(startedContainer, mappedInspectResult, false);
}
if (this.hostConfig.BulkDelete) {
ContainerManager.addContainer(startedContainer);
}

return startedContainer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { mapInspectResult } from "../utils/map-inspect-result";
import { waitForContainer } from "../wait-strategies/wait-for-container";
import { WaitStrategy } from "../wait-strategies/wait-strategy";
import { StoppedGenericContainer } from "./stopped-generic-container";
import { ContainerManager } from "../container-manager/destory-manager";

export class StartedGenericContainer implements StartedTestContainer {
private stoppedContainer?: StoppedTestContainer;
Expand All @@ -36,6 +37,9 @@ export class StartedGenericContainer implements StartedTestContainer {
return this.stoppedContainer;
}
this.stoppedContainer = await this.stopContainer(options);
if (this.getLabels()["testcontainers.bulkDelete"] === "true") {
ContainerManager.removeContainer(this);
}
return this.stoppedContainer;
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/testcontainers/src/test-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface TestContainer {
withSharedMemorySize(bytes: number): this;
withLogConsumer(logConsumer: (stream: Readable) => unknown): this;
withHostname(hostname: string): this;
withSignal(signal: string): this;
}

export interface RestartOptions {
Expand Down