Skip to content

Commit 2487ba8

Browse files
author
Ranjith Muniyappa
committed
Add getMappedPort support for UDP protocol (#554)
This commit adds support for retrieving mapped ports with protocol specification, enabling proper UDP port mapping support. Changes: - ContainerState: Add getMappedPort(int, InternetProtocol) method that supports both TCP and UDP protocols. The existing getMappedPort(int) now delegates to this method with TCP as the default protocol. - Container: Add addExposedPort(int, InternetProtocol) to the interface for adding ports with specific protocols. - GenericContainer: Implement addExposedPort(int, InternetProtocol). - ContainerStateTest: Add unit tests for UDP port mapping functionality. Fixes #554
1 parent 43c6a97 commit 2487ba8

File tree

4 files changed

+162
-3
lines changed

4 files changed

+162
-3
lines changed

core/src/main/java/org/testcontainers/containers/Container.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ default void addFileSystemBind(final String hostPath, final String containerPath
123123
*/
124124
void addExposedPorts(int... ports);
125125

126+
/**
127+
* Add an exposed port with the specified protocol.
128+
*
129+
* @param port the port to expose
130+
* @param protocol the protocol (TCP or UDP)
131+
*/
132+
void addExposedPort(int port, InternetProtocol protocol);
133+
126134
/**
127135
* Specify the {@link WaitStrategy} to use to determine if the container is ready.
128136
*

core/src/main/java/org/testcontainers/containers/ContainerState.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ default Integer getFirstMappedPort() {
145145
}
146146

147147
/**
148-
* Get the actual mapped port for a given port exposed by the container.
148+
* Get the actual mapped port for a given TCP port exposed by the container.
149149
* It should be used in conjunction with {@link #getHost()}.
150150
* <p>
151151
* Note: The returned port number might be outdated (for instance, after disconnecting from a network and reconnecting
@@ -156,8 +156,29 @@ default Integer getFirstMappedPort() {
156156
* @return the port that the exposed port is mapped to, or null if it is not exposed
157157
* @see #getContainerInfo()
158158
* @see #getCurrentContainerInfo()
159+
* @see #getMappedPort(int, InternetProtocol)
159160
*/
160161
default Integer getMappedPort(int originalPort) {
162+
return getMappedPort(originalPort, InternetProtocol.TCP);
163+
}
164+
165+
/**
166+
* Get the actual mapped port for a given port and protocol exposed by the container.
167+
* It should be used in conjunction with {@link #getHost()}.
168+
* <p>
169+
* Note: The returned port number might be outdated (for instance, after disconnecting from a network and reconnecting
170+
* again). If you always need up-to-date value, override the {@link #getContainerInfo()} to return the
171+
* {@link #getCurrentContainerInfo()}.
172+
*
173+
* @param originalPort the original port that is exposed
174+
* @param protocol the protocol (TCP or UDP) of the exposed port
175+
* @return the port that the exposed port is mapped to
176+
* @throws IllegalStateException if the container is not started
177+
* @throws IllegalArgumentException if the requested port is not mapped
178+
* @see #getContainerInfo()
179+
* @see #getCurrentContainerInfo()
180+
*/
181+
default Integer getMappedPort(int originalPort, InternetProtocol protocol) {
161182
Preconditions.checkState(
162183
this.getContainerId() != null,
163184
"Mapped port can only be obtained after the container is started"
@@ -166,13 +187,19 @@ default Integer getMappedPort(int originalPort) {
166187
Ports.Binding[] binding = new Ports.Binding[0];
167188
final InspectContainerResponse containerInfo = this.getContainerInfo();
168189
if (containerInfo != null) {
169-
binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort));
190+
ExposedPort exposedPort = new ExposedPort(
191+
originalPort,
192+
com.github.dockerjava.api.model.InternetProtocol.parse(protocol.name())
193+
);
194+
binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(exposedPort);
170195
}
171196

172197
if (binding != null && binding.length > 0 && binding[0] != null) {
173198
return Integer.valueOf(binding[0].getHostPortSpec());
174199
} else {
175-
throw new IllegalArgumentException("Requested port (" + originalPort + ") is not mapped");
200+
throw new IllegalArgumentException(
201+
"Requested port (" + originalPort + "/" + protocol.toDockerNotation() + ") is not mapped"
202+
);
176203
}
177204
}
178205

core/src/main/java/org/testcontainers/containers/GenericContainer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,14 @@ public void addExposedPorts(int... ports) {
10481048
this.containerDef.addExposedTcpPorts(ports);
10491049
}
10501050

1051+
@Override
1052+
public void addExposedPort(int port, InternetProtocol protocol) {
1053+
this.containerDef.addExposedPort(
1054+
port,
1055+
com.github.dockerjava.api.model.InternetProtocol.parse(protocol.name())
1056+
);
1057+
}
1058+
10511059
/**
10521060
* {@inheritDoc}
10531061
*/

core/src/test/java/org/testcontainers/containers/ContainerStateTest.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package org.testcontainers.containers;
22

3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import com.github.dockerjava.api.model.ExposedPort;
5+
import com.github.dockerjava.api.model.NetworkSettings;
6+
import com.github.dockerjava.api.model.Ports;
7+
import org.junit.jupiter.api.Test;
38
import org.junit.jupiter.params.ParameterizedTest;
49
import org.junit.jupiter.params.provider.MethodSource;
510

611
import java.util.Collections;
12+
import java.util.HashMap;
713
import java.util.List;
14+
import java.util.Map;
815

916
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1018
import static org.mockito.Mockito.doCallRealMethod;
1119
import static org.mockito.Mockito.mock;
1220
import static org.mockito.Mockito.when;
@@ -35,4 +43,112 @@ void test(String name, String testSet, List<Integer> expectedResult) {
3543
List<Integer> result = containerState.getBoundPortNumbers();
3644
assertThat(result).hasSameElementsAs(expectedResult);
3745
}
46+
47+
@Test
48+
void shouldGetMappedPortForUdpProtocol() {
49+
ContainerState containerState = mock(ContainerState.class);
50+
InspectContainerResponse containerInfo = mock(InspectContainerResponse.class);
51+
NetworkSettings networkSettings = mock(NetworkSettings.class);
52+
Ports ports = mock(Ports.class);
53+
54+
// Set up the mock port bindings for UDP port 5353
55+
Map<ExposedPort, Ports.Binding[]> bindings = new HashMap<>();
56+
ExposedPort udpPort = new ExposedPort(5353, com.github.dockerjava.api.model.InternetProtocol.UDP);
57+
bindings.put(udpPort, new Ports.Binding[] { Ports.Binding.bindPort(12345) });
58+
59+
when(containerState.getContainerId()).thenReturn("test-container-id");
60+
when(containerState.getContainerInfo()).thenReturn(containerInfo);
61+
when(containerInfo.getNetworkSettings()).thenReturn(networkSettings);
62+
when(networkSettings.getPorts()).thenReturn(ports);
63+
when(ports.getBindings()).thenReturn(bindings);
64+
doCallRealMethod().when(containerState).getMappedPort(5353, InternetProtocol.UDP);
65+
66+
Integer mappedPort = containerState.getMappedPort(5353, InternetProtocol.UDP);
67+
assertThat(mappedPort).isEqualTo(12345);
68+
}
69+
70+
@Test
71+
void shouldGetMappedPortForTcpUsingProtocol() {
72+
ContainerState containerState = mock(ContainerState.class);
73+
InspectContainerResponse containerInfo = mock(InspectContainerResponse.class);
74+
NetworkSettings networkSettings = mock(NetworkSettings.class);
75+
Ports ports = mock(Ports.class);
76+
77+
// Set up the mock port bindings for TCP port 8080
78+
Map<ExposedPort, Ports.Binding[]> bindings = new HashMap<>();
79+
ExposedPort tcpPort = new ExposedPort(8080, com.github.dockerjava.api.model.InternetProtocol.TCP);
80+
bindings.put(tcpPort, new Ports.Binding[] { Ports.Binding.bindPort(54321) });
81+
82+
when(containerState.getContainerId()).thenReturn("test-container-id");
83+
when(containerState.getContainerInfo()).thenReturn(containerInfo);
84+
when(containerInfo.getNetworkSettings()).thenReturn(networkSettings);
85+
when(networkSettings.getPorts()).thenReturn(ports);
86+
when(ports.getBindings()).thenReturn(bindings);
87+
doCallRealMethod().when(containerState).getMappedPort(8080, InternetProtocol.TCP);
88+
doCallRealMethod().when(containerState).getMappedPort(8080);
89+
90+
// Test with explicit TCP protocol
91+
Integer mappedPort = containerState.getMappedPort(8080, InternetProtocol.TCP);
92+
assertThat(mappedPort).isEqualTo(54321);
93+
94+
// Test default getMappedPort (should also return same value since it defaults to TCP)
95+
Integer defaultMappedPort = containerState.getMappedPort(8080);
96+
assertThat(defaultMappedPort).isEqualTo(54321);
97+
}
98+
99+
@Test
100+
void shouldThrowForUnmappedUdpPort() {
101+
ContainerState containerState = mock(ContainerState.class);
102+
InspectContainerResponse containerInfo = mock(InspectContainerResponse.class);
103+
NetworkSettings networkSettings = mock(NetworkSettings.class);
104+
Ports ports = mock(Ports.class);
105+
106+
// Set up the mock port bindings with only TCP port
107+
Map<ExposedPort, Ports.Binding[]> bindings = new HashMap<>();
108+
ExposedPort tcpPort = new ExposedPort(8080, com.github.dockerjava.api.model.InternetProtocol.TCP);
109+
bindings.put(tcpPort, new Ports.Binding[] { Ports.Binding.bindPort(54321) });
110+
111+
when(containerState.getContainerId()).thenReturn("test-container-id");
112+
when(containerState.getContainerInfo()).thenReturn(containerInfo);
113+
when(containerInfo.getNetworkSettings()).thenReturn(networkSettings);
114+
when(networkSettings.getPorts()).thenReturn(ports);
115+
when(ports.getBindings()).thenReturn(bindings);
116+
doCallRealMethod().when(containerState).getMappedPort(8080, InternetProtocol.UDP);
117+
118+
// Should throw when trying to get unmapped UDP port
119+
assertThatThrownBy(() -> containerState.getMappedPort(8080, InternetProtocol.UDP))
120+
.isInstanceOf(IllegalArgumentException.class)
121+
.hasMessageContaining("8080/udp")
122+
.hasMessageContaining("is not mapped");
123+
}
124+
125+
@Test
126+
void shouldSupportBothTcpAndUdpOnSamePort() {
127+
ContainerState containerState = mock(ContainerState.class);
128+
InspectContainerResponse containerInfo = mock(InspectContainerResponse.class);
129+
NetworkSettings networkSettings = mock(NetworkSettings.class);
130+
Ports ports = mock(Ports.class);
131+
132+
// Set up the mock port bindings with both TCP and UDP on the same port
133+
Map<ExposedPort, Ports.Binding[]> bindings = new HashMap<>();
134+
ExposedPort tcpPort = new ExposedPort(5000, com.github.dockerjava.api.model.InternetProtocol.TCP);
135+
ExposedPort udpPort = new ExposedPort(5000, com.github.dockerjava.api.model.InternetProtocol.UDP);
136+
bindings.put(tcpPort, new Ports.Binding[] { Ports.Binding.bindPort(11111) });
137+
bindings.put(udpPort, new Ports.Binding[] { Ports.Binding.bindPort(22222) });
138+
139+
when(containerState.getContainerId()).thenReturn("test-container-id");
140+
when(containerState.getContainerInfo()).thenReturn(containerInfo);
141+
when(containerInfo.getNetworkSettings()).thenReturn(networkSettings);
142+
when(networkSettings.getPorts()).thenReturn(ports);
143+
when(ports.getBindings()).thenReturn(bindings);
144+
doCallRealMethod().when(containerState).getMappedPort(5000, InternetProtocol.TCP);
145+
doCallRealMethod().when(containerState).getMappedPort(5000, InternetProtocol.UDP);
146+
147+
// Both should be mapped to different host ports
148+
Integer tcpMappedPort = containerState.getMappedPort(5000, InternetProtocol.TCP);
149+
Integer udpMappedPort = containerState.getMappedPort(5000, InternetProtocol.UDP);
150+
151+
assertThat(tcpMappedPort).isEqualTo(11111);
152+
assertThat(udpMappedPort).isEqualTo(22222);
153+
}
38154
}

0 commit comments

Comments
 (0)