Skip to content

Commit aebb6fc

Browse files
authored
Add SocatContainer (#964)
1 parent ae710b1 commit aebb6fc

File tree

4 files changed

+110
-3
lines changed

4 files changed

+110
-3
lines changed

docs/features/containers.md

+29-3
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,35 @@ const container = await new GenericContainer("alpine")
583583
.start();
584584
```
585585

586+
## SocatContainer as a TCP proxy
587+
588+
`SocatContainer` enables any TCP port of another container to be exposed publicly.
589+
590+
```javascript
591+
const network = await new Network().start();
592+
593+
const container = await new GenericContainer("testcontainers/helloworld:1.2.0")
594+
.withExposedPorts(8080)
595+
.withNetwork(network)
596+
.withNetworkAliases("helloworld")
597+
.start();
598+
599+
const socat = await new SocatContainer()
600+
.withNetwork(network)
601+
.withTarget(8081, "helloworld", 8080)
602+
.start();
603+
604+
const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8081)}`;
605+
606+
const response = await fetch(`${socatUrl}/ping`);
607+
608+
expect(response.status).toBe(200);
609+
expect(await response.text()).toBe("PONG");
610+
```
611+
612+
The example above starts a `testcontainers/helloworld` container and a `socat` container.
613+
The `socat` container is configured to forward traffic from port `8081` to the `testcontainers/helloworld` container on port `8080`.
614+
586615
## Running commands
587616

588617
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:
605634

606635
3. **`env`:** A map of environment variables to set inside the container.
607636

608-
609637
```javascript
610638
const container = await new GenericContainer("alpine")
611639
.withCommand(["sleep", "infinity"])
@@ -621,8 +649,6 @@ const { output, stdout, stderr, exitCode } = await container.exec(["echo", "hell
621649
});
622650
```
623651

624-
625-
626652
## Streaming logs
627653

628654
Logs can be consumed either from a started container:

packages/testcontainers/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { GenericContainer } from "./generic-container/generic-container";
1010
export { BuildOptions, GenericContainerBuilder } from "./generic-container/generic-container-builder";
1111
export { Network, StartedNetwork, StoppedNetwork } from "./network/network";
1212
export { getReaper } from "./reaper/reaper";
13+
export { SocatContainer, StartedSocatContainer } from "./socat/socat-container";
1314
export {
1415
RestartOptions,
1516
StartedTestContainer,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { GenericContainer } from "../generic-container/generic-container";
2+
import { Network } from "../network/network";
3+
import { SocatContainer } from "./socat-container";
4+
5+
describe("SocatContainer", { timeout: 120_000 }, () => {
6+
it("should forward requests to helloworld container", async () => {
7+
const network = await new Network().start();
8+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
9+
.withExposedPorts(8080)
10+
.withNetwork(network)
11+
.withNetworkAliases("helloworld")
12+
.start();
13+
const socat = await new SocatContainer().withNetwork(network).withTarget(8080, "helloworld").start();
14+
15+
const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8080)}`;
16+
const response = await fetch(`${socatUrl}/hello-world`);
17+
18+
expect(response.status).toBe(200);
19+
expect(await response.text()).toBe("hello-world");
20+
21+
await socat.stop();
22+
await container.stop();
23+
await network.stop();
24+
});
25+
26+
it("should forward requests to helloworld container in a different port", async () => {
27+
const network = await new Network().start();
28+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
29+
.withExposedPorts(8080)
30+
.withNetwork(network)
31+
.withNetworkAliases("helloworld")
32+
.start();
33+
const socat = await new SocatContainer().withNetwork(network).withTarget(8081, "helloworld", 8080).start();
34+
35+
const socatUrl = `http://${socat.getHost()}:${socat.getMappedPort(8081)}`;
36+
const response = await fetch(`${socatUrl}/hello-world`);
37+
38+
expect(response.status).toBe(200);
39+
expect(await response.text()).toBe("hello-world");
40+
41+
await socat.stop();
42+
await container.stop();
43+
await network.stop();
44+
});
45+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { RandomUuid } from "../common";
2+
import { AbstractStartedContainer } from "../generic-container/abstract-started-container";
3+
import { GenericContainer } from "../generic-container/generic-container";
4+
import { StartedTestContainer } from "../test-container";
5+
6+
export class SocatContainer extends GenericContainer {
7+
private targets: { [exposePort: number]: string } = {};
8+
9+
constructor(image = "alpine/socat:1.7.4.3-r0") {
10+
super(image);
11+
this.withEntrypoint(["/bin/sh"]);
12+
this.withName(`testcontainers-socat-${new RandomUuid().nextUuid()}`);
13+
}
14+
15+
public withTarget(exposePort: number, host: string, internalPort = exposePort): this {
16+
this.withExposedPorts(exposePort);
17+
this.targets[exposePort] = `${host}:${internalPort}`;
18+
return this;
19+
}
20+
21+
public override async start(): Promise<StartedSocatContainer> {
22+
const command = Object.entries(this.targets)
23+
.map(([exposePort, target]) => `socat TCP-LISTEN:${exposePort},fork,reuseaddr TCP:${target}`)
24+
.join(" & ");
25+
26+
this.withCommand(["-c", command]);
27+
return new StartedSocatContainer(await super.start());
28+
}
29+
}
30+
31+
export class StartedSocatContainer extends AbstractStartedContainer {
32+
constructor(startedTestcontainers: StartedTestContainer) {
33+
super(startedTestcontainers);
34+
}
35+
}

0 commit comments

Comments
 (0)