Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/TestContainers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module TestContainers
M.withNetworkAlias,
M.setLink,
M.setExpose,
M.setPortBindings,
M.setWaitingFor,
M.withFollowLogs,

Expand Down
89 changes: 59 additions & 30 deletions src/TestContainers/Docker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
withNetworkAlias,
setLink,
setExpose,
setPortBindings,
setWaitingFor,
run,

Expand Down Expand Up @@ -279,6 +280,7 @@
cmd :: Maybe [Text],
env :: [(Text, Text)],
exposedPorts :: [Port],
portBindings :: [(Int, Port)],
volumeMounts :: [(Text, Text)],
network :: Maybe (Either Network Text),
networkAlias :: Maybe Text,
Expand Down Expand Up @@ -317,6 +319,7 @@
cmd = Nothing,
env = [],
exposedPorts = [],
portBindings = [],
volumeMounts = [],
network = Nothing,
networkAlias = Nothing,
Expand Down Expand Up @@ -549,6 +552,24 @@
setExpose newExpose req =
req {exposedPorts = newExpose}

-- | Set fixed port bindings on the container. This is equivalent to setting
-- @--publish HOST_PORT:CONTAINER_PORT@ for each binding. If a host port is
-- already in use, Docker will fail to start the container.
--
-- Example:
--
-- @
-- container <- `run` $ `containerRequest` postgres
-- & `setPortBindings` [(5432, 5432), (5433, 5433)]
-- @
--
-- This will fail if port 5432 or 5433 is already bound on the host.
--
-- @since 0.5.1.0
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO doc string is probably wrong

setPortBindings :: [(Int, Port)] -> ContainerRequest -> ContainerRequest
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any reason to have NO port bindings, i.e. the empty list? Otherwise, I'd like to see a NonEmpty (Int, Port) instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mostly just to not have to deal with wrapping no op in the user level

setPortBindings bindings req =
req {portBindings = bindings}

-- | Set the waiting strategy on the container. Depending on a Docker container
-- it can take some time until the provided service is ready. You will want to
-- use to `setWaitingFor` to block until the container is ready to use.
Expand All @@ -571,6 +592,7 @@
cmd,
env,
exposedPorts,
portBindings,
volumeMounts,
network,
networkAlias,
Expand Down Expand Up @@ -622,6 +644,7 @@
++ [["--network", networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
++ [["--network-alias", alias] | Just alias <- [networkAlias]]
++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]
++ [["--publish", pack (show hostPort) <> ":" <> pack (show containerPort) <> "/" <> protocol] | (hostPort, Port {port = containerPort, protocol}) <- portBindings]

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

View workflow job for this annotation

GitHub Actions / stack / ghc 9.0.2

This binding for ‘containerPort’ shadows the existing binding

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

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.4.8

This binding for ‘containerPort’ shadows the existing binding

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

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.8.2

This binding for ‘containerPort’ shadows the existing binding

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

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.2.8

This binding for ‘containerPort’ shadows the existing binding

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

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.6.4

This binding for ‘containerPort’ shadows the existing binding
++ [["--rm"] | rmOnExit]
++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]
++ [["--workdir", workdir] | Just workdir <- [workDirectory]]
Expand Down Expand Up @@ -658,8 +681,9 @@
{ id,
releaseKey,
image,
inspectOutput,
config
config,
containerPortBindings = portBindings,
inspectOutput
}

-- Last but not least, execute the WaitUntilReady checks
Expand Down Expand Up @@ -1022,7 +1046,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 1049 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 Expand Up @@ -1165,6 +1189,8 @@
image :: Image,
-- | Configuration used to create and run this container.
config :: Config,
-- | Fixed port bindings specified in the ContainerRequest.
containerPortBindings :: [(Int, Port)],
-- | Memoized output of `docker inspect`. This is being calculated lazily.
inspectOutput :: InspectOutput
}
Expand Down Expand Up @@ -1270,34 +1296,37 @@
--
-- @since 0.1.0.0
containerPort :: Container -> Port -> Int
containerPort Container {id, inspectOutput, image} Port {port, protocol} =
let -- TODO also support UDP ports
-- Using IsString so it works both with Text (aeson<2) and Aeson.Key (aeson>=2)
textPort :: (IsString s) => s
textPort = fromString $ show port <> "/" <> unpack protocol
in -- TODO be more mindful, make sure to grab the
-- port from the right host address

case inspectOutput
^? pre
( Optics.key "NetworkSettings"
% Optics.key "Ports"
% Optics.key textPort
% Optics.values
% Optics.key "HostPort"
% Optics._String
) of
Nothing ->
let containerName = inspectOutput ^? Optics.key "Name" % Optics._String
in throw $
UnknownPortMapping
{ id,
port = textPort,
imageName = Just (imageTag image),
containerName = containerName
}
Just hostPort ->
read (unpack hostPort)
containerPort Container {id, inspectOutput, image, containerPortBindings} requestedPort@(Port {port, protocol}) =
-- First check if there's a fixed binding for this port
case find (\(_, boundPort) -> boundPort == requestedPort) containerPortBindings of
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: i want to be able to handle this at the user level instead

Just (hostPort, _) -> hostPort
Nothing ->
let -- TODO also support UDP ports
-- Using IsString so it works both with Text (aeson<2) and Aeson.Key (aeson>=2)
textPort :: (IsString s) => s
textPort = fromString $ show port <> "/" <> unpack protocol
in -- TODO be more mindful, make sure to grab the
-- port from the right host address
case inspectOutput
^? pre
( Optics.key "NetworkSettings"
% Optics.key "Ports"
% Optics.key textPort
% Optics.values
% Optics.key "HostPort"
% Optics._String
) of
Nothing ->
let containerName = inspectOutput ^? Optics.key "Name" % Optics._String
in throw $
UnknownPortMapping
{ id,
port = textPort,
imageName = Just (imageTag image),
containerName = containerName
}
Just hostPort ->
read (unpack hostPort)

-- | Returns the domain and port exposing the given container's port. Differs
-- from 'containerPort' in that 'containerAddress' will return the container's
Expand Down
22 changes: 20 additions & 2 deletions test/TestContainers/HspecSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import TestContainers.Hspec
containerRequest,
redis,
run,
setPortBindings,
withContainers,
(&),
)

data ContainerPorts = ContainerPorts
Expand All @@ -26,12 +28,28 @@ containers1 = do
{ redisPort = containerPort redisContainer "6379/tcp"
}

containers2 :: TestContainer ContainerPorts
containers2 = do
redisContainer <-
run $
containerRequest redis
& setPortBindings [(16379, "6379/tcp")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's interesting that you can specify a port and protocol. Can we add this to the example in the setPortBindings docstring?

pure
ContainerPorts
{ redisPort = containerPort redisContainer "6379/tcp"
}

main :: IO ()
main = hspec spec_all

spec_all :: Spec
spec_all =
spec_all = do
around (withContainers containers1) $
describe "TestContainers tests" $
describe "TestContainers tests with random ports" $
it "test1" $ \ContainerPorts {} ->
shouldBe () ()

around (withContainers containers2) $
describe "TestContainers tests with fixed port bindings" $ do
it "should use the fixed host port" $ \ContainerPorts {..} ->
redisPort `shouldBe` 16379
Loading