{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module TestContainers.Docker
( MonadDocker,
TestContainer,
Config (..),
defaultDockerConfig,
determineConfig,
Tracer,
Trace (..),
newTracer,
withTrace,
ImageTag,
Image,
imageTag,
Port (..),
ContainerId,
Container,
containerId,
containerImage,
containerAlias,
containerGateway,
containerIp,
containerPort,
containerAddress,
containerReleaseKey,
State,
Status (..),
stateError,
stateExitCode,
stateFinishedAt,
stateOOMKilled,
statePid,
stateStartedAt,
stateStatus,
successfulExit,
ToImage,
fromTag,
fromBuildContext,
fromDockerfile,
build,
DockerException (..),
ContainerRequest,
containerRequest,
withLabels,
setName,
setFixedName,
setSuffixedName,
setRandomName,
setCmd,
setVolumeMounts,
setRm,
setEnv,
setNetwork,
withNetwork,
withNetworkAlias,
setLink,
setExpose,
setWaitingFor,
run,
LogConsumer,
consoleLogConsumer,
withFollowLogs,
NetworkId,
Network,
NetworkRequest,
networkId,
networkRequest,
createNetwork,
withIpv6,
withDriver,
InspectOutput,
inspect,
stop,
kill,
rm,
withLogs,
WaitUntilReady,
waitUntilReady,
TimeoutException (..),
waitUntilTimeout,
waitForState,
waitWithLogs,
Pipe (..),
UnexpectedEndOfPipe (..),
waitForLogLine,
dockerHostOs,
isDockerOnLinux,
waitUntilMappedPortReachable,
waitForHttp,
createRyukReaper,
ResIO,
runResourceT,
(&),
)
where
import Control.Concurrent (threadDelay)
import Control.Exception (IOException, throw)
import Control.Monad (forM_, replicateM, unless)
import Control.Monad.Catch
( Exception,
MonadCatch,
MonadThrow,
bracket,
throwM,
try,
)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.IO.Unlift (MonadUnliftIO (withRunInIO))
import Control.Monad.Reader (MonadReader (..))
import Control.Monad.Trans.Resource
( ReleaseKey,
ResIO,
register,
runResourceT,
)
import Data.Aeson (decode')
import qualified Data.Aeson.Optics as Optics
import qualified Data.ByteString.Lazy.Char8 as LazyByteString
import Data.Function ((&))
import Data.List (find)
import Data.String (IsString (..))
import Data.Text (Text, pack, splitOn, strip, unpack)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.Encoding.Error (lenientDecode)
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Encoding as LazyText
import Data.Text.Read (decimal)
import GHC.Stack (withFrozenCallStack)
import Network.HTTP.Client
( HttpException,
Manager,
Request (..),
defaultManagerSettings,
defaultRequest,
httpNoBody,
newManager,
responseStatus,
)
import Network.HTTP.Types (statusCode)
import qualified Network.Socket as Socket
import Optics.Fold (pre)
import Optics.Operators ((^?))
import Optics.Optic ((%))
import System.Directory (doesFileExist)
import System.IO (Handle, hClose)
import System.IO.Unsafe (unsafePerformIO)
import qualified System.Process as Process
import qualified System.Random as Random
import System.Timeout (timeout)
import TestContainers.Config
( Config (..),
defaultDockerConfig,
determineConfig,
)
import TestContainers.Docker.Internal
( ContainerId,
DockerException (..),
InspectOutput,
LogConsumer,
Pipe (..),
consoleLogConsumer,
docker,
dockerFollowLogs,
dockerWithStdin,
)
import TestContainers.Docker.Network
( Network,
NetworkId,
NetworkRequest,
createNetwork,
networkId,
networkRequest,
withDriver,
withIpv6,
)
import TestContainers.Docker.Reaper
( Reaper,
newRyukReaper,
reaperLabels,
ryukImageTag,
ryukPort,
)
import TestContainers.Docker.State
( State,
Status (..),
containerState,
stateError,
stateExitCode,
stateFinishedAt,
stateOOMKilled,
statePid,
stateStartedAt,
stateStatus,
)
import TestContainers.Monad
( MonadDocker,
TestContainer,
)
import TestContainers.Trace (Trace (..), Tracer, newTracer, withTrace)
import Prelude hiding (error, id)
import qualified Prelude
data ContainerRequest = ContainerRequest
{ ContainerRequest -> ToImage
toImage :: ToImage,
ContainerRequest -> Maybe [Text]
cmd :: Maybe [Text],
ContainerRequest -> [(Text, Text)]
env :: [(Text, Text)],
ContainerRequest -> [Port]
exposedPorts :: [Port],
ContainerRequest -> [(Text, Text)]
volumeMounts :: [(Text, Text)],
ContainerRequest -> Maybe (Either Network Text)
network :: Maybe (Either Network Text),
ContainerRequest -> Maybe Text
networkAlias :: Maybe Text,
ContainerRequest -> [Text]
links :: [ContainerId],
ContainerRequest -> NamingStrategy
naming :: NamingStrategy,
ContainerRequest -> Bool
rmOnExit :: Bool,
ContainerRequest -> WaitUntilReady
readiness :: WaitUntilReady,
ContainerRequest -> [(Text, Text)]
labels :: [(Text, Text)],
ContainerRequest -> Bool
noReaper :: Bool,
ContainerRequest -> Maybe LogConsumer
followLogs :: Maybe LogConsumer
}
data NamingStrategy
= RandomName
| FixedName Text
| SuffixedName Text
containerRequest :: ToImage -> ContainerRequest
containerRequest :: ToImage -> ContainerRequest
containerRequest ToImage
image =
ContainerRequest
{ $sel:toImage:ContainerRequest :: ToImage
toImage = ToImage
image,
$sel:naming:ContainerRequest :: NamingStrategy
naming = NamingStrategy
RandomName,
$sel:cmd:ContainerRequest :: Maybe [Text]
cmd = forall a. Maybe a
Nothing,
$sel:env:ContainerRequest :: [(Text, Text)]
env = [],
$sel:exposedPorts:ContainerRequest :: [Port]
exposedPorts = [],
$sel:volumeMounts:ContainerRequest :: [(Text, Text)]
volumeMounts = [],
$sel:network:ContainerRequest :: Maybe (Either Network Text)
network = forall a. Maybe a
Nothing,
$sel:networkAlias:ContainerRequest :: Maybe Text
networkAlias = forall a. Maybe a
Nothing,
$sel:links:ContainerRequest :: [Text]
links = [],
$sel:rmOnExit:ContainerRequest :: Bool
rmOnExit = Bool
False,
$sel:readiness:ContainerRequest :: WaitUntilReady
readiness = forall a. Monoid a => a
mempty,
$sel:labels:ContainerRequest :: [(Text, Text)]
labels = forall a. Monoid a => a
mempty,
$sel:noReaper:ContainerRequest :: Bool
noReaper = Bool
False,
$sel:followLogs:ContainerRequest :: Maybe LogConsumer
followLogs = forall a. Maybe a
Nothing
}
setName :: Text -> ContainerRequest -> ContainerRequest
setName :: Text -> ContainerRequest -> ContainerRequest
setName = Text -> ContainerRequest -> ContainerRequest
setFixedName
{-# DEPRECATED setName "See setFixedName" #-}
setFixedName :: Text -> ContainerRequest -> ContainerRequest
setFixedName :: Text -> ContainerRequest -> ContainerRequest
setFixedName Text
newName ContainerRequest
req =
ContainerRequest
req {$sel:naming:ContainerRequest :: NamingStrategy
naming = Text -> NamingStrategy
FixedName Text
newName}
setRandomName :: ContainerRequest -> ContainerRequest
setRandomName :: ContainerRequest -> ContainerRequest
setRandomName ContainerRequest
req =
ContainerRequest
req {$sel:naming:ContainerRequest :: NamingStrategy
naming = NamingStrategy
RandomName}
setSuffixedName :: Text -> ContainerRequest -> ContainerRequest
setSuffixedName :: Text -> ContainerRequest -> ContainerRequest
setSuffixedName Text
preffix ContainerRequest
req =
ContainerRequest
req {$sel:naming:ContainerRequest :: NamingStrategy
naming = Text -> NamingStrategy
SuffixedName Text
preffix}
setCmd :: [Text] -> ContainerRequest -> ContainerRequest
setCmd :: [Text] -> ContainerRequest -> ContainerRequest
setCmd [Text]
newCmd ContainerRequest
req =
ContainerRequest
req {$sel:cmd:ContainerRequest :: Maybe [Text]
cmd = forall a. a -> Maybe a
Just [Text]
newCmd}
setVolumeMounts :: [(Text, Text)] -> ContainerRequest -> ContainerRequest
setVolumeMounts :: [(Text, Text)] -> ContainerRequest -> ContainerRequest
setVolumeMounts [(Text, Text)]
newVolumeMounts ContainerRequest
req =
ContainerRequest
req {$sel:volumeMounts:ContainerRequest :: [(Text, Text)]
volumeMounts = [(Text, Text)]
newVolumeMounts}
setRm :: Bool -> ContainerRequest -> ContainerRequest
setRm :: Bool -> ContainerRequest -> ContainerRequest
setRm Bool
newRm ContainerRequest
req =
ContainerRequest
req {$sel:rmOnExit:ContainerRequest :: Bool
rmOnExit = Bool
newRm}
setEnv :: [(Text, Text)] -> ContainerRequest -> ContainerRequest
setEnv :: [(Text, Text)] -> ContainerRequest -> ContainerRequest
setEnv [(Text, Text)]
newEnv ContainerRequest
req =
ContainerRequest
req {$sel:env:ContainerRequest :: [(Text, Text)]
env = [(Text, Text)]
newEnv}
setNetwork :: Text -> ContainerRequest -> ContainerRequest
setNetwork :: Text -> ContainerRequest -> ContainerRequest
setNetwork Text
networkName ContainerRequest
req =
ContainerRequest
req {$sel:network:ContainerRequest :: Maybe (Either Network Text)
network = forall a. a -> Maybe a
Just (forall a b. b -> Either a b
Right Text
networkName)}
withNetwork :: Network -> ContainerRequest -> ContainerRequest
withNetwork :: Network -> ContainerRequest -> ContainerRequest
withNetwork Network
network ContainerRequest
req =
ContainerRequest
req {$sel:network:ContainerRequest :: Maybe (Either Network Text)
network = forall a. a -> Maybe a
Just (forall a b. a -> Either a b
Left Network
network)}
withNetworkAlias :: Text -> ContainerRequest -> ContainerRequest
withNetworkAlias :: Text -> ContainerRequest -> ContainerRequest
withNetworkAlias Text
alias ContainerRequest
req =
ContainerRequest
req {$sel:networkAlias:ContainerRequest :: Maybe Text
networkAlias = forall a. a -> Maybe a
Just Text
alias}
withLabels :: [(Text, Text)] -> ContainerRequest -> ContainerRequest
withLabels :: [(Text, Text)] -> ContainerRequest -> ContainerRequest
withLabels [(Text, Text)]
xs ContainerRequest
request =
ContainerRequest
request {$sel:labels:ContainerRequest :: [(Text, Text)]
labels = [(Text, Text)]
xs}
setLink :: [ContainerId] -> ContainerRequest -> ContainerRequest
setLink :: [Text] -> ContainerRequest -> ContainerRequest
setLink [Text]
newLink ContainerRequest
req =
ContainerRequest
req {$sel:links:ContainerRequest :: [Text]
links = [Text]
newLink}
withFollowLogs :: LogConsumer -> ContainerRequest -> ContainerRequest
withFollowLogs :: LogConsumer -> ContainerRequest -> ContainerRequest
withFollowLogs LogConsumer
logConsumer ContainerRequest
request =
ContainerRequest
request {$sel:followLogs:ContainerRequest :: Maybe LogConsumer
followLogs = forall a. a -> Maybe a
Just LogConsumer
logConsumer}
data Port = Port
{ Port -> Int
port :: Int,
Port -> Text
protocol :: Text
}
deriving stock (Port -> Port -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: Port -> Port -> Bool
$c/= :: Port -> Port -> Bool
== :: Port -> Port -> Bool
$c== :: Port -> Port -> Bool
Eq, Eq Port
Port -> Port -> Bool
Port -> Port -> Ordering
Port -> Port -> Port
forall a.
Eq a
-> (a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
min :: Port -> Port -> Port
$cmin :: Port -> Port -> Port
max :: Port -> Port -> Port
$cmax :: Port -> Port -> Port
>= :: Port -> Port -> Bool
$c>= :: Port -> Port -> Bool
> :: Port -> Port -> Bool
$c> :: Port -> Port -> Bool
<= :: Port -> Port -> Bool
$c<= :: Port -> Port -> Bool
< :: Port -> Port -> Bool
$c< :: Port -> Port -> Bool
compare :: Port -> Port -> Ordering
$ccompare :: Port -> Port -> Ordering
Ord)
defaultProtocol :: Text
defaultProtocol :: Text
defaultProtocol = Text
"tcp"
instance Show Port where
show :: Port -> String
show Port {Int
port :: Int
$sel:port:Port :: Port -> Int
port, Text
protocol :: Text
$sel:protocol:Port :: Port -> Text
protocol} =
forall a. Show a => a -> String
show Int
port forall a. Semigroup a => a -> a -> a
<> String
"/" forall a. Semigroup a => a -> a -> a
<> Text -> String
unpack Text
protocol
instance Num Port where
fromInteger :: Integer -> Port
fromInteger Integer
x =
Port {$sel:port:Port :: Int
port = forall a b. (Integral a, Num b) => a -> b
fromIntegral Integer
x, $sel:protocol:Port :: Text
protocol = Text
defaultProtocol}
+ :: Port -> Port -> Port
(+) = forall a. HasCallStack => String -> a
Prelude.error String
"not implemented"
* :: Port -> Port -> Port
(*) = forall a. HasCallStack => String -> a
Prelude.error String
"not implemented"
abs :: Port -> Port
abs = forall a. HasCallStack => String -> a
Prelude.error String
"not implemented"
signum :: Port -> Port
signum = forall a. HasCallStack => String -> a
Prelude.error String
"not implemented"
negate :: Port -> Port
negate = forall a. HasCallStack => String -> a
Prelude.error String
"not implemented"
instance IsString Port where
fromString :: String -> Port
fromString String
input = case Text -> Text -> [Text]
splitOn Text
"/" (String -> Text
pack String
input) of
[Text
numberish]
| Right (Int
port, Text
"") <- forall a. Integral a => Reader a
decimal Text
numberish ->
Port {Int
port :: Int
$sel:port:Port :: Int
port, $sel:protocol:Port :: Text
protocol = Text
defaultProtocol}
[Text
numberish, Text
protocol]
| Right (Int
port, Text
"") <- forall a. Integral a => Reader a
decimal Text
numberish ->
Port {Int
port :: Int
$sel:port:Port :: Int
port, Text
protocol :: Text
$sel:protocol:Port :: Text
protocol}
[Text]
_ ->
forall a. HasCallStack => String -> a
Prelude.error (String
"invalid port literal: " forall a. Semigroup a => a -> a -> a
<> String
input)
setExpose :: [Port] -> ContainerRequest -> ContainerRequest
setExpose :: [Port] -> ContainerRequest -> ContainerRequest
setExpose [Port]
newExpose ContainerRequest
req =
ContainerRequest
req {$sel:exposedPorts:ContainerRequest :: [Port]
exposedPorts = [Port]
newExpose}
setWaitingFor :: WaitUntilReady -> ContainerRequest -> ContainerRequest
setWaitingFor :: WaitUntilReady -> ContainerRequest -> ContainerRequest
setWaitingFor WaitUntilReady
newWaitingFor ContainerRequest
req =
ContainerRequest
req {$sel:readiness:ContainerRequest :: WaitUntilReady
readiness = WaitUntilReady
newWaitingFor}
run :: ContainerRequest -> TestContainer Container
run :: ContainerRequest -> TestContainer Container
run ContainerRequest
request = do
let ContainerRequest
{ ToImage
toImage :: ToImage
$sel:toImage:ContainerRequest :: ContainerRequest -> ToImage
toImage,
NamingStrategy
naming :: NamingStrategy
$sel:naming:ContainerRequest :: ContainerRequest -> NamingStrategy
naming,
Maybe [Text]
cmd :: Maybe [Text]
$sel:cmd:ContainerRequest :: ContainerRequest -> Maybe [Text]
cmd,
[(Text, Text)]
env :: [(Text, Text)]
$sel:env:ContainerRequest :: ContainerRequest -> [(Text, Text)]
env,
[Port]
exposedPorts :: [Port]
$sel:exposedPorts:ContainerRequest :: ContainerRequest -> [Port]
exposedPorts,
[(Text, Text)]
volumeMounts :: [(Text, Text)]
$sel:volumeMounts:ContainerRequest :: ContainerRequest -> [(Text, Text)]
volumeMounts,
Maybe (Either Network Text)
network :: Maybe (Either Network Text)
$sel:network:ContainerRequest :: ContainerRequest -> Maybe (Either Network Text)
network,
Maybe Text
networkAlias :: Maybe Text
$sel:networkAlias:ContainerRequest :: ContainerRequest -> Maybe Text
networkAlias,
[Text]
links :: [Text]
$sel:links:ContainerRequest :: ContainerRequest -> [Text]
links,
Bool
rmOnExit :: Bool
$sel:rmOnExit:ContainerRequest :: ContainerRequest -> Bool
rmOnExit,
WaitUntilReady
readiness :: WaitUntilReady
$sel:readiness:ContainerRequest :: ContainerRequest -> WaitUntilReady
readiness,
[(Text, Text)]
labels :: [(Text, Text)]
$sel:labels:ContainerRequest :: ContainerRequest -> [(Text, Text)]
labels,
Bool
noReaper :: Bool
$sel:noReaper:ContainerRequest :: ContainerRequest -> Bool
noReaper,
Maybe LogConsumer
followLogs :: Maybe LogConsumer
$sel:followLogs:ContainerRequest :: ContainerRequest -> Maybe LogConsumer
followLogs
} = ContainerRequest
request
config :: Config
config@Config {Tracer
configTracer :: Config -> Tracer
configTracer :: Tracer
configTracer, TestContainer Reaper
configCreateReaper :: Config -> TestContainer Reaper
configCreateReaper :: TestContainer Reaper
configCreateReaper} <-
forall r (m :: * -> *). MonadReader r m => m r
ask
[(Text, Text)]
additionalLabels <-
if Bool
noReaper
then do
forall (f :: * -> *) a. Applicative f => a -> f a
pure []
else Reaper -> [(Text, Text)]
reaperLabels forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> TestContainer Reaper
configCreateReaper
image :: Image
image@Image {Text
$sel:tag:Image :: Image -> Text
tag :: Text
tag} <- ToImage -> TestContainer Image
runToImage ToImage
toImage
Maybe Text
name <-
case NamingStrategy
naming of
NamingStrategy
RandomName -> forall (m :: * -> *) a. Monad m => a -> m a
return forall a. Maybe a
Nothing
FixedName Text
n -> forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall a. a -> Maybe a
Just Text
n
SuffixedName Text
prefix ->
forall a. a -> Maybe a
Just forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text
prefix forall a. Semigroup a => a -> a -> a
<>) forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text
"-" forall a. Semigroup a => a -> a -> a
<>) forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
pack
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall (m :: * -> *) a. Applicative m => Int -> m a -> m [a]
replicateM Int
6 (forall a (m :: * -> *). (Random a, MonadIO m) => (a, a) -> m a
Random.randomRIO (Char
'a', Char
'z'))
let dockerRun :: [Text]
dockerRun :: [Text]
dockerRun =
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat forall a b. (a -> b) -> a -> b
$
[[Text
"run"]]
forall a. [a] -> [a] -> [a]
++ [[Text
"--detach"]]
forall a. [a] -> [a] -> [a]
++ [[Text
"--name", Text
containerName] | Just Text
containerName <- [Maybe Text
name]]
forall a. [a] -> [a] -> [a]
++ [[Text
"--label", Text
label forall a. Semigroup a => a -> a -> a
<> Text
"=" forall a. Semigroup a => a -> a -> a
<> Text
value] | (Text
label, Text
value) <- [(Text, Text)]
additionalLabels forall a. [a] -> [a] -> [a]
++ [(Text, Text)]
labels]
forall a. [a] -> [a] -> [a]
++ [[Text
"--env", Text
variable forall a. Semigroup a => a -> a -> a
<> Text
"=" forall a. Semigroup a => a -> a -> a
<> Text
value] | (Text
variable, Text
value) <- [(Text, Text)]
env]
forall a. [a] -> [a] -> [a]
++ [[Text
"--publish", String -> Text
pack (forall a. Show a => a -> String
show Int
port) forall a. Semigroup a => a -> a -> a
<> Text
"/" forall a. Semigroup a => a -> a -> a
<> Text
protocol] | Port {Int
port :: Int
$sel:port:Port :: Port -> Int
port, Text
protocol :: Text
$sel:protocol:Port :: Port -> Text
protocol} <- [Port]
exposedPorts]
forall a. [a] -> [a] -> [a]
++ [[Text
"--network", Text
networkName] | Just (Right Text
networkName) <- [Maybe (Either Network Text)
network]]
forall a. [a] -> [a] -> [a]
++ [[Text
"--network", Network -> Text
networkId Network
dockerNetwork] | Just (Left Network
dockerNetwork) <- [Maybe (Either Network Text)
network]]
forall a. [a] -> [a] -> [a]
++ [[Text
"--network-alias", Text
alias] | Just Text
alias <- [Maybe Text
networkAlias]]
forall a. [a] -> [a] -> [a]
++ [[Text
"--link", Text
container] | Text
container <- [Text]
links]
forall a. [a] -> [a] -> [a]
++ [[Text
"--volume", Text
src forall a. Semigroup a => a -> a -> a
<> Text
":" forall a. Semigroup a => a -> a -> a
<> Text
dest] | (Text
src, Text
dest) <- [(Text, Text)]
volumeMounts]
forall a. [a] -> [a] -> [a]
++ [[Text
"--rm"] | Bool
rmOnExit]
forall a. [a] -> [a] -> [a]
++ [[Text
tag]]
forall a. [a] -> [a] -> [a]
++ [[Text]
command | Just [Text]
command <- [Maybe [Text]
cmd]]
String
stdout <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
configTracer [Text]
dockerRun
let id :: ContainerId
!id :: Text
id =
Text -> Text
strip (String -> Text
pack String
stdout)
~InspectOutput
inspectOutput =
forall a. IO a -> a
unsafePerformIO forall a b. (a -> b) -> a -> b
$
forall (m :: * -> *).
(MonadThrow m, MonadIO m) =>
Tracer -> Text -> m InspectOutput
internalInspect Tracer
configTracer Text
id
ReleaseKey
releaseKey <- forall (m :: * -> *). MonadResource m => IO () -> m ReleaseKey
register (forall (f :: * -> *) a. Applicative f => a -> f a
pure ())
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ Maybe LogConsumer
followLogs forall a b. (a -> b) -> a -> b
$
forall (m :: * -> *).
MonadResource m =>
Tracer -> Text -> LogConsumer -> m ()
dockerFollowLogs Tracer
configTracer Text
id
let container :: Container
container =
Container
{ Text
$sel:id:Container :: Text
id :: Text
id,
ReleaseKey
$sel:releaseKey:Container :: ReleaseKey
releaseKey :: ReleaseKey
releaseKey,
Image
$sel:image:Container :: Image
image :: Image
image,
InspectOutput
$sel:inspectOutput:Container :: InspectOutput
inspectOutput :: InspectOutput
inspectOutput,
Config
$sel:config:Container :: Config
config :: Config
config
}
Container -> WaitUntilReady -> TestContainer ()
waitUntilReady Container
container WaitUntilReady
readiness
forall (f :: * -> *) a. Applicative f => a -> f a
pure Container
container
createRyukReaper :: TestContainer Reaper
createRyukReaper :: TestContainer Reaper
createRyukReaper = do
Container
ryukContainer <-
ContainerRequest -> TestContainer Container
run forall a b. (a -> b) -> a -> b
$
ToImage -> ContainerRequest
containerRequest (Text -> ToImage
fromTag Text
ryukImageTag)
forall a b. a -> (a -> b) -> b
& ContainerRequest -> ContainerRequest
skipReaper
forall a b. a -> (a -> b) -> b
& [(Text, Text)] -> ContainerRequest -> ContainerRequest
setVolumeMounts [(Text
"/var/run/docker.sock", Text
"/var/run/docker.sock")]
forall a b. a -> (a -> b) -> b
& [Port] -> ContainerRequest -> ContainerRequest
setExpose [forall a. Num a => a
ryukPort]
forall a b. a -> (a -> b) -> b
& WaitUntilReady -> ContainerRequest -> ContainerRequest
setWaitingFor (Port -> WaitUntilReady
waitUntilMappedPortReachable forall a. Num a => a
ryukPort)
forall a b. a -> (a -> b) -> b
& Bool -> ContainerRequest -> ContainerRequest
setRm Bool
True
(Text
ryukContainerAddress, Int
ryukContainerPort) <-
forall (m :: * -> *).
MonadIO m =>
Container -> Port -> m (Text, Int)
containerAddress Container
ryukContainer forall a. Num a => a
ryukPort
forall (m :: * -> *). MonadResource m => Text -> Int -> m Reaper
newRyukReaper Text
ryukContainerAddress Int
ryukContainerPort
skipReaper :: ContainerRequest -> ContainerRequest
skipReaper :: ContainerRequest -> ContainerRequest
skipReaper ContainerRequest
request =
ContainerRequest
request {$sel:noReaper:ContainerRequest :: Bool
noReaper = Bool
True}
kill :: Container -> TestContainer ()
kill :: Container -> TestContainer ()
kill Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} = do
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
String
_ <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text
"kill", Text
id]
forall (m :: * -> *) a. Monad m => a -> m a
return ()
stop :: Container -> TestContainer ()
stop :: Container -> TestContainer ()
stop Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} = do
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
String
_ <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text
"stop", Text
id]
forall (m :: * -> *) a. Monad m => a -> m a
return ()
rm :: Container -> TestContainer ()
rm :: Container -> TestContainer ()
rm Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} = do
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
String
_ <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text
"rm", Text
"-f", Text
"-v", Text
id]
forall (m :: * -> *) a. Monad m => a -> m a
return ()
withLogs :: Container -> (Handle -> Handle -> TestContainer a) -> TestContainer a
withLogs :: forall a.
Container
-> (Handle -> Handle -> TestContainer a) -> TestContainer a
withLogs Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} Handle -> Handle -> TestContainer a
logger = do
let acquire :: TestContainer (Handle, Handle, Handle, Process.ProcessHandle)
acquire :: TestContainer (Handle, Handle, Handle, ProcessHandle)
acquire =
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$
String
-> [String]
-> Maybe String
-> Maybe [(String, String)]
-> IO (Handle, Handle, Handle, ProcessHandle)
Process.runInteractiveProcess
String
"docker"
[String
"logs", String
"--follow", Text -> String
unpack Text
id]
forall a. Maybe a
Nothing
forall a. Maybe a
Nothing
release :: (Handle, Handle, Handle, Process.ProcessHandle) -> TestContainer ()
release :: (Handle, Handle, Handle, ProcessHandle) -> TestContainer ()
release (Handle
stdin, Handle
stdout, Handle
stderr, ProcessHandle
handle) =
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$
(Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO ()
Process.cleanupProcess
(forall a. a -> Maybe a
Just Handle
stdin, forall a. a -> Maybe a
Just Handle
stdout, forall a. a -> Maybe a
Just Handle
stderr, ProcessHandle
handle)
forall (m :: * -> *) a c b.
MonadMask m =>
m a -> (a -> m c) -> (a -> m b) -> m b
bracket TestContainer (Handle, Handle, Handle, ProcessHandle)
acquire (Handle, Handle, Handle, ProcessHandle) -> TestContainer ()
release forall a b. (a -> b) -> a -> b
$ \(Handle
stdin, Handle
stdout, Handle
stderr, ProcessHandle
_handle) -> do
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ Handle -> IO ()
hClose Handle
stdin
Handle -> Handle -> TestContainer a
logger Handle
stdout Handle
stderr
type ImageTag = Text
data ToImage = ToImage
{ ToImage -> TestContainer Image
runToImage :: TestContainer Image
}
build :: ToImage -> TestContainer ToImage
build :: ToImage -> TestContainer ToImage
build ToImage
toImage = do
Image
image <- ToImage -> TestContainer Image
runToImage ToImage
toImage
forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$
ToImage
toImage
{ $sel:runToImage:ToImage :: TestContainer Image
runToImage = forall (f :: * -> *) a. Applicative f => a -> f a
pure Image
image
}
defaultToImage :: TestContainer Image -> ToImage
defaultToImage :: TestContainer Image -> ToImage
defaultToImage TestContainer Image
action =
ToImage
{ $sel:runToImage:ToImage :: TestContainer Image
runToImage = TestContainer Image
action
}
fromTag :: ImageTag -> ToImage
fromTag :: Text -> ToImage
fromTag Text
tag = TestContainer Image -> ToImage
defaultToImage forall a b. (a -> b) -> a -> b
$ do
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
String
output <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text
"pull", Text
"--quiet", Text
tag]
forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$
Image
{ $sel:tag:Image :: Text
tag = Text -> Text
strip (String -> Text
pack String
output)
}
fromBuildContext ::
FilePath ->
Maybe FilePath ->
ToImage
fromBuildContext :: String -> Maybe String -> ToImage
fromBuildContext String
path Maybe String
mdockerfile = TestContainer Image -> ToImage
defaultToImage forall a b. (a -> b) -> a -> b
$ do
let args :: [Text]
args
| Just String
dockerfile <- Maybe String
mdockerfile =
[Text
"build", Text
"--quiet", Text
"--file", String -> Text
pack String
dockerfile, String -> Text
pack String
path]
| Bool
otherwise =
[Text
"build", Text
"--quiet", String -> Text
pack String
path]
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
String
output <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text]
args
forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$
Image
{ $sel:tag:Image :: Text
tag = Text -> Text
strip (String -> Text
pack String
output)
}
fromDockerfile ::
Text ->
ToImage
fromDockerfile :: Text -> ToImage
fromDockerfile Text
dockerfile = TestContainer Image -> ToImage
defaultToImage forall a b. (a -> b) -> a -> b
$ do
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
String
output <- forall (m :: * -> *).
MonadIO m =>
Tracer -> [Text] -> Text -> m String
dockerWithStdin Tracer
tracer [Text
"build", Text
"--quiet", Text
"-"] Text
dockerfile
forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$
Image
{ $sel:tag:Image :: Text
tag = Text -> Text
strip (String -> Text
pack String
output)
}
data WaitUntilReady
=
WaitReady
(Container -> TestContainer ())
|
WaitUntilTimeout
Int
WaitUntilReady
| WaitMany
WaitUntilReady
WaitUntilReady
instance Semigroup WaitUntilReady where
<> :: WaitUntilReady -> WaitUntilReady -> WaitUntilReady
(<>) = WaitUntilReady -> WaitUntilReady -> WaitUntilReady
WaitMany
instance Monoid WaitUntilReady where
mempty :: WaitUntilReady
mempty = (Container -> TestContainer ()) -> WaitUntilReady
WaitReady forall a. Monoid a => a
mempty
newtype UnexpectedEndOfPipe = UnexpectedEndOfPipe
{
UnexpectedEndOfPipe -> Text
id :: ContainerId
}
deriving (UnexpectedEndOfPipe -> UnexpectedEndOfPipe -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: UnexpectedEndOfPipe -> UnexpectedEndOfPipe -> Bool
$c/= :: UnexpectedEndOfPipe -> UnexpectedEndOfPipe -> Bool
== :: UnexpectedEndOfPipe -> UnexpectedEndOfPipe -> Bool
$c== :: UnexpectedEndOfPipe -> UnexpectedEndOfPipe -> Bool
Eq, Int -> UnexpectedEndOfPipe -> ShowS
[UnexpectedEndOfPipe] -> ShowS
UnexpectedEndOfPipe -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [UnexpectedEndOfPipe] -> ShowS
$cshowList :: [UnexpectedEndOfPipe] -> ShowS
show :: UnexpectedEndOfPipe -> String
$cshow :: UnexpectedEndOfPipe -> String
showsPrec :: Int -> UnexpectedEndOfPipe -> ShowS
$cshowsPrec :: Int -> UnexpectedEndOfPipe -> ShowS
Show)
instance Exception UnexpectedEndOfPipe
newtype TimeoutException = TimeoutException
{
TimeoutException -> Text
id :: ContainerId
}
deriving (TimeoutException -> TimeoutException -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: TimeoutException -> TimeoutException -> Bool
$c/= :: TimeoutException -> TimeoutException -> Bool
== :: TimeoutException -> TimeoutException -> Bool
$c== :: TimeoutException -> TimeoutException -> Bool
Eq, Int -> TimeoutException -> ShowS
[TimeoutException] -> ShowS
TimeoutException -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [TimeoutException] -> ShowS
$cshowList :: [TimeoutException] -> ShowS
show :: TimeoutException -> String
$cshow :: TimeoutException -> String
showsPrec :: Int -> TimeoutException -> ShowS
$cshowsPrec :: Int -> TimeoutException -> ShowS
Show)
instance Exception TimeoutException
newtype InvalidStateException = InvalidStateException
{
InvalidStateException -> Text
id :: ContainerId
}
deriving stock (InvalidStateException -> InvalidStateException -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: InvalidStateException -> InvalidStateException -> Bool
$c/= :: InvalidStateException -> InvalidStateException -> Bool
== :: InvalidStateException -> InvalidStateException -> Bool
$c== :: InvalidStateException -> InvalidStateException -> Bool
Eq, Int -> InvalidStateException -> ShowS
[InvalidStateException] -> ShowS
InvalidStateException -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [InvalidStateException] -> ShowS
$cshowList :: [InvalidStateException] -> ShowS
show :: InvalidStateException -> String
$cshow :: InvalidStateException -> String
showsPrec :: Int -> InvalidStateException -> ShowS
$cshowsPrec :: Int -> InvalidStateException -> ShowS
Show)
instance Exception InvalidStateException
waitForState :: (State -> Bool) -> WaitUntilReady
waitForState :: (State -> Bool) -> WaitUntilReady
waitForState State -> Bool
isReady = (Container -> TestContainer ()) -> WaitUntilReady
WaitReady forall a b. (a -> b) -> a -> b
$ \Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} -> do
let wait :: TestContainer ()
wait = do
Config {Tracer
configTracer :: Tracer
configTracer :: Config -> Tracer
configTracer} <-
forall r (m :: * -> *). MonadReader r m => m r
ask
InspectOutput
inspectOutput <-
forall (m :: * -> *).
(MonadThrow m, MonadIO m) =>
Tracer -> Text -> m InspectOutput
internalInspect Tracer
configTracer Text
id
let state :: State
state = InspectOutput -> State
containerState InspectOutput
inspectOutput
if State -> Bool
isReady State
state
then forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
else do
case State -> Status
stateStatus State
state of
Status
Exited ->
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM InvalidStateException {Text
id :: Text
$sel:id:InvalidStateException :: Text
id}
Status
Dead ->
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM InvalidStateException {Text
id :: Text
$sel:id:InvalidStateException :: Text
id}
Status
_ -> do
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Int -> IO ()
threadDelay Int
500000)
TestContainer ()
wait
TestContainer ()
wait
successfulExit :: State -> Bool
successfulExit :: State -> Bool
successfulExit State
state =
State -> Status
stateStatus State
state forall a. Eq a => a -> a -> Bool
== Status
Exited Bool -> Bool -> Bool
&& State -> Maybe Int
stateExitCode State
state forall a. Eq a => a -> a -> Bool
== forall a. a -> Maybe a
Just Int
0
waitUntilTimeout :: Int -> WaitUntilReady -> WaitUntilReady
waitUntilTimeout :: Int -> WaitUntilReady -> WaitUntilReady
waitUntilTimeout = Int -> WaitUntilReady -> WaitUntilReady
WaitUntilTimeout
waitForHttp ::
Port ->
String ->
[Int] ->
WaitUntilReady
waitForHttp :: Port -> String -> [Int] -> WaitUntilReady
waitForHttp Port
port String
path [Int]
acceptableStatusCodes = (Container -> TestContainer ()) -> WaitUntilReady
WaitReady forall a b. (a -> b) -> a -> b
$ \Container
container -> do
Config {Tracer
configTracer :: Tracer
configTracer :: Config -> Tracer
configTracer} <- forall r (m :: * -> *). MonadReader r m => m r
ask
let wait :: (MonadIO m, MonadCatch m) => m ()
wait :: forall (m :: * -> *). (MonadIO m, MonadCatch m) => m ()
wait =
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (ManagerSettings -> IO Manager
newManager ManagerSettings
defaultManagerSettings) forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= forall (m :: * -> *). (MonadIO m, MonadCatch m) => Manager -> m ()
retry
retry :: (MonadIO m, MonadCatch m) => Manager -> m ()
retry :: forall (m :: * -> *). (MonadIO m, MonadCatch m) => Manager -> m ()
retry Manager
manager = do
(Text
endpointHost, Int
endpointPort) <- forall (m :: * -> *).
MonadIO m =>
Container -> Port -> m (Text, Int)
containerAddress Container
container Port
port
let request :: Request
request =
Request
defaultRequest
{ host :: ByteString
host = Text -> ByteString
encodeUtf8 Text
endpointHost,
port :: Int
port = Int
endpointPort,
path :: ByteString
path = Text -> ByteString
encodeUtf8 (String -> Text
pack String
path)
}
Either HttpException Int
result <-
forall (m :: * -> *) e a.
(MonadCatch m, Exception e) =>
m a -> m (Either e a)
try forall a b. (a -> b) -> a -> b
$
Status -> Int
statusCode forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall body. Response body -> Status
responseStatus forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Request -> Manager -> IO (Response ())
httpNoBody Request
request Manager
manager)
case Either HttpException Int
result of
Right Int
code -> do
forall (m :: * -> *). MonadIO m => Tracer -> Trace -> m ()
withTrace
Tracer
configTracer
(Text -> Int -> Either String Int -> Trace
TraceHttpCall Text
endpointHost Int
endpointPort (forall a b. b -> Either a b
Right Int
code))
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Int
code forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Int]
acceptableStatusCodes) forall a b. (a -> b) -> a -> b
$
forall (m :: * -> *). (MonadIO m, MonadCatch m) => Manager -> m ()
retry Manager
manager
Left (HttpException
exception :: HttpException) -> do
forall (m :: * -> *). MonadIO m => Tracer -> Trace -> m ()
withTrace
Tracer
configTracer
(Text -> Int -> Either String Int -> Trace
TraceHttpCall Text
endpointHost Int
endpointPort (forall a b. a -> Either a b
Left forall a b. (a -> b) -> a -> b
$ forall a. Show a => a -> String
show HttpException
exception))
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Int -> IO ()
threadDelay Int
500000)
forall (m :: * -> *). (MonadIO m, MonadCatch m) => Manager -> m ()
retry Manager
manager
forall (m :: * -> *). (MonadIO m, MonadCatch m) => m ()
wait
waitUntilMappedPortReachable ::
Port ->
WaitUntilReady
waitUntilMappedPortReachable :: Port -> WaitUntilReady
waitUntilMappedPortReachable Port
port = (Container -> TestContainer ()) -> WaitUntilReady
WaitReady forall a b. (a -> b) -> a -> b
$ \Container
container -> do
forall a. HasCallStack => (HasCallStack => a) -> a
withFrozenCallStack forall a b. (a -> b) -> a -> b
$ do
Config {Tracer
configTracer :: Tracer
configTracer :: Config -> Tracer
configTracer} <- forall r (m :: * -> *). MonadReader r m => m r
ask
let resolve :: String -> a -> IO AddrInfo
resolve String
endpointHost a
endpointPort = do
let hints :: AddrInfo
hints = AddrInfo
Socket.defaultHints {addrSocketType :: SocketType
Socket.addrSocketType = SocketType
Socket.Stream}
forall a. [a] -> a
head forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe AddrInfo -> Maybe String -> Maybe String -> IO [AddrInfo]
Socket.getAddrInfo (forall a. a -> Maybe a
Just AddrInfo
hints) (forall a. a -> Maybe a
Just String
endpointHost) (forall a. a -> Maybe a
Just (forall a. Show a => a -> String
show a
endpointPort))
open :: AddrInfo -> IO Socket
open AddrInfo
addr = do
Socket
socket <-
Family -> SocketType -> ProtocolNumber -> IO Socket
Socket.socket
(AddrInfo -> Family
Socket.addrFamily AddrInfo
addr)
(AddrInfo -> SocketType
Socket.addrSocketType AddrInfo
addr)
(AddrInfo -> ProtocolNumber
Socket.addrProtocol AddrInfo
addr)
Socket -> SockAddr -> IO ()
Socket.connect
Socket
socket
(AddrInfo -> SockAddr
Socket.addrAddress AddrInfo
addr)
forall (f :: * -> *) a. Applicative f => a -> f a
pure Socket
socket
wait :: IO ()
wait = do
(Text
endpointHost, Int
endpointPort) <- forall (m :: * -> *).
MonadIO m =>
Container -> Port -> m (Text, Int)
containerAddress Container
container Port
port
Either IOException Socket
result <- forall (m :: * -> *) e a.
(MonadCatch m, Exception e) =>
m a -> m (Either e a)
try (forall {a}. Show a => String -> a -> IO AddrInfo
resolve (Text -> String
unpack Text
endpointHost) Int
endpointPort forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= AddrInfo -> IO Socket
open)
case Either IOException Socket
result of
Right Socket
socket -> do
forall (m :: * -> *). MonadIO m => Tracer -> Trace -> m ()
withTrace Tracer
configTracer (Text -> Int -> Maybe IOException -> Trace
TraceOpenSocket Text
endpointHost Int
endpointPort forall a. Maybe a
Nothing)
Socket -> IO ()
Socket.close Socket
socket
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
Left (IOException
exception :: IOException) -> do
forall (m :: * -> *). MonadIO m => Tracer -> Trace -> m ()
withTrace
Tracer
configTracer
(Text -> Int -> Maybe IOException -> Trace
TraceOpenSocket Text
endpointHost Int
endpointPort (forall a. a -> Maybe a
Just IOException
exception))
Int -> IO ()
threadDelay Int
500000
IO ()
wait
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO IO ()
wait
waitWithLogs :: (Container -> Handle -> Handle -> IO ()) -> WaitUntilReady
waitWithLogs :: (Container -> Handle -> Handle -> IO ()) -> WaitUntilReady
waitWithLogs Container -> Handle -> Handle -> IO ()
waiter = (Container -> TestContainer ()) -> WaitUntilReady
WaitReady forall a b. (a -> b) -> a -> b
$ \Container
container ->
forall a.
Container
-> (Handle -> Handle -> TestContainer a) -> TestContainer a
withLogs Container
container forall a b. (a -> b) -> a -> b
$ \Handle
stdout Handle
stderr ->
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ Container -> Handle -> Handle -> IO ()
waiter Container
container Handle
stdout Handle
stderr
waitForLogLine :: Pipe -> (LazyText.Text -> Bool) -> WaitUntilReady
waitForLogLine :: Pipe -> (Text -> Bool) -> WaitUntilReady
waitForLogLine Pipe
whereToLook Text -> Bool
matches = (Container -> Handle -> Handle -> IO ()) -> WaitUntilReady
waitWithLogs forall a b. (a -> b) -> a -> b
$ \Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} Handle
stdout Handle
stderr -> do
let logs :: Handle
logs :: Handle
logs = case Pipe
whereToLook of
Pipe
Stdout -> Handle
stdout
Pipe
Stderr -> Handle
stderr
ByteString
logContent <- Handle -> IO ByteString
LazyByteString.hGetContents Handle
logs
let logLines :: [LazyText.Text]
logLines :: [Text]
logLines =
forall a b. (a -> b) -> [a] -> [b]
map
(OnDecodeError -> ByteString -> Text
LazyText.decodeUtf8With OnDecodeError
lenientDecode)
(ByteString -> [ByteString]
LazyByteString.lines ByteString
logContent)
case forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find Text -> Bool
matches [Text]
logLines of
Just Text
_ -> forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
Maybe Text
Nothing -> forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM forall a b. (a -> b) -> a -> b
$ UnexpectedEndOfPipe {Text
id :: Text
$sel:id:UnexpectedEndOfPipe :: Text
id}
waitUntilReady :: Container -> WaitUntilReady -> TestContainer ()
waitUntilReady :: Container -> WaitUntilReady -> TestContainer ()
waitUntilReady container :: Container
container@Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} WaitUntilReady
input = do
Config {Maybe Int
configDefaultWaitTimeout :: Config -> Maybe Int
configDefaultWaitTimeout :: Maybe Int
configDefaultWaitTimeout} <- forall r (m :: * -> *). MonadReader r m => m r
ask
WaitUntilReady -> TestContainer ()
interpreter forall a b. (a -> b) -> a -> b
$ case Maybe Int
configDefaultWaitTimeout of
Just Int
seconds -> Int -> WaitUntilReady -> WaitUntilReady
waitUntilTimeout Int
seconds WaitUntilReady
input
Maybe Int
Nothing -> WaitUntilReady
input
where
interpreter :: WaitUntilReady -> TestContainer ()
interpreter :: WaitUntilReady -> TestContainer ()
interpreter WaitUntilReady
wait =
case WaitUntilReady
wait of
WaitReady Container -> TestContainer ()
check ->
Container -> TestContainer ()
check Container
container
WaitUntilTimeout Int
seconds WaitUntilReady
rest ->
forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO forall a b. (a -> b) -> a -> b
$ \forall a. TestContainer a -> IO a
runInIO -> do
Maybe ()
result <-
forall a. Int -> IO a -> IO (Maybe a)
timeout (Int
seconds forall a. Num a => a -> a -> a
* Int
1000000) forall a b. (a -> b) -> a -> b
$
forall a. TestContainer a -> IO a
runInIO (WaitUntilReady -> TestContainer ()
interpreter WaitUntilReady
rest)
case Maybe ()
result of
Maybe ()
Nothing ->
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM forall a b. (a -> b) -> a -> b
$ TimeoutException {Text
id :: Text
$sel:id:TimeoutException :: Text
id}
Just {} ->
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
WaitMany WaitUntilReady
first WaitUntilReady
second -> do
WaitUntilReady -> TestContainer ()
interpreter WaitUntilReady
first
WaitUntilReady -> TestContainer ()
interpreter WaitUntilReady
second
data Image = Image
{
Image -> Text
tag :: ImageTag
}
deriving (Image -> Image -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: Image -> Image -> Bool
$c/= :: Image -> Image -> Bool
== :: Image -> Image -> Bool
$c== :: Image -> Image -> Bool
Eq, Int -> Image -> ShowS
[Image] -> ShowS
Image -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Image] -> ShowS
$cshowList :: [Image] -> ShowS
show :: Image -> String
$cshow :: Image -> String
showsPrec :: Int -> Image -> ShowS
$cshowsPrec :: Int -> Image -> ShowS
Show)
imageTag :: Image -> ImageTag
imageTag :: Image -> Text
imageTag Image {Text
tag :: Text
$sel:tag:Image :: Image -> Text
tag} = Text
tag
data Container = Container
{
Container -> Text
id :: ContainerId,
Container -> ReleaseKey
releaseKey :: ReleaseKey,
Container -> Image
image :: Image,
Container -> Config
config :: Config,
Container -> InspectOutput
inspectOutput :: InspectOutput
}
containerId :: Container -> ContainerId
containerId :: Container -> Text
containerId Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id} = Text
id
containerImage :: Container -> Image
containerImage :: Container -> Image
containerImage Container {Image
image :: Image
$sel:image:Container :: Container -> Image
image} = Image
image
containerReleaseKey :: Container -> ReleaseKey
containerReleaseKey :: Container -> ReleaseKey
containerReleaseKey Container {ReleaseKey
releaseKey :: ReleaseKey
$sel:releaseKey:Container :: Container -> ReleaseKey
releaseKey} = ReleaseKey
releaseKey
{-# DEPRECATED containerReleaseKey "Containers are cleaned up with a separate resource reaper. Releasing the container manually is not going to work." #-}
containerIp :: Container -> Text
containerIp :: Container -> Text
containerIp =
Container -> Text
internalContainerIp
internalContainerIp :: Container -> Text
internalContainerIp :: Container -> Text
internalContainerIp Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id, InspectOutput
inspectOutput :: InspectOutput
$sel:inspectOutput:Container :: Container -> InspectOutput
inspectOutput} =
case InspectOutput
inspectOutput
forall k s (is :: IxList) a.
Is k An_AffineFold =>
s -> Optic' k is s a -> Maybe a
^? forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"NetworkSettings"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"IPAddress"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Prism' t Text
Optics._String of
Maybe Text
Nothing ->
forall a e. Exception e => e -> a
throw forall a b. (a -> b) -> a -> b
$ InspectOutputUnexpected {Text
id :: Text
id :: Text
id}
Just Text
address ->
Text
address
containerAlias :: Container -> Text
containerAlias :: Container -> Text
containerAlias Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id, InspectOutput
inspectOutput :: InspectOutput
$sel:inspectOutput:Container :: Container -> InspectOutput
inspectOutput} =
case InspectOutput
inspectOutput
forall k s (is :: IxList) a.
Is k An_AffineFold =>
s -> Optic' k is s a -> Maybe a
^? forall k (is :: IxList) s a.
Is k A_Fold =>
Optic' k is s a -> AffineFold s a
pre
( forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"NetworkSettings"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"Networks"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => IxTraversal' Key t InspectOutput
Optics.members
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"Aliases"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => IxTraversal' Int t InspectOutput
Optics.values
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Prism' t Text
Optics._String
) of
Maybe Text
Nothing ->
forall a e. Exception e => e -> a
throw forall a b. (a -> b) -> a -> b
$
InspectOutputMissingNetwork
{ Text
id :: Text
id :: Text
id
}
Just Text
alias ->
Text
alias
containerGateway :: Container -> Text
containerGateway :: Container -> Text
containerGateway Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id, InspectOutput
inspectOutput :: InspectOutput
$sel:inspectOutput:Container :: Container -> InspectOutput
inspectOutput} =
case InspectOutput
inspectOutput
forall k s (is :: IxList) a.
Is k An_AffineFold =>
s -> Optic' k is s a -> Maybe a
^? forall k (is :: IxList) s a.
Is k A_Fold =>
Optic' k is s a -> AffineFold s a
pre
( forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"NetworkSettings"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"Networks"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => IxTraversal' Key t InspectOutput
Optics.members
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"Gateway"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Prism' t Text
Optics._String
) of
Maybe Text
Nothing ->
forall a e. Exception e => e -> a
throw forall a b. (a -> b) -> a -> b
$
InspectOutputMissingNetwork
{ Text
id :: Text
id :: Text
id
}
Just Text
gatewayIp ->
Text
gatewayIp
containerPort :: Container -> Port -> Int
containerPort :: Container -> Port -> Int
containerPort Container {Text
id :: Text
$sel:id:Container :: Container -> Text
id, InspectOutput
inspectOutput :: InspectOutput
$sel:inspectOutput:Container :: Container -> InspectOutput
inspectOutput} Port {Int
port :: Int
$sel:port:Port :: Port -> Int
port, Text
protocol :: Text
$sel:protocol:Port :: Port -> Text
protocol} =
let
textPort :: (IsString s) => s
textPort :: forall s. IsString s => s
textPort = forall a. IsString a => String -> a
fromString forall a b. (a -> b) -> a -> b
$ forall a. Show a => a -> String
show Int
port forall a. Semigroup a => a -> a -> a
<> String
"/" forall a. Semigroup a => a -> a -> a
<> Text -> String
unpack Text
protocol
in
case InspectOutput
inspectOutput
forall k s (is :: IxList) a.
Is k An_AffineFold =>
s -> Optic' k is s a -> Maybe a
^? forall k (is :: IxList) s a.
Is k A_Fold =>
Optic' k is s a -> AffineFold s a
pre
( forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"NetworkSettings"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"Ports"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key forall s. IsString s => s
textPort
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => IxTraversal' Int t InspectOutput
Optics.values
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Key -> AffineTraversal' t InspectOutput
Optics.key Key
"HostPort"
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% forall t. AsValue t => Prism' t Text
Optics._String
) of
Maybe Text
Nothing ->
forall a e. Exception e => e -> a
throw forall a b. (a -> b) -> a -> b
$
UnknownPortMapping
{ Text
id :: Text
id :: Text
id,
port :: Text
port = forall s. IsString s => s
textPort
}
Just Text
hostPort ->
forall a. Read a => String -> a
read (Text -> String
unpack Text
hostPort)
containerAddress :: (MonadIO m) => Container -> Port -> m (Text, Int)
containerAddress :: forall (m :: * -> *).
MonadIO m =>
Container -> Port -> m (Text, Int)
containerAddress Container
container Port {Int
port :: Int
$sel:port:Port :: Port -> Int
port, Text
protocol :: Text
$sel:protocol:Port :: Port -> Text
protocol} = do
Bool
inDocker <- forall (m :: * -> *). MonadIO m => m Bool
isRunningInDocker
forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a b. (a -> b) -> a -> b
$
if Bool
inDocker
then (Container -> Text
containerAlias Container
container, Int
port)
else (Text
"localhost", Container -> Port -> Int
containerPort Container
container (Port {Int
port :: Int
$sel:port:Port :: Int
port, Text
protocol :: Text
$sel:protocol:Port :: Text
protocol}))
inspect :: Container -> TestContainer InspectOutput
inspect :: Container -> TestContainer InspectOutput
inspect Container {InspectOutput
inspectOutput :: InspectOutput
$sel:inspectOutput:Container :: Container -> InspectOutput
inspectOutput} =
forall (f :: * -> *) a. Applicative f => a -> f a
pure InspectOutput
inspectOutput
internalInspect :: (MonadThrow m, MonadIO m) => Tracer -> ContainerId -> m InspectOutput
internalInspect :: forall (m :: * -> *).
(MonadThrow m, MonadIO m) =>
Tracer -> Text -> m InspectOutput
internalInspect Tracer
tracer Text
id = do
String
stdout <- forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text
"inspect", Text
id]
case forall a. FromJSON a => ByteString -> Maybe a
decode' (String -> ByteString
LazyByteString.pack String
stdout) of
Maybe [InspectOutput]
Nothing ->
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM forall a b. (a -> b) -> a -> b
$ InspectOutputInvalidJSON {Text
id :: Text
id :: Text
id}
Just [] ->
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM forall a b. (a -> b) -> a -> b
$ InspectUnknownContainerId {Text
id :: Text
id :: Text
id}
Just [InspectOutput
value] ->
forall (f :: * -> *) a. Applicative f => a -> f a
pure InspectOutput
value
Just [InspectOutput]
_ ->
forall a. HasCallStack => String -> a
Prelude.error String
"Internal: Multiple results where I expected single result"
askTracer :: (MonadReader Config m) => m Tracer
askTracer :: forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer = do
Config {Tracer
configTracer :: Tracer
configTracer :: Config -> Tracer
configTracer} <- forall r (m :: * -> *). MonadReader r m => m r
ask
forall (f :: * -> *) a. Applicative f => a -> f a
pure Tracer
configTracer
{-# INLINE askTracer #-}
dockerHostOs :: TestContainer Text
dockerHostOs :: TestContainer Text
dockerHostOs = do
Tracer
tracer <- forall (m :: * -> *). MonadReader Config m => m Tracer
askTracer
Text -> Text
strip forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
pack forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall (m :: * -> *). MonadIO m => Tracer -> [Text] -> m String
docker Tracer
tracer [Text
"version", Text
"--format", Text
"{{.Server.Os}}"]
isDockerOnLinux :: TestContainer Bool
isDockerOnLinux :: TestContainer Bool
isDockerOnLinux =
(Text
"linux" forall a. Eq a => a -> a -> Bool
==) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> TestContainer Text
dockerHostOs
isRunningInDocker :: (MonadIO m) => m Bool
isRunningInDocker :: forall (m :: * -> *). MonadIO m => m Bool
isRunningInDocker = forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ String -> IO Bool
doesFileExist String
"/.dockerenv"