Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for reusing stopped containers #849

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -7,12 +7,16 @@ import Dockerode, {
Network,
} from "dockerode";
import { Readable } from "stream";
import { ExecOptions, ExecResult } from "./types";
import { ContainerStatus, ExecOptions, ExecResult } from "./types";

export interface ContainerClient {
dockerode: Dockerode;
getById(id: string): Container;
fetchByLabel(labelName: string, labelValue: string): Promise<Container | undefined>;
fetchByLabel(
labelName: string,
labelValue: string,
opts?: { status?: ContainerStatus[] }
): Promise<Container | undefined>;
fetchArchive(container: Container, path: string): Promise<NodeJS.ReadableStream>;
putArchive(container: Dockerode.Container, stream: Readable, path: string): Promise<void>;
list(): Promise<ContainerInfo[]>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Dockerode, {
} from "dockerode";
import { PassThrough, Readable } from "stream";
import { IncomingMessage } from "http";
import { ExecOptions, ExecResult } from "./types";
import { ContainerStatus, ExecOptions, ExecResult } from "./types";
import byline from "byline";
import { ContainerClient } from "./container-client";
import { execLog, log, streamToString } from "../../../common";
Expand All @@ -29,15 +29,24 @@ export class DockerContainerClient implements ContainerClient {
}
}

async fetchByLabel(labelName: string, labelValue: string): Promise<Container | undefined> {
async fetchByLabel(
labelName: string,
labelValue: string,
opts: { status?: ContainerStatus[] } | undefined = undefined
): Promise<Container | undefined> {
try {
const filters: { [key: string]: string[] } = {
label: [`${labelName}=${labelValue}`],
};

if (opts?.status) {
filters.status = opts.status;
}

log.debug(`Fetching container by label "${labelName}=${labelValue}"...`);
const containers = await this.dockerode.listContainers({
limit: 1,
filters: {
status: ["running"],
label: [`${labelName}=${labelValue}`],
},
filters,
});
if (containers.length === 0) {
log.debug(`No container found with label "${labelName}=${labelValue}"`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export type Environment = { [key in string]: string };
export type ExecOptions = { workingDir: string; user: string; env: Environment; log: boolean };

export type ExecResult = { output: string; exitCode: number };

export const ContainerStatus = ["created", "restarting", "running", "removing", "paused", "exited", "dead"] as const;
cbrevik marked this conversation as resolved.
Show resolved Hide resolved

export type ContainerStatus = (typeof ContainerStatus)[number];
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe("GenericContainer reuse", () => {
await container1.stop();
});

it("should create a new container when an existing reusable container has stopped", async () => {
it("should create a new container when an existing reusable container has stopped and is removed", async () => {
const container1 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName("there_can_only_be_one")
.withExposedPorts(8080)
Expand All @@ -102,6 +102,25 @@ describe("GenericContainer reuse", () => {
await container2.stop();
});

it("should reuse container when an existing reusable container has stopped but not removed", async () => {
const container1 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName("there_can_only_be_one")
.withExposedPorts(8080)
.withReuse()
.start();
await container1.stop({ remove: false, timeout: 10000 });

const container2 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName("there_can_only_be_one")
.withExposedPorts(8080)
.withReuse()
.start();
await checkContainerIsHealthy(container2);

expect(container1.getId()).toBe(container2.getId());
await container2.stop();
});

it("should keep the labels passed in when a new reusable container is created", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName("there_can_only_be_one")
Expand Down
16 changes: 14 additions & 2 deletions packages/testcontainers/src/generic-container/generic-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { containerLog, hash, log } from "../common";
import { BoundPorts } from "../utils/bound-ports";
import { StartedNetwork } from "../network/network";
import { mapInspectResult } from "../utils/map-inspect-result";
import { ContainerStatus } from "../container-runtime/clients/container/types";

const reusableContainerCreationLock = new AsyncLock();

Expand Down Expand Up @@ -117,7 +118,11 @@ export class GenericContainer implements TestContainer {
log.debug(`Container reuse has been enabled with hash "${containerHash}"`);

return reusableContainerCreationLock.acquire(containerHash, async () => {
const container = await client.container.fetchByLabel(LABEL_TESTCONTAINERS_CONTAINER_HASH, containerHash);
const container = await client.container.fetchByLabel(LABEL_TESTCONTAINERS_CONTAINER_HASH, containerHash, {
status: ContainerStatus.filter(
(status) => status !== "removing" && status !== "dead" && status !== "restarting"
),
});
if (container !== undefined) {
log.debug(`Found container to reuse with hash "${containerHash}"`, { containerId: container.id });
return this.reuseContainer(client, container);
Expand All @@ -128,7 +133,14 @@ export class GenericContainer implements TestContainer {
}

private async reuseContainer(client: ContainerRuntimeClient, container: Container) {
const inspectResult = await client.container.inspect(container);
let inspectResult = await client.container.inspect(container);
if (!inspectResult.State.Running) {
log.debug("Reused container is not running, attempting to start it");
await client.container.start(container);
// Refetch the inspect result to get the updated state
inspectResult = await client.container.inspect(container);
}

const mappedInspectResult = mapInspectResult(inspectResult);
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(
this.exposedPorts
Expand Down
Loading