Skip to content

Commit fe52983

Browse files
committed
Merge branch 'main' into use-run-function
* main: fix: workaround for moby/moby#50133 when reusing container in testcontainers.GenericContainer. (testcontainers#3197) feat(kafka,redpanda): support for waiting for mapped ports without external checks (testcontainers#3165)
2 parents de88678 + 9533bca commit fe52983

File tree

9 files changed

+228
-50
lines changed

9 files changed

+228
-50
lines changed

docker.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1364,11 +1364,19 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
13641364
lifecycleHooks: []ContainerLifecycleHooks{combineContainerHooks(defaultHooks, req.LifecycleHooks)},
13651365
}
13661366

1367+
// Workaround for https://github.com/moby/moby/issues/50133.
1368+
// /containers/{id}/json API endpoint of Docker Engine takes data about container from master (not replica) database
1369+
// which is synchronized with container state after call of /containers/{id}/stop API endpoint.
1370+
dcState, err := dc.State(ctx)
1371+
if err != nil {
1372+
return nil, fmt.Errorf("docker container state: %w", err)
1373+
}
1374+
13671375
// If a container was stopped programmatically, we want to ensure the container
13681376
// is running again, but only if it is not paused, as it's not possible to start
13691377
// a paused container. The Docker Engine returns the "cannot start a paused container,
13701378
// try unpause instead" error.
1371-
switch c.State {
1379+
switch dcState.Status {
13721380
case "running":
13731381
// cannot re-start a running container, but we still need
13741382
// to call the startup hooks.

docs/features/wait/host_port.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,42 @@ req := ContainerRequest{
6060
WaitingFor: wait.ForExposedPort().SkipInternalCheck(),
6161
}
6262
```
63+
64+
## Skipping the external check
65+
66+
_Testcontainers for Go_ checks if the container is listening to the port externally (outside of container,
67+
from the host where _Testcontainers for Go_ is used) before returning the control to the caller.
68+
69+
But there are cases where this external check is not needed.
70+
In this case, the `wait.ForListeningPort.SkipExternalCheck` can be used to skip the external check.
71+
72+
```golang
73+
req := ContainerRequest{
74+
Image: "nginx:alpine",
75+
// Do not check port 80 externally, check it internally only
76+
WaitingFor: wait.ForListeningPort("80/tcp").SkipExternalCheck(),
77+
}
78+
```
79+
80+
If there is a need to wait only for completion of container port mapping (which doesn't happen immediately after container is started),
81+
then both internal and external checks can be skipped:
82+
83+
```golang
84+
req := ContainerRequest{
85+
Image: "nginx:alpine",
86+
ExposedPorts: []string{"80/tcp"},
87+
// Wait only for completion of port 80 mapping (from container runtime perspective), do not connect to 80 port
88+
WaitingFor: wait.ForListeningPort("80/tcp").SkipInternalCheck().SkipExternalCheck(),
89+
}
90+
```
91+
92+
Alternatively, `wait.ForMappedPort` can be used:
93+
94+
```golang
95+
req := ContainerRequest{
96+
Image: "nginx:alpine",
97+
ExposedPorts: []string{"80/tcp"},
98+
// Wait only for completion of port 80 mapping (from container runtime perspective), do not connect to 80 port
99+
WaitingFor: wait.ForMappedPort("80/tcp"),
100+
}
101+
```

modules/kafka/kafka.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,9 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
121121

122122
// copyStarterScript copies the starter script into the container.
123123
func copyStarterScript(ctx context.Context, c testcontainers.Container) error {
124-
if err := wait.ForListeningPort(publicPort).
125-
SkipInternalCheck().
124+
if err := wait.ForMappedPort(publicPort).
126125
WaitUntilReady(ctx, c); err != nil {
127-
return fmt.Errorf("wait for exposed port: %w", err)
126+
return fmt.Errorf("wait for mapped port: %w", err)
128127
}
129128

130129
host, err := c.Host(ctx)

modules/redpanda/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ require (
4848
github.com/klauspost/compress v1.18.0 // indirect
4949
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
5050
github.com/magiconair/properties v1.8.10 // indirect
51-
github.com/mdelapenya/tlscert v0.1.0
51+
github.com/mdelapenya/tlscert v0.2.0
5252
github.com/moby/patternmatcher v0.6.0 // indirect
5353
github.com/moby/sys/sequential v0.6.0 // indirect
5454
github.com/moby/sys/user v0.4.0 // indirect

modules/redpanda/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
6363
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
6464
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
6565
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
66-
github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM=
67-
github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64=
66+
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
67+
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
6868
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
6969
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
7070
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=

modules/redpanda/redpanda.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"fmt"
1111
"math"
1212
"net/http"
13-
"path/filepath"
13+
"path"
1414
"strings"
1515
"text/template"
1616
"time"
@@ -38,7 +38,6 @@ const (
3838
defaultKafkaAPIPort = "9092/tcp"
3939
defaultAdminAPIPort = "9644/tcp"
4040
defaultSchemaRegistryPort = "8081/tcp"
41-
defaultDockerKafkaAPIPort = "29092"
4241

4342
redpandaDir = "/etc/redpanda"
4443
entrypointFile = "/entrypoint-tc.sh"
@@ -71,11 +70,12 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
7170
testcontainers.WithCmd("redpanda", "start", "--mode=dev-container", "--smp=1", "--memory=1G"),
7271
testcontainers.WithExposedPorts(defaultKafkaAPIPort, defaultAdminAPIPort, defaultSchemaRegistryPort),
7372
testcontainers.WithWaitStrategy(wait.ForAll(
74-
// Wait for the ports to be exposed only as the container needs configuration
75-
// before it will bind to the ports and be ready to serve requests.
76-
wait.ForListeningPort(defaultKafkaAPIPort).SkipInternalCheck(),
77-
wait.ForListeningPort(defaultAdminAPIPort).SkipInternalCheck(),
78-
wait.ForListeningPort(defaultSchemaRegistryPort).SkipInternalCheck(),
73+
// Wait for the ports to be mapped without accessing them,
74+
// because container needs Redpanda configuration before Redpanda is started
75+
// and the mapped ports are part of that configuration.
76+
wait.ForMappedPort(defaultKafkaAPIPort),
77+
wait.ForMappedPort(defaultAdminAPIPort),
78+
wait.ForMappedPort(defaultSchemaRegistryPort),
7979
)),
8080
testcontainers.WithConfigModifier(func(c *container.Config) {
8181
c.User = "root:root"
@@ -141,7 +141,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
141141
},
142142
testcontainers.ContainerFile{
143143
Reader: bytes.NewReader(bootstrapConfig),
144-
ContainerFilePath: filepath.Join(redpandaDir, bootstrapConfigFile),
144+
ContainerFilePath: path.Join(redpandaDir, bootstrapConfigFile),
145145
FileMode: 600,
146146
},
147147
))
@@ -151,12 +151,12 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
151151
moduleOpts = append(moduleOpts, testcontainers.WithFiles(
152152
testcontainers.ContainerFile{
153153
Reader: bytes.NewReader(settings.cert),
154-
ContainerFilePath: filepath.Join(redpandaDir, certFile),
154+
ContainerFilePath: path.Join(redpandaDir, certFile),
155155
FileMode: 600,
156156
},
157157
testcontainers.ContainerFile{
158158
Reader: bytes.NewReader(settings.key),
159-
ContainerFilePath: filepath.Join(redpandaDir, keyFile),
159+
ContainerFilePath: path.Join(redpandaDir, keyFile),
160160
FileMode: 600,
161161
},
162162
))
@@ -189,7 +189,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
189189
return c, err
190190
}
191191

192-
err = ctr.CopyToContainer(ctx, nodeConfig, filepath.Join(redpandaDir, "redpanda.yaml"), 0o600)
192+
err = ctr.CopyToContainer(ctx, nodeConfig, path.Join(redpandaDir, "redpanda.yaml"), 0o600)
193193
if err != nil {
194194
return c, fmt.Errorf("copy to container: %w", err)
195195
}

modules/redpanda/redpanda_test.go

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ import (
2020
"github.com/twmb/franz-go/pkg/sasl/scram"
2121

2222
"github.com/testcontainers/testcontainers-go"
23+
"github.com/testcontainers/testcontainers-go/log"
2324
"github.com/testcontainers/testcontainers-go/modules/redpanda"
2425
"github.com/testcontainers/testcontainers-go/network"
2526
)
2627

28+
const testImage = "docker.redpanda.com/redpandadata/redpanda:v23.3.3"
29+
2730
func TestRedpanda(t *testing.T) {
2831
ctx := context.Background()
2932

30-
ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3")
33+
ctr, err := redpanda.Run(ctx, testImage)
3134
testcontainers.CleanupContainer(t, ctr)
3235
require.NoError(t, err)
3336

@@ -78,7 +81,7 @@ func TestRedpandaWithAuthentication(t *testing.T) {
7881
ctx := context.Background()
7982
// redpandaCreateContainer {
8083
ctr, err := redpanda.Run(ctx,
81-
"docker.redpanda.com/redpandadata/redpanda:v23.3.3",
84+
testImage,
8285
redpanda.WithEnableSASL(),
8386
redpanda.WithEnableKafkaAuthorization(),
8487
redpanda.WithEnableWasmTransform(),
@@ -192,7 +195,7 @@ func TestRedpandaWithAuthentication(t *testing.T) {
192195
func TestRedpandaWithBootstrapUserAuthentication(t *testing.T) {
193196
ctx := context.Background()
194197
ctr, err := redpanda.Run(ctx,
195-
"docker.redpanda.com/redpandadata/redpanda:v23.3.3",
198+
testImage,
196199
redpanda.WithEnableSASL(),
197200
redpanda.WithEnableKafkaAuthorization(),
198201
redpanda.WithEnableWasmTransform(),
@@ -427,7 +430,7 @@ func TestRedpandaWithOldVersionAndWasm(t *testing.T) {
427430
func TestRedpandaProduceWithAutoCreateTopics(t *testing.T) {
428431
ctx := context.Background()
429432

430-
ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithAutoCreateTopics())
433+
ctr, err := redpanda.Run(ctx, testImage, redpanda.WithAutoCreateTopics())
431434
testcontainers.CleanupContainer(t, ctr)
432435
require.NoError(t, err)
433436

@@ -446,17 +449,17 @@ func TestRedpandaProduceWithAutoCreateTopics(t *testing.T) {
446449
}
447450

448451
func TestRedpandaWithTLS(t *testing.T) {
449-
tmp := t.TempDir()
450-
cert := tlscert.SelfSignedFromRequest(tlscert.Request{
451-
Name: "client",
452-
Host: "localhost,127.0.0.1",
453-
ParentDir: tmp,
454-
})
455-
require.NotNil(t, cert, "failed to generate cert")
456-
457452
ctx := context.Background()
458453

459-
ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithTLS(cert.Bytes, cert.KeyBytes))
454+
containerHostAddress, err := containerHost(ctx)
455+
require.NoError(t, err)
456+
cert, err := tlscert.SelfSignedFromRequestE(tlscert.Request{
457+
Name: "client",
458+
Host: "localhost,127.0.0.1," + containerHostAddress,
459+
})
460+
require.NoError(t, err, "failed to generate cert")
461+
462+
ctr, err := redpanda.Run(ctx, testImage, redpanda.WithTLS(cert.Bytes, cert.KeyBytes))
460463
testcontainers.CleanupContainer(t, ctr)
461464
require.NoError(t, err)
462465

@@ -509,19 +512,18 @@ func TestRedpandaWithTLS(t *testing.T) {
509512
}
510513

511514
func TestRedpandaWithTLSAndSASL(t *testing.T) {
512-
tmp := t.TempDir()
515+
ctx := context.Background()
513516

514-
cert := tlscert.SelfSignedFromRequest(tlscert.Request{
515-
Name: "client",
516-
Host: "localhost,127.0.0.1",
517-
ParentDir: tmp,
517+
containerHostAddress, err := containerHost(ctx)
518+
require.NoError(t, err)
519+
cert, err := tlscert.SelfSignedFromRequestE(tlscert.Request{
520+
Name: "client",
521+
Host: "localhost,127.0.0.1," + containerHostAddress,
518522
})
519-
require.NotNil(t, cert, "failed to generate cert")
520-
521-
ctx := context.Background()
523+
require.NoError(t, err, "failed to generate cert")
522524

523525
ctr, err := redpanda.Run(ctx,
524-
"docker.redpanda.com/redpandadata/redpanda:v23.3.3",
526+
testImage,
525527
redpanda.WithTLS(cert.Bytes, cert.KeyBytes),
526528
redpanda.WithEnableSASL(),
527529
redpanda.WithEnableKafkaAuthorization(),
@@ -688,3 +690,29 @@ func TestRedpandaBootstrapConfig(t *testing.T) {
688690
require.False(t, needsRestart)
689691
}
690692
}
693+
694+
func containerHost(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (string, error) {
695+
// Use a dummy request to get the provider from options.
696+
var req testcontainers.GenericContainerRequest
697+
for _, opt := range opts {
698+
if err := opt.Customize(&req); err != nil {
699+
return "", err
700+
}
701+
}
702+
703+
logging := req.Logger
704+
if logging == nil {
705+
logging = log.Default()
706+
}
707+
p, err := req.ProviderType.GetProvider(testcontainers.WithLogger(logging))
708+
if err != nil {
709+
return "", err
710+
}
711+
712+
if p, ok := p.(*testcontainers.DockerProvider); ok {
713+
return p.DaemonHost(ctx)
714+
}
715+
716+
// Fall back to localhost.
717+
return "localhost", nil
718+
}

wait/host_port.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ type HostPortStrategy struct {
4242
// a shell is not available in the container or when the container doesn't bind
4343
// the port internally until additional conditions are met.
4444
skipInternalCheck bool
45+
46+
// skipExternalCheck is a flag to skip the external check, which, if used with
47+
// skipInternalCheck, makes strategy waiting only for port mapping completion
48+
// without accessing port.
49+
skipExternalCheck bool
4550
}
4651

4752
// NewHostPortStrategy constructs a default host port strategy that waits for the given
@@ -70,6 +75,12 @@ func ForExposedPort() *HostPortStrategy {
7075
return NewHostPortStrategy("")
7176
}
7277

78+
// ForMappedPort returns a host port strategy that waits for the given port
79+
// to be mapped without accessing the port itself.
80+
func ForMappedPort(port nat.Port) *HostPortStrategy {
81+
return NewHostPortStrategy(port).SkipInternalCheck().SkipExternalCheck()
82+
}
83+
7384
// SkipInternalCheck changes the host port strategy to skip the internal check,
7485
// which is useful when a shell is not available in the container or when the
7586
// container doesn't bind the port internally until additional conditions are met.
@@ -79,6 +90,15 @@ func (hp *HostPortStrategy) SkipInternalCheck() *HostPortStrategy {
7990
return hp
8091
}
8192

93+
// SkipExternalCheck changes the host port strategy to skip the external check,
94+
// which, if used with SkipInternalCheck, makes strategy waiting only for port
95+
// mapping completion without accessing port.
96+
func (hp *HostPortStrategy) SkipExternalCheck() *HostPortStrategy {
97+
hp.skipExternalCheck = true
98+
99+
return hp
100+
}
101+
82102
// WithStartupTimeout can be used to change the default startup timeout
83103
func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *HostPortStrategy {
84104
hp.timeout = &startupTimeout
@@ -124,16 +144,12 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
124144
ctx, cancel := context.WithTimeout(ctx, timeout)
125145
defer cancel()
126146

127-
ipAddress, err := target.Host(ctx)
128-
if err != nil {
129-
return err
130-
}
131-
132147
waitInterval := hp.PollInterval
133148

134149
internalPort := hp.Port
135150
i := 0
136151
if internalPort == "" {
152+
var err error
137153
// Port is not specified, so we need to detect it.
138154
internalPort, err = hp.detectInternalPort(ctx, target)
139155
if err != nil {
@@ -157,8 +173,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
157173
}
158174
}
159175

160-
var port nat.Port
161-
port, err = target.MappedPort(ctx, internalPort)
176+
port, err := target.MappedPort(ctx, internalPort)
162177
i = 0
163178

164179
for port == "" {
@@ -178,8 +193,15 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
178193
}
179194
}
180195

181-
if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil {
182-
return fmt.Errorf("external check: %w", err)
196+
if !hp.skipExternalCheck {
197+
ipAddress, err := target.Host(ctx)
198+
if err != nil {
199+
return fmt.Errorf("host: %w", err)
200+
}
201+
202+
if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil {
203+
return fmt.Errorf("external check: %w", err)
204+
}
183205
}
184206

185207
if hp.skipInternalCheck {

0 commit comments

Comments
 (0)