Skip to content

Commit 9bf42a1

Browse files
authored
feat: add socat container (#3071)
* chore: create socat module from modulegen * chore: implement socat * chore: bump tc-go * chore: mod tidy * chore: fix examples * fix: lint * chore: make field private * fix: remove from copy&paste * chore: make option return error * chore: simplify passing the port * docs: document functions * chore: simplify WithTarget option to avoid variad arguments * docs: describe socat * chore: add a test with multiple targets * chore: more illustrative examples * chore: add unit tests for target validation * fix: use errors.New
1 parent 3bdcef9 commit 9bf42a1

File tree

13 files changed

+962
-0
lines changed

13 files changed

+962
-0
lines changed

.github/dependabot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ updates:
6363
- /modules/redpanda
6464
- /modules/registry
6565
- /modules/scylladb
66+
- /modules/socat
6667
- /modules/surrealdb
6768
- /modules/valkey
6869
- /modules/vault

.vscode/.testcontainers-go.code-workspace

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@
201201
"name": "module / scylladb",
202202
"path": "../modules/scylladb"
203203
},
204+
{
205+
"name": "module / socat",
206+
"path": "../modules/socat"
207+
},
204208
{
205209
"name": "module / surrealdb",
206210
"path": "../modules/surrealdb"

docs/modules/socat.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Socat
2+
3+
Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
4+
5+
## Introduction
6+
7+
The Testcontainers module for Socat, a utility container that provides TCP port forwarding and network tunneling between services, enabling transparent communication between containers and networks.
8+
9+
This is particularly useful in testing scenarios where you need to simulate network connections or provide transparent access to services running in different containers.
10+
11+
## Adding this module to your project dependencies
12+
13+
Please run the following command to add the Socat module to your Go dependencies:
14+
15+
```
16+
go get github.com/testcontainers/testcontainers-go/modules/socat
17+
```
18+
19+
## Usage example
20+
21+
<!--codeinclude-->
22+
[Create a Network](../../modules/socat/examples_test.go) inside_block:createNetwork
23+
[Create a Hello World Container](../../modules/socat/examples_test.go) inside_block:createHelloWorldContainer
24+
[Create a Socat Container](../../modules/socat/examples_test.go) inside_block:createSocatContainer
25+
[Read from Socat Container](../../modules/socat/examples_test.go) inside_block:readFromSocat
26+
<!--/codeinclude-->
27+
28+
## Module Reference
29+
30+
### Run function
31+
32+
- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
33+
34+
The Socat module exposes one entrypoint function to create the Socat container, and this function receives three parameters:
35+
36+
```golang
37+
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*SocatContainer, error)
38+
```
39+
40+
- `context.Context`, the Go context.
41+
- `string`, the Docker image to use.
42+
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.
43+
44+
### Container Options
45+
46+
When starting the Socat container, you can pass options in a variadic way to configure it.
47+
48+
#### Image
49+
50+
Use the second argument in the `Run` function to set a valid Docker image.
51+
In example: `Run(context.Background(), "alpine/socat:1.8.0.1")`.
52+
53+
{% include "../features/common_functional_options.md" %}
54+
55+
#### WithTarget
56+
57+
The `WithTarget` function sets a single target for the Socat container, defined by the `Target` struct.
58+
This struct can be built using the the following functions:
59+
60+
- `NewTarget(exposedPort int, host string)`: Creates a new target for the Socat container. The target's internal port is set to the same value as the exposed port.
61+
- `NewTargetWithInternalPort(exposedPort int, internalPort int, host string)`: Creates a new target for the Socat container with an internal port. Use this function when you want to map a container to a different port than the default one.
62+
63+
<!--codeinclude-->
64+
[Passing a target](../../modules/socat/examples_test.go) inside_block:createSocatContainer
65+
<!--/codeinclude-->
66+
67+
In the above example, there is a `helloworld` container thatis listening on port `8080` and `8081`. Please check [the helloworld container source code](https://github.com/testcontainers/helloworld/blob/141af7909907e04b124e691d3cd6fc7c32da2207/internal/server/server.go#L26-L27) for more details.
68+
69+
### Container Methods
70+
71+
The Socat container exposes the following methods:
72+
73+
#### TargetURL
74+
75+
The `TargetURL(port int)` method returns the URL for the exposed port of a target, nil if the port is not mapped.
76+
77+
<!--codeinclude-->
78+
[Read from Socat using TargetURL](../../modules/socat/examples_test.go) inside_block:readFromSocat
79+
<!--/codeinclude-->

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ nav:
114114
- modules/redpanda.md
115115
- modules/registry.md
116116
- modules/scylladb.md
117+
- modules/socat.md
117118
- modules/surrealdb.md
118119
- modules/valkey.md
119120
- modules/vault.md

modules/socat/Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include ../../commons-test.mk
2+
3+
.PHONY: test
4+
test:
5+
$(MAKE) test-socat

modules/socat/examples_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package socat_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
10+
"github.com/testcontainers/testcontainers-go"
11+
"github.com/testcontainers/testcontainers-go/modules/socat"
12+
"github.com/testcontainers/testcontainers-go/network"
13+
)
14+
15+
func ExampleRun() {
16+
ctx := context.Background()
17+
18+
nw, err := network.New(ctx)
19+
if err != nil {
20+
log.Printf("failed to create network: %v", err)
21+
return
22+
}
23+
defer func() {
24+
if err := nw.Remove(ctx); err != nil {
25+
log.Printf("failed to remove network: %s", err)
26+
}
27+
}()
28+
29+
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
30+
ContainerRequest: testcontainers.ContainerRequest{
31+
Image: "testcontainers/helloworld:1.2.0",
32+
ExposedPorts: []string{"8080/tcp"},
33+
Networks: []string{nw.Name},
34+
NetworkAliases: map[string][]string{
35+
nw.Name: {"helloworld"},
36+
},
37+
},
38+
Started: true,
39+
})
40+
if err != nil {
41+
log.Printf("failed to create container: %v", err)
42+
return
43+
}
44+
defer func() {
45+
if err := testcontainers.TerminateContainer(ctr); err != nil {
46+
log.Printf("failed to terminate container: %s", err)
47+
}
48+
}()
49+
50+
target := socat.NewTarget(8080, "helloworld")
51+
52+
socatContainer, err := socat.Run(
53+
ctx, "alpine/socat:1.8.0.1",
54+
socat.WithTarget(target),
55+
network.WithNetwork([]string{"socat"}, nw),
56+
)
57+
if err != nil {
58+
log.Printf("failed to create container: %v", err)
59+
return
60+
}
61+
defer func() {
62+
if err := testcontainers.TerminateContainer(socatContainer); err != nil {
63+
log.Printf("failed to terminate container: %s", err)
64+
}
65+
}()
66+
67+
// readFromSocat {
68+
httpClient := http.DefaultClient
69+
70+
baseURI := socatContainer.TargetURL(target.ExposedPort())
71+
72+
resp, err := httpClient.Get(baseURI.String() + "/ping")
73+
if err != nil {
74+
log.Printf("failed to get response: %v", err)
75+
return
76+
}
77+
defer resp.Body.Close()
78+
// }
79+
80+
body, err := io.ReadAll(resp.Body)
81+
if err != nil {
82+
log.Printf("failed to read body: %v", err)
83+
return
84+
}
85+
86+
fmt.Printf("%d - %s", resp.StatusCode, string(body))
87+
88+
// Output:
89+
// 200 - PONG
90+
}
91+
92+
func ExampleRun_multipleTargets() {
93+
ctx := context.Background()
94+
95+
// createNetwork {
96+
nw, err := network.New(ctx)
97+
if err != nil {
98+
log.Printf("failed to create network: %v", err)
99+
return
100+
}
101+
defer func() {
102+
if err := nw.Remove(ctx); err != nil {
103+
log.Printf("failed to remove network: %s", err)
104+
}
105+
}()
106+
// }
107+
108+
// createHelloWorldContainer {
109+
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
110+
ContainerRequest: testcontainers.ContainerRequest{
111+
Image: "testcontainers/helloworld:1.2.0",
112+
ExposedPorts: []string{"8080/tcp"},
113+
Networks: []string{nw.Name},
114+
NetworkAliases: map[string][]string{
115+
nw.Name: {"helloworld"},
116+
},
117+
},
118+
Started: true,
119+
})
120+
if err != nil {
121+
log.Printf("failed to create container: %v", err)
122+
return
123+
}
124+
defer func() {
125+
if err := testcontainers.TerminateContainer(ctr); err != nil {
126+
log.Printf("failed to terminate container: %s", err)
127+
}
128+
}()
129+
// }
130+
131+
// createSocatContainer {
132+
const (
133+
// The helloworld container is listening on both ports: 8080 and 8081
134+
port1 = 8080
135+
port2 = 8081
136+
// The helloworld container is not listening on these ports,
137+
// but the socat container will forward the traffic to the correct port
138+
port3 = 9080
139+
port4 = 9081
140+
)
141+
142+
targets := []socat.Target{
143+
socat.NewTarget(port1, "helloworld"), // using a default port
144+
socat.NewTarget(port2, "helloworld"), // using a default port
145+
socat.NewTargetWithInternalPort(port3, port1, "helloworld"), // using a different port
146+
socat.NewTargetWithInternalPort(port4, port2, "helloworld"), // using a different port
147+
}
148+
149+
socatContainer, err := socat.Run(
150+
ctx, "alpine/socat:1.8.0.1",
151+
socat.WithTarget(targets[0]),
152+
socat.WithTarget(targets[1]),
153+
socat.WithTarget(targets[2]),
154+
socat.WithTarget(targets[3]),
155+
network.WithNetwork([]string{"socat"}, nw),
156+
)
157+
if err != nil {
158+
log.Printf("failed to create container: %v", err)
159+
return
160+
}
161+
defer func() {
162+
if err := testcontainers.TerminateContainer(socatContainer); err != nil {
163+
log.Printf("failed to terminate container: %s", err)
164+
}
165+
}()
166+
// }
167+
168+
httpClient := http.DefaultClient
169+
170+
for _, target := range targets {
171+
baseURI := socatContainer.TargetURL(target.ExposedPort())
172+
173+
resp, err := httpClient.Get(baseURI.String() + "/ping")
174+
if err != nil {
175+
log.Printf("failed to get response: %v", err)
176+
return
177+
}
178+
defer resp.Body.Close()
179+
180+
body, err := io.ReadAll(resp.Body)
181+
if err != nil {
182+
log.Printf("failed to read body: %v", err)
183+
return
184+
}
185+
186+
fmt.Printf("%d - %s\n", resp.StatusCode, string(body))
187+
}
188+
189+
// Output:
190+
// 200 - PONG
191+
// 200 - PONG
192+
// 200 - PONG
193+
// 200 - PONG
194+
}

modules/socat/go.mod

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module github.com/testcontainers/testcontainers-go/modules/socat
2+
3+
go 1.23.0
4+
5+
toolchain go1.23.6
6+
7+
require (
8+
github.com/docker/go-connections v0.5.0
9+
github.com/stretchr/testify v1.10.0
10+
github.com/testcontainers/testcontainers-go v0.36.0
11+
)
12+
13+
require (
14+
dario.cat/mergo v1.0.1 // indirect
15+
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
16+
github.com/Microsoft/go-winio v0.6.2 // indirect
17+
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
18+
github.com/containerd/log v0.1.0 // indirect
19+
github.com/containerd/platforms v0.2.1 // indirect
20+
github.com/cpuguy83/dockercfg v0.3.2 // indirect
21+
github.com/davecgh/go-spew v1.1.1 // indirect
22+
github.com/distribution/reference v0.6.0 // indirect
23+
github.com/docker/docker v28.0.1+incompatible // indirect
24+
github.com/docker/go-units v0.5.0 // indirect
25+
github.com/ebitengine/purego v0.8.2 // indirect
26+
github.com/felixge/httpsnoop v1.0.4 // indirect
27+
github.com/go-logr/logr v1.4.2 // indirect
28+
github.com/go-logr/stdr v1.2.2 // indirect
29+
github.com/go-ole/go-ole v1.2.6 // indirect
30+
github.com/gogo/protobuf v1.3.2 // indirect
31+
github.com/google/uuid v1.6.0 // indirect
32+
github.com/klauspost/compress v1.17.4 // indirect
33+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
34+
github.com/magiconair/properties v1.8.9 // indirect
35+
github.com/moby/docker-image-spec v1.3.1 // indirect
36+
github.com/moby/patternmatcher v0.6.0 // indirect
37+
github.com/moby/sys/sequential v0.5.0 // indirect
38+
github.com/moby/sys/user v0.1.0 // indirect
39+
github.com/moby/sys/userns v0.1.0 // indirect
40+
github.com/moby/term v0.5.0 // indirect
41+
github.com/morikuni/aec v1.0.0 // indirect
42+
github.com/opencontainers/go-digest v1.0.0 // indirect
43+
github.com/opencontainers/image-spec v1.1.1 // indirect
44+
github.com/pkg/errors v0.9.1 // indirect
45+
github.com/pmezard/go-difflib v1.0.0 // indirect
46+
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
47+
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
48+
github.com/sirupsen/logrus v1.9.3 // indirect
49+
github.com/tklauser/go-sysconf v0.3.12 // indirect
50+
github.com/tklauser/numcpus v0.6.1 // indirect
51+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
52+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
53+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
54+
go.opentelemetry.io/otel v1.35.0 // indirect
55+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
56+
go.opentelemetry.io/otel/metric v1.35.0 // indirect
57+
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
58+
go.opentelemetry.io/otel/trace v1.35.0 // indirect
59+
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
60+
golang.org/x/crypto v0.31.0 // indirect
61+
golang.org/x/sys v0.31.0 // indirect
62+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
63+
google.golang.org/protobuf v1.36.5 // indirect
64+
gopkg.in/yaml.v3 v3.0.1 // indirect
65+
)
66+
67+
replace github.com/testcontainers/testcontainers-go => ../..

0 commit comments

Comments
 (0)