diff --git a/packages/testcontainers/src/container-manager/container-manager-master.ts b/packages/testcontainers/src/container-manager/container-manager-master.ts new file mode 100644 index 000000000..10b2bbd33 --- /dev/null +++ b/packages/testcontainers/src/container-manager/container-manager-master.ts @@ -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 = 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 { + 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 { + 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; + } +} \ No newline at end of file diff --git a/packages/testcontainers/src/container-manager/destory-manager.ts b/packages/testcontainers/src/container-manager/destory-manager.ts new file mode 100644 index 000000000..34c427695 --- /dev/null +++ b/packages/testcontainers/src/container-manager/destory-manager.ts @@ -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 { + await ContainerManagerMaster.getInstance().addContainer(container); + } + public static async destroyAllContainers(): Promise { + await ContainerManagerMaster.getInstance().destroyAllContainers(); + } + public static async removeContainer(container: StartedTestContainer): Promise { + return ContainerManagerMaster.getInstance().removeContainer(container); + } +} \ No newline at end of file diff --git a/packages/testcontainers/src/generic-container/abstract-started-container.ts b/packages/testcontainers/src/generic-container/abstract-started-container.ts index 1ccac33b7..d9ae4014c 100644 --- a/packages/testcontainers/src/generic-container/abstract-started-container.ts +++ b/packages/testcontainers/src/generic-container/abstract-started-container.ts @@ -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) {} @@ -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; } diff --git a/packages/testcontainers/src/generic-container/generic-container-signal.test.ts b/packages/testcontainers/src/generic-container/generic-container-signal.test.ts new file mode 100644 index 000000000..00caf6fed --- /dev/null +++ b/packages/testcontainers/src/generic-container/generic-container-signal.test.ts @@ -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(); + }); +}); diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 7c2ec2358..60425f185 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -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"; @@ -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; @@ -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; } diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index 7bb052d76..7ca67f1f1 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -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; @@ -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; }); } diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index 148ef18e6..50729a6bf 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -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 {