Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Revision history for testcontainer-hs

## 0.5.2.0 -- Unreleased

* Introduce `withCopyFileToContainer` to copy local files to the container (@LaurentRDC, https://github.com/testcontainers/testcontainers-hs/pull/62)

## 0.5.1.0 -- 2025-01-14

* Introduce `withWorkingDirectory` to set the working directory inside a container (@alexbiehl, https://github.com/testcontainers/testcontainers-hs/pull/37)
Expand Down
1 change: 1 addition & 0 deletions src/TestContainers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module TestContainers
M.setMemory,
M.setCpus,
M.withWorkingDirectory,
M.withCopyFileToContainer,
M.withNetwork,
M.withNetworkAlias,
M.setLink,
Expand Down
77 changes: 55 additions & 22 deletions src/TestContainers/Docker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
setRm,
setEnv,
withWorkingDirectory,
withCopyFileToContainer,
withNetwork,
withNetworkAlias,
setLink,
Expand Down Expand Up @@ -160,7 +161,7 @@

import Control.Concurrent (threadDelay)
import Control.Exception (IOException, throw)
import Control.Monad (forM_, replicateM, unless)
import Control.Monad (forM_, replicateM, unless, void)
import Control.Monad.Catch
( Exception,
MonadCatch,
Expand Down Expand Up @@ -290,7 +291,8 @@
labels :: [(Text, Text)],
noReaper :: Bool,
followLogs :: Maybe LogConsumer,
workDirectory :: Maybe Text
workDirectory :: Maybe Text,
copyFilesToContainer :: [(FilePath, FilePath)]
}

instance WithoutReaper ContainerRequest where
Expand Down Expand Up @@ -326,7 +328,8 @@
labels = mempty,
noReaper = False,
followLogs = Nothing,
workDirectory = Nothing
workDirectory = Nothing,
copyFilesToContainer = mempty
}

-- | Set the name of a Docker container. This is equivalent to invoking @docker run@
Expand Down Expand Up @@ -417,6 +420,27 @@
withWorkingDirectory workdir request =
request {workDirectory = Just workdir}

-- | Copies a file from the host to the container. Call this function
-- multiple times to copy multiple files to the container.
--
-- This can be used, for example, to initialize a database:
--
-- >>> :{
-- containerRequest (fromTag "postgres:16-alpine")
-- & withCopyFileToContainer "my-init-script.sql" "/docker-entrypoint-initdb.d/"
-- :}
--
-- @since 0.5.2.0
withCopyFileToContainer ::
-- | File on the host
FilePath ->
-- | Directory in the container
FilePath ->
ContainerRequest ->
ContainerRequest
withCopyFileToContainer fileFromHost containerDirectory request =
request {copyFilesToContainer = copyFilesToContainer request <> [(fileFromHost, containerDirectory)]}

-- | Set the network the container will connect to. This is equivalent to passing
-- @--network network_name@ to @docker run@.
--
Expand Down Expand Up @@ -558,7 +582,8 @@
labels,
noReaper,
followLogs,
workDirectory
workDirectory,
copyFilesToContainer
} = request

config@Config {configTracer, configCreateReaper} <-
Expand All @@ -580,35 +605,43 @@
Just . (prefix <>) . ("-" <>) . pack
<$> replicateM 6 (Random.randomRIO ('a', 'z'))

let dockerRun :: [Text]
dockerRun =
-- Instead of using `docker run`, we use the more manual `docker create` + `docker start`.
-- This allows to get the container ID early from `docker create`, and thus
-- optionally copy files using `docker cp`.
let dockerCreate :: [Text]
dockerCreate =
concat $
[["run"]]
++ [["--detach"]]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the --detach flag isn't supported by docker start, because this is the default behavior

++ [["--name", containerName] | Just containerName <- [name]]
++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]
[["create"]]
++ [["--cpus", value] | Just value <- [cpus]]
++ [["--env", variable <> "=" <> value] | (variable, value) <- env]
++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]
++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]
++ [["--link", container] | container <- links]
++ [["--memory", value] | Just value <- [memory]]
++ [["--name", containerName] | Just containerName <- [name]]
++ [["--network", networkName] | Just (Right networkName) <- [network]]
++ [["--network", networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
++ [["--network-alias", alias] | Just alias <- [networkAlias]]
++ [["--link", container] | container <- links]
++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]
++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]
++ [["--rm"] | rmOnExit]
++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]
++ [["--workdir", workdir] | Just workdir <- [workDirectory]]
++ [["--memory", value] | Just value <- [memory]]
++ [["--cpus", value] | Just value <- [cpus]]
++ [[tag]]
++ [command | Just command <- [cmd]]

stdout <- docker configTracer dockerRun
(id :: ContainerId) <- strip . pack <$> docker configTracer dockerCreate

forM_ copyFilesToContainer $ \(hostFile, containerFile) ->
docker configTracer ["cp", pack hostFile, id <> ":" <> pack containerFile]

let dockerStart :: [Text]
dockerStart =
concat $
[["start"]]
++ [[id]]
++ [command | Just command <- [cmd]]

let id :: ContainerId
!id =
-- N.B. Force to not leak STDOUT String
strip (pack stdout)
void $ docker configTracer dockerStart

-- Careful, this is really meant to be lazy
let -- Careful, this is really meant to be lazy
~inspectOutput =
unsafePerformIO $
internalInspect configTracer id
Expand Down Expand Up @@ -989,7 +1022,7 @@

let resolve endpointHost endpointPort = do
let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream}
head <$> Socket.getAddrInfo (Just hints) (Just endpointHost) (Just (show endpointPort))

Check warning on line 1025 in src/TestContainers/Docker.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.8.2

In the use of ‘head’

open addr = do
socket <-
Expand Down
6 changes: 6 additions & 0 deletions test/TestContainers/TastySpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import TestContainers.Tasty
waitUntilMappedPortReachable,
waitUntilTimeout,
withContainers,
withCopyFileToContainer,
withFollowLogs,
withNetwork,
(&),
Expand Down Expand Up @@ -78,6 +79,11 @@ containers1 = do
& setWaitingFor
(waitForHttp "16686/tcp" "/" [200])

_postgres <-
run $
containerRequest (fromTag "postgres:16-alpine")
& withCopyFileToContainer "test/data/init-script.sql" "/docker-entrypoint-initdb.d/"

_helloWorld <-
run $
containerRequest (fromTag "hello-world:latest")
Expand Down
6 changes: 6 additions & 0 deletions test/data/init-script.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

create table customers (
id bigint not null,
name varchar not null,
primary key (id)
);
1 change: 1 addition & 0 deletions testcontainers.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ build-type: Simple
extra-source-files:
CHANGELOG.md
README.md
test/data/init-script.sql

tested-with:
GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.2 || ==9.8.2
Expand Down
Loading