Skip to content
Open
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
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions packages/modules/kafka/src/kafka-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class KafkaContainer extends GenericContainer {
private zooKeeperHost?: string;
private zooKeeperPort?: number;
private saslSslConfig?: SaslSslListenerOptions;
private originalWaitinStrategy: WaitStrategy;
private originalWaitinStrategy: WaitStrategy | undefined;

constructor(image: string) {
super(image);
Expand Down Expand Up @@ -193,7 +193,12 @@ export class KafkaContainer extends GenericContainer {
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, inspectResult).filter(
this.exposedPorts
);
await waitForContainer(client, dockerContainer, this.originalWaitinStrategy, boundPorts);
await waitForContainer(
client,
dockerContainer,
this.originalWaitinStrategy ?? Wait.forListeningPorts(),
boundPorts
);

if (this.saslSslConfig && this.mode !== KafkaMode.KRAFT) {
await this.createUser(container, this.saslSslConfig.sasl);
Expand Down
9 changes: 7 additions & 2 deletions packages/modules/redpanda/src/redpanda-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const STARTER_SCRIPT = "/testcontainers_start.sh";
const WAIT_FOR_SCRIPT_MESSAGE = "Waiting for script...";

export class RedpandaContainer extends GenericContainer {
private originalWaitinStrategy: WaitStrategy;
private originalWaitinStrategy: WaitStrategy | undefined;

constructor(image: string) {
super(image);
Expand Down Expand Up @@ -71,7 +71,12 @@ export class RedpandaContainer extends GenericContainer {
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, inspectResult).filter(
this.exposedPorts
);
await waitForContainer(client, dockerContainer, this.originalWaitinStrategy, boundPorts);
await waitForContainer(
client,
dockerContainer,
this.originalWaitinStrategy ?? Wait.forListeningPorts(),
boundPorts
);
}

private renderRedpandaFile(host: string, port: number): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/testcontainers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"@types/dockerode": "^3.3.42",
"@types/dockerode": "^3.3.43",
"archiver": "^7.0.1",
"async-lock": "^1.4.1",
"byline": "^5.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getVolumeNames,
waitForDockerEvent,
} from "../utils/test-helper";
import { HealthCheckWaitStrategy } from "../wait-strategies/health-check-wait-strategy";
import { HostPortWaitStrategy } from "../wait-strategies/host-port-wait-strategy";
import { Wait } from "../wait-strategies/wait";
import { DockerComposeEnvironment } from "./docker-compose-environment";

Expand Down Expand Up @@ -97,6 +99,31 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => {
await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container"));
});

if (!process.env.CI_PODMAN) {
it("should use wait strategy Wait.forHealthCheck() if healthcheck is defined in service", async () => {
await using startedEnvironment = await new DockerComposeEnvironment(
fixtures,
"docker-compose-with-healthcheck.yml"
).up();

await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container"));

const waitStrategy = startedEnvironment.getContainer("container-1")["getWaitStrategy"]();
expect(waitStrategy).toBeInstanceOf(HealthCheckWaitStrategy);
});
it("should use wait strategy Wait.forListeningPorts() if healthcheck is NOT defined in service", async () => {
await using startedEnvironment = await new DockerComposeEnvironment(
fixtures,
"docker-compose-with-name.yml"
).up();

await checkEnvironmentContainerIsHealthy(startedEnvironment, "custom_container_name");

const waitStrategy = startedEnvironment.getContainer("custom_container_name")["getWaitStrategy"]();
expect(waitStrategy).toBeInstanceOf(HostPortWaitStrategy);
});
}

it("should support log message wait strategy", async () => {
await using startedEnvironment = await new DockerComposeEnvironment(fixtures, "docker-compose.yml")
.withWaitStrategy(await composeContainerName("container"), Wait.forLogMessage("Listening on port 8080"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContainerInfo } from "dockerode";
import { ContainerInfo, ContainerInspectInfo } from "dockerode";
import { containerLog, log, RandomUuid, Uuid } from "../common";
import { ComposeOptions, getContainerRuntimeClient, parseComposeContainerName } from "../container-runtime";
import { StartedGenericContainer } from "../generic-container/started-generic-container";
Expand All @@ -23,7 +23,7 @@ export class DockerComposeEnvironment {
private profiles: string[] = [];
private environment: Environment = {};
private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
private defaultWaitStrategy: WaitStrategy = Wait.forListeningPorts();
private defaultWaitStrategy: WaitStrategy | undefined;
private waitStrategy: { [containerName: string]: WaitStrategy } = {};
private startupTimeoutMs?: number;
private clientOptions: Partial<ComposeOptions> = {};
Expand Down Expand Up @@ -159,9 +159,7 @@ export class DockerComposeEnvironment {
const inspectResult = await client.container.inspect(container);
const mappedInspectResult = mapInspectResult(inspectResult);
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult);
const waitStrategy = this.waitStrategy[containerName]
? this.waitStrategy[containerName]
: this.defaultWaitStrategy;
const waitStrategy = this.selectWaitStrategy(containerName, inspectResult);
if (this.startupTimeoutMs !== undefined) {
waitStrategy.withStartupTimeout(this.startupTimeoutMs);
}
Expand Down Expand Up @@ -207,4 +205,16 @@ export class DockerComposeEnvironment {
environment: this.environment,
});
}

private selectWaitStrategy(containerName: string, inspectResult: ContainerInspectInfo): WaitStrategy {
const containerWaitStrategy = this.waitStrategy[containerName]
? this.waitStrategy[containerName]
: this.defaultWaitStrategy;
if (containerWaitStrategy) return containerWaitStrategy;
const healthcheck = inspectResult.Config.Healthcheck;
if (healthcheck?.Test) {
return Wait.forHealthCheck();
}
return Wait.forListeningPorts();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import path from "path";
import { HealthCheckWaitStrategy } from "../wait-strategies/health-check-wait-strategy";
import { HostPortWaitStrategy } from "../wait-strategies/host-port-wait-strategy";
import { Wait } from "../wait-strategies/wait";
import { GenericContainer } from "./generic-container";
import { StartedGenericContainer } from "./started-generic-container";

const fixtures = path.resolve(__dirname, "..", "..", "fixtures", "docker");

describe("GenericContainer wait strategy", { timeout: 180_000 }, () => {
it("should use Wait.forListeningPorts if healthcheck is not defined in DOCKERFILE", async () => {
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.start();
expect((container as StartedGenericContainer)["getWaitStrategy"]()).toBeInstanceOf(HostPortWaitStrategy);
});
it("should use Wait.forHealthCheck if withHealthCheck() explicitly called", async () => {
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.withHealthCheck({
test: ["CMD-SHELL", "echo 'started' && exit 0"],
})
.start();
expect((container as StartedGenericContainer)["getWaitStrategy"]()).toBeInstanceOf(HealthCheckWaitStrategy);
});
it("should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE", async () => {
const context = path.resolve(fixtures, "docker-with-health-check");
const genericContainer = await GenericContainer.fromDockerfile(context).build();
await using startedContainer = await genericContainer.start();
expect((startedContainer as StartedGenericContainer)["getWaitStrategy"]()).toBeInstanceOf(HealthCheckWaitStrategy);
});
it("should use same WaitStrategy if it's explicitly defined in withWaitStrategy() even if image defines healthcheck", async () => {
const context = path.resolve(fixtures, "docker-with-health-check");
const genericContainer = await GenericContainer.fromDockerfile(context).build();
await using container = await genericContainer
.withExposedPorts(8080)
.withWaitStrategy(Wait.forListeningPorts())
.start();
expect((container as StartedGenericContainer)["getWaitStrategy"]()).toBeInstanceOf(HostPortWaitStrategy);
});
it("should use same WaitStrategy if it's explicitly defined in withWaitStrategy() even if withHealthCheck() is called", async () => {
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.withHealthCheck({
test: ["CMD-SHELL", "echo 'started' && exit 0"],
})
.withWaitStrategy(Wait.forListeningPorts())
.start();
expect((container as StartedGenericContainer)["getWaitStrategy"]()).toBeInstanceOf(HostPortWaitStrategy);
});
});
32 changes: 25 additions & 7 deletions packages/testcontainers/src/generic-container/generic-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class GenericContainer implements TestContainer {

protected imageName: ImageName;
protected startupTimeoutMs?: number;
protected waitStrategy: WaitStrategy = Wait.forListeningPorts();
protected waitStrategy: WaitStrategy | undefined;
protected environment: Record<string, string> = {};
protected exposedPorts: PortWithOptionalBinding[] = [];
protected reuse = false;
Expand Down Expand Up @@ -117,6 +117,18 @@ export class GenericContainer implements TestContainer {
return this.startContainer(client);
}

private async selectWaitStrategy(client: ContainerRuntimeClient, container: Container): Promise<WaitStrategy> {
if (this.waitStrategy) return this.waitStrategy;
if (this.healthCheck) {
return Wait.forHealthCheck();
}
const containerInfo = await client.container.inspect(container);
if (containerInfo.Config.Healthcheck?.Test) {
return Wait.forHealthCheck();
}
return Wait.forListeningPorts();
}

private async reuseOrStartContainer(client: ContainerRuntimeClient) {
const containerHash = hash(JSON.stringify(this.createOpts));
this.createOpts.Labels = { ...this.createOpts.Labels, [LABEL_TESTCONTAINERS_CONTAINER_HASH]: containerHash };
Expand Down Expand Up @@ -150,25 +162,29 @@ export class GenericContainer implements TestContainer {
this.exposedPorts
);
if (this.startupTimeoutMs !== undefined) {
this.waitStrategy.withStartupTimeout(this.startupTimeoutMs);
this.waitStrategy?.withStartupTimeout(this.startupTimeoutMs);
}

await waitForContainer(client, container, this.waitStrategy, boundPorts);
const waitStrategy = this.waitStrategy ?? Wait.forListeningPorts();

await waitForContainer(client, container, waitStrategy, boundPorts);

return new StartedGenericContainer(
container,
client.info.containerRuntime.host,
inspectResult,
boundPorts,
inspectResult.Name,
this.waitStrategy,
waitStrategy,
this.autoRemove
);
}

private async startContainer(client: ContainerRuntimeClient): Promise<StartedTestContainer> {
const container = await client.container.create({ ...this.createOpts, HostConfig: this.hostConfig });

this.waitStrategy = await this.selectWaitStrategy(client, container);

if (!this.isHelperContainer() && PortForwarderInstance.isRunning()) {
await this.connectContainerToPortForwarder(client, container);
}
Expand Down Expand Up @@ -206,7 +222,7 @@ export class GenericContainer implements TestContainer {
);

if (this.startupTimeoutMs !== undefined) {
this.waitStrategy.withStartupTimeout(this.startupTimeoutMs);
this.waitStrategy?.withStartupTimeout(this.startupTimeoutMs);
}

if (containerLog.enabled() || this.logConsumer !== undefined) {
Expand All @@ -225,15 +241,17 @@ export class GenericContainer implements TestContainer {
await this.containerStarting(mappedInspectResult, false);
}

await waitForContainer(client, container, this.waitStrategy, boundPorts);
const waitStrategy = this.waitStrategy ?? Wait.forListeningPorts();

await waitForContainer(client, container, waitStrategy, boundPorts);

const startedContainer = new StartedGenericContainer(
container,
client.info.containerRuntime.host,
inspectResult,
boundPorts,
inspectResult.Name,
this.waitStrategy,
waitStrategy,
this.autoRemove
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,8 @@ export class StartedGenericContainer implements StartedTestContainer {
async [Symbol.asyncDispose]() {
await this.stop();
}

private getWaitStrategy() {
return this.waitStrategy;
}
}