@@ -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
523526setExpose 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
12031229containerPort :: 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
0 commit comments