@@ -89,6 +89,7 @@ module TestContainers.Docker
8989 setRm ,
9090 setEnv ,
9191 withWorkingDirectory ,
92+ withCopyFileToContainer ,
9293 withNetwork ,
9394 withNetworkAlias ,
9495 setLink ,
@@ -160,7 +161,7 @@ where
160161
161162import Control.Concurrent (threadDelay )
162163import Control.Exception (IOException , throw )
163- import Control.Monad (forM_ , replicateM , unless )
164+ import Control.Monad (forM_ , replicateM , unless , void )
164165import Control.Monad.Catch
165166 ( Exception ,
166167 MonadCatch ,
@@ -290,7 +291,8 @@ data ContainerRequest = ContainerRequest
290291 labels :: [(Text , Text )],
291292 noReaper :: Bool ,
292293 followLogs :: Maybe LogConsumer ,
293- workDirectory :: Maybe Text
294+ workDirectory :: Maybe Text ,
295+ copyFilesToContainer :: [(FilePath , FilePath )]
294296 }
295297
296298instance WithoutReaper ContainerRequest where
@@ -326,7 +328,8 @@ containerRequest image =
326328 labels = mempty ,
327329 noReaper = False ,
328330 followLogs = Nothing ,
329- workDirectory = Nothing
331+ workDirectory = Nothing ,
332+ copyFilesToContainer = mempty
330333 }
331334
332335-- | Set the name of a Docker container. This is equivalent to invoking @docker run@
@@ -417,6 +420,27 @@ withWorkingDirectory :: Text -> ContainerRequest -> ContainerRequest
417420withWorkingDirectory workdir request =
418421 request {workDirectory = Just workdir}
419422
423+ -- | Copies a file from the host to the container. Call this function
424+ -- multiple times to copy multiple files to the container.
425+ --
426+ -- This can be used, for example, to initialize a database:
427+ --
428+ -- >>> :{
429+ -- containerRequest (fromTag "postgres:16-alpine")
430+ -- & withCopyFileToContainer "my-init-script.sql" "/docker-entrypoint-initdb.d/"
431+ -- :}
432+ --
433+ -- @since 0.5.2.0
434+ withCopyFileToContainer ::
435+ -- | File on the host
436+ FilePath ->
437+ -- | Directory in the container
438+ FilePath ->
439+ ContainerRequest ->
440+ ContainerRequest
441+ withCopyFileToContainer fileFromHost containerDirectory request =
442+ request {copyFilesToContainer = copyFilesToContainer request <> [(fileFromHost, containerDirectory)]}
443+
420444-- | Set the network the container will connect to. This is equivalent to passing
421445-- @--network network_name@ to @docker run@.
422446--
@@ -558,7 +582,8 @@ run request = do
558582 labels,
559583 noReaper,
560584 followLogs,
561- workDirectory
585+ workDirectory,
586+ copyFilesToContainer
562587 } = request
563588
564589 config@ Config {configTracer, configCreateReaper} <-
@@ -580,35 +605,43 @@ run request = do
580605 Just . (prefix <> ) . (" -" <> ) . pack
581606 <$> replicateM 6 (Random. randomRIO (' a' , ' z' ))
582607
583- let dockerRun :: [Text ]
584- dockerRun =
608+ -- Instead of using `docker run`, we use the more manual `docker create` + `docker start`.
609+ -- This allows to get the container ID early from `docker create`, and thus
610+ -- optionally copy files using `docker cp`.
611+ let dockerCreate :: [Text ]
612+ dockerCreate =
585613 concat $
586- [[" run" ]]
587- ++ [[" --detach" ]]
588- ++ [[" --name" , containerName] | Just containerName <- [name]]
589- ++ [[" --label" , label <> " =" <> value] | (label, value) <- additionalLabels ++ labels]
614+ [[" create" ]]
615+ ++ [[" --cpus" , value] | Just value <- [cpus]]
590616 ++ [[" --env" , variable <> " =" <> value] | (variable, value) <- env]
591- ++ [[" --publish" , pack (show port) <> " /" <> protocol] | Port {port, protocol} <- exposedPorts]
617+ ++ [[" --label" , label <> " =" <> value] | (label, value) <- additionalLabels ++ labels]
618+ ++ [[" --link" , container] | container <- links]
619+ ++ [[" --memory" , value] | Just value <- [memory]]
620+ ++ [[" --name" , containerName] | Just containerName <- [name]]
592621 ++ [[" --network" , networkName] | Just (Right networkName) <- [network]]
593622 ++ [[" --network" , networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
594623 ++ [[" --network-alias" , alias] | Just alias <- [networkAlias]]
595- ++ [[" --link" , container] | container <- links]
596- ++ [[" --volume" , src <> " :" <> dest] | (src, dest) <- volumeMounts]
624+ ++ [[" --publish" , pack (show port) <> " /" <> protocol] | Port {port, protocol} <- exposedPorts]
597625 ++ [[" --rm" ] | rmOnExit]
626+ ++ [[" --volume" , src <> " :" <> dest] | (src, dest) <- volumeMounts]
598627 ++ [[" --workdir" , workdir] | Just workdir <- [workDirectory]]
599- ++ [[" --memory" , value] | Just value <- [memory]]
600- ++ [[" --cpus" , value] | Just value <- [cpus]]
601628 ++ [[tag]]
602- ++ [command | Just command <- [cmd]]
603629
604- stdout <- docker configTracer dockerRun
630+ (id :: ContainerId ) <- strip . pack <$> docker configTracer dockerCreate
631+
632+ forM_ copyFilesToContainer $ \ (hostFile, containerFile) ->
633+ docker configTracer [" cp" , pack hostFile, id <> " :" <> pack containerFile]
634+
635+ let dockerStart :: [Text ]
636+ dockerStart =
637+ concat $
638+ [[" start" ]]
639+ ++ [[id ]]
640+ ++ [command | Just command <- [cmd]]
605641
606- let id :: ContainerId
607- ! id =
608- -- N.B. Force to not leak STDOUT String
609- strip (pack stdout)
642+ void $ docker configTracer dockerStart
610643
611- -- Careful, this is really meant to be lazy
644+ let -- Careful, this is really meant to be lazy
612645 ~ inspectOutput =
613646 unsafePerformIO $
614647 internalInspect configTracer id
0 commit comments