From 27cc8fc7be79d7372afa39957d6e779556119a16 Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Sun, 17 Mar 2024 12:20:51 +0200 Subject: [PATCH 1/7] feat: one shot wait startegy --- .../one-shot-startup-startegy.test.ts | 35 +++++++++++++++++++ .../one-shot-startup-startegy.ts | 32 +++++++++++++++++ .../src/wait-strategies/wait.ts | 7 ++++ 3 files changed, 74 insertions(+) create mode 100644 packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts create mode 100644 packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.ts 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..b233c57e --- /dev/null +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts @@ -0,0 +1,35 @@ +import { GenericContainer } from "../generic-container/generic-container"; +import { Wait } from "./wait"; +import { checkContainerIsHealthy, getRunningContainerNames } from "../utils/test-helper"; +import { RandomUuid } from "../common"; +import Dockerode from "dockerode"; + +jest.setTimeout(180_000); + +const mockImageInspect = jest.fn(); +jest.mock( + "dockerode", + () => + function () { + return { + getContainer: () => ({ + inspect: mockImageInspect, + }), + }; + } +); + +describe("OneShotStartupCheckStrategy", () => { + it("should wait for log", async () => { + const wait = await Wait.forOneShotStartup(); + + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withExposedPorts(8080) + .withWaitStrategy(wait) + .start(); + + await checkContainerIsHealthy(container); + + await container.stop(); + }); +}); 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/wait.ts b/packages/testcontainers/src/wait-strategies/wait.ts index 3e08899b..8edc2fd6 100644 --- a/packages/testcontainers/src/wait-strategies/wait.ts +++ b/packages/testcontainers/src/wait-strategies/wait.ts @@ -5,6 +5,8 @@ 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"; +import { getContainerRuntimeClient } from "../container-runtime"; export class Wait { public static forAll(waitStrategies: WaitStrategy[]): CompositeWaitStrategy { @@ -23,6 +25,11 @@ export class Wait { return new HealthCheckWaitStrategy(); } + public static async forOneShotStartup(): Promise { + const containerRuntimeClient = await getContainerRuntimeClient(); + return new OneShotStartupCheckStrategy(containerRuntimeClient); + } + public static forHttp( path: string, port: number, From 7908fdfdf144d955c92b7214e764e062a2e4db46 Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Sun, 17 Mar 2024 14:47:20 +0200 Subject: [PATCH 2/7] . --- .../src/wait-strategies/one-shot-startup-startegy.test.ts | 4 +--- .../src/wait-strategies/startup-check-strategy.ts | 8 +++++--- packages/testcontainers/src/wait-strategies/wait.ts | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) 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 index b233c57e..7bb6a9a4 100644 --- a/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts @@ -1,8 +1,6 @@ import { GenericContainer } from "../generic-container/generic-container"; import { Wait } from "./wait"; -import { checkContainerIsHealthy, getRunningContainerNames } from "../utils/test-helper"; -import { RandomUuid } from "../common"; -import Dockerode from "dockerode"; +import { checkContainerIsHealthy } from "../utils/test-helper"; jest.setTimeout(180_000); diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts index 76e673cf..d526c32b 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 { ContainerRuntimeClient, 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 8edc2fd6..1dd1c83b 100644 --- a/packages/testcontainers/src/wait-strategies/wait.ts +++ b/packages/testcontainers/src/wait-strategies/wait.ts @@ -25,9 +25,8 @@ export class Wait { return new HealthCheckWaitStrategy(); } - public static async forOneShotStartup(): Promise { - const containerRuntimeClient = await getContainerRuntimeClient(); - return new OneShotStartupCheckStrategy(containerRuntimeClient); + public static forOneShotStartup(): WaitStrategy { + return new OneShotStartupCheckStrategy(); } public static forHttp( From e5d0abc1d0ec73432d4a924b3da9da591fb0e79e Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Sun, 17 Mar 2024 14:49:55 +0200 Subject: [PATCH 3/7] . --- .../src/wait-strategies/one-shot-startup-startegy.test.ts | 4 +--- .../src/wait-strategies/startup-check-strategy.ts | 2 +- packages/testcontainers/src/wait-strategies/wait.ts | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) 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 index 7bb6a9a4..e1d7a197 100644 --- a/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts @@ -19,11 +19,9 @@ jest.mock( describe("OneShotStartupCheckStrategy", () => { it("should wait for log", async () => { - const wait = await Wait.forOneShotStartup(); - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withExposedPorts(8080) - .withWaitStrategy(wait) + .withWaitStrategy(Wait.forOneShotStartup()) .start(); await checkContainerIsHealthy(container); diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts index d526c32b..dd6fe202 100644 --- a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts @@ -1,6 +1,6 @@ import { AbstractWaitStrategy } from "./wait-strategy"; import Dockerode from "dockerode"; -import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; +import { getContainerRuntimeClient } from "../container-runtime"; import { IntervalRetry, log } from "../common"; export type StartupStatus = "PENDING" | "SUCCESS" | "FAIL"; diff --git a/packages/testcontainers/src/wait-strategies/wait.ts b/packages/testcontainers/src/wait-strategies/wait.ts index 1dd1c83b..c209d679 100644 --- a/packages/testcontainers/src/wait-strategies/wait.ts +++ b/packages/testcontainers/src/wait-strategies/wait.ts @@ -6,7 +6,6 @@ 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"; -import { getContainerRuntimeClient } from "../container-runtime"; export class Wait { public static forAll(waitStrategies: WaitStrategy[]): CompositeWaitStrategy { From 2c40ad06546c87a049d66ba0c8970da0c2143167 Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Wed, 3 Apr 2024 15:39:04 +0300 Subject: [PATCH 4/7] . --- .../one-shot-startup-startegy.test.ts | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) 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 index e1d7a197..16cd4e90 100644 --- a/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts @@ -1,30 +1,11 @@ import { GenericContainer } from "../generic-container/generic-container"; import { Wait } from "./wait"; -import { checkContainerIsHealthy } from "../utils/test-helper"; jest.setTimeout(180_000); -const mockImageInspect = jest.fn(); -jest.mock( - "dockerode", - () => - function () { - return { - getContainer: () => ({ - inspect: mockImageInspect, - }), - }; - } -); - describe("OneShotStartupCheckStrategy", () => { - it("should wait for log", async () => { - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") - .withExposedPorts(8080) - .withWaitStrategy(Wait.forOneShotStartup()) - .start(); - - await checkContainerIsHealthy(container); + it("should wait for container to finish", async () => { + const container = await new GenericContainer("hello-world").withWaitStrategy(Wait.forOneShotStartup()).start(); await container.stop(); }); From d5342b636fc988e3306a6c967d6aa016d2dbe8bb Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Wed, 3 Apr 2024 18:39:49 +0300 Subject: [PATCH 5/7] . --- .../one-shot-startup-startegy.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 index 16cd4e90..2aed7239 100644 --- a/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts +++ b/packages/testcontainers/src/wait-strategies/one-shot-startup-startegy.test.ts @@ -5,8 +5,20 @@ jest.setTimeout(180_000); describe("OneShotStartupCheckStrategy", () => { it("should wait for container to finish", async () => { - const container = await new GenericContainer("hello-world").withWaitStrategy(Wait.forOneShotStartup()).start(); + 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"); + }); }); From b345faa6f0c12a698b02c4c938c7a1c56c3b8d39 Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Sun, 7 Apr 2024 07:17:28 +0300 Subject: [PATCH 6/7] . --- docs/features/wait-strategies.md | 45 ++++++++++--------- .../startup-check-strategy.test.ts | 13 ++---- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/docs/features/wait-strategies.md b/docs/features/wait-strategies.md index f77f4376..6a304eb4 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 startup strategy example + +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/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") From c228531674821c8ad739021c30dfaa6de3f568ad Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 15 Apr 2024 15:03:34 +0100 Subject: [PATCH 7/7] Update docs/features/wait-strategies.md --- docs/features/wait-strategies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/wait-strategies.md b/docs/features/wait-strategies.md index 6a304eb4..e396229c 100644 --- a/docs/features/wait-strategies.md +++ b/docs/features/wait-strategies.md @@ -178,7 +178,7 @@ const container = await new GenericContainer("alpine") .start(); ``` -## One shot startup strategy example +## 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.