88{-# LANGUAGE NamedFieldPuns #-}
99{-# LANGUAGE OverloadedStrings #-}
1010{-# LANGUAGE RankNTypes #-}
11+ {-# LANGUAGE RecordWildCards #-}
1112{-# LANGUAGE ScopedTypeVariables #-}
1213{-# LANGUAGE StandaloneDeriving #-}
1314{-# LANGUAGE TypeFamilies #-}
@@ -94,6 +95,7 @@ module TestContainers.Docker
9495 setLink ,
9596 setExpose ,
9697 setWaitingFor ,
98+ withDependencies ,
9799 run ,
98100
99101 -- * Following logs
@@ -158,7 +160,8 @@ module TestContainers.Docker
158160where
159161
160162import Control.Concurrent (threadDelay )
161- import Control.Exception (IOException , throw )
163+ import qualified Control.Concurrent.Async
164+ import Control.Exception (IOException , evaluate , throw )
162165import Control.Monad (forM_ , replicateM , unless )
163166import Control.Monad.Catch
164167 ( Exception ,
@@ -169,11 +172,12 @@ import Control.Monad.Catch
169172 try ,
170173 )
171174import Control.Monad.IO.Class (MonadIO (liftIO ))
172- import Control.Monad.IO.Unlift (MonadUnliftIO (withRunInIO ))
175+ import Control.Monad.IO.Unlift (MonadUnliftIO (withRunInIO ), askRunInIO )
173176import Control.Monad.Reader (MonadReader (.. ))
174177import Control.Monad.Trans.Resource
175178 ( ReleaseKey ,
176179 ResIO ,
180+ allocate ,
177181 register ,
178182 runResourceT ,
179183 )
@@ -209,12 +213,13 @@ import Optics.Optic ((%), (<&>))
209213import System.Directory (doesFileExist )
210214import System.Environment (lookupEnv )
211215import System.IO (Handle , hClose )
212- import System.IO.Unsafe (unsafePerformIO )
216+ import System.IO.Unsafe (unsafeInterleaveIO , unsafePerformIO )
213217import qualified System.Process as Process
214218import qualified System.Random as Random
215219import System.Timeout (timeout )
216220import TestContainers.Config
217221 ( Config (.. ),
222+ RunStrategy (.. ),
218223 defaultDockerConfig ,
219224 determineConfig ,
220225 )
@@ -263,6 +268,7 @@ import TestContainers.Docker.State
263268import TestContainers.Monad
264269 ( MonadDocker ,
265270 TestContainer ,
271+ defer ,
266272 )
267273import TestContainers.Trace (Trace (.. ), Tracer , newTracer , withTrace )
268274import Prelude hiding (error , id )
@@ -288,7 +294,8 @@ data ContainerRequest = ContainerRequest
288294 labels :: [(Text , Text )],
289295 noReaper :: Bool ,
290296 followLogs :: Maybe LogConsumer ,
291- workDirectory :: Maybe Text
297+ workDirectory :: Maybe Text ,
298+ dependencies :: [Container ]
292299 }
293300
294301instance WithoutReaper ContainerRequest where
@@ -324,7 +331,8 @@ containerRequest image =
324331 labels = mempty ,
325332 noReaper = False ,
326333 followLogs = Nothing ,
327- workDirectory = Nothing
334+ workDirectory = Nothing ,
335+ dependencies = []
328336 }
329337
330338-- | Set the name of a Docker container. This is equivalent to invoking @docker run@
@@ -453,6 +461,13 @@ withFollowLogs :: LogConsumer -> ContainerRequest -> ContainerRequest
453461withFollowLogs logConsumer request =
454462 request {followLogs = Just logConsumer}
455463
464+ -- |
465+ --
466+ -- @since x.x.x
467+ withDependencies :: [Container ] -> ContainerRequest -> ContainerRequest
468+ withDependencies dependencies request =
469+ request {dependencies}
470+
456471-- | Defintion of a 'Port'. Allows for specifying ports using various protocols. Due to the
457472-- 'Num' and 'IsString' instance allows for convenient Haskell literals.
458473--
@@ -556,7 +571,8 @@ run request = do
556571 labels,
557572 noReaper,
558573 followLogs,
559- workDirectory
574+ workDirectory,
575+ dependencies
560576 } = request
561577
562578 config@ Config {configTracer, configCreateReaper} <-
@@ -568,8 +584,6 @@ run request = do
568584 pure []
569585 else reaperLabels <$> configCreateReaper
570586
571- image@ Image {tag} <- runToImage toImage
572-
573587 name <-
574588 case naming of
575589 RandomName -> return Nothing
@@ -578,59 +592,110 @@ run request = do
578592 Just . (prefix <> ) . (" -" <> ) . pack
579593 <$> replicateM 6 (Random. randomRIO (' a' , ' z' ))
580594
581- let dockerRun :: [Text ]
582- dockerRun =
583- concat $
584- [[" run" ]]
585- ++ [[" --detach" ]]
586- ++ [[" --name" , containerName] | Just containerName <- [name]]
587- ++ [[" --label" , label <> " =" <> value] | (label, value) <- additionalLabels ++ labels]
588- ++ [[" --env" , variable <> " =" <> value] | (variable, value) <- env]
589- ++ [[" --publish" , pack (show port) <> " /" <> protocol] | Port {port, protocol} <- exposedPorts]
590- ++ [[" --network" , networkName] | Just (Right networkName) <- [network]]
591- ++ [[" --network" , networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
592- ++ [[" --network-alias" , alias] | Just alias <- [networkAlias]]
593- ++ [[" --link" , container] | container <- links]
594- ++ [[" --volume" , src <> " :" <> dest] | (src, dest) <- volumeMounts]
595- ++ [[" --rm" ] | rmOnExit]
596- ++ [[" --workdir" , workdir] | Just workdir <- [workDirectory]]
597- ++ [[" --memory" , value] | Just value <- [memory]]
598- ++ [[" --cpus" , value] | Just value <- [cpus]]
599- ++ [[tag]]
600- ++ [command | Just command <- [cmd]]
601-
602- stdout <- docker configTracer dockerRun
603-
604- let id :: ContainerId
605- ! id =
606- -- N.B. Force to not leak STDOUT String
607- strip (pack stdout)
608-
609- -- Careful, this is really meant to be lazy
610- ~ inspectOutput =
611- unsafePerformIO $
612- internalInspect configTracer id
595+ (actuallyRunDocker, waitOnContainer) <- applyRunStrategy $ do
596+ image@ Image {tag} <- runToImage toImage
613597
614- -- We don't issue 'ReleaseKeys' for cleanup anymore. Ryuk takes care of cleanup
615- -- for us once the session has been closed.
616- releaseKey <- register (pure () )
598+ liftIO $
599+ forM_ dependencies $
600+ \ Container {wait} -> wait
601+
602+ let dockerRun :: [Text ]
603+ dockerRun =
604+ concat $
605+ [[" run" ]]
606+ ++ [[" --detach" ]]
607+ ++ [[" --name" , containerName] | Just containerName <- [name]]
608+ ++ [[" --label" , label <> " =" <> value] | (label, value) <- additionalLabels ++ labels]
609+ ++ [[" --env" , variable <> " =" <> value] | (variable, value) <- env]
610+ ++ [[" --publish" , pack (show port) <> " /" <> protocol] | Port {port, protocol} <- exposedPorts]
611+ ++ [[" --network" , networkName] | Just (Right networkName) <- [network]]
612+ ++ [[" --network" , networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
613+ ++ [[" --network-alias" , alias] | Just alias <- [networkAlias]]
614+ ++ [[" --link" , container] | container <- links]
615+ ++ [[" --volume" , src <> " :" <> dest] | (src, dest) <- volumeMounts]
616+ ++ [[" --rm" ] | rmOnExit]
617+ ++ [[" --workdir" , workdir] | Just workdir <- [workDirectory]]
618+ ++ [[" --memory" , value] | Just value <- [memory]]
619+ ++ [[" --cpus" , value] | Just value <- [cpus]]
620+ ++ [[tag]]
621+ ++ [command | Just command <- [cmd]]
622+
623+ stdout <- docker configTracer dockerRun
624+
625+ let id :: ContainerId
626+ ! id =
627+ -- N.B. Force to not leak STDOUT String
628+ strip (pack stdout)
629+
630+ -- Careful, this is really meant to be lazy
631+ ~ inspectOutput =
632+ unsafePerformIO $
633+ internalInspect configTracer id
634+
635+ -- We don't issue 'ReleaseKeys' for cleanup anymore. Ryuk takes care of cleanup
636+ -- for us once the session has been closed.
637+ releaseKey <- register (pure () )
638+
639+ forM_ followLogs $
640+ dockerFollowLogs configTracer id
641+
642+ let container =
643+ Container
644+ { id ,
645+ releaseKey,
646+ image,
647+ inspectOutput,
648+ config,
649+ wait = pure ()
650+ }
651+
652+ -- Last but not least, execute the WaitUntilReady checks
653+ waitUntilReady container readiness
654+
655+ pure container
656+
657+ -- We want to be very gentle and not force the container if possible so that we can
658+ -- install the wait action.
659+ ~ container <-
660+ actuallyRunDocker
661+
662+ pure $
663+ let ~ Container {.. } = container
664+ in Container
665+ { wait = waitOnContainer,
666+ ..
667+ }
617668
618- forM_ followLogs $
619- dockerFollowLogs configTracer id
669+ applyRunStrategy ::
670+ TestContainer a ->
671+ TestContainer (TestContainer a , IO () )
672+ applyRunStrategy action = do
673+ Config {configTracer, configRunStrategy} <-
674+ ask
675+ case configRunStrategy of
676+ SequentialRunStrategy ->
677+ pure (action, pure () )
678+ ConcurrentRunStrategy _limit -> do
679+ runInIO <-
680+ askRunInIO
620681
621- let container =
622- Container
623- { id ,
624- releaseKey,
625- image,
626- inspectOutput,
627- config
628- }
682+ (_releaseKey, handle) <-
683+ allocate
684+ (Control.Concurrent.Async. async (runInIO action))
685+ Control.Concurrent.Async. cancel
686+
687+ let returnAction =
688+ liftIO $ unsafeInterleaveIO $ do
689+ Control.Concurrent.Async. wait handle
690+
691+ waitAction = do
692+ withTrace configTracer TraceWaitOnDependency
693+ _ <- Control.Concurrent.Async. wait handle
694+ pure ()
629695
630- -- Last but not least, execute the WaitUntilReady checks
631- waitUntilReady container readiness
696+ defer waitAction
632697
633- pure container
698+ pure (returnAction, waitAction)
634699
635700-- | Sets up a Ryuk 'Reaper'.
636701--
@@ -1109,7 +1174,9 @@ data Container = Container
11091174 -- | Configuration used to create and run this container.
11101175 config :: Config ,
11111176 -- | Memoized output of `docker inspect`. This is being calculated lazily.
1112- inspectOutput :: InspectOutput
1177+ inspectOutput :: InspectOutput ,
1178+ -- | Wait on the container as a dependency.
1179+ wait :: IO ()
11131180 }
11141181
11151182-- | Returns the id of the container.
0 commit comments