@@ -15,7 +15,6 @@ import (
15
15
"os"
16
16
"path/filepath"
17
17
"regexp"
18
- "strings"
19
18
"sync"
20
19
"time"
21
20
@@ -889,6 +888,32 @@ func (c *DockerContainer) GetLogProductionErrorChannel() <-chan error {
889
888
return errCh
890
889
}
891
890
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
+
892
917
// DockerNetwork represents a network started using Docker
893
918
type DockerNetwork struct {
894
919
ID string // Network ID from Docker
@@ -1035,28 +1060,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
1035
1060
req .Labels = make (map [string ]string )
1036
1061
}
1037
1062
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
-
1060
1063
if err = req .Validate (); err != nil {
1061
1064
return nil , err
1062
1065
}
@@ -1120,7 +1123,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
1120
1123
}
1121
1124
}
1122
1125
1123
- if ! isReaperContainer {
1126
+ if ! isReaperImage ( imageName ) {
1124
1127
// Add the labels that identify this as a testcontainers container and
1125
1128
// allow the reaper to terminate it if requested.
1126
1129
AddGenericLabels (req .Labels )
@@ -1198,26 +1201,35 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
1198
1201
}
1199
1202
}
1200
1203
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 ,
1213
1216
}
1214
1217
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.
1218
1220
}
1219
1221
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
1221
1233
}
1222
1234
1223
1235
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) (
1229
1241
filter := filters .NewArgs (filters .Arg ("name" , fmt .Sprintf ("^%s$" , name )))
1230
1242
containers , err := p .client .ContainerList (ctx , container.ListOptions {Filters : filter })
1231
1243
if err != nil {
1232
- return nil , err
1244
+ return nil , fmt . Errorf ( "container list: %w" , err )
1233
1245
}
1234
1246
defer p .Close ()
1235
1247
@@ -1284,7 +1296,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
1284
1296
}
1285
1297
}
1286
1298
1287
- sessionID := core . SessionID ()
1299
+ sessionID := req . sessionID ()
1288
1300
1289
1301
var termSignal chan bool
1290
1302
if ! p .config .RyukDisabled {
@@ -1425,10 +1437,13 @@ func (p *DockerProvider) Config() TestcontainersConfig {
1425
1437
// Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel
1426
1438
// You can use the "TESTCONTAINERS_HOST_OVERRIDE" env variable to set this yourself
1427
1439
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 )
1429
1444
}
1430
1445
1431
- func daemonHost ( ctx context.Context , p * DockerProvider ) (string , error ) {
1446
+ func ( p * DockerProvider ) daemonHostLocked ( ctx context.Context ) (string , error ) {
1432
1447
if p .hostCache != "" {
1433
1448
return p .hostCache , nil
1434
1449
}
@@ -1492,7 +1507,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
1492
1507
IPAM : req .IPAM ,
1493
1508
}
1494
1509
1495
- sessionID := core . SessionID ()
1510
+ sessionID := req . sessionID ()
1496
1511
1497
1512
var termSignal chan bool
1498
1513
if ! p .config .RyukDisabled {
@@ -1617,45 +1632,50 @@ func (p *DockerProvider) ensureDefaultNetwork(ctx context.Context) (string, erro
1617
1632
return p .defaultNetwork , nil
1618
1633
}
1619
1634
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
+ },
1625
1655
}
1626
1656
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
1637
1659
}
1638
- ctr .provider = provider
1639
-
1640
- ctr .sessionID = core .SessionID ()
1641
- ctr .consumers = []LogConsumer {}
1642
- ctr .isRunning = response .State == "running"
1643
1660
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 )
1646
1665
1647
1666
// populate the raw representation of the container
1648
1667
jsonRaw , err := ctr .inspectRawContainer (ctx )
1649
1668
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 )
1651
1671
}
1652
1672
1653
1673
// the health status of the container, if any
1654
1674
if health := jsonRaw .State .Health ; health != nil {
1655
1675
ctr .healthStatus = health .Status
1656
1676
}
1657
1677
1658
- return & ctr , nil
1678
+ return ctr , nil
1659
1679
}
1660
1680
1661
1681
// ListImages list images from the provider. If an image has multiple Tags, each tag is reported
0 commit comments