Skip to content

Commit d72fc00

Browse files
authored
feat(kafka,redpanda): support for waiting for mapped ports without external checks (testcontainers#3165)
* fix(kafka): waiting for mapped port without trying to connect to it (which is not possible due to Kafka is not started at that point of time) in container post start hook which prepares configuration for Kafka (fix for testcontainers#2748). Signed-off-by: Marat Abrarov <[email protected]> * fix(redpanda): waiting for mapped ports without trying to connect to them (which is not possible due to Redpanda is not started at that point of time) in container post start hook before preparing configuration for Redpanda which requires completion of port mapping (fix for testcontainers#2748). Signed-off-by: Marat Abrarov <[email protected]> * fix(redpanda): UNIX path for target file when copying files into container with Redpanda (which is Linux based) to run correctly when host OS is Windows. Signed-off-by: Marat Abrarov <[email protected]> * fix(redpanda): support of remote Docker Engine in tests utilizing TLS. Signed-off-by: Marat Abrarov <[email protected]> * chore(redpanda): unused const removed. Signed-off-by: Marat Abrarov <[email protected]> * docs(wait): skipping external check when waiting for listening port, waiting for port mapping completion. Signed-off-by: Marat Abrarov <[email protected]> --------- Signed-off-by: Marat Abrarov <[email protected]>
1 parent 4244b04 commit d72fc00

File tree

8 files changed

+219
-49
lines changed

8 files changed

+219
-49
lines changed

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
@@ -123,10 +123,9 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
123123

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

132131
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"
@@ -87,11 +86,12 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
8786
"--memory=1G",
8887
},
8988
WaitingFor: wait.ForAll(
90-
// Wait for the ports to be exposed only as the container needs configuration
91-
// before it will bind to the ports and be ready to serve requests.
92-
wait.ForListeningPort(defaultKafkaAPIPort).SkipInternalCheck(),
93-
wait.ForListeningPort(defaultAdminAPIPort).SkipInternalCheck(),
94-
wait.ForListeningPort(defaultSchemaRegistryPort).SkipInternalCheck(),
89+
// Wait for the ports to be mapped without accessing them,
90+
// because container needs Redpanda configuration before Redpanda is started
91+
// and the mapped ports are part of that configuration.
92+
wait.ForMappedPort(defaultKafkaAPIPort),
93+
wait.ForMappedPort(defaultAdminAPIPort),
94+
wait.ForMappedPort(defaultSchemaRegistryPort),
9595
),
9696
},
9797
Started: true,
@@ -158,7 +158,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
158158
},
159159
testcontainers.ContainerFile{
160160
Reader: bytes.NewReader(bootstrapConfig),
161-
ContainerFilePath: filepath.Join(redpandaDir, bootstrapConfigFile),
161+
ContainerFilePath: path.Join(redpandaDir, bootstrapConfigFile),
162162
FileMode: 600,
163163
},
164164
)
@@ -168,12 +168,12 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
168168
req.Files = append(req.Files,
169169
testcontainers.ContainerFile{
170170
Reader: bytes.NewReader(settings.cert),
171-
ContainerFilePath: filepath.Join(redpandaDir, certFile),
171+
ContainerFilePath: path.Join(redpandaDir, certFile),
172172
FileMode: 600,
173173
},
174174
testcontainers.ContainerFile{
175175
Reader: bytes.NewReader(settings.key),
176-
ContainerFilePath: filepath.Join(redpandaDir, keyFile),
176+
ContainerFilePath: path.Join(redpandaDir, keyFile),
177177
FileMode: 600,
178178
},
179179
)
@@ -206,7 +206,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
206206
return c, err
207207
}
208208

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

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(),
@@ -698,3 +700,29 @@ func TestRedpandaBootstrapConfig(t *testing.T) {
698700
require.False(t, needsRestart)
699701
}
700702
}
703+
704+
func containerHost(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (string, error) {
705+
// Use a dummy request to get the provider from options.
706+
var req testcontainers.GenericContainerRequest
707+
for _, opt := range opts {
708+
if err := opt.Customize(&req); err != nil {
709+
return "", err
710+
}
711+
}
712+
713+
logging := req.Logger
714+
if logging == nil {
715+
logging = log.Default()
716+
}
717+
p, err := req.ProviderType.GetProvider(testcontainers.WithLogger(logging))
718+
if err != nil {
719+
return "", err
720+
}
721+
722+
if p, ok := p.(*testcontainers.DockerProvider); ok {
723+
return p.DaemonHost(ctx)
724+
}
725+
726+
// Fall back to localhost.
727+
return "localhost", nil
728+
}

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)