packages feed

testcontainers 0.5.0.0 → 0.5.1.0

raw patch · 11 files changed

+199/−59 lines, 11 filesdep ~bytestringdep ~networkdep ~tasty-discoverPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: bytestring, network, tasty-discover

API changes (from Hackage documentation)

+ TestContainers: fromExistingNetwork :: NetworkId -> TestContainer Network
+ TestContainers: fromImageId :: Text -> ToImage
+ TestContainers: setCpus :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers: setMemory :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers: withWorkingDirectory :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers: withoutReaper :: WithoutReaper request => request -> request
+ TestContainers.Docker: fromExistingNetwork :: NetworkId -> TestContainer Network
+ TestContainers.Docker: fromImageId :: Text -> ToImage
+ TestContainers.Docker: instance TestContainers.Docker.Internal.WithoutReaper TestContainers.Docker.ContainerRequest
+ TestContainers.Docker: setCpus :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers.Docker: setMemory :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers.Docker: withWorkingDirectory :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers.Docker: withoutReaper :: WithoutReaper request => request -> request
+ TestContainers.Docker.Internal: class WithoutReaper request
+ TestContainers.Docker.Internal: withoutReaper :: WithoutReaper request => request -> request
+ TestContainers.Docker.Network: fromExistingNetwork :: NetworkId -> TestContainer Network
+ TestContainers.Docker.Network: instance TestContainers.Docker.Internal.WithoutReaper TestContainers.Docker.Network.NetworkRequest
+ TestContainers.Docker.Network: withoutReaper :: WithoutReaper request => request -> request
+ TestContainers.Docker.Reaper: Reaper :: (Text -> Text -> IO ()) -> [(Text, Text)] -> Reaper
+ TestContainers.Docker.Reaper: [labels] :: Reaper -> [(Text, Text)]
+ TestContainers.Docker.Reaper: [register] :: Reaper -> Text -> Text -> IO ()
+ TestContainers.Image: fromImageId :: Text -> ToImage
+ TestContainers.Tasty: fromExistingNetwork :: NetworkId -> TestContainer Network
+ TestContainers.Tasty: fromImageId :: Text -> ToImage
+ TestContainers.Tasty: setCpus :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers.Tasty: setMemory :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers.Tasty: withWorkingDirectory :: Text -> ContainerRequest -> ContainerRequest
+ TestContainers.Tasty: withoutReaper :: WithoutReaper request => request -> request
- TestContainers.Tasty: pattern TraceDockerStdout :: () => Text -> Trace
+ TestContainers.Tasty: pattern TraceHttpCall :: () => Text -> Int -> Either String Int -> Trace

Files

CHANGELOG.md view
@@ -1,5 +1,17 @@ # Revision history for testcontainer-hs +## 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)++* Add `fromExistingNetwork` to allow connecting containers to existing, unmanaged networks (@alexbiehl, https://github.com/testcontainers/testcontainers-hs/pull/41)++* Add `setMemory` to limit the memory of a Docker container;++* Add `setCpus` to limit the CPUs limit of a docker container;++* Add `fromImageId` to get an `Image` from an image id;+ ## 0.5.0.0 -- 2023-02-20  * BREAKING: Refined lifecycle management. testcontainers is now using testcontainers/ryuk resource reaper to cleanup containers, networks and volumes. Release keys for containers are deprecated. (@alexbiehl, https://github.com/testcontainers/testcontainers-hs/pull/33)
src/TestContainers.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DuplicateRecordFields #-}+ -- | -- This module shall be used as entrypoint to the testcontainers library. It exports -- all the necessary types and functions for most common use-cases.@@ -10,12 +12,14 @@     -- * Referring to Docker images     M.ToImage,     M.fromTag,+    M.fromImageId,     M.fromBuildContext,     M.build,      -- * @docker run@ parameters     M.ContainerRequest,     M.containerRequest,+    M.withoutReaper,     M.setName,     M.setFixedName,     M.setSuffixedName,@@ -24,6 +28,9 @@     M.setVolumeMounts,     M.setRm,     M.setEnv,+    M.setMemory,+    M.setCpus,+    M.withWorkingDirectory,     M.withNetwork,     M.withNetworkAlias,     M.setLink,@@ -45,6 +52,7 @@     M.Network,     M.NetworkId,     M.createNetwork,+    M.fromExistingNetwork,      -- * Port     M.Port (..),
src/TestContainers/Docker.hs view
@@ -65,6 +65,7 @@     -- * Referring to images     ToImage,     fromTag,+    fromImageId,     fromBuildContext,     fromDockerfile,     build,@@ -75,15 +76,19 @@     -- * Running containers     ContainerRequest,     containerRequest,+    withoutReaper,     withLabels,     setName,     setFixedName,     setSuffixedName,     setRandomName,     setCmd,+    setMemory,+    setCpus,     setVolumeMounts,     setRm,     setEnv,+    withWorkingDirectory,     withNetwork,     withNetworkAlias,     setLink,@@ -103,6 +108,7 @@     networkId,     networkRequest,     createNetwork,+    fromExistingNetwork,     withIpv6,     withDriver, @@ -175,7 +181,8 @@ import qualified Data.Aeson.Optics as Optics import qualified Data.ByteString.Lazy.Char8 as LazyByteString import Data.Function ((&))-import Data.List (find)+import Data.List (find, stripPrefix)+import Data.Maybe (fromMaybe) import Data.String (IsString (..)) import Data.Text (Text, pack, splitOn, strip, unpack) import Data.Text.Encoding (encodeUtf8)@@ -198,8 +205,9 @@ import qualified Network.Socket as Socket import Optics.Fold (pre) import Optics.Operators ((^?))-import Optics.Optic ((%))+import Optics.Optic ((%), (<&>)) import System.Directory (doesFileExist)+import System.Environment (lookupEnv) import System.IO (Handle, hClose) import System.IO.Unsafe (unsafePerformIO) import qualified System.Process as Process@@ -216,6 +224,7 @@     InspectOutput,     LogConsumer,     Pipe (..),+    WithoutReaper (..),     consoleLogConsumer,     docker,     dockerFollowLogs,@@ -226,6 +235,7 @@     NetworkId,     NetworkRequest,     createNetwork,+    fromExistingNetwork,     networkId,     networkRequest,     withDriver,@@ -269,15 +279,21 @@     volumeMounts :: [(Text, Text)],     network :: Maybe (Either Network Text),     networkAlias :: Maybe Text,+    cpus :: Maybe Text,+    memory :: Maybe Text,     links :: [ContainerId],     naming :: NamingStrategy,     rmOnExit :: Bool,     readiness :: WaitUntilReady,     labels :: [(Text, Text)],     noReaper :: Bool,-    followLogs :: Maybe LogConsumer+    followLogs :: Maybe LogConsumer,+    workDirectory :: Maybe Text   } +instance WithoutReaper ContainerRequest where+  withoutReaper request = request {noReaper = True}+ -- | Parameters for a naming a Docker container. -- -- @since 0.5.0.0@@ -300,12 +316,15 @@       volumeMounts = [],       network = Nothing,       networkAlias = Nothing,+      memory = Nothing,+      cpus = Nothing,       links = [],       rmOnExit = False,       readiness = mempty,       labels = mempty,       noReaper = False,-      followLogs = Nothing+      followLogs = Nothing,+      workDirectory = Nothing     }  -- | Set the name of a Docker container. This is equivalent to invoking @docker run@@@ -351,6 +370,22 @@ setCmd newCmd req =   req {cmd = Just newCmd} +-- | Set the memory limit of a Docker container. This is equivalent to+-- invoking @docker run@ with the @--memory@ parameter.+--+-- @since 0.5.1.0+setMemory :: Text -> ContainerRequest -> ContainerRequest+setMemory newMemory req =+  req {memory = Just newMemory}++-- | Set the cpus limit of a Docker container. This is equivalent to+-- invoking @docker run@ with the @--cpus@ parameter.+--+-- @since 0.5.1.0+setCpus :: Text -> ContainerRequest -> ContainerRequest+setCpus newCpus req =+  req {cpus = Just newCpus}+ -- | The volume mounts to link to Docker container. This is the equivalent -- of passing the command on the @docker run -v@ invocation. setVolumeMounts :: [(Text, Text)] -> ContainerRequest -> ContainerRequest@@ -373,6 +408,13 @@ setEnv newEnv req =   req {env = newEnv} +-- | Sets the working directory inside the container.+--+-- @since 0.5.1.0+withWorkingDirectory :: Text -> ContainerRequest -> ContainerRequest+withWorkingDirectory workdir request =+  request {workDirectory = Just workdir}+ -- | Set the network the container will connect to. This is equivalent to passing -- @--network network_name@ to @docker run@. --@@ -506,12 +548,15 @@           volumeMounts,           network,           networkAlias,+          memory,+          cpus,           links,           rmOnExit,           readiness,           labels,           noReaper,-          followLogs+          followLogs,+          workDirectory         } = request    config@Config {configTracer, configCreateReaper} <-@@ -548,6 +593,9 @@             ++ [["--link", container] | container <- links]             ++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]             ++ [["--rm"] | rmOnExit]+            ++ [["--workdir", workdir] | Just workdir <- [workDirectory]]+            ++ [["--memory", value] | Just value <- [memory]]+            ++ [["--cpus", value] | Just value <- [cpus]]             ++ [[tag]]             ++ [command | Just command <- [cmd]] @@ -589,11 +637,19 @@ -- @since 0.5.0.0 createRyukReaper :: TestContainer Reaper createRyukReaper = do+  dockerSocketLocation <-+    liftIO $+      lookupEnv "DOCKER_HOST"+        <&> (>>= stripPrefix "unix://")+        <&> fromMaybe "/var/run/docker.sock"   ryukContainer <-     run $       containerRequest (fromTag ryukImageTag)-        & skipReaper-        & setVolumeMounts [("/var/run/docker.sock", "/var/run/docker.sock")]+        &+        -- Ryuk destroys itself once it reaped the resources,+        -- no need to register itself with itself.+        withoutReaper+        & setVolumeMounts [(pack dockerSocketLocation, "/var/run/docker.sock")]         & setExpose [ryukPort]         & setWaitingFor (waitUntilMappedPortReachable ryukPort)         & setRm True@@ -603,14 +659,6 @@    newRyukReaper ryukContainerAddress ryukContainerPort --- | Internal attribute, serving as a loop breaker: When runnign a container--- we ensure a 'Reaper' is present, since the 'Reaper' itself is a running--- container we need to break the loop to avoid spinning up a new 'Reaper' for--- the 'Reaper'.-skipReaper :: ContainerRequest -> ContainerRequest-skipReaper request =-  request {noReaper = True}- -- | Kills a Docker container. `kill` is essentially @docker kill@. -- -- @since 0.1.0.0@@ -699,7 +747,7 @@     { runToImage = action     } --- | Get an `Image` from a tag.+-- | Get an `Image` from a tag. This runs @docker pull --quiet <tag>@ to obtain an image id. -- -- @since 0.1.0.0 fromTag :: ImageTag -> ToImage@@ -711,6 +759,15 @@       { tag = strip (pack output)       } +-- | Get an `Image` from an image id. This doesn't run @docker pull@ or any other Docker command+-- on construction.+--+-- @since 0.5.1.0+fromImageId :: Text -> ToImage+fromImageId imageId =+  defaultToImage $+    pure Image {tag = imageId}+ -- | Build the image from a build path and an optional path to the -- Dockerfile (default is Dockerfile) --@@ -890,7 +947,8 @@             withTrace               configTracer               (TraceHttpCall endpointHost endpointPort (Right code))-            unless (code `elem` acceptableStatusCodes) $+            unless (code `elem` acceptableStatusCodes) $ do+              liftIO (threadDelay 500000)               retry manager           Left (exception :: HttpException) -> do             withTrace@@ -1087,8 +1145,8 @@ internalContainerIp Container {id, inspectOutput} =   case inspectOutput     ^? Optics.key "NetworkSettings"-    % Optics.key "IPAddress"-    % Optics._String of+      % Optics.key "IPAddress"+      % Optics._String of     Nothing ->       throw $ InspectOutputUnexpected {id}     Just address ->
src/TestContainers/Docker/Internal.hs view
@@ -21,6 +21,9 @@     LogConsumer,     consoleLogConsumer,     dockerFollowLogs,++    -- * Common abstractions for Docker resources+    WithoutReaper (..),   ) where @@ -39,6 +42,14 @@ import qualified System.IO import qualified System.Process as Process import TestContainers.Trace (Trace (..), Tracer, withTrace)++-- | Shared property between Docker resources.+class WithoutReaper request where+  -- | Do not register the docker resource (container, register, etc.) with the resource reaper.+  -- Careful, doing this will make your container leak on shutdown if not explicitly stopped.+  --+  -- @since 0.5.1.0+  withoutReaper :: request -> request  -- | Identifies a network within the Docker runtime. Assigned by @docker network create@ --
src/TestContainers/Docker/Network.hs view
@@ -10,11 +10,13 @@     networkId,      -- * Creating networks+    fromExistingNetwork,     createNetwork,     NetworkRequest,     networkRequest,     withDriver,     withIpv6,+    withoutReaper,   ) where @@ -22,7 +24,7 @@ import Control.Monad.Reader (ask) import Data.Text (Text, pack, strip) import qualified System.Random as Random-import TestContainers.Docker.Internal (NetworkId, docker)+import TestContainers.Docker.Internal (NetworkId, WithoutReaper (..), docker) import TestContainers.Docker.Reaper (reaperLabels) import TestContainers.Monad (Config (..), TestContainer) import Prelude hiding (id)@@ -46,9 +48,13 @@ data NetworkRequest = NetworkRequest   { ipv6 :: Bool,     driver :: Maybe Text,-    labels :: [(Text, Text)]+    labels :: [(Text, Text)],+    noReaper :: Bool   } +instance WithoutReaper NetworkRequest where+  withoutReaper request = request {noReaper = True}+ -- | Default parameters for creating a new Docker network. -- -- @since 0.5.0.0@@ -57,7 +63,8 @@   NetworkRequest     { ipv6 = False,       driver = Nothing,-      labels = []+      labels = [],+      noReaper = False     }  -- | Enable IPv6 for the Docker network.@@ -74,6 +81,15 @@ withDriver driver request =   request {driver = Just driver} +-- | Creates a 'Network' from an existing 'NetworkId'. Note that the 'Network' is+-- not managed by the 'TestContainer' monad and as such is not being cleaned up+-- afterwards.+--+-- @since 0.5.1.0+fromExistingNetwork :: NetworkId -> TestContainer Network+fromExistingNetwork id =+  pure Network {id}+ -- | Creates a new 'Network' from a 'NetworkRequest'. -- -- @since 0.5.0.0@@ -90,7 +106,7 @@   -- Creating the network with the reaper labels ensures cleanup   -- at the end of the session   let additionalLabels =-        reaperLabels reaper+        if noReaper then [] else reaperLabels reaper    stdout <-     docker configTracer $
src/TestContainers/Docker/Reaper.hs view
@@ -4,7 +4,7 @@ {-# LANGUAGE OverloadedStrings #-}  module TestContainers.Docker.Reaper-  ( Reaper,+  ( Reaper (..),     reaperLabels,      -- * Ryuk based reaper@@ -22,13 +22,19 @@ import qualified Network.Socket.ByteString as Socket import qualified System.Random as Random --- | Reaper for safe resource cleanup.+-- | Reaper for safe resource cleanup. This type is exposed to allow users to+-- create their own 'Reapers'. -- -- @since 0.5.0.0 data Reaper = Reaper-  { -- | @runReaper label value@ reaps Docker any Docker resource with a matching-    -- label.-    runReaper :: Text -> Text -> IO (),+  { -- | Registers a @label = value@ pair for reaping. Reaping happens when+    -- closing/de-allocating of the 'Reaper' through 'MonadResource'.+    register ::+      -- \| Label+      Text ->+      -- \| Value+      Text ->+      IO (),     -- | Additional labels to add to any Docker resource on creation. Adding the     -- labels is necessary in order for the 'Reaper' to find resources for cleanup.     labels :: [(Text, Text)]@@ -83,25 +89,37 @@           address <-             head <$> Socket.getAddrInfo (Just hints) (Just (unpack host)) (Just (show port))           socket <--            Socket.openSocket address+            Socket.socket+              (Socket.addrFamily address)+              (Socket.addrSocketType address)+              (Socket.addrProtocol address)           Socket.connect socket (Socket.addrAddress address)-          pure (socket, runRyuk sessionId (Ryuk socket))++          -- Construct the reaper and regiter the session with it.+          -- Doing it here intead of in the teardown (like we did before)+          -- guarantees the Reaper knows about our session.+          let reaper =+                newReaper sessionId (Ryuk socket)+          register reaper sessionIdLabel sessionId++          pure (socket, reaper)       )-      ( \(socket, ryuk) -> do-          runReaper ryuk sessionIdLabel sessionId+      ( \(socket, _ryuk) -> do+          -- Tearing down the connection lets Ryuk know it can reap the+          -- running containers.           Socket.close socket       )    pure ryuk -runRyuk ::+newReaper ::   -- | Session id   Text ->   Ryuk ->   Reaper-runRyuk sessionId ryuk =+newReaper sessionId ryuk =   Reaper-    { runReaper = \label value -> do+    { register = \label value -> do         Socket.sendAll           (ryukSocket ryuk)           ("label=" <> encodeUtf8 label <> "=" <> encodeUtf8 value <> "\n")
src/TestContainers/Docker/State.hs view
@@ -68,7 +68,7 @@ stateStatus (State value) =   case value     ^? Optics.key "Status"-    % Optics._String of+      % Optics._String of     Just "created" -> Created     Just "running" -> Running     Just "paused" -> Paused@@ -86,7 +86,7 @@ stateOOMKilled (State value) =   case value     ^? Optics.key "OOMKilled"-    % Optics._Bool of+      % Optics._Bool of     Just True -> True     _ -> False @@ -97,7 +97,7 @@ statePid (State value) =   case value     ^? Optics.key "Pid"-    % Optics._Integer of+      % Optics._Integer of     Just pid -> Just (fromIntegral pid)     _ -> Nothing @@ -108,7 +108,7 @@ stateExitCode (State value) =   case value     ^? Optics.key "ExitCode"-    % Optics._Integer of+      % Optics._Integer of     Just exitCode -> Just (fromIntegral exitCode)     _ -> Nothing @@ -119,7 +119,7 @@ stateError (State value) =   case value     ^? Optics.key "Error"-    % Optics._String of+      % Optics._String of     Just err -> Just err     _ -> Nothing @@ -130,7 +130,7 @@ stateStartedAt (State value) =   case value     ^? Optics.key "StartedAt"-    % Optics._String of+      % Optics._String of     Just err -> Just err     _ -> Nothing @@ -141,6 +141,6 @@ stateFinishedAt (State value) =   case value     ^? Optics.key "FinishedAt"-    % Optics._String of+      % Optics._String of     Just err -> Just err     _ -> Nothing
src/TestContainers/Hspec.hs view
@@ -30,18 +30,25 @@ -- initialization and de-initialization of the containers. -- -- @+-- data ContainerPorts = ContainerPorts {+--   redisPort :: Int,+--   kafkaPort :: Int+-- } ----- containers :: MonadDocker m => m Boolean+-- containers :: MonadDocker m => m ContainerPorts -- containers = do---   _redis <- TestContainers.run $ TestContainers.containerRequest TestContainers.redis---   _kafka <- TestContainers.run $ TestContainers.containerRequest TestContainers.kafka---   pure True+--   redis <- TestContainers.run $ TestContainers.containerRequest TestContainers.redis+--   kafka <- TestContainers.run $ TestContainers.containerRequest TestContainers.kafka+--   pure ContainerPorts {+--     redisPort = TestContainers.containerPort redis "6379/tcp",+--     kafkaPort = TestContainers.containerPort kafka "9092/tcp"+--   } -- -- example :: Spec -- example = --   around (withContainers containers) $ describe "Example tests"---     it "first test" $ \\isBoolean -> do---       isBoolean `shouldBe` True+--     it "some test that uses redis and kafka" $ \ContainerPorts{redisPort, kafkaPort} -> do+--       redisPort `shouldNotBe` kafkaPort -- @ -- -- `withContainers` allows you naturally scope the handling of containers for your tests.
src/TestContainers/Image.hs view
@@ -16,6 +16,7 @@     build,     fromBuildContext,     fromDockerfile,+    fromImageId,     fromTag,   ) 
test/TestContainers/HspecSpec.hs view
@@ -1,21 +1,30 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module TestContainers.HspecSpec (main, spec_all) where  import Test.Hspec import TestContainers.Hspec   ( TestContainer,+    containerPort,     containerRequest,     redis,     run,     withContainers,   ) -containers1 ::-  TestContainer ()+data ContainerPorts = ContainerPorts+  { redisPort :: Int+  }++containers1 :: TestContainer ContainerPorts containers1 = do-  _ <- run $ containerRequest redis-  pure ()+  redisContainer <- run $ containerRequest redis+  pure+    ContainerPorts+      { redisPort = containerPort redisContainer "6379/tcp"+      }  main :: IO () main = hspec spec_all@@ -24,5 +33,5 @@ spec_all =   around (withContainers containers1) $     describe "TestContainers tests" $-      it "test1" $-        shouldBe ()+      it "test1" $ \ContainerPorts {} ->+        shouldBe () ()
testcontainers.cabal view
@@ -1,6 +1,6 @@ cabal-version:      >=1.10 name:               testcontainers-version:            0.5.0.0+version:            0.5.1.0 synopsis:           Docker containers for your integration tests. description:   testcontainers is a Haskell library that provides a friendly API to@@ -20,7 +20,7 @@   README.md  tested-with:-  GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.2+  GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.2 || ==9.8.2  source-repository head   type:     git@@ -48,7 +48,7 @@     , aeson-optics   >=1.1     && <2     , async     , base           >=4.12    && <5-    , bytestring     >=0.10.8  && <0.12+    , bytestring     >=0.10.8  && <0.13     , directory      >=1.3.6   && <2     , exceptions     >=0.10.4  && <0.11     , http-client    >=0.5.14  && <1@@ -59,7 +59,7 @@     , process        >=1.6.5   && <1.7     , random         >=1.2     && <2     , resourcet      >=1.2.4   && <1.4-    , tasty          >=1.0     && <1.5+    , tasty          >=1.0     && <1.6     , text           >=1.2.3   && <3     , unliftio-core  >=0.1.0   && <0.3 @@ -84,7 +84,7 @@       base     , hspec           >=2.0   && <3.0     , tasty-    , tasty-discover  >=4.2.1 && <4.3+    , tasty-discover  >=4.2.1 && <6     , tasty-hspec     , tasty-hunit     , testcontainers