@@ -21,6 +21,7 @@ import (
21
21
"path/filepath"
22
22
"strconv"
23
23
"strings"
24
+ "sync"
24
25
"testing"
25
26
"time"
26
27
)
@@ -56,6 +57,7 @@ func (g *TestLogConsumer) Accept(l testcontainers.Log) {
56
57
}
57
58
58
59
type OctopusContainerTest struct {
60
+ mu sync.Mutex
59
61
}
60
62
61
63
func (o * OctopusContainerTest ) enableContainerLogging (container testcontainers.Container , ctx context.Context ) error {
@@ -231,6 +233,55 @@ func (o *OctopusContainerTest) setupOctopus(ctx context.Context, connString stri
231
233
return & OctopusContainer {Container : container , URI : uri }, nil
232
234
}
233
235
236
+ // createDockerInfrastructure attemptes to create the complete Docker stack containing a
237
+ // network, MSSQL container, and Octopus container. The return values include as much of
238
+ // the partial stack as possible in the case of an error.
239
+ func (o * OctopusContainerTest ) createDockerInfrastructure (t * testing.T , ctx context.Context ) (testcontainers.Network , * OctopusContainer , * mysqlContainer , error ) {
240
+
241
+ network , networkName , err := o .setupNetwork (ctx )
242
+ if err != nil {
243
+ return nil , nil , nil , err
244
+ }
245
+
246
+ sqlServer , err := o .setupDatabase (ctx , networkName )
247
+ if err != nil {
248
+ return network , nil , sqlServer , err
249
+ }
250
+
251
+ sqlIp , err := sqlServer .Container .ContainerIP (ctx )
252
+ if err != nil {
253
+ return network , nil , sqlServer , err
254
+ }
255
+
256
+ sqlName , err := sqlServer .Container .Name (ctx )
257
+ if err != nil {
258
+ return network , nil , sqlServer , err
259
+ }
260
+
261
+ t .Log ("SQL Server IP: " + sqlIp )
262
+ t .Log ("SQL Server Container Name: " + sqlName )
263
+
264
+ octopusContainer , err := o .setupOctopus (ctx , "Server=" + sqlIp + ",1433;Database=OctopusDeploy;User=sa;Password=Password01!" , networkName , t )
265
+ if err != nil {
266
+ return network , octopusContainer , sqlServer , err
267
+ }
268
+
269
+ octoIp , err := octopusContainer .Container .ContainerIP (ctx )
270
+ if err != nil {
271
+ return network , octopusContainer , sqlServer , err
272
+ }
273
+
274
+ octoName , err := octopusContainer .Container .Name (ctx )
275
+ if err != nil {
276
+ return network , octopusContainer , sqlServer , err
277
+ }
278
+
279
+ t .Log ("Octopus IP: " + octoIp )
280
+ t .Log ("Octopus Container Name: " + octoName )
281
+
282
+ return network , octopusContainer , sqlServer , nil
283
+ }
284
+
234
285
// ArrangeTest is wrapper that initialises Octopus, runs a test, and cleans up the containers
235
286
func (o * OctopusContainerTest ) ArrangeTest (t * testing.T , testFunc func (t * testing.T , container * OctopusContainer , client * client.Client ) error ) {
236
287
err := retry .Do (
@@ -242,93 +293,77 @@ func (o *OctopusContainerTest) ArrangeTest(t *testing.T, testFunc func(t *testin
242
293
243
294
ctx := context .Background ()
244
295
245
- network , networkName , err := o .setupNetwork (ctx )
246
- if err != nil {
247
- return err
248
- }
249
-
250
- sqlServer , err := o .setupDatabase (ctx , networkName )
251
- if err != nil {
252
- return err
253
- }
296
+ // I don't think test containers are thread safe - parallel tests
297
+ // frequently show that multiple tests access the same containers.
298
+ // So only one thread can create a stack at a time
299
+ o .mu .Lock ()
300
+ network , octopusContainer , sqlServer , err := o .createDockerInfrastructure (t , ctx )
301
+ o .mu .Unlock ()
254
302
255
- sqlIp , err := sqlServer .Container .ContainerIP (ctx )
256
- if err != nil {
257
- return err
258
- }
303
+ // Attempt to clean up whatever resources were created.
304
+ // Don't return errors for the cleanup, just report them
305
+ defer func () {
306
+ o .mu .Lock ()
307
+ defer o .mu .Unlock ()
259
308
260
- sqlName , err := sqlServer .Container .Name (ctx )
261
- if err != nil {
262
- return err
263
- }
309
+ stopTime := 1 * time .Minute
264
310
265
- t .Log ("SQL Server IP: " + sqlIp )
266
- t .Log ("SQL Server Container Name: " + sqlName )
311
+ if octopusContainer != nil {
312
+ // This fixes the "can not get logs from container which is dead or marked for removal" error
313
+ // See https://github.com/testcontainers/testcontainers-go/issues/606
314
+ if os .Getenv ("OCTODISABLEOCTOCONTAINERLOGGING" ) != "true" {
315
+ stopProducerErr := octopusContainer .StopLogProducer ()
267
316
268
- octopusContainer , err := o .setupOctopus (ctx , "Server=" + sqlIp + ",1433;Database=OctopusDeploy;User=sa;Password=Password01!" , networkName , t )
269
- if err != nil {
270
- return err
271
- }
317
+ // try to continue on if there was an error stopping the producer
318
+ if stopProducerErr != nil {
319
+ t .Log (stopProducerErr )
320
+ }
321
+ }
272
322
273
- octoIp , err := octopusContainer .Container .ContainerIP (ctx )
274
- if err != nil {
275
- return err
276
- }
323
+ // Stop the containers
324
+ octoStopErr := octopusContainer .Stop (ctx , & stopTime )
277
325
278
- octoName , err := octopusContainer .Container .Name (ctx )
279
- if err != nil {
280
- return err
281
- }
326
+ if octoStopErr != nil {
327
+ t .Log ("Failed to stop the Octopus container" )
328
+ }
282
329
283
- t .Log ("Octopus IP: " + octoIp )
284
- t .Log ("Octopus Container Name: " + octoName )
330
+ octoTerminateErr := octopusContainer .Terminate (ctx )
285
331
286
- // Clean up the container after the test is complete
287
- defer func () {
288
- // This fixes the "can not get logs from container which is dead or marked for removal" error
289
- // See https://github.com/testcontainers/testcontainers-go/issues/606
290
- if os .Getenv ("OCTODISABLEOCTOCONTAINERLOGGING" ) != "true" {
291
- stopProducerErr := octopusContainer .StopLogProducer ()
292
-
293
- // try to continue on if there was an error stopping the producer
294
- if stopProducerErr != nil {
295
- t .Log (stopProducerErr )
332
+ if octoTerminateErr != nil {
333
+ t .Log ("Failed to terminate the Octopus container" )
296
334
}
297
335
}
298
336
299
- // Stop the containers
300
- stopTime := 1 * time .Minute
301
- octoStopErr := octopusContainer .Stop (ctx , & stopTime )
337
+ if sqlServer != nil {
338
+ sqlStopErr := sqlServer .Stop (ctx , & stopTime )
302
339
303
- if octoStopErr != nil {
304
- t .Log ("Failed to stop the Octopus container" )
305
- }
340
+ if sqlStopErr != nil {
341
+ t .Log ("Failed to stop the MSSQL container" )
342
+ }
306
343
307
- sqlStopErr := sqlServer .Stop (ctx , & stopTime )
344
+ sqlTerminateErr := sqlServer .Terminate (ctx )
308
345
309
- if sqlStopErr != nil {
310
- t .Log ("Failed to stop the MSSQL container" )
346
+ if sqlTerminateErr != nil {
347
+ t .Log ("Failed to terminate the MSSQL container" )
348
+ }
311
349
}
312
350
313
351
// Terminate the containers
314
- octoTerminateErr := octopusContainer .Terminate (ctx )
315
- sqlTerminateErr := sqlServer .Terminate (ctx )
316
-
317
- networkErr := network .Remove (ctx )
352
+ if network != nil {
353
+ networkErr := network .Remove (ctx )
318
354
319
- if octoTerminateErr != nil || sqlTerminateErr != nil {
320
- t .Fatalf ("failed to terminate container: %v %v" , octoTerminateErr , sqlTerminateErr )
321
- }
322
-
323
- if networkErr != nil {
324
- t .Fatalf ("failed to remove network: %v" , networkErr )
355
+ if networkErr != nil {
356
+ t .Log ("failed to remove network: %v" , networkErr )
357
+ }
325
358
}
326
-
327
- // I've noticed some race conditions where it appears a terminated container is reused in the
328
- // retry loop. We give the Docker resources a chance to clean up before continuing.
329
- time .Sleep (30 * time .Second )
330
359
}()
331
360
361
+ // In the event of a failed stack creation, use the defer function above
362
+ // to clean up and then return the error
363
+ if err != nil {
364
+ return err
365
+ }
366
+
332
367
// give the server 5 minutes to start up
333
368
err = lintwait .WaitForResource (func () error {
334
369
resp , err := http .Get (octopusContainer .URI + "/api" )
0 commit comments