Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,12 @@ default Integer getFirstMappedPort() {
* Get the actual mapped port for a given port exposed by the container.
* It should be used in conjunction with {@link #getHost()}.
* <p>
* Note: The returned port number might be outdated (for instance, after disconnecting from a network and reconnecting
* again). If you always need up-to-date value, override the {@link #getContainerInfo()} to return the
* {@link #getCurrentContainerInfo()}.
* The host port is resolved from a live container inspect so it stays correct after Docker
* reassigns published ports (for example after disconnecting from a network and reconnecting,
* or after attaching the container to an additional network).
*
* @param originalPort the original TCP port that is exposed
* @return the port that the exposed port is mapped to, or null if it is not exposed
* @see #getContainerInfo()
* @see #getCurrentContainerInfo()
*/
default Integer getMappedPort(int originalPort) {
Expand All @@ -164,7 +163,9 @@ default Integer getMappedPort(int originalPort) {
);

Ports.Binding[] binding = new Ports.Binding[0];
final InspectContainerResponse containerInfo = this.getContainerInfo();
// Live inspect: cached {@link #getContainerInfo()} can hold stale host ports after
// network attach/detach (see https://github.com/testcontainers/testcontainers-java/issues/11779).
final InspectContainerResponse containerInfo = this.getCurrentContainerInfo();
if (containerInfo != null) {
binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.NetworkSettings;
import com.github.dockerjava.api.model.Ports;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

Expand Down Expand Up @@ -35,4 +40,48 @@ void test(String name, String testSet, List<Integer> expectedResult) {
List<Integer> result = containerState.getBoundPortNumbers();
assertThat(result).hasSameElementsAs(expectedResult);
}

@Test
void getMappedPortUsesCurrentContainerInfo() {
ContainerState containerState = mock(ContainerState.class);
doCallRealMethod().when(containerState).getMappedPort(8080);
InspectContainerResponse cachedInfo = inspectResponse(8080, 18080);
InspectContainerResponse currentInfo = inspectResponse(8080, 28080);

when(containerState.getContainerId()).thenReturn("container-id");
when(containerState.getContainerInfo()).thenReturn(cachedInfo);
when(containerState.getCurrentContainerInfo()).thenReturn(currentInfo);

assertThat(containerState.getMappedPort(8080)).isEqualTo(28080);
}

@Test
void getFirstMappedPortUsesCurrentContainerInfo() {
ContainerState containerState = mock(ContainerState.class);
doCallRealMethod().when(containerState).getMappedPort(8080);
doCallRealMethod().when(containerState).getFirstMappedPort();
InspectContainerResponse cachedInfo = inspectResponse(8080, 18080);
InspectContainerResponse currentInfo = inspectResponse(8080, 28080);

when(containerState.getContainerId()).thenReturn("container-id");
when(containerState.getExposedPorts()).thenReturn(Collections.singletonList(8080));
when(containerState.getContainerInfo()).thenReturn(cachedInfo);
when(containerState.getCurrentContainerInfo()).thenReturn(currentInfo);

assertThat(containerState.getFirstMappedPort()).isEqualTo(28080);
}

private static InspectContainerResponse inspectResponse(int containerPort, int hostPort) {
InspectContainerResponse response = mock(InspectContainerResponse.class);
NetworkSettings networkSettings = mock(NetworkSettings.class);
Ports ports = new Ports();
ExposedPort exposedPort = new ExposedPort(containerPort);
Ports.Binding binding = Ports.Binding.bindPort(hostPort);
ports.bind(exposedPort, binding);

when(response.getNetworkSettings()).thenReturn(networkSettings);
when(networkSettings.getPorts()).thenReturn(ports);

return response;
}
}
114 changes: 114 additions & 0 deletions core/src/test/java/org/testcontainers/containers/NetworkTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.Ports;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.testcontainers.DockerClientFactory;
Expand Down Expand Up @@ -109,5 +113,115 @@ void testReusability() {
.isNotEqualTo(firstId);
}
}

@Test
void getFirstMappedPortRemainsCorrectAfterSecondaryNetworkAttach() {
// Regression for https://github.com/testcontainers/testcontainers-java/issues/11779
try (
Network primary = Network.newNetwork();
Network secondary = Network.newNetwork();
GenericContainer<?> container = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(primary)
.withNetworkAliases("test-host")
.withExposedPorts(7077)
.withCommand(
"/bin/sh",
"-c",
"while true ; do printf 'HTTP/1.1 200 OK\\n\\nok' | nc -l -p 7077; done"
)
) {
container.start();

Integer mappedPortBefore = container.getFirstMappedPort();
assertThat(mappedPortBefore).isEqualTo(publishedHostPort(container, 7077));

DockerClient dockerClient = DockerClientFactory.instance().client();
dockerClient
.connectToNetworkCmd()
.withContainerId(container.getContainerId())
.withNetworkId(secondary.getId())
.exec();

assertThat(container.getFirstMappedPort())
.as("mapped port after secondary network attach matches Docker")
.isEqualTo(publishedHostPort(container, 7077));
assertThat(container.getMappedPort(7077)).isEqualTo(publishedHostPort(container, 7077));
}
}

@Test
void getMappedPortReflectsDockerRemapAfterNetworkDisconnectReconnect() {
// Docker reassigns published host ports after network disconnect/reconnect.
// Cached inspect data must not be used for mapped-port lookups.
try (
Network primary = Network.newNetwork();
Network secondary = Network.newNetwork();
GenericContainer<?> container = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(primary)
.withExposedPorts(7077)
.withCommand(
"/bin/sh",
"-c",
"while true ; do printf 'HTTP/1.1 200 OK\\n\\nok' | nc -l -p 7077; done"
)
) {
container.start();

Integer mappedPortBefore = container.getMappedPort(7077);
Integer cachedPortBefore = hostPortFromInspect(container.getContainerInfo(), 7077);
assertThat(mappedPortBefore).isEqualTo(cachedPortBefore);

DockerClient dockerClient = DockerClientFactory.instance().client();
dockerClient
.connectToNetworkCmd()
.withContainerId(container.getContainerId())
.withNetworkId(secondary.getId())
.exec();
dockerClient
.disconnectFromNetworkCmd()
.withContainerId(container.getContainerId())
.withNetworkId(primary.getId())
.exec();
dockerClient
.connectToNetworkCmd()
.withContainerId(container.getContainerId())
.withNetworkId(primary.getId())
.exec();

Integer publishedAfter = publishedHostPort(container, 7077);
Integer stillCachedFromStart = hostPortFromInspect(container.getContainerInfo(), 7077);

// When Docker remaps the host port, the start-time cache is stale — getMappedPort
// must still return Docker's current binding (not the cached one).
if (!publishedAfter.equals(stillCachedFromStart)) {
assertThat(container.getMappedPort(7077))
.as("must not return stale cached host port after network remap")
.isNotEqualTo(stillCachedFromStart)
.isEqualTo(publishedAfter);
} else {
assertThat(container.getMappedPort(7077)).isEqualTo(publishedAfter);
}
assertThat(container.getFirstMappedPort()).isEqualTo(publishedAfter);
}
}

private static Integer publishedHostPort(GenericContainer<?> container, int containerPort) {
InspectContainerResponse inspect = DockerClientFactory
.instance()
.client()
.inspectContainerCmd(container.getContainerId())
.exec();
return hostPortFromInspect(inspect, containerPort);
}

private static Integer hostPortFromInspect(InspectContainerResponse inspect, int containerPort) {
Ports.Binding[] bindings = inspect
.getNetworkSettings()
.getPorts()
.getBindings()
.get(new ExposedPort(containerPort));
assertThat(bindings).isNotNull().isNotEmpty();
return Integer.valueOf(bindings[0].getHostPortSpec());
}
}
}