effectful-zoo-0.0.3.0: components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack.hs
{- HLINT ignore "Use camelCase" -}
module Effectful.Zoo.TestContainers.LocalStack (
LocalStackEndpoint (..),
TC.Container,
setupContainers,
setupContainers',
waitForLocalStack,
runReaderLocalAwsEnvDiscover,
getLocalStackEndpoint,
inspectContainer,
) where
import Amazonka qualified as AWS
import Amazonka.Auth qualified as AWS
import Control.Concurrent (threadDelay)
import Control.Concurrent qualified as IO
import Control.Exception (try)
import Control.Exception qualified as E
import Control.Monad.IO.Class
import Data.Aeson qualified as J
import Data.ByteString.Lazy qualified as LBS
import Data.Function
import Data.Generics.Product.Any
import Data.Text qualified as T
import Data.Time.Clock.POSIX (getPOSIXTime)
import Effectful
import Effectful.Zoo.Core
import Effectful.Zoo.Reader.Static
import Effectful.Zoo.TestContainers.LocalStack.Types (LocalStackEndpoint (LocalStackEndpoint))
import Effectful.Zoo.TestContainers.LocalStack.Types qualified as Z
import HaskellWorks.Prelude
import Lens.Micro
import Network.HTTP.Conduit (HttpException, simpleHttp)
import System.Environment qualified as IO
import System.IO qualified as IO
import TestContainers.Monad qualified as TC
import TestContainers.Tasty qualified as TC
-- | Sets up and runs the containers required for this test suite.
setupContainers ::
() =>
(TC.MonadDocker m) =>
m TC.Container
setupContainers = setupContainers' "localstack/localstack-pro:latest"
-- | Sets up and runs the containers required for this test suite.
setupContainers' ::
() =>
(TC.MonadDocker m) =>
Text ->
m TC.Container
setupContainers' dockerTag = do
authToken <- liftIO $ IO.lookupEnv "LOCALSTACK_AUTH_TOKEN"
-- Launch the container based on the postgres image.
localstackContainer <-
TC.run $
TC.containerRequest (TC.fromTag dockerTag)
& TC.setEnv [("LOCALSTACK_AUTH_TOKEN", maybe "" T.pack authToken)]
-- Expose the port 4566 from within the container. The respective port
-- on the host machine can be looked up using `containerPort` (see below).
& TC.setExpose
( mconcat
[ [4566]
]
)
-- Wait until the container is ready to accept requests. `run` blocks until
-- readiness can be established.
& TC.setWaitingFor (TC.waitUntilMappedPortReachable 4566)
-- Look up the corresponding port on the host machine for the exposed port 4566.
let localStackPort = TC.containerPort localstackContainer 4566
liftIO $ waitForLocalStack "localhost" localStackPort 100
pure localstackContainer
waitForLocalStack :: String -> Int -> Int -> IO ()
waitForLocalStack host port timeout = do
startTime <- getPOSIXTime
let url = "http://" <> host <> ":" <> show port
checkLoop startTime url
where
checkLoop startTime url = do
result <- try $ simpleHttp url :: IO (Either HttpException LBS.ByteString)
case result of
Right _ -> do
IO.threadDelay 1_000_000
IO.putStrLn ""
Left e -> do
currentTime <- getPOSIXTime
let elapsedTime = currentTime - startTime
when (elapsedTime < fromIntegral timeout) $ do
threadDelay 500_000
checkLoop startTime url
when (elapsedTime >= fromIntegral timeout) $ do
IO.putStrLn "Timeout reached. LocalStack is not ready."
E.throw e
runReaderLocalAwsEnvDiscover :: forall a r. ()
=> r <: IOE
=> IO TC.Container
-> Eff (Reader AWS.Env : r) a
-> Eff r a
runReaderLocalAwsEnvDiscover mk f = do
container <- liftIO mk
ep <- getLocalStackEndpoint container
logger' <- liftIO $ AWS.newLogger AWS.Debug IO.stdout
let creds = AWS.fromKeys (AWS.AccessKey "test") (AWS.SecretKey "test")
credEnv <- liftIO $ AWS.newEnv (AWS.runCredentialChain [pure . creds])
awsEnv <- pure $
credEnv
& the @"logger" .~ logger'
& the @"overrides" %~ (. AWS.setEndpoint False "localhost" ep.port)
runReader awsEnv f
getLocalStackEndpoint :: ()
=> TC.Container
-> Eff r LocalStackEndpoint
getLocalStackEndpoint container = do
let localStackPort = TC.containerPort container 4566
pure Z.LocalStackEndpoint
{ Z.host = "0.0.0.0"
, Z.port = localStackPort
}
inspectContainer :: ()
=> r <: IOE
=> TC.Container
-> Eff r J.Value
inspectContainer container =
liftIO $ TC.runTestContainer TC.defaultDockerConfig $ TC.inspect container