Skip to content

Commit 3264b0d

Browse files
committed
fixed port bindings for testcontainers
1 parent cf63245 commit 3264b0d

3 files changed

Lines changed: 78 additions & 29 deletions

File tree

src/TestContainers.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module TestContainers
3535
M.withNetworkAlias,
3636
M.setLink,
3737
M.setExpose,
38+
M.setPortBindings,
3839
M.setWaitingFor,
3940
M.withFollowLogs,
4041

src/TestContainers/Docker.hs

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ module TestContainers.Docker
9393
withNetworkAlias,
9494
setLink,
9595
setExpose,
96+
setPortBindings,
9697
setWaitingFor,
9798
run,
9899

@@ -276,6 +277,7 @@ data ContainerRequest = ContainerRequest
276277
cmd :: Maybe [Text],
277278
env :: [(Text, Text)],
278279
exposedPorts :: [Port],
280+
portBindings :: [(Int, Port)],
279281
volumeMounts :: [(Text, Text)],
280282
network :: Maybe (Either Network Text),
281283
networkAlias :: Maybe Text,
@@ -313,6 +315,7 @@ containerRequest image =
313315
cmd = Nothing,
314316
env = [],
315317
exposedPorts = [],
318+
portBindings = [],
316319
volumeMounts = [],
317320
network = Nothing,
318321
networkAlias = Nothing,
@@ -523,6 +526,24 @@ setExpose :: [Port] -> ContainerRequest -> ContainerRequest
523526
setExpose newExpose req =
524527
req {exposedPorts = newExpose}
525528

529+
-- | Set fixed port bindings on the container. This is equivalent to setting
530+
-- @--publish HOST_PORT:CONTAINER_PORT@ for each binding. If a host port is
531+
-- already in use, Docker will fail to start the container.
532+
--
533+
-- Example:
534+
--
535+
-- @
536+
-- container <- `run` $ `containerRequest` postgres
537+
-- & `setPortBindings` [(5432, 5432), (5433, 5433)]
538+
-- @
539+
--
540+
-- This will fail if port 5432 or 5433 is already bound on the host.
541+
--
542+
-- @since 0.5.1.0
543+
setPortBindings :: [(Int, Port)] -> ContainerRequest -> ContainerRequest
544+
setPortBindings bindings req =
545+
req {portBindings = bindings}
546+
526547
-- | Set the waiting strategy on the container. Depending on a Docker container
527548
-- it can take some time until the provided service is ready. You will want to
528549
-- use to `setWaitingFor` to block until the container is ready to use.
@@ -545,6 +566,7 @@ run request = do
545566
cmd,
546567
env,
547568
exposedPorts,
569+
portBindings,
548570
volumeMounts,
549571
network,
550572
networkAlias,
@@ -587,6 +609,7 @@ run request = do
587609
++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]
588610
++ [["--env", variable <> "=" <> value] | (variable, value) <- env]
589611
++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]
612+
++ [["--publish", pack (show hostPort) <> ":" <> pack (show containerPort) <> "/" <> protocol] | (hostPort, Port {port = containerPort, protocol}) <- portBindings]
590613
++ [["--network", networkName] | Just (Right networkName) <- [network]]
591614
++ [["--network", networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
592615
++ [["--network-alias", alias] | Just alias <- [networkAlias]]
@@ -623,8 +646,9 @@ run request = do
623646
{ id,
624647
releaseKey,
625648
image,
626-
inspectOutput,
627-
config
649+
config,
650+
containerPortBindings = portBindings,
651+
inspectOutput
628652
}
629653

630654
-- Last but not least, execute the WaitUntilReady checks
@@ -1108,6 +1132,8 @@ data Container = Container
11081132
image :: Image,
11091133
-- | Configuration used to create and run this container.
11101134
config :: Config,
1135+
-- | Fixed port bindings specified in the ContainerRequest.
1136+
containerPortBindings :: [(Int, Port)],
11111137
-- | Memoized output of `docker inspect`. This is being calculated lazily.
11121138
inspectOutput :: InspectOutput
11131139
}
@@ -1201,31 +1227,35 @@ containerGateway Container {id, inspectOutput} =
12011227
--
12021228
-- @since 0.1.0.0
12031229
containerPort :: Container -> Port -> Int
1204-
containerPort Container {id, inspectOutput} Port {port, protocol} =
1205-
let -- TODO also support UDP ports
1206-
-- Using IsString so it works both with Text (aeson<2) and Aeson.Key (aeson>=2)
1207-
textPort :: (IsString s) => s
1208-
textPort = fromString $ show port <> "/" <> unpack protocol
1209-
in -- TODO be more mindful, make sure to grab the
1210-
-- port from the right host address
1211-
1212-
case inspectOutput
1213-
^? pre
1214-
( Optics.key "NetworkSettings"
1215-
% Optics.key "Ports"
1216-
% Optics.key textPort
1217-
% Optics.values
1218-
% Optics.key "HostPort"
1219-
% Optics._String
1220-
) of
1221-
Nothing ->
1222-
throw $
1223-
UnknownPortMapping
1224-
{ id,
1225-
port = textPort
1226-
}
1227-
Just hostPort ->
1228-
read (unpack hostPort)
1230+
containerPort Container {id, inspectOutput, containerPortBindings} requestedPort@(Port {port, protocol}) =
1231+
-- First check if there's a fixed binding for this port
1232+
case find (\(_, boundPort) -> boundPort == requestedPort) containerPortBindings of
1233+
Just (hostPort, _) -> hostPort
1234+
Nothing ->
1235+
let -- TODO also support UDP ports
1236+
-- Using IsString so it works both with Text (aeson<2) and Aeson.Key (aeson>=2)
1237+
textPort :: (IsString s) => s
1238+
textPort = fromString $ show port <> "/" <> unpack protocol
1239+
in -- TODO be more mindful, make sure to grab the
1240+
-- port from the right host address
1241+
1242+
case inspectOutput
1243+
^? pre
1244+
( Optics.key "NetworkSettings"
1245+
% Optics.key "Ports"
1246+
% Optics.key textPort
1247+
% Optics.values
1248+
% Optics.key "HostPort"
1249+
% Optics._String
1250+
) of
1251+
Nothing ->
1252+
throw $
1253+
UnknownPortMapping
1254+
{ id,
1255+
port = textPort
1256+
}
1257+
Just hostPort ->
1258+
read (unpack hostPort)
12291259

12301260
-- | Returns the domain and port exposing the given container's port. Differs
12311261
-- from 'containerPort' in that 'containerAddress' will return the container's

test/TestContainers/HspecSpec.hs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import TestContainers.Hspec
1111
containerRequest,
1212
redis,
1313
run,
14+
setPortBindings,
1415
withContainers,
16+
(&),
1517
)
1618

1719
data ContainerPorts = ContainerPorts
@@ -26,12 +28,28 @@ containers1 = do
2628
{ redisPort = containerPort redisContainer "6379/tcp"
2729
}
2830

31+
containers2 :: TestContainer ContainerPorts
32+
containers2 = do
33+
redisContainer <-
34+
run $
35+
containerRequest redis
36+
& setPortBindings [(16379, "6379/tcp")]
37+
pure
38+
ContainerPorts
39+
{ redisPort = containerPort redisContainer "6379/tcp"
40+
}
41+
2942
main :: IO ()
3043
main = hspec spec_all
3144

3245
spec_all :: Spec
33-
spec_all =
46+
spec_all = do
3447
around (withContainers containers1) $
35-
describe "TestContainers tests" $
48+
describe "TestContainers tests with random ports" $
3649
it "test1" $ \ContainerPorts {} ->
3750
shouldBe () ()
51+
52+
around (withContainers containers2) $
53+
describe "TestContainers tests with fixed port bindings" $ do
54+
it "should use the fixed host port" $ \ContainerPorts {..} ->
55+
redisPort `shouldBe` 16379

0 commit comments

Comments
 (0)