Skip to content

Commit ad3f698

Browse files
committed
setup ExposeHostPorts forwards on container start
Fixes testcontainers#2811 Previously ExposedHostPorts would start an SSHD container prior to starting the testcontainer and inject a PostReadies lifecycle hook into the testcontainer in order to set up remote port forwarding from the host to the SSHD container so the testcontainer can talk to the host via the SSHD container This would be an issue if the testcontainer depends on accessing the host port on startup ( e.g., a proxy server ) as the forwarding for the host access isn't set up until all the WiatFor strategies on the testcontainer have completed. The fix is to move the forwarding setup to the PreCreates hook on the testcontainer. Since remote forwarding doesn't establish a connection to the host port until a connection is made to the remote port, this should not be an issue even if the host isn't listening yet and ensures the remote port is available to the testcontainer immediately.
1 parent 7c53667 commit ad3f698

File tree

2 files changed

+53
-75
lines changed

2 files changed

+53
-75
lines changed

port_forwarding.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) (
150150
// after the container is ready, create the SSH tunnel
151151
// for each exposed port from the host.
152152
sshdConnectHook = ContainerLifecycleHooks{
153-
PostReadies: []ContainerHook{
154-
func(ctx context.Context, c Container) error {
153+
PreCreates: []ContainerRequestHook{
154+
func(ctx context.Context, req ContainerRequest) error {
155155
return sshdContainer.exposeHostPort(ctx, req.HostAccessPorts...)
156156
},
157157
},

port_forwarding_test.go

Lines changed: 51 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import (
88
"net/http"
99
"net/http/httptest"
1010
"testing"
11-
"time"
1211

1312
"github.com/stretchr/testify/require"
1413

1514
"github.com/testcontainers/testcontainers-go"
16-
tcexec "github.com/testcontainers/testcontainers-go/exec"
1715
"github.com/testcontainers/testcontainers-go/network"
16+
"github.com/testcontainers/testcontainers-go/wait"
1817
)
1918

2019
const (
@@ -23,42 +22,59 @@ const (
2322

2423
func TestExposeHostPorts(t *testing.T) {
2524
tests := []struct {
26-
name string
27-
numberOfPorts int
28-
hasNetwork bool
29-
hasHostAccess bool
25+
name string
26+
numberOfPorts int
27+
hasNetwork bool
28+
bindOnPostStarts bool
3029
}{
3130
{
3231
name: "single port",
3332
numberOfPorts: 1,
34-
hasHostAccess: true,
3533
},
3634
{
3735
name: "single port using a network",
3836
numberOfPorts: 1,
3937
hasNetwork: true,
40-
hasHostAccess: true,
4138
},
4239
{
4340
name: "multiple ports",
4441
numberOfPorts: 3,
45-
hasHostAccess: true,
4642
},
4743
{
48-
name: "single port with cancellation",
49-
numberOfPorts: 1,
50-
hasHostAccess: false,
44+
name: "multiple ports bound on PostStarts",
45+
numberOfPorts: 3,
46+
bindOnPostStarts: true,
5147
},
5248
}
5349

5450
for _, tc := range tests {
5551
t.Run(tc.name, func(tt *testing.T) {
52+
servers := make([]*httptest.Server, tc.numberOfPorts)
5653
freePorts := make([]int, tc.numberOfPorts)
54+
waitStrategies := make([]wait.Strategy, tc.numberOfPorts)
5755
for i := range freePorts {
58-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5957
fmt.Fprint(w, expectedResponse)
6058
}))
61-
freePorts[i] = server.Listener.Addr().(*net.TCPAddr).Port
59+
60+
if !tc.bindOnPostStarts {
61+
server.Start()
62+
}
63+
64+
servers[i] = server
65+
freePort := server.Listener.Addr().(*net.TCPAddr).Port
66+
freePorts[i] = freePort
67+
waitStrategies[i] = wait.
68+
ForExec([]string{"wget", "-q", "-O", "-", fmt.Sprintf("http://%s:%d", testcontainers.HostInternal, freePort)}).
69+
WithExitCodeMatcher(func(code int) bool {
70+
return code == 0
71+
}).
72+
WithResponseMatcher(func(body io.Reader) bool {
73+
bs, err := io.ReadAll(body)
74+
require.NoError(tt, err)
75+
return string(bs) == expectedResponse
76+
})
77+
6278
tt.Cleanup(func() {
6379
server.Close()
6480
})
@@ -69,7 +85,26 @@ func TestExposeHostPorts(t *testing.T) {
6985
ContainerRequest: testcontainers.ContainerRequest{
7086
Image: "alpine:3.17",
7187
HostAccessPorts: freePorts,
72-
Cmd: []string{"top"},
88+
WaitingFor: wait.ForAll(waitStrategies...),
89+
LifecycleHooks: []testcontainers.ContainerLifecycleHooks{
90+
{
91+
PostStarts: []testcontainers.ContainerHook{
92+
func(ctx context.Context, c testcontainers.Container) error {
93+
if tc.bindOnPostStarts {
94+
for _, server := range servers {
95+
server.Start()
96+
}
97+
}
98+
99+
return nil
100+
},
101+
func(ctx context.Context, c testcontainers.Container) error {
102+
return waitStrategies[0].WaitUntilReady(ctx, c)
103+
},
104+
},
105+
},
106+
},
107+
Cmd: []string{"top"},
73108
},
74109
// }
75110
Started: true,
@@ -87,66 +122,9 @@ func TestExposeHostPorts(t *testing.T) {
87122
}
88123

89124
ctx := context.Background()
90-
if !tc.hasHostAccess {
91-
var cancel context.CancelFunc
92-
ctx, cancel = context.WithTimeout(ctx, 10*time.Second)
93-
defer cancel()
94-
}
95-
96125
c, err := testcontainers.GenericContainer(ctx, req)
97-
testcontainers.CleanupContainer(t, c)
98126
require.NoError(tt, err)
99-
100-
if tc.hasHostAccess {
101-
// create a container that has host access, which will
102-
// automatically forward the port to the container
103-
assertContainerHasHostAccess(tt, c, freePorts...)
104-
} else {
105-
// force cancellation because of timeout
106-
time.Sleep(11 * time.Second)
107-
108-
assertContainerHasNoHostAccess(tt, c, freePorts...)
109-
}
127+
_ = c.Terminate(ctx)
110128
})
111129
}
112130
}
113-
114-
func httpRequest(t *testing.T, c testcontainers.Container, port int) (int, string) {
115-
// wgetHostInternal {
116-
code, reader, err := c.Exec(
117-
context.Background(),
118-
[]string{"wget", "-q", "-O", "-", fmt.Sprintf("http://%s:%d", testcontainers.HostInternal, port)},
119-
tcexec.Multiplexed(),
120-
)
121-
// }
122-
require.NoError(t, err)
123-
124-
// read the response
125-
bs, err := io.ReadAll(reader)
126-
require.NoError(t, err)
127-
128-
return code, string(bs)
129-
}
130-
131-
func assertContainerHasHostAccess(t *testing.T, c testcontainers.Container, ports ...int) {
132-
for _, port := range ports {
133-
code, response := httpRequest(t, c, port)
134-
if code != 0 {
135-
t.Fatalf("expected status code [%d] but got [%d]", 0, code)
136-
}
137-
138-
if response != expectedResponse {
139-
t.Fatalf("expected [%s] but got [%s]", expectedResponse, response)
140-
}
141-
}
142-
}
143-
144-
func assertContainerHasNoHostAccess(t *testing.T, c testcontainers.Container, ports ...int) {
145-
for _, port := range ports {
146-
_, response := httpRequest(t, c, port)
147-
148-
if response == expectedResponse {
149-
t.Fatalf("expected not to get [%s] but got [%s]", expectedResponse, response)
150-
}
151-
}
152-
}

0 commit comments

Comments
 (0)