From 287c1ad22309275085f7b6c7c4b44e602f326168 Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Sun, 24 Nov 2024 23:38:45 +0900 Subject: [PATCH 1/9] Add an execVerbose function for output separated into stdout and stderr --- .../clients/container/container-client.ts | 19 +++++ .../container/docker-container-client.ts | 71 ++++++++++++++++- .../container/podman-container-client.ts | 78 ++++++++++++++++++- .../clients/container/types.ts | 2 + .../abstract-started-container.ts | 14 +++- .../generic-container.test.ts | 33 ++++++++ .../started-generic-container.ts | 21 ++++- packages/testcontainers/src/test-container.ts | 47 +++++++++++ packages/testcontainers/src/types.ts | 2 + 9 files changed, 283 insertions(+), 4 deletions(-) diff --git a/packages/testcontainers/src/container-runtime/clients/container/container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/container-client.ts index 3c9d13784..c528421b2 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/container-client.ts @@ -8,27 +8,46 @@ import Dockerode, { } from "dockerode"; import { Readable } from "stream"; import { ContainerStatus, ExecOptions, ExecResult } from "./types"; +import { ExecVerboseResult } from "../../../types"; export interface ContainerClient { dockerode: Dockerode; + getById(id: string): Container; + fetchByLabel( labelName: string, labelValue: string, opts?: { status?: ContainerStatus[] } ): Promise; + fetchArchive(container: Container, path: string): Promise; + putArchive(container: Dockerode.Container, stream: Readable, path: string): Promise; + list(): Promise; + create(opts: ContainerCreateOptions): Promise; + start(container: Container): Promise; + inspect(container: Container): Promise; + stop(container: Container, opts?: { timeout: number }): Promise; + attach(container: Container): Promise; + logs(container: Container, opts?: ContainerLogsOptions): Promise; + exec(container: Container, command: string[], opts?: Partial): Promise; + + execVerbose(container: Container, command: string[], opts?: Partial): Promise; + restart(container: Container, opts?: { timeout: number }): Promise; + events(container: Container, eventNames: string[]): Promise; + remove(container: Container, opts?: { removeVolumes: boolean }): Promise; + connectToNetwork(container: Container, network: Network, networkAliases: string[]): Promise; } diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts index 226e58b73..ff121104b 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts @@ -9,7 +9,7 @@ import Dockerode, { } from "dockerode"; import { PassThrough, Readable } from "stream"; import { IncomingMessage } from "http"; -import { ContainerStatus, ExecOptions, ExecResult } from "./types"; +import { ContainerStatus, ExecOptions, ExecResult, ExecVerboseResult } from "./types"; import byline from "byline"; import { ContainerClient } from "./container-client"; import { execLog, log, streamToString } from "../../../common"; @@ -235,6 +235,75 @@ export class DockerContainerClient implements ContainerClient { } } + async execVerbose(container: Container, command: string[], opts?: Partial): Promise { + const execOptions: ExecCreateOptions = { + Cmd: command, + AttachStdout: true, + AttachStderr: true, + }; + + if (opts?.env !== undefined) { + execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); + } + if (opts?.workingDir !== undefined) { + execOptions.WorkingDir = opts.workingDir; + } + if (opts?.user !== undefined) { + execOptions.User = opts.user; + } + + const stdoutChunks: string[] = []; + const stderrChunks: string[] = []; + + try { + if (opts?.log) { + log.debug(`Execing container with command "${command.join(" ")}"...`, { containerId: container.id }); + } + + const exec = await container.exec(execOptions); + const stream = await exec.start({ stdin: true, Detach: false, Tty: false }); + + const stdoutStream = new PassThrough(); + const stderrStream = new PassThrough(); + + this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); + + const processStream = (stream: Readable, label: "stdout" | "stderr") => { + stream.on("data", (chunk) => { + if (label === "stdout") stdoutChunks.push(chunk); + if (label === "stderr") stderrChunks.push(chunk); + + if (opts?.log && execLog.enabled()) { + execLog.trace(chunk.toString(), { containerId: container.id }); + } + }); + }; + + processStream(stdoutStream, "stdout"); + processStream(stderrStream, "stderr"); + + await new Promise((res, rej) => { + stream.on("end", res); + stream.on("error", rej); + }); + stream.destroy(); + + const inspectResult = await exec.inspect(); + const exitCode = inspectResult.ExitCode ?? -1; + const stdout = stdoutChunks.join(""); + const stderr = stderrChunks.join(""); + if (opts?.log) { + log.debug(`Execed container with command "${command.join(" ")}"...`, { containerId: container.id }); + } + return { stdout, stderr, exitCode }; + } catch (err) { + log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${stderrChunks.join("")}`, { + containerId: container.id, + }); + throw err; + } + } + async restart(container: Container, opts?: { timeout: number }): Promise { try { log.debug(`Restarting container...`, { containerId: container.id }); diff --git a/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts index 20a0fab4f..692047917 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts @@ -1,8 +1,9 @@ import { Container, ExecCreateOptions } from "dockerode"; -import { ExecOptions, ExecResult } from "./types"; +import { ExecOptions, ExecResult, ExecVerboseResult } from "./types"; import byline from "byline"; import { DockerContainerClient } from "./docker-container-client"; import { execLog, log } from "../../../common"; +import { PassThrough, Readable } from "stream"; export class PodmanContainerClient extends DockerContainerClient { override async exec(container: Container, command: string[], opts?: Partial): Promise { @@ -53,4 +54,79 @@ export class PodmanContainerClient extends DockerContainerClient { throw err; } } + + override async execVerbose( + container: Container, + command: string[], + opts?: Partial + ): Promise { + const execOptions: ExecCreateOptions = { + Cmd: command, + AttachStdout: true, + AttachStderr: true, + }; + + if (opts?.env !== undefined) { + execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); + } + if (opts?.workingDir !== undefined) { + execOptions.WorkingDir = opts.workingDir; + } + if (opts?.user !== undefined) { + execOptions.User = opts.user; + } + + const stdoutChunks: string[] = []; + const stderrChunks: string[] = []; + + try { + if (opts?.log) { + log.debug(`Execing container verbosely with command "${command.join(" ")}"...`, { containerId: container.id }); + } + + const exec = await container.exec(execOptions); + const stream = await exec.start({ stdin: true, Detach: false, Tty: false }); + + const stdoutStream = new PassThrough(); + const stderrStream = new PassThrough(); + + // Podman may use the same demuxing approach as Docker + this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); + + const processStream = (stream: Readable, chunks: string[], label: "stdout" | "stderr") => { + stream.on("data", (chunk) => { + chunks.push(chunk.toString()); + if (opts?.log && execLog.enabled()) { + execLog.trace(chunk.toString(), { containerId: container.id }); + } + }); + }; + + processStream(stdoutStream, stdoutChunks, "stdout"); + processStream(stderrStream, stderrChunks, "stderr"); + + await new Promise((res, rej) => { + stream.on("end", res); + stream.on("error", rej); + }); + stream.destroy(); + + const inspectResult = await exec.inspect(); + const exitCode = inspectResult.ExitCode ?? -1; + + const stdout = stdoutChunks.join(""); + const stderr = stderrChunks.join(""); + + if (opts?.log) { + log.debug(`ExecVerbose completed with command "${command.join(" ")}"`, { containerId: container.id }); + } + + return { stdout, stderr, exitCode }; + } catch (err) { + log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${stderrChunks.join("")}`, { + containerId: container.id, + }); + throw err; + } + } } diff --git a/packages/testcontainers/src/container-runtime/clients/container/types.ts b/packages/testcontainers/src/container-runtime/clients/container/types.ts index 7062d54ce..9fd08922e 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/types.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/types.ts @@ -4,6 +4,8 @@ export type ExecOptions = { workingDir: string; user: string; env: Environment; export type ExecResult = { output: string; exitCode: number }; +export type ExecVerboseResult = { stdout: string; stderr: string; exitCode: number }; + export const CONTAINER_STATUSES = ["created", "restarting", "running", "removing", "paused", "exited", "dead"] as const; export type ContainerStatus = (typeof CONTAINER_STATUSES)[number]; diff --git a/packages/testcontainers/src/generic-container/abstract-started-container.ts b/packages/testcontainers/src/generic-container/abstract-started-container.ts index a88ec6fff..73ca45dbc 100644 --- a/packages/testcontainers/src/generic-container/abstract-started-container.ts +++ b/packages/testcontainers/src/generic-container/abstract-started-container.ts @@ -1,5 +1,13 @@ import { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container"; -import { ContentToCopy, DirectoryToCopy, ExecOptions, ExecResult, FileToCopy, Labels } from "../types"; +import { + ContentToCopy, + DirectoryToCopy, + ExecOptions, + ExecResult, + ExecVerboseResult, + FileToCopy, + Labels, +} from "../types"; import { Readable } from "stream"; export class AbstractStartedContainer implements StartedTestContainer { @@ -83,6 +91,10 @@ export class AbstractStartedContainer implements StartedTestContainer { return this.startedTestContainer.exec(command, opts); } + public execVerbose(command: string | string[], opts?: Partial): Promise { + return this.startedTestContainer.execVerbose(command, opts); + } + public logs(opts?: { since?: number; tail?: number }): Promise { return this.startedTestContainer.logs(opts); } diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 6484c0ef6..8882f0c91 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -84,6 +84,39 @@ describe("GenericContainer", () => { await container.stop(); }); + it("should execute a command on a running container with verbose output", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); + + const { stdout, exitCode } = await container.execVerbose(["echo", "hello", "world"]); + + expect(exitCode).toBe(0); + expect(stdout).toEqual(expect.stringContaining("hello world")); + + await container.stop(); + }); + + it("should capture warnings from stderr with verbose output", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); + + const { stderr, exitCode } = await container.execVerbose(["sh", "-c", "echo 'Warning!' 1>&2"]); + + expect(exitCode).toBe(0); + expect(stderr).toEqual(expect.stringContaining("Warning!")); + + await container.stop(); + }); + + it("should capture errors from stderr with verbose logging", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); + + const { stderr, exitCode } = await container.execVerbose(["sh", "-c", "exit 1"]); + + expect(exitCode).toBe(1); + expect(stderr).toEqual(expect.stringContaining("")); + + await container.stop(); + }); + it("should set environment variables", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withEnvironment({ customKey: "customValue" }) diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index c5ff8865a..67ed02a4a 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -1,6 +1,14 @@ import { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container"; import Dockerode, { ContainerInspectInfo } from "dockerode"; -import { ContentToCopy, DirectoryToCopy, ExecOptions, ExecResult, FileToCopy, Labels } from "../types"; +import { + ContentToCopy, + DirectoryToCopy, + ExecOptions, + ExecResult, + ExecVerboseResult, + FileToCopy, + Labels, +} from "../types"; import { Readable } from "stream"; import { StoppedGenericContainer } from "./stopped-generic-container"; import { WaitStrategy } from "../wait-strategies/wait-strategy"; @@ -181,6 +189,17 @@ export class StartedGenericContainer implements StartedTestContainer { return output; } + public async execVerbose(command: string | string[], opts?: Partial): Promise { + const commandArr = Array.isArray(command) ? command : command.split(" "); + const commandStr = commandArr.join(" "); + const client = await getContainerRuntimeClient(); + log.debug(`Executing command "${commandStr}"...`, { containerId: this.container.id }); + const output = await client.container.execVerbose(this.container, commandArr, opts); + log.debug(`Executed command "${commandStr}"...`, { containerId: this.container.id }); + + return output; + } + public async logs(opts?: { since?: number; tail?: number }): Promise { const client = await getContainerRuntimeClient(); diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index b2410024c..b1cbb4c83 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -7,6 +7,7 @@ import { Environment, ExecOptions, ExecResult, + ExecVerboseResult, ExtraHost, FileToCopy, Labels, @@ -20,32 +21,59 @@ import { ImagePullPolicy } from "./utils/pull-policy"; export interface TestContainer { start(): Promise; + withEnvironment(environment: Environment): this; + withCommand(command: string[]): this; + withEntrypoint(entrypoint: string[]): this; + withTmpFs(tmpFs: TmpFs): this; + withUlimits(ulimits: Ulimits): this; + withAddedCapabilities(...capabilities: string[]): this; + withDroppedCapabilities(...capabilities: string[]): this; + withExposedPorts(...ports: PortWithOptionalBinding[]): this; + withBindMounts(bindMounts: BindMount[]): this; + withWaitStrategy(waitStrategy: WaitStrategy): this; + withStartupTimeout(startupTimeoutMs: number): this; + withNetwork(network: StartedNetwork): this; + withNetworkMode(networkMode: string): this; + withExtraHosts(extraHosts: ExtraHost[]): this; + withDefaultLogDriver(): this; + withPrivilegedMode(): this; + withPlatform(platform: string): this; + withUser(user: string): this; + withPullPolicy(pullPolicy: ImagePullPolicy): this; + withReuse(): this; + withCopyFilesToContainer(filesToCopy: FileToCopy[]): this; + withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this; + withCopyContentToContainer(contentsToCopy: ContentToCopy[]): this; + withWorkingDir(workingDir: string): this; + withResourcesQuota(resourcesQuota: ResourcesQuota): this; + withSharedMemorySize(bytes: number): this; + withLogConsumer(logConsumer: (stream: Readable) => unknown): this; } @@ -61,25 +89,44 @@ export interface StopOptions { export interface StartedTestContainer { stop(options?: Partial): Promise; + restart(options?: Partial): Promise; + getHost(): string; + getFirstMappedPort(): number; + getMappedPort(port: number): number; + getName(): string; + getLabels(): Labels; + getId(): string; + getNetworkNames(): string[]; + getNetworkId(networkName: string): string; + getIpAddress(networkName: string): string; + copyArchiveFromContainer(path: string): Promise; + copyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): Promise; + copyFilesToContainer(filesToCopy: FileToCopy[]): Promise; + copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise; + exec(command: string | string[], opts?: Partial): Promise; + + execVerbose(command: string | string[], opts?: Partial): Promise; + logs(opts?: { since?: number; tail?: number }): Promise; } export interface StoppedTestContainer { getId(): string; + copyArchiveFromContainer(path: string): Promise; } diff --git a/packages/testcontainers/src/types.ts b/packages/testcontainers/src/types.ts index 2840dd4f0..ddebca52a 100644 --- a/packages/testcontainers/src/types.ts +++ b/packages/testcontainers/src/types.ts @@ -85,6 +85,8 @@ export type ExecOptions = { workingDir: string; user: string; env: Environment } export type ExecResult = { output: string; exitCode: number }; +export type ExecVerboseResult = { stdout: string; stderr: string; exitCode: number }; + export type HealthCheckStatus = "none" | "starting" | "unhealthy" | "healthy"; export type NetworkSettings = { From e628034a92031a76040918e4fe2dc710f3a2f733 Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 02:44:46 +0900 Subject: [PATCH 2/9] execVerbose docs --- docs/features/containers.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/features/containers.md b/docs/features/containers.md index 43bbbb0ce..b1b9e4833 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -537,6 +537,26 @@ const { output, exitCode } = await container.exec(["echo", "hello", "world"], { } }); ``` +To handle cases where you need separate outputs for `stdout` and `stderr`, you can use the `execVerbose` method. +It functions similarly to `exec`, but provides detailed output including `stdout`, `stderr`, and `exitCode`. + +```javascript +const container = await new GenericContainer("alpine") + .withCommand(["sleep", "infinity"]) + .start(); + +const { stdout, stderr, exitCode } = await container.execVerbose(["echo", "hello", "world"], { + workingDir: "/app/src/", + user: "1000:1000", + env: { + "VAR1": "enabled", + "VAR2": "/app/debug.log", + } +}); +``` +Use `execVerbose` when you require more granular control over command outputs, +while retaining similar options and functionality as `exec`. + ## Streaming logs From c224fca8ca70c1d2a0b044b748f5945bbfc6f68e Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 03:54:23 +0900 Subject: [PATCH 3/9] remove the new line --- .../clients/container/container-client.ts | 14 ------ .../container/podman-container-client.ts | 6 +-- packages/testcontainers/src/test-container.ts | 44 ------------------- 3 files changed, 3 insertions(+), 61 deletions(-) diff --git a/packages/testcontainers/src/container-runtime/clients/container/container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/container-client.ts index c528421b2..0a56d8b91 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/container-client.ts @@ -22,32 +22,18 @@ export interface ContainerClient { ): Promise; fetchArchive(container: Container, path: string): Promise; - putArchive(container: Dockerode.Container, stream: Readable, path: string): Promise; - list(): Promise; - create(opts: ContainerCreateOptions): Promise; - start(container: Container): Promise; - inspect(container: Container): Promise; - stop(container: Container, opts?: { timeout: number }): Promise; - attach(container: Container): Promise; - logs(container: Container, opts?: ContainerLogsOptions): Promise; - exec(container: Container, command: string[], opts?: Partial): Promise; - execVerbose(container: Container, command: string[], opts?: Partial): Promise; - restart(container: Container, opts?: { timeout: number }): Promise; - events(container: Container, eventNames: string[]): Promise; - remove(container: Container, opts?: { removeVolumes: boolean }): Promise; - connectToNetwork(container: Container, network: Network, networkAliases: string[]): Promise; } diff --git a/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts index 692047917..a35a28331 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts @@ -93,7 +93,7 @@ export class PodmanContainerClient extends DockerContainerClient { // Podman may use the same demuxing approach as Docker this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); - const processStream = (stream: Readable, chunks: string[], label: "stdout" | "stderr") => { + const processStream = (stream: Readable, chunks: string[]) => { stream.on("data", (chunk) => { chunks.push(chunk.toString()); if (opts?.log && execLog.enabled()) { @@ -102,8 +102,8 @@ export class PodmanContainerClient extends DockerContainerClient { }); }; - processStream(stdoutStream, stdoutChunks, "stdout"); - processStream(stderrStream, stderrChunks, "stderr"); + processStream(stdoutStream, stdoutChunks); + processStream(stderrStream, stderrChunks); await new Promise((res, rej) => { stream.on("end", res); diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index b1cbb4c83..40be458d7 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -21,59 +21,32 @@ import { ImagePullPolicy } from "./utils/pull-policy"; export interface TestContainer { start(): Promise; - withEnvironment(environment: Environment): this; - withCommand(command: string[]): this; - withEntrypoint(entrypoint: string[]): this; - withTmpFs(tmpFs: TmpFs): this; - withUlimits(ulimits: Ulimits): this; - withAddedCapabilities(...capabilities: string[]): this; - withDroppedCapabilities(...capabilities: string[]): this; - withExposedPorts(...ports: PortWithOptionalBinding[]): this; - withBindMounts(bindMounts: BindMount[]): this; - withWaitStrategy(waitStrategy: WaitStrategy): this; - withStartupTimeout(startupTimeoutMs: number): this; - withNetwork(network: StartedNetwork): this; - withNetworkMode(networkMode: string): this; - withExtraHosts(extraHosts: ExtraHost[]): this; - withDefaultLogDriver(): this; - withPrivilegedMode(): this; - withPlatform(platform: string): this; - withUser(user: string): this; - withPullPolicy(pullPolicy: ImagePullPolicy): this; - withReuse(): this; - withCopyFilesToContainer(filesToCopy: FileToCopy[]): this; - withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this; - withCopyContentToContainer(contentsToCopy: ContentToCopy[]): this; - withWorkingDir(workingDir: string): this; - withResourcesQuota(resourcesQuota: ResourcesQuota): this; - withSharedMemorySize(bytes: number): this; - withLogConsumer(logConsumer: (stream: Readable) => unknown): this; } @@ -89,39 +62,22 @@ export interface StopOptions { export interface StartedTestContainer { stop(options?: Partial): Promise; - restart(options?: Partial): Promise; - getHost(): string; - getFirstMappedPort(): number; - getMappedPort(port: number): number; - getName(): string; - getLabels(): Labels; - getId(): string; - getNetworkNames(): string[]; - getNetworkId(networkName: string): string; - getIpAddress(networkName: string): string; - copyArchiveFromContainer(path: string): Promise; - copyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): Promise; - copyFilesToContainer(filesToCopy: FileToCopy[]): Promise; - copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise; - exec(command: string | string[], opts?: Partial): Promise; - execVerbose(command: string | string[], opts?: Partial): Promise; - logs(opts?: { since?: number; tail?: number }): Promise; } From f09b80d25f7b47d0559f62db2d3a1ee4d34e87fa Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 11:25:10 +0900 Subject: [PATCH 4/9] Combine execVerbose functionality into exec method. --- .../clients/container/container-client.ts | 2 - .../container/docker-container-client.ts | 68 +++--------------- .../container/podman-container-client.ts | 71 +++---------------- .../clients/container/types.ts | 4 +- .../generic-container.test.ts | 62 ++++++++-------- .../started-generic-container.ts | 12 ---- packages/testcontainers/src/test-container.ts | 2 - packages/testcontainers/src/types.ts | 4 +- 8 files changed, 54 insertions(+), 171 deletions(-) diff --git a/packages/testcontainers/src/container-runtime/clients/container/container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/container-client.ts index 0a56d8b91..857dd35ca 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/container-client.ts @@ -8,7 +8,6 @@ import Dockerode, { } from "dockerode"; import { Readable } from "stream"; import { ContainerStatus, ExecOptions, ExecResult } from "./types"; -import { ExecVerboseResult } from "../../../types"; export interface ContainerClient { dockerode: Dockerode; @@ -31,7 +30,6 @@ export interface ContainerClient { attach(container: Container): Promise; logs(container: Container, opts?: ContainerLogsOptions): Promise; exec(container: Container, command: string[], opts?: Partial): Promise; - execVerbose(container: Container, command: string[], opts?: Partial): Promise; restart(container: Container, opts?: { timeout: number }): Promise; events(container: Container, eventNames: string[]): Promise; remove(container: Container, opts?: { removeVolumes: boolean }): Promise; diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts index ff121104b..9096e8a6b 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts @@ -9,7 +9,7 @@ import Dockerode, { } from "dockerode"; import { PassThrough, Readable } from "stream"; import { IncomingMessage } from "http"; -import { ContainerStatus, ExecOptions, ExecResult, ExecVerboseResult } from "./types"; +import { ContainerStatus, ExecOptions, ExecResult } from "./types"; import byline from "byline"; import { ContainerClient } from "./container-client"; import { execLog, log, streamToString } from "../../../common"; @@ -201,57 +201,7 @@ export class DockerContainerClient implements ContainerClient { execOptions.User = opts.user; } - const chunks: string[] = []; - try { - if (opts?.log) { - log.debug(`Execing container with command "${command.join(" ")}"...`, { containerId: container.id }); - } - - const exec = await container.exec(execOptions); - const stream = await exec.start({ stdin: true, Detach: false, Tty: true }); - if (opts?.log && execLog.enabled()) { - byline(stream).on("data", (line) => execLog.trace(line, { containerId: container.id })); - } - - await new Promise((res, rej) => { - stream.on("data", (chunk) => chunks.push(chunk)); - stream.on("end", res); - stream.on("error", rej); - }); - stream.destroy(); - - const inspectResult = await exec.inspect(); - const exitCode = inspectResult.ExitCode ?? -1; - const output = chunks.join(""); - if (opts?.log) { - log.debug(`Execed container with command "${command.join(" ")}"...`, { containerId: container.id }); - } - return { output, exitCode }; - } catch (err) { - log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${chunks.join("")}`, { - containerId: container.id, - }); - throw err; - } - } - - async execVerbose(container: Container, command: string[], opts?: Partial): Promise { - const execOptions: ExecCreateOptions = { - Cmd: command, - AttachStdout: true, - AttachStderr: true, - }; - - if (opts?.env !== undefined) { - execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); - } - if (opts?.workingDir !== undefined) { - execOptions.WorkingDir = opts.workingDir; - } - if (opts?.user !== undefined) { - execOptions.User = opts.user; - } - + const outputChunks: string[] = []; const stdoutChunks: string[] = []; const stderrChunks: string[] = []; @@ -268,10 +218,10 @@ export class DockerContainerClient implements ContainerClient { this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); - const processStream = (stream: Readable, label: "stdout" | "stderr") => { + const processStream = (stream: Readable, chunks: string[]) => { stream.on("data", (chunk) => { - if (label === "stdout") stdoutChunks.push(chunk); - if (label === "stderr") stderrChunks.push(chunk); + chunks.push(chunk.toString()); + outputChunks.push(chunk.toString()); if (opts?.log && execLog.enabled()) { execLog.trace(chunk.toString(), { containerId: container.id }); @@ -279,8 +229,8 @@ export class DockerContainerClient implements ContainerClient { }); }; - processStream(stdoutStream, "stdout"); - processStream(stderrStream, "stderr"); + processStream(stdoutStream, stdoutChunks); + processStream(stderrStream, stderrChunks); await new Promise((res, rej) => { stream.on("end", res); @@ -290,12 +240,14 @@ export class DockerContainerClient implements ContainerClient { const inspectResult = await exec.inspect(); const exitCode = inspectResult.ExitCode ?? -1; + const output = outputChunks.join(""); const stdout = stdoutChunks.join(""); const stderr = stderrChunks.join(""); + if (opts?.log) { log.debug(`Execed container with command "${command.join(" ")}"...`, { containerId: container.id }); } - return { stdout, stderr, exitCode }; + return { output, stdout, stderr, exitCode }; } catch (err) { log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${stderrChunks.join("")}`, { containerId: container.id, diff --git a/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts index a35a28331..903ecdf2e 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts @@ -1,5 +1,5 @@ import { Container, ExecCreateOptions } from "dockerode"; -import { ExecOptions, ExecResult, ExecVerboseResult } from "./types"; +import { ExecOptions, ExecResult } from "./types"; import byline from "byline"; import { DockerContainerClient } from "./docker-container-client"; import { execLog, log } from "../../../common"; @@ -23,79 +23,28 @@ export class PodmanContainerClient extends DockerContainerClient { execOptions.User = opts.user; } - const chunks: string[] = []; - try { - if (opts?.log) { - log.debug(`Execing container with command "${command.join(" ")}"...`, { containerId: container.id }); - } - - const exec = await container.exec(execOptions); - const stream = await this.demuxStream(container.id, await exec.start({ stdin: true, Detach: false, Tty: true })); - if (opts?.log && execLog.enabled()) { - byline(stream).on("data", (line) => execLog.trace(line, { containerId: container.id })); - } - - await new Promise((res, rej) => { - stream.on("data", (chunk) => chunks.push(chunk)); - stream.on("end", res); - stream.on("error", rej); - }); - stream.destroy(); - - const inspectResult = await exec.inspect(); - const exitCode = inspectResult.ExitCode ?? -1; - const output = chunks.join(""); - - return { output, exitCode }; - } catch (err) { - log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${chunks.join("")}`, { - containerId: container.id, - }); - throw err; - } - } - - override async execVerbose( - container: Container, - command: string[], - opts?: Partial - ): Promise { - const execOptions: ExecCreateOptions = { - Cmd: command, - AttachStdout: true, - AttachStderr: true, - }; - - if (opts?.env !== undefined) { - execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); - } - if (opts?.workingDir !== undefined) { - execOptions.WorkingDir = opts.workingDir; - } - if (opts?.user !== undefined) { - execOptions.User = opts.user; - } - + const outputChunks: string[] = []; const stdoutChunks: string[] = []; const stderrChunks: string[] = []; try { if (opts?.log) { - log.debug(`Execing container verbosely with command "${command.join(" ")}"...`, { containerId: container.id }); + log.debug(`Execing container with command "${command.join(" ")}"...`, { containerId: container.id }); } const exec = await container.exec(execOptions); - const stream = await exec.start({ stdin: true, Detach: false, Tty: false }); + const stream = await this.demuxStream(container.id, await exec.start({ stdin: true, Detach: false, Tty: true })); const stdoutStream = new PassThrough(); const stderrStream = new PassThrough(); - // Podman may use the same demuxing approach as Docker this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); const processStream = (stream: Readable, chunks: string[]) => { stream.on("data", (chunk) => { + outputChunks.push(chunk.toString()); chunks.push(chunk.toString()); + if (opts?.log && execLog.enabled()) { execLog.trace(chunk.toString(), { containerId: container.id }); } @@ -113,15 +62,11 @@ export class PodmanContainerClient extends DockerContainerClient { const inspectResult = await exec.inspect(); const exitCode = inspectResult.ExitCode ?? -1; - + const output = outputChunks.join(""); const stdout = stdoutChunks.join(""); const stderr = stderrChunks.join(""); - if (opts?.log) { - log.debug(`ExecVerbose completed with command "${command.join(" ")}"`, { containerId: container.id }); - } - - return { stdout, stderr, exitCode }; + return { output, stdout, stderr, exitCode }; } catch (err) { log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${stderrChunks.join("")}`, { containerId: container.id, diff --git a/packages/testcontainers/src/container-runtime/clients/container/types.ts b/packages/testcontainers/src/container-runtime/clients/container/types.ts index 9fd08922e..c076a71f5 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/types.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/types.ts @@ -2,9 +2,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 type ExecVerboseResult = { stdout: string; stderr: string; exitCode: number }; +export type ExecResult = { output: string; stdout: string; stderr: string; exitCode: number }; export const CONTAINER_STATUSES = ["created", "restarting", "running", "removing", "paused", "exited", "dead"] as const; diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 8882f0c91..9eff27f52 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -42,10 +42,12 @@ describe("GenericContainer", () => { it("should execute a command on a running container", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - const { output, exitCode } = await container.exec(["echo", "hello", "world"]); + const { output, stdout, stderr, exitCode } = await container.exec(["echo", "hello", "world"]); expect(exitCode).toBe(0); - expect(output).toEqual(expect.stringContaining("hello world")); + expect(stdout).toEqual(expect.stringContaining("hello world")); + expect(stderr).toBe(""); + expect(output).toEqual(stdout); await container.stop(); }); @@ -53,10 +55,12 @@ describe("GenericContainer", () => { it("should execute a command in a different working directory", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - const { output, exitCode } = await container.exec(["pwd"], { workingDir: "/var/log" }); + const { output, stdout, stderr, exitCode } = await container.exec(["pwd"], { workingDir: "/var/log" }); expect(exitCode).toBe(0); - expect(output).toEqual(expect.stringContaining("/var/log")); + expect(stdout).toEqual(expect.stringContaining("/var/log")); + expect(stderr).toBe(""); + expect(output).toEqual(stdout); await container.stop(); }); @@ -64,10 +68,12 @@ describe("GenericContainer", () => { it("should execute a command with custom environment variables", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - const { output, exitCode } = await container.exec(["env"], { env: { TEST_ENV: "test" } }); + const { output, stdout, stderr, exitCode } = await container.exec(["env"], { env: { TEST_ENV: "test" } }); expect(exitCode).toBe(0); - expect(output).toEqual(expect.stringContaining("TEST_ENV=test")); + expect(stdout).toEqual(expect.stringContaining("TEST_ENV=test")); + expect(stderr).toBe(""); + expect(output).toEqual(stdout); await container.stop(); }); @@ -76,43 +82,43 @@ describe("GenericContainer", () => { // By default, node:alpine runs as root const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - const { output, exitCode } = await container.exec("whoami", { user: "node" }); + const { output, stdout, stderr, exitCode } = await container.exec(["whoami"], { user: "node" }); expect(exitCode).toBe(0); - expect(output).toEqual(expect.stringContaining("node")); + expect(stdout).toEqual(expect.stringContaining("node")); + expect(stderr).toBe(""); + expect(output).toEqual(stdout); await container.stop(); }); - it("should execute a command on a running container with verbose output", async () => { + it("should capture stderr when a command fails", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - const { stdout, exitCode } = await container.execVerbose(["echo", "hello", "world"]); + const { output, stdout, stderr, exitCode } = await container.exec(["ls", "/nonexistent/path"]); - expect(exitCode).toBe(0); - expect(stdout).toEqual(expect.stringContaining("hello world")); + expect(exitCode).not.toBe(0); + expect(stdout).toBe(""); + expect(stderr).toEqual(expect.stringContaining("No such file or directory")); + expect(output).toEqual(stderr); await container.stop(); }); - it("should capture warnings from stderr with verbose output", async () => { + it("should capture stdout and stderr in the correct order", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - const { stderr, exitCode } = await container.execVerbose(["sh", "-c", "echo 'Warning!' 1>&2"]); - - expect(exitCode).toBe(0); - expect(stderr).toEqual(expect.stringContaining("Warning!")); - - await container.stop(); - }); - - it("should capture errors from stderr with verbose logging", async () => { - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start(); - - const { stderr, exitCode } = await container.execVerbose(["sh", "-c", "exit 1"]); - - expect(exitCode).toBe(1); - expect(stderr).toEqual(expect.stringContaining("")); + // The command first writes to stdout and then tries to access a nonexistent file (stderr) + const { output, stdout, stderr, exitCode } = await container.exec([ + "sh", + "-c", + "echo 'This is stdout'; ls /nonexistent/path", + ]); + + expect(exitCode).not.toBe(0); // The command should fail due to the ls error + expect(stdout).toEqual(expect.stringContaining("This is stdout")); + expect(stderr).toEqual(expect.stringContaining("No such file or directory")); + expect(output).toMatch(/This is stdout[\s\S]*No such file or directory/); await container.stop(); }); diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index 67ed02a4a..b94c2e242 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -5,7 +5,6 @@ import { DirectoryToCopy, ExecOptions, ExecResult, - ExecVerboseResult, FileToCopy, Labels, } from "../types"; @@ -189,17 +188,6 @@ export class StartedGenericContainer implements StartedTestContainer { return output; } - public async execVerbose(command: string | string[], opts?: Partial): Promise { - const commandArr = Array.isArray(command) ? command : command.split(" "); - const commandStr = commandArr.join(" "); - const client = await getContainerRuntimeClient(); - log.debug(`Executing command "${commandStr}"...`, { containerId: this.container.id }); - const output = await client.container.execVerbose(this.container, commandArr, opts); - log.debug(`Executed command "${commandStr}"...`, { containerId: this.container.id }); - - return output; - } - public async logs(opts?: { since?: number; tail?: number }): Promise { const client = await getContainerRuntimeClient(); diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index 40be458d7..056921ac8 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -7,7 +7,6 @@ import { Environment, ExecOptions, ExecResult, - ExecVerboseResult, ExtraHost, FileToCopy, Labels, @@ -77,7 +76,6 @@ export interface StartedTestContainer { copyFilesToContainer(filesToCopy: FileToCopy[]): Promise; copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise; exec(command: string | string[], opts?: Partial): Promise; - execVerbose(command: string | string[], opts?: Partial): Promise; logs(opts?: { since?: number; tail?: number }): Promise; } diff --git a/packages/testcontainers/src/types.ts b/packages/testcontainers/src/types.ts index ddebca52a..ad8eb865e 100644 --- a/packages/testcontainers/src/types.ts +++ b/packages/testcontainers/src/types.ts @@ -83,9 +83,7 @@ export type BuildArgs = { [key in string]: string }; export type ExecOptions = { workingDir: string; user: string; env: Environment }; -export type ExecResult = { output: string; exitCode: number }; - -export type ExecVerboseResult = { stdout: string; stderr: string; exitCode: number }; +export type ExecResult = { output: string; stdout: string; stderr: string; exitCode: number }; export type HealthCheckStatus = "none" | "starting" | "unhealthy" | "healthy"; From 798730429ef12de0c3310e1b6be33033379a11e9 Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 11:26:03 +0900 Subject: [PATCH 5/9] Update exec method documentation --- docs/features/containers.md | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/docs/features/containers.md b/docs/features/containers.md index b1b9e4833..1f5501e3c 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -503,15 +503,16 @@ const container = await new GenericContainer("alpine") ## Running commands -To run a command inside an already started container use the `exec` method. The command will be run in the container's -working directory, returning the command output and exit code: +To run a command inside an already started container, use the exec method. +The command will be run in the container's working directory, +returning the combined output (`output`), standard output (`stdout`), standard error (`stderr`), and exit code (`exitCode`). ```javascript const container = await new GenericContainer("alpine") .withCommand(["sleep", "infinity"]) .start(); -const { output, exitCode } = await container.exec(["echo", "hello", "world"]); +const { output, stdout, stderr, exitCode } = await container.exec(["echo", "hello", "world"]); ``` The following options can be provided to modify the command execution: @@ -528,7 +529,7 @@ const container = await new GenericContainer("alpine") .withCommand(["sleep", "infinity"]) .start(); -const { output, exitCode } = await container.exec(["echo", "hello", "world"], { +const { output, stdout, stderr, exitCode } = await container.exec(["echo", "hello", "world"], { workingDir: "/app/src/", user: "1000:1000", env: { @@ -537,25 +538,7 @@ const { output, exitCode } = await container.exec(["echo", "hello", "world"], { } }); ``` -To handle cases where you need separate outputs for `stdout` and `stderr`, you can use the `execVerbose` method. -It functions similarly to `exec`, but provides detailed output including `stdout`, `stderr`, and `exitCode`. -```javascript -const container = await new GenericContainer("alpine") - .withCommand(["sleep", "infinity"]) - .start(); - -const { stdout, stderr, exitCode } = await container.execVerbose(["echo", "hello", "world"], { - workingDir: "/app/src/", - user: "1000:1000", - env: { - "VAR1": "enabled", - "VAR2": "/app/debug.log", - } -}); -``` -Use `execVerbose` when you require more granular control over command outputs, -while retaining similar options and functionality as `exec`. ## Streaming logs From 62279d732827ce9d1ea4a22ada6c4763188e8c44 Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 18:39:34 +0900 Subject: [PATCH 6/9] change err log return all exec output --- .../clients/container/docker-container-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts index 9096e8a6b..9857489f0 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts @@ -249,7 +249,7 @@ export class DockerContainerClient implements ContainerClient { } return { output, stdout, stderr, exitCode }; } catch (err) { - log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${stderrChunks.join("")}`, { + log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${outputChunks.join("")}`, { containerId: container.id, }); throw err; From b2a23b364dafaeae2a0f4cdfe807027c8809befd Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 18:41:08 +0900 Subject: [PATCH 7/9] remove remained execVerbose --- .../src/generic-container/abstract-started-container.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/testcontainers/src/generic-container/abstract-started-container.ts b/packages/testcontainers/src/generic-container/abstract-started-container.ts index 73ca45dbc..4e7b78ea6 100644 --- a/packages/testcontainers/src/generic-container/abstract-started-container.ts +++ b/packages/testcontainers/src/generic-container/abstract-started-container.ts @@ -4,7 +4,6 @@ import { DirectoryToCopy, ExecOptions, ExecResult, - ExecVerboseResult, FileToCopy, Labels, } from "../types"; @@ -91,10 +90,6 @@ export class AbstractStartedContainer implements StartedTestContainer { return this.startedTestContainer.exec(command, opts); } - public execVerbose(command: string | string[], opts?: Partial): Promise { - return this.startedTestContainer.execVerbose(command, opts); - } - public logs(opts?: { since?: number; tail?: number }): Promise { return this.startedTestContainer.logs(opts); } From e7bd1c9fc92536924e1e6dd58691769c4679f526 Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 20:53:27 +0900 Subject: [PATCH 8/9] Remove Podman container clients --- .../src/container-runtime/clients/client.ts | 5 +- .../container/podman-container-client.ts | 77 ------------------- 2 files changed, 1 insertion(+), 81 deletions(-) delete mode 100644 packages/testcontainers/src/container-runtime/clients/container/podman-container-client.ts diff --git a/packages/testcontainers/src/container-runtime/clients/client.ts b/packages/testcontainers/src/container-runtime/clients/client.ts index 898f14664..70af6f61c 100644 --- a/packages/testcontainers/src/container-runtime/clients/client.ts +++ b/packages/testcontainers/src/container-runtime/clients/client.ts @@ -12,7 +12,6 @@ import { ComposeInfo, ContainerRuntimeInfo, Info, NodeInfo } from "./types"; import Dockerode, { DockerOptions } from "dockerode"; import { getRemoteContainerRuntimeSocketPath } from "../utils/remote-container-runtime-socket-path"; import { resolveHost } from "../utils/resolve-host"; -import { PodmanContainerClient } from "./container/podman-container-client"; import { DockerContainerClient } from "./container/docker-container-client"; import { DockerImageClient } from "./image/docker-image-client"; import { DockerNetworkClient } from "./network/docker-network-client"; @@ -105,9 +104,7 @@ async function initStrategy(strategy: ContainerRuntimeClientStrategy): Promise): Promise { - const execOptions: ExecCreateOptions = { - Cmd: command, - AttachStdout: true, - AttachStderr: true, - }; - - if (opts?.env !== undefined) { - execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); - } - if (opts?.workingDir !== undefined) { - execOptions.WorkingDir = opts.workingDir; - } - if (opts?.user !== undefined) { - execOptions.User = opts.user; - } - - const outputChunks: string[] = []; - const stdoutChunks: string[] = []; - const stderrChunks: string[] = []; - - try { - if (opts?.log) { - log.debug(`Execing container with command "${command.join(" ")}"...`, { containerId: container.id }); - } - - const exec = await container.exec(execOptions); - const stream = await this.demuxStream(container.id, await exec.start({ stdin: true, Detach: false, Tty: true })); - - const stdoutStream = new PassThrough(); - const stderrStream = new PassThrough(); - - this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); - - const processStream = (stream: Readable, chunks: string[]) => { - stream.on("data", (chunk) => { - outputChunks.push(chunk.toString()); - chunks.push(chunk.toString()); - - if (opts?.log && execLog.enabled()) { - execLog.trace(chunk.toString(), { containerId: container.id }); - } - }); - }; - - processStream(stdoutStream, stdoutChunks); - processStream(stderrStream, stderrChunks); - - await new Promise((res, rej) => { - stream.on("end", res); - stream.on("error", rej); - }); - stream.destroy(); - - const inspectResult = await exec.inspect(); - const exitCode = inspectResult.ExitCode ?? -1; - const output = outputChunks.join(""); - const stdout = stdoutChunks.join(""); - const stderr = stderrChunks.join(""); - - return { output, stdout, stderr, exitCode }; - } catch (err) { - log.error(`Failed to exec container with command "${command.join(" ")}": ${err}: ${stderrChunks.join("")}`, { - containerId: container.id, - }); - throw err; - } - } -} From ba36f28ec0719cbb8dc24229253c87c8c3cbe39c Mon Sep 17 00:00:00 2001 From: chungjung-d Date: Mon, 25 Nov 2024 22:43:17 +0900 Subject: [PATCH 9/9] npm run foramt --- .../clients/container/docker-container-client.ts | 1 - .../src/generic-container/abstract-started-container.ts | 9 +-------- .../src/generic-container/started-generic-container.ts | 9 +-------- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts index 9857489f0..7367b5d18 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts @@ -10,7 +10,6 @@ import Dockerode, { import { PassThrough, Readable } from "stream"; import { IncomingMessage } from "http"; import { ContainerStatus, ExecOptions, ExecResult } from "./types"; -import byline from "byline"; import { ContainerClient } from "./container-client"; import { execLog, log, streamToString } from "../../../common"; diff --git a/packages/testcontainers/src/generic-container/abstract-started-container.ts b/packages/testcontainers/src/generic-container/abstract-started-container.ts index 4e7b78ea6..a88ec6fff 100644 --- a/packages/testcontainers/src/generic-container/abstract-started-container.ts +++ b/packages/testcontainers/src/generic-container/abstract-started-container.ts @@ -1,12 +1,5 @@ import { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container"; -import { - ContentToCopy, - DirectoryToCopy, - ExecOptions, - ExecResult, - FileToCopy, - Labels, -} from "../types"; +import { ContentToCopy, DirectoryToCopy, ExecOptions, ExecResult, FileToCopy, Labels } from "../types"; import { Readable } from "stream"; export class AbstractStartedContainer implements StartedTestContainer { diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index b94c2e242..c5ff8865a 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -1,13 +1,6 @@ import { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container"; import Dockerode, { ContainerInspectInfo } from "dockerode"; -import { - ContentToCopy, - DirectoryToCopy, - ExecOptions, - ExecResult, - FileToCopy, - Labels, -} from "../types"; +import { ContentToCopy, DirectoryToCopy, ExecOptions, ExecResult, FileToCopy, Labels } from "../types"; import { Readable } from "stream"; import { StoppedGenericContainer } from "./stopped-generic-container"; import { WaitStrategy } from "../wait-strategies/wait-strategy";