Skip to content

Commit e64665a

Browse files
LaurentRDCalexbiehl
authored andcommitted
Add withCopyFileToContainer to copy files to containers before running them
1 parent 820179e commit e64665a

6 files changed

Lines changed: 73 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Revision history for testcontainer-hs
22

3+
## 0.5.2.0 -- Unreleased
4+
5+
* Introduce `withCopyFileToContainer` to copy local files to the container (@LaurentRDC, https://github.com/testcontainers/testcontainers-hs/pull/62)
6+
37
## 0.5.1.0 -- 2025-01-14
48

59
* Introduce `withWorkingDirectory` to set the working directory inside a container (@alexbiehl, https://github.com/testcontainers/testcontainers-hs/pull/37)

src/TestContainers.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module TestContainers
3131
M.setMemory,
3232
M.setCpus,
3333
M.withWorkingDirectory,
34+
M.withCopyFileToContainer,
3435
M.withNetwork,
3536
M.withNetworkAlias,
3637
M.setLink,

src/TestContainers/Docker.hs

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

161162
import Control.Concurrent (threadDelay)
162163
import Control.Exception (IOException, throw)
163-
import Control.Monad (forM_, replicateM, unless)
164+
import Control.Monad (forM_, replicateM, unless, void)
164165
import 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

296298
instance 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
417420
withWorkingDirectory 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

test/TestContainers/TastySpec.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import TestContainers.Tasty
2727
waitUntilMappedPortReachable,
2828
waitUntilTimeout,
2929
withContainers,
30+
withCopyFileToContainer,
3031
withFollowLogs,
3132
withNetwork,
3233
(&),
@@ -78,6 +79,11 @@ containers1 = do
7879
& setWaitingFor
7980
(waitForHttp "16686/tcp" "/" [200])
8081

82+
_postgres <-
83+
run $
84+
containerRequest (fromTag "postgres:16-alpine")
85+
& withCopyFileToContainer "test/data/init-script.sql" "/docker-entrypoint-initdb.d/"
86+
8187
_helloWorld <-
8288
run $
8389
containerRequest (fromTag "hello-world:latest")

test/data/init-script.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
create table customers (
3+
id bigint not null,
4+
name varchar not null,
5+
primary key (id)
6+
);

testcontainers.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ build-type: Simple
1818
extra-source-files:
1919
CHANGELOG.md
2020
README.md
21+
test/data/init-script.sql
2122

2223
tested-with:
2324
GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.2 || ==9.8.2

0 commit comments

Comments
 (0)