From d3285368df186aee2584786ffc359f0efd51cfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Thu, 27 Mar 2025 20:07:27 -0600 Subject: [PATCH 1/5] Add SocatContainer --- .../src/socat/socat-container.test.ts | 29 ++++++++++++++ .../src/socat/socat-container.ts | 40 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 packages/testcontainers/src/socat/socat-container.test.ts create mode 100644 packages/testcontainers/src/socat/socat-container.ts diff --git a/packages/testcontainers/src/socat/socat-container.test.ts b/packages/testcontainers/src/socat/socat-container.test.ts new file mode 100644 index 000000000..50de89325 --- /dev/null +++ b/packages/testcontainers/src/socat/socat-container.test.ts @@ -0,0 +1,29 @@ +import { fetch } from "undici"; +import { GenericContainer } from "../generic-container/generic-container"; +import { Network } from "../network/network"; +import { SocatContainer } from "./socat-container"; + +describe("Socat", { timeout: 120_000 }, () => { + it("should forward requests to helloworld container", async () => { + const network = await new Network().start(); + + const helloworld = await new GenericContainer("testcontainers/helloworld:1.2.0") + .withExposedPorts(8080) + .withNetwork(network) + .withNetworkAliases("helloworld") + .start(); + + const socat = await new SocatContainer().withNetwork(network).withTarget(8080, "helloworld").start(); + + const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8080)}`; + + const response = await fetch(`${socatUrl}/ping`); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("PONG"); + + await socat.stop(); + await helloworld.stop(); + await network.stop(); + }); +}); diff --git a/packages/testcontainers/src/socat/socat-container.ts b/packages/testcontainers/src/socat/socat-container.ts new file mode 100644 index 000000000..84c2ead3f --- /dev/null +++ b/packages/testcontainers/src/socat/socat-container.ts @@ -0,0 +1,40 @@ +import { RandomUuid } from "../common"; +import { AbstractStartedContainer } from "../generic-container/abstract-started-container"; +import { GenericContainer } from "../generic-container/generic-container"; +import { StartedTestContainer } from "../test-container"; + +export class SocatContainer extends GenericContainer { + private targets: { [key in number]: string } = {}; + + constructor(image = "alpine/socat:1.7.4.3-r0") { + super(image); + this.withEntrypoint(["/bin/sh"]); + this.withName(`testcontainers-socat-${new RandomUuid().nextUuid()}`); + } + + public withTarget(exposePort: number, host: string): this; + public withTarget(exposePort: number, host: string, internalPort: number): this; + public withTarget(exposePort: number, host: string, internalPort?: number): this { + this.withExposedPorts(exposePort); + if (internalPort == null) { + internalPort = exposePort; + } + this.targets[exposePort] = `${host}:${internalPort}`; + return this; + } + + public override async start(): Promise { + const command = Object.entries(this.targets) + .map(([exposePort, target]) => `socat TCP-LISTEN:${exposePort},fork,reuseaddr TCP:${target}`) + .join(" & "); + + this.withCommand(["-c", command]); + return new StartedSocatContainer(await super.start()); + } +} + +export class StartedSocatContainer extends AbstractStartedContainer { + constructor(startedTestcontainers: StartedTestContainer) { + super(startedTestcontainers); + } +} From 1cc348204799c83da3aeb8209317716915e89f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Mon, 31 Mar 2025 10:14:58 -0600 Subject: [PATCH 2/5] Add new test using a different internal port --- .../src/socat/socat-container.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/testcontainers/src/socat/socat-container.test.ts b/packages/testcontainers/src/socat/socat-container.test.ts index 50de89325..54359e22d 100644 --- a/packages/testcontainers/src/socat/socat-container.test.ts +++ b/packages/testcontainers/src/socat/socat-container.test.ts @@ -22,6 +22,28 @@ describe("Socat", { timeout: 120_000 }, () => { expect(response.status).toBe(200); expect(await response.text()).toBe("PONG"); + await socat.stop(); + await helloworld.stop(); + await network.stop(); + }); + it("should forward requests to helloworld container in a different port", async () => { + const network = await new Network().start(); + + const helloworld = await new GenericContainer("testcontainers/helloworld:1.2.0") + .withExposedPorts(8080) + .withNetwork(network) + .withNetworkAliases("helloworld") + .start(); + + const socat = await new SocatContainer().withNetwork(network).withTarget(8081, "helloworld", 8080).start(); + + const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8081)}`; + + const response = await fetch(`${socatUrl}/ping`); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("PONG"); + await socat.stop(); await helloworld.stop(); await network.stop(); From 39413b5482cd852d8b0a2d217030b4056a4f9bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Wed, 2 Apr 2025 10:17:50 -0600 Subject: [PATCH 3/5] Export SocatContainer, StartedSocatContainer and add docs --- docs/features/containers.md | 21 +++++++++++++++++++++ packages/testcontainers/src/types.ts | 2 ++ 2 files changed, 23 insertions(+) diff --git a/docs/features/containers.md b/docs/features/containers.md index 98ac5b95c..1545c360f 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -583,6 +583,27 @@ const container = await new GenericContainer("alpine") .start(); ``` +## SocatContainer as a TCP proxy + +`SocatContainer` enables any TCP port of another container to be exposed publicly. + +```javascript +const network = await new Network().start(); + +const helloworld = await new GenericContainer("testcontainers/helloworld:1.2.0") + .withExposedPorts(8080) + .withNetwork(network) + .withNetworkAliases("helloworld") + .start(); + +const socat = await new SocatContainer().withNetwork(network).withTarget(8081, "helloworld", 8080).start(); + +const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8081)}`; +``` + +The example above starts a `testcontainers/helloworld` container and a `socat` container. +The `socat` container is configured to forward traffic from port `8081` to the `testcontainers/helloworld` container on port `8080`. + ## Running commands To run a command inside an already started container, use the exec method. diff --git a/packages/testcontainers/src/types.ts b/packages/testcontainers/src/types.ts index 6b5961326..1e9c12361 100644 --- a/packages/testcontainers/src/types.ts +++ b/packages/testcontainers/src/types.ts @@ -104,3 +104,5 @@ export type NetworkSettings = { networkId: string; ipAddress: string; }; + +export { SocatContainer, StartedSocatContainer } from "./socat/socat-container"; From 7b790bd2e7875cf59d86f728216d7e351ca19544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Wed, 2 Apr 2025 10:57:30 -0600 Subject: [PATCH 4/5] Move export to index.ts --- packages/testcontainers/src/index.ts | 1 + packages/testcontainers/src/types.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/testcontainers/src/index.ts b/packages/testcontainers/src/index.ts index 08a4e910d..f440ea62c 100644 --- a/packages/testcontainers/src/index.ts +++ b/packages/testcontainers/src/index.ts @@ -10,6 +10,7 @@ export { GenericContainer } from "./generic-container/generic-container"; export { BuildOptions, GenericContainerBuilder } from "./generic-container/generic-container-builder"; export { Network, StartedNetwork, StoppedNetwork } from "./network/network"; export { getReaper } from "./reaper/reaper"; +export { SocatContainer, StartedSocatContainer } from "./socat/socat-container"; export { RestartOptions, StartedTestContainer, diff --git a/packages/testcontainers/src/types.ts b/packages/testcontainers/src/types.ts index 1e9c12361..6b5961326 100644 --- a/packages/testcontainers/src/types.ts +++ b/packages/testcontainers/src/types.ts @@ -104,5 +104,3 @@ export type NetworkSettings = { networkId: string; ipAddress: string; }; - -export { SocatContainer, StartedSocatContainer } from "./socat/socat-container"; From 7ff800cf9b57e18dfe996c05ec5ab99f8d63c1c8 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Wed, 2 Apr 2025 20:46:30 +0100 Subject: [PATCH 5/5] Minor refactor --- docs/features/containers.md | 23 +++++++++------- .../src/socat/socat-container.test.ts | 26 +++++++------------ .../src/socat/socat-container.ts | 9 ++----- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/docs/features/containers.md b/docs/features/containers.md index 1545c360f..2e03f0829 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -590,15 +590,23 @@ const container = await new GenericContainer("alpine") ```javascript const network = await new Network().start(); -const helloworld = await new GenericContainer("testcontainers/helloworld:1.2.0") - .withExposedPorts(8080) - .withNetwork(network) - .withNetworkAliases("helloworld") - .start(); +const container = await new GenericContainer("testcontainers/helloworld:1.2.0") + .withExposedPorts(8080) + .withNetwork(network) + .withNetworkAliases("helloworld") + .start(); -const socat = await new SocatContainer().withNetwork(network).withTarget(8081, "helloworld", 8080).start(); +const socat = await new SocatContainer() + .withNetwork(network) + .withTarget(8081, "helloworld", 8080) + .start(); const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8081)}`; + +const response = await fetch(`${socatUrl}/ping`); + +expect(response.status).toBe(200); +expect(await response.text()).toBe("PONG"); ``` The example above starts a `testcontainers/helloworld` container and a `socat` container. @@ -626,7 +634,6 @@ The following options can be provided to modify the command execution: 3. **`env`:** A map of environment variables to set inside the container. - ```javascript const container = await new GenericContainer("alpine") .withCommand(["sleep", "infinity"]) @@ -642,8 +649,6 @@ const { output, stdout, stderr, exitCode } = await container.exec(["echo", "hell }); ``` - - ## Streaming logs Logs can be consumed either from a started container: diff --git a/packages/testcontainers/src/socat/socat-container.test.ts b/packages/testcontainers/src/socat/socat-container.test.ts index 54359e22d..b74a9f351 100644 --- a/packages/testcontainers/src/socat/socat-container.test.ts +++ b/packages/testcontainers/src/socat/socat-container.test.ts @@ -1,51 +1,45 @@ -import { fetch } from "undici"; import { GenericContainer } from "../generic-container/generic-container"; import { Network } from "../network/network"; import { SocatContainer } from "./socat-container"; -describe("Socat", { timeout: 120_000 }, () => { +describe("SocatContainer", { timeout: 120_000 }, () => { it("should forward requests to helloworld container", async () => { const network = await new Network().start(); - - const helloworld = await new GenericContainer("testcontainers/helloworld:1.2.0") + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withExposedPorts(8080) .withNetwork(network) .withNetworkAliases("helloworld") .start(); - const socat = await new SocatContainer().withNetwork(network).withTarget(8080, "helloworld").start(); const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8080)}`; - - const response = await fetch(`${socatUrl}/ping`); + const response = await fetch(`${socatUrl}/hello-world`); expect(response.status).toBe(200); - expect(await response.text()).toBe("PONG"); + expect(await response.text()).toBe("hello-world"); await socat.stop(); - await helloworld.stop(); + await container.stop(); await network.stop(); }); + it("should forward requests to helloworld container in a different port", async () => { const network = await new Network().start(); - - const helloworld = await new GenericContainer("testcontainers/helloworld:1.2.0") + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withExposedPorts(8080) .withNetwork(network) .withNetworkAliases("helloworld") .start(); - const socat = await new SocatContainer().withNetwork(network).withTarget(8081, "helloworld", 8080).start(); const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8081)}`; - - const response = await fetch(`${socatUrl}/ping`); + const response = await fetch(`${socatUrl}/hello-world`); expect(response.status).toBe(200); - expect(await response.text()).toBe("PONG"); + expect(await response.text()).toBe("hello-world"); await socat.stop(); - await helloworld.stop(); + await container.stop(); await network.stop(); }); }); diff --git a/packages/testcontainers/src/socat/socat-container.ts b/packages/testcontainers/src/socat/socat-container.ts index 84c2ead3f..845d0482b 100644 --- a/packages/testcontainers/src/socat/socat-container.ts +++ b/packages/testcontainers/src/socat/socat-container.ts @@ -4,7 +4,7 @@ import { GenericContainer } from "../generic-container/generic-container"; import { StartedTestContainer } from "../test-container"; export class SocatContainer extends GenericContainer { - private targets: { [key in number]: string } = {}; + private targets: { [exposePort: number]: string } = {}; constructor(image = "alpine/socat:1.7.4.3-r0") { super(image); @@ -12,13 +12,8 @@ export class SocatContainer extends GenericContainer { this.withName(`testcontainers-socat-${new RandomUuid().nextUuid()}`); } - public withTarget(exposePort: number, host: string): this; - public withTarget(exposePort: number, host: string, internalPort: number): this; - public withTarget(exposePort: number, host: string, internalPort?: number): this { + public withTarget(exposePort: number, host: string, internalPort = exposePort): this { this.withExposedPorts(exposePort); - if (internalPort == null) { - internalPort = exposePort; - } this.targets[exposePort] = `${host}:${internalPort}`; return this; }