diff --git a/docs/features/wait-strategies.md b/docs/features/wait-strategies.md index f77f4376..e396229c 100644 --- a/docs/features/wait-strategies.md +++ b/docs/features/wait-strategies.md @@ -17,9 +17,7 @@ The default wait strategy used by Testcontainers. It will wait up to 60 seconds ```javascript const { GenericContainer } = require("testcontainers"); -const container = await new GenericContainer("alpine") - .withExposedPorts(6379) - .start(); +const container = await new GenericContainer("alpine").withExposedPorts(6379).start(); ``` It can be set explicitly but is not required: @@ -72,9 +70,7 @@ Wait until the container's health check is successful: ```javascript const { GenericContainer, Wait } = require("testcontainers"); -const container = await new GenericContainer("alpine") - .withWaitStrategy(Wait.forHealthCheck()) - .start(); +const container = await new GenericContainer("alpine").withWaitStrategy(Wait.forHealthCheck()).start(); ``` Define your own health check: @@ -88,7 +84,7 @@ const container = await new GenericContainer("alpine") interval: 1000, timeout: 3000, retries: 5, - startPeriod: 1000 + startPeriod: 1000, }) .withWaitStrategy(Wait.forHealthCheck()) .start(); @@ -99,13 +95,13 @@ Note that `interval`, `timeout`, `retries` and `startPeriod` are optional as the To execute the test with a shell use the form `["CMD-SHELL", "command"]`: ```javascript -["CMD-SHELL", "curl -f http://localhost:8000 || exit 1"] +["CMD-SHELL", "curl -f http://localhost:8000 || exit 1"]; ``` To execute the test without a shell, use the form: `["CMD", "command", "arg1", "arg2"]`. This may be needed when working with distroless images: ```javascript -["CMD", "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/hello-world"] +["CMD", "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/hello-world"]; ``` ## HTTP @@ -115,9 +111,7 @@ Wait for an HTTP request to satisfy a condition. By default, it will wait for a ```javascript const { GenericContainer, Wait } = require("testcontainers"); -const container = await new GenericContainer("redis") - .withWaitStrategy(Wait.forHttp("/health", 8080)) - .start(); +const container = await new GenericContainer("redis").withWaitStrategy(Wait.forHttp("/health", 8080)).start(); ``` Stop waiting after container exited if waiting for container restart not needed. @@ -184,6 +178,18 @@ const container = await new GenericContainer("alpine") .start(); ``` +## One shot + +This strategy is intended for use with containers that only run briefly and exit of their own accord. As such, success is deemed to be when the container has stopped with exit code 0. + +```javascript +const { GenericContainer, Wait } = require("testcontainers"); + +const container = await new GenericContainer("alpine") + .withWaitStrategy(Wait.forOneShotStartup())) + .start(); +``` + ## Composite Multiple wait strategies can be chained together: @@ -192,10 +198,7 @@ Multiple wait strategies can be chained together: const { GenericContainer, Wait } = require("testcontainers"); const container = await new GenericContainer("alpine") - .withWaitStrategy(Wait.forAll([ - Wait.forListeningPorts(), - Wait.forLogMessage("Ready to accept connections") - ])) + .withWaitStrategy(Wait.forAll([Wait.forListeningPorts(), Wait.forLogMessage("Ready to accept connections")])) .start(); ``` @@ -205,7 +208,7 @@ The composite wait strategy by default will respect each individual wait strateg const w1 = Wait.forListeningPorts().withStartupTimeout(1000); const w2 = Wait.forLogMessage("READY").withStartupTimeout(2000); -const composite = Wait.forAll([w1, w2]) +const composite = Wait.forAll([w1, w2]); expect(w1.getStartupTimeout()).toBe(1000); expect(w2.getStartupTimeout()).toBe(2000); @@ -217,7 +220,7 @@ The startup timeout of inner wait strategies that have not defined their own sta const w1 = Wait.forListeningPorts().withStartupTimeout(1000); const w2 = Wait.forLogMessage("READY"); -const composite = Wait.forAll([w1, w2]).withStartupTimeout(2000) +const composite = Wait.forAll([w1, w2]).withStartupTimeout(2000); expect(w1.getStartupTimeout()).toBe(1000); expect(w2.getStartupTimeout()).toBe(2000); @@ -228,7 +231,7 @@ The startup timeout of all wait strategies can be controlled by setting a deadli ```javascript const w1 = Wait.forListeningPorts(); const w2 = Wait.forLogMessage("READY"); -const composite = Wait.forAll([w1, w2]).withDeadline(2000) +const composite = Wait.forAll([w1, w2]).withDeadline(2000); ``` ## Other startup strategies @@ -237,8 +240,8 @@ If these options do not meet your requirements, you can subclass `StartupCheckSt ```javascript const Dockerode = require("dockerode"); -const { - GenericContainer, +const { + GenericContainer, StartupCheckStrategy, StartupStatus } = require("testcontainers"); diff --git a/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts new file mode 100644 index 00000000..2aed7239 --- /dev/null +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts @@ -0,0 +1,24 @@ +import { GenericContainer } from "../generic-container/generic-container"; +import { Wait } from "./wait"; + +jest.setTimeout(180_000); + +describe("OneShotStartupCheckStrategy", () => { + it("should wait for container to finish", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withCommand(["/bin/sh", "-c", 'sleep 2; echo "Ready"']) + .withWaitStrategy(Wait.forOneShotStartup()) + .start(); + + await container.stop(); + }); + + it("should fail if container did not finish succesfully", async () => { + await expect(() => + new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withCommand(["/bin/sh", "-c", "not-existing"]) + .withWaitStrategy(Wait.forOneShotStartup()) + .start() + ).rejects.toThrow("Container failed to start for"); + }); +}); diff --git a/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.ts b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.ts new file mode 100644 index 00000000..8e619af5 --- /dev/null +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.ts @@ -0,0 +1,32 @@ +import Dockerode, { ContainerInspectInfo } from "dockerode"; +import { StartupCheckStrategy, StartupStatus } from "./startup-check-strategy"; + +export class OneShotStartupCheckStrategy extends StartupCheckStrategy { + DOCKER_TIMESTAMP_ZERO = "0001-01-01T00:00:00Z"; + + private isDockerTimestampNonEmpty(dockerTimestamp: string) { + return dockerTimestamp !== "" && dockerTimestamp !== this.DOCKER_TIMESTAMP_ZERO && Date.parse(dockerTimestamp) > 0; + } + + private isContainerStopped({ State: state }: ContainerInspectInfo): boolean { + if (state.Running || state.Paused) { + return false; + } + + return this.isDockerTimestampNonEmpty(state.StartedAt) && this.isDockerTimestampNonEmpty(state.FinishedAt); + } + + public async checkStartupState(dockerClient: Dockerode, containerId: string): Promise { + const info = await dockerClient.getContainer(containerId).inspect(); + + if (!this.isContainerStopped(info)) { + return "PENDING"; + } + + if (info.State.ExitCode === 0) { + return "SUCCESS"; + } + + return "FAIL"; + } +} diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts index a7530334..a6bc16f6 100644 --- a/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts +++ b/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts @@ -1,16 +1,9 @@ import { GenericContainer } from "../generic-container/generic-container"; import { StartupCheckStrategy, StartupStatus } from "./startup-check-strategy"; -import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; jest.setTimeout(180_000); describe("StartupCheckStrategy", () => { - let client: ContainerRuntimeClient; - - beforeAll(async () => { - client = await getContainerRuntimeClient(); - }); - it("should wait until ready", async () => { const waitStrategy = new (class extends StartupCheckStrategy { private count = 0; @@ -23,7 +16,7 @@ describe("StartupCheckStrategy", () => { return "SUCCESS"; } } - })(client); + })(); const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withWaitStrategy(waitStrategy) @@ -37,7 +30,7 @@ describe("StartupCheckStrategy", () => { public override async checkStartupState(): Promise { return "PENDING"; } - })(client); + })(); await expect(() => new GenericContainer("cristianrgreco/testcontainer:1.1.14") @@ -55,7 +48,7 @@ describe("StartupCheckStrategy", () => { this.count++; return "FAIL"; } - })(client); + })(); await expect(() => new GenericContainer("cristianrgreco/testcontainer:1.1.14") diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts index 76e673cf..dd6fe202 100644 --- a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts @@ -1,20 +1,22 @@ import { AbstractWaitStrategy } from "./wait-strategy"; import Dockerode from "dockerode"; -import { ContainerRuntimeClient } from "../container-runtime"; +import { getContainerRuntimeClient } from "../container-runtime"; import { IntervalRetry, log } from "../common"; export type StartupStatus = "PENDING" | "SUCCESS" | "FAIL"; export abstract class StartupCheckStrategy extends AbstractWaitStrategy { - constructor(private readonly client: ContainerRuntimeClient) { + constructor() { super(); } public abstract checkStartupState(dockerClient: Dockerode, containerId: string): Promise; public override async waitUntilReady(container: Dockerode.Container): Promise { + const client = await getContainerRuntimeClient(); + const startupStatus = await new IntervalRetry(1000).retryUntil( - async () => await this.checkStartupState(this.client.container.dockerode, container.id), + async () => await this.checkStartupState(client.container.dockerode, container.id), (startupStatus) => startupStatus === "SUCCESS" || startupStatus === "FAIL", () => { const message = `Container not accessible after ${this.startupTimeout}ms`; diff --git a/packages/testcontainers/src/wait-strategies/wait.ts b/packages/testcontainers/src/wait-strategies/wait.ts index 3e08899b..c209d679 100644 --- a/packages/testcontainers/src/wait-strategies/wait.ts +++ b/packages/testcontainers/src/wait-strategies/wait.ts @@ -5,6 +5,7 @@ import { Log, LogWaitStrategy } from "./log-wait-strategy"; import { ShellWaitStrategy } from "./shell-wait-strategy"; import { HostPortWaitStrategy } from "./host-port-wait-strategy"; import { CompositeWaitStrategy } from "./composite-wait-strategy"; +import { OneShotStartupCheckStrategy } from "./one-shot-startup-startegy"; export class Wait { public static forAll(waitStrategies: WaitStrategy[]): CompositeWaitStrategy { @@ -23,6 +24,10 @@ export class Wait { return new HealthCheckWaitStrategy(); } + public static forOneShotStartup(): WaitStrategy { + return new OneShotStartupCheckStrategy(); + } + public static forHttp( path: string, port: number,