Skip to content

Commit 66f17a7

Browse files
stevenhmdelapenya
andauthored
fix(compose): container initialisation (#2844)
Fix compose to fully initialise the containers it returns. This ensures that running things like checks for running behave as expected. Extracts the functionality to connect to reaper into a helper method so its consistent across uses. Fix data race in daemonHost function converting it to a method to make use of encapsulation. Fix container and network requests so they use sessionID from labels if available so that user specified values are respected. Export the functionality to create a container from a ContainerList response via provider.ContainerFromType. Enforce no bare returns instead of no named returns as that was the original intention. Fixes #2667 Co-authored-by: Manuel de la Peña <[email protected]>
1 parent 032a69f commit 66f17a7

File tree

9 files changed

+154
-99
lines changed

9 files changed

+154
-99
lines changed

.golangci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ linters:
77
- gofumpt
88
- misspell
99
- nolintlint
10-
- nonamedreturns
10+
- nakedret
1111
- testifylint
1212
- thelper
1313

1414
linters-settings:
15+
nakedret:
16+
max-func-lines: 0
1517
errorlint:
1618
# Check whether fmt.Errorf uses the %w verb for formatting errors.
1719
# See the https://github.com/polyfloyd/go-errorlint for caveats.

container.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ type ContainerRequest struct {
167167
LogConsumerCfg *LogConsumerConfig // define the configuration for the log producer and its log consumers to follow the logs
168168
}
169169

170+
// sessionID returns the session ID for the container request.
171+
func (c *ContainerRequest) sessionID() string {
172+
if sessionID := c.Labels[core.LabelSessionID]; sessionID != "" {
173+
return sessionID
174+
}
175+
176+
return core.SessionID()
177+
}
178+
170179
// containerOptions functional options for a container
171180
type containerOptions struct {
172181
ImageName string

docker.go

Lines changed: 89 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"os"
1616
"path/filepath"
1717
"regexp"
18-
"strings"
1918
"sync"
2019
"time"
2120

@@ -889,6 +888,32 @@ func (c *DockerContainer) GetLogProductionErrorChannel() <-chan error {
889888
return errCh
890889
}
891890

891+
// connectReaper connects the reaper to the container if it is needed.
892+
func (c *DockerContainer) connectReaper(ctx context.Context) error {
893+
if c.provider.config.RyukDisabled || isReaperImage(c.Image) {
894+
// Reaper is disabled or we are the reaper container.
895+
return nil
896+
}
897+
898+
reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, c.provider.host), core.SessionID(), c.provider)
899+
if err != nil {
900+
return fmt.Errorf("reaper: %w", err)
901+
}
902+
903+
if c.terminationSignal, err = reaper.Connect(); err != nil {
904+
return fmt.Errorf("reaper connect: %w", err)
905+
}
906+
907+
return nil
908+
}
909+
910+
// cleanupTermSignal triggers the termination signal if it was created and an error occurred.
911+
func (c *DockerContainer) cleanupTermSignal(err error) {
912+
if c.terminationSignal != nil && err != nil {
913+
c.terminationSignal <- true
914+
}
915+
}
916+
892917
// DockerNetwork represents a network started using Docker
893918
type DockerNetwork struct {
894919
ID string // Network ID from Docker
@@ -1035,28 +1060,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
10351060
req.Labels = make(map[string]string)
10361061
}
10371062

1038-
var termSignal chan bool
1039-
// the reaper does not need to start a reaper for itself
1040-
isReaperContainer := strings.HasSuffix(imageName, config.ReaperDefaultImage)
1041-
if !p.config.RyukDisabled && !isReaperContainer {
1042-
r, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), core.SessionID(), p)
1043-
if err != nil {
1044-
return nil, fmt.Errorf("reaper: %w", err)
1045-
}
1046-
1047-
termSignal, err := r.Connect()
1048-
if err != nil {
1049-
return nil, fmt.Errorf("reaper connect: %w", err)
1050-
}
1051-
1052-
// Cleanup on error.
1053-
defer func() {
1054-
if err != nil {
1055-
termSignal <- true
1056-
}
1057-
}()
1058-
}
1059-
10601063
if err = req.Validate(); err != nil {
10611064
return nil, err
10621065
}
@@ -1120,7 +1123,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
11201123
}
11211124
}
11221125

1123-
if !isReaperContainer {
1126+
if !isReaperImage(imageName) {
11241127
// Add the labels that identify this as a testcontainers container and
11251128
// allow the reaper to terminate it if requested.
11261129
AddGenericLabels(req.Labels)
@@ -1198,26 +1201,35 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
11981201
}
11991202
}
12001203

1201-
c := &DockerContainer{
1202-
ID: resp.ID,
1203-
WaitingFor: req.WaitingFor,
1204-
Image: imageName,
1205-
imageWasBuilt: req.ShouldBuildImage(),
1206-
keepBuiltImage: req.ShouldKeepBuiltImage(),
1207-
sessionID: core.SessionID(),
1208-
exposedPorts: req.ExposedPorts,
1209-
provider: p,
1210-
terminationSignal: termSignal,
1211-
logger: p.Logger,
1212-
lifecycleHooks: req.LifecycleHooks,
1204+
// This should match the fields set in ContainerFromDockerResponse.
1205+
ctr := &DockerContainer{
1206+
ID: resp.ID,
1207+
WaitingFor: req.WaitingFor,
1208+
Image: imageName,
1209+
imageWasBuilt: req.ShouldBuildImage(),
1210+
keepBuiltImage: req.ShouldKeepBuiltImage(),
1211+
sessionID: req.sessionID(),
1212+
exposedPorts: req.ExposedPorts,
1213+
provider: p,
1214+
logger: p.Logger,
1215+
lifecycleHooks: req.LifecycleHooks,
12131216
}
12141217

1215-
err = c.createdHook(ctx)
1216-
if err != nil {
1217-
return nil, err
1218+
if err = ctr.connectReaper(ctx); err != nil {
1219+
return ctr, err // No wrap as it would stutter.
12181220
}
12191221

1220-
return c, nil
1222+
// Wrapped so the returned error is passed to the cleanup function.
1223+
defer func(ctr *DockerContainer) {
1224+
ctr.cleanupTermSignal(err)
1225+
}(ctr)
1226+
1227+
if err = ctr.createdHook(ctx); err != nil {
1228+
// Return the container to allow caller to clean up.
1229+
return ctr, fmt.Errorf("created hook: %w", err)
1230+
}
1231+
1232+
return ctr, nil
12211233
}
12221234

12231235
func (p *DockerProvider) findContainerByName(ctx context.Context, name string) (*types.Container, error) {
@@ -1229,7 +1241,7 @@ func (p *DockerProvider) findContainerByName(ctx context.Context, name string) (
12291241
filter := filters.NewArgs(filters.Arg("name", fmt.Sprintf("^%s$", name)))
12301242
containers, err := p.client.ContainerList(ctx, container.ListOptions{Filters: filter})
12311243
if err != nil {
1232-
return nil, err
1244+
return nil, fmt.Errorf("container list: %w", err)
12331245
}
12341246
defer p.Close()
12351247

@@ -1284,7 +1296,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
12841296
}
12851297
}
12861298

1287-
sessionID := core.SessionID()
1299+
sessionID := req.sessionID()
12881300

12891301
var termSignal chan bool
12901302
if !p.config.RyukDisabled {
@@ -1425,10 +1437,13 @@ func (p *DockerProvider) Config() TestcontainersConfig {
14251437
// Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel
14261438
// You can use the "TESTCONTAINERS_HOST_OVERRIDE" env variable to set this yourself
14271439
func (p *DockerProvider) DaemonHost(ctx context.Context) (string, error) {
1428-
return daemonHost(ctx, p)
1440+
p.mtx.Lock()
1441+
defer p.mtx.Unlock()
1442+
1443+
return p.daemonHostLocked(ctx)
14291444
}
14301445

1431-
func daemonHost(ctx context.Context, p *DockerProvider) (string, error) {
1446+
func (p *DockerProvider) daemonHostLocked(ctx context.Context) (string, error) {
14321447
if p.hostCache != "" {
14331448
return p.hostCache, nil
14341449
}
@@ -1492,7 +1507,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
14921507
IPAM: req.IPAM,
14931508
}
14941509

1495-
sessionID := core.SessionID()
1510+
sessionID := req.sessionID()
14961511

14971512
var termSignal chan bool
14981513
if !p.config.RyukDisabled {
@@ -1617,45 +1632,50 @@ func (p *DockerProvider) ensureDefaultNetwork(ctx context.Context) (string, erro
16171632
return p.defaultNetwork, nil
16181633
}
16191634

1620-
// containerFromDockerResponse builds a Docker container struct from the response of the Docker API
1621-
func containerFromDockerResponse(ctx context.Context, response types.Container) (*DockerContainer, error) {
1622-
provider, err := NewDockerProvider()
1623-
if err != nil {
1624-
return nil, err
1635+
// ContainerFromType builds a Docker container struct from the response of the Docker API
1636+
func (p *DockerProvider) ContainerFromType(ctx context.Context, response types.Container) (ctr *DockerContainer, err error) {
1637+
exposedPorts := make([]string, len(response.Ports))
1638+
for i, port := range response.Ports {
1639+
exposedPorts[i] = fmt.Sprintf("%d/%s", port.PublicPort, port.Type)
1640+
}
1641+
1642+
// This should match the fields set in CreateContainer.
1643+
ctr = &DockerContainer{
1644+
ID: response.ID,
1645+
Image: response.Image,
1646+
imageWasBuilt: false,
1647+
sessionID: response.Labels[core.LabelSessionID],
1648+
isRunning: response.State == "running",
1649+
exposedPorts: exposedPorts,
1650+
provider: p,
1651+
logger: p.Logger,
1652+
lifecycleHooks: []ContainerLifecycleHooks{
1653+
DefaultLoggingHook(p.Logger),
1654+
},
16251655
}
16261656

1627-
ctr := DockerContainer{}
1628-
1629-
ctr.ID = response.ID
1630-
ctr.WaitingFor = nil
1631-
ctr.Image = response.Image
1632-
ctr.imageWasBuilt = false
1633-
1634-
ctr.logger = provider.Logger
1635-
ctr.lifecycleHooks = []ContainerLifecycleHooks{
1636-
DefaultLoggingHook(ctr.logger),
1657+
if err = ctr.connectReaper(ctx); err != nil {
1658+
return nil, err
16371659
}
1638-
ctr.provider = provider
1639-
1640-
ctr.sessionID = core.SessionID()
1641-
ctr.consumers = []LogConsumer{}
1642-
ctr.isRunning = response.State == "running"
16431660

1644-
// the termination signal should be obtained from the reaper
1645-
ctr.terminationSignal = nil
1661+
// Wrapped so the returned error is passed to the cleanup function.
1662+
defer func(ctr *DockerContainer) {
1663+
ctr.cleanupTermSignal(err)
1664+
}(ctr)
16461665

16471666
// populate the raw representation of the container
16481667
jsonRaw, err := ctr.inspectRawContainer(ctx)
16491668
if err != nil {
1650-
return nil, fmt.Errorf("inspect raw container: %w", err)
1669+
// Return the container to allow caller to clean up.
1670+
return ctr, fmt.Errorf("inspect raw container: %w", err)
16511671
}
16521672

16531673
// the health status of the container, if any
16541674
if health := jsonRaw.State.Health; health != nil {
16551675
ctr.healthStatus = health.Status
16561676
}
16571677

1658-
return &ctr, nil
1678+
return ctr, nil
16591679
}
16601680

16611681
// ListImages list images from the provider. If an image has multiple Tags, each tag is reported

generic_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,14 @@ func TestGenericReusableContainerInSubprocess(t *testing.T) {
156156
require.NoError(t, err)
157157
require.Len(t, ctrs, 1)
158158

159-
nginxC, err := containerFromDockerResponse(context.Background(), ctrs[0])
159+
provider, err := NewDockerProvider()
160160
require.NoError(t, err)
161161

162+
provider.SetClient(cli)
163+
164+
nginxC, err := provider.ContainerFromType(context.Background(), ctrs[0])
162165
CleanupContainer(t, nginxC)
166+
require.NoError(t, err)
163167
}
164168

165169
func createReuseContainerInSubprocess(t *testing.T) string {

modules/compose/compose.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,27 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*dockerCompose, error) {
153153
return nil, fmt.Errorf("initialize docker client: %w", err)
154154
}
155155

156+
provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(composeOptions.Logger))
157+
if err != nil {
158+
return nil, fmt.Errorf("new docker provider: %w", err)
159+
}
160+
161+
dockerClient := dockerCli.Client()
162+
provider.SetClient(dockerClient)
163+
156164
composeAPI := &dockerCompose{
157165
name: composeOptions.Identifier,
158166
configs: composeOptions.Paths,
159167
temporaryConfigs: composeOptions.temporaryPaths,
160168
logger: composeOptions.Logger,
161169
projectProfiles: composeOptions.Profiles,
162170
composeService: compose.NewComposeService(dockerCli),
163-
dockerClient: dockerCli.Client(),
171+
dockerClient: dockerClient,
164172
waitStrategies: make(map[string]wait.Strategy),
165173
containers: make(map[string]*testcontainers.DockerContainer),
166174
networks: make(map[string]*testcontainers.DockerNetwork),
167175
sessionID: testcontainers.SessionID(),
176+
provider: provider,
168177
}
169178

170179
return composeAPI, nil

modules/compose/compose_api.go

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ type dockerCompose struct {
229229

230230
// sessionID is used to identify the reaper session
231231
sessionID string
232+
233+
// provider is used to docker operations.
234+
provider *testcontainers.DockerProvider
232235
}
233236

234237
func (d *dockerCompose) ServiceContainer(ctx context.Context, svcName string) (*testcontainers.DockerContainer, error) {
@@ -325,17 +328,12 @@ func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) (err erro
325328
return err
326329
}
327330

328-
provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(d.logger))
329-
if err != nil {
330-
return fmt.Errorf("new docker provider: %w", err)
331-
}
332-
333331
var termSignals []chan bool
334332
var reaper *testcontainers.Reaper
335-
if !provider.Config().Config.RyukDisabled {
333+
if !d.provider.Config().Config.RyukDisabled {
336334
// NewReaper is deprecated: we need to find a way to create the reaper for compose
337335
// bypassing the deprecation.
338-
reaper, err = testcontainers.NewReaper(ctx, testcontainers.SessionID(), provider, "")
336+
reaper, err = testcontainers.NewReaper(ctx, testcontainers.SessionID(), d.provider, "")
339337
if err != nil {
340338
return fmt.Errorf("create reaper: %w", err)
341339
}
@@ -492,26 +490,11 @@ func (d *dockerCompose) lookupContainer(ctx context.Context, svcName string) (*t
492490
return nil, fmt.Errorf("no container found for service name %s", svcName)
493491
}
494492

495-
containerInstance := containers[0]
496-
// TODO: Fix as this is only setting a subset of the fields
497-
// and the container is not fully initialized, for example
498-
// the isRunning flag is not set.
499-
// See: https://github.com/testcontainers/testcontainers-go/issues/2667
500-
ctr := &testcontainers.DockerContainer{
501-
ID: containerInstance.ID,
502-
Image: containerInstance.Image,
503-
}
504-
ctr.SetLogger(d.logger)
505-
506-
dockerProvider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(d.logger))
493+
ctr, err := d.provider.ContainerFromType(ctx, containers[0])
507494
if err != nil {
508-
return nil, fmt.Errorf("new docker provider: %w", err)
495+
return nil, fmt.Errorf("container from type: %w", err)
509496
}
510497

511-
dockerProvider.SetClient(d.dockerClient)
512-
513-
ctr.SetProvider(dockerProvider)
514-
515498
d.containersLock.Lock()
516499
defer d.containersLock.Unlock()
517500
d.containers[svcName] = ctr

modules/compose/compose_api_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ func TestDockerComposeAPI(t *testing.T) {
3333
err = compose.Up(ctx, Wait(true))
3434
cleanup(t, compose)
3535
require.NoError(t, err, "compose.Up()")
36+
37+
for _, service := range compose.Services() {
38+
container, err := compose.ServiceContainer(context.Background(), service)
39+
require.NoError(t, err, "compose.ServiceContainer()")
40+
require.True(t, container.IsRunning())
41+
}
3642
}
3743

3844
func TestDockerComposeAPIStrategyForInvalidService(t *testing.T) {

0 commit comments

Comments
 (0)