diff --git a/docs/features/containers.md b/docs/features/containers.md index 98ac5b95c..2e03f0829 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -583,6 +583,35 @@ 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 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 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. +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. @@ -605,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"]) @@ -621,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/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/socat/socat-container.test.ts b/packages/testcontainers/src/socat/socat-container.test.ts new file mode 100644 index 000000000..b74a9f351 --- /dev/null +++ b/packages/testcontainers/src/socat/socat-container.test.ts @@ -0,0 +1,45 @@ +import { GenericContainer } from "../generic-container/generic-container"; +import { Network } from "../network/network"; +import { SocatContainer } from "./socat-container"; + +describe("SocatContainer", { timeout: 120_000 }, () => { + it("should forward requests to helloworld container", async () => { + const network = await new Network().start(); + 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}/hello-world`); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("hello-world"); + + await socat.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 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}/hello-world`); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("hello-world"); + + await socat.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 new file mode 100644 index 000000000..845d0482b --- /dev/null +++ b/packages/testcontainers/src/socat/socat-container.ts @@ -0,0 +1,35 @@ +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: { [exposePort: 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, internalPort = exposePort): this { + this.withExposedPorts(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); + } +}