Skip to content

Commit 034daa6

Browse files
kiviewbsideup
andauthored
Remove withPublishAllPorts from Ryuk and stabilize containerInfo content on start (testcontainers#4263)
`containerInfo` is queried until all ports expected by Testcontainers are mapped. Introduces Awaitility for retry logic. Removing `withPublishAllPorts` is especially important because else this can lead to publishing of excluded ports on Windows Co-authored-by: Sergei Egorov <[email protected]>
1 parent 3c614f2 commit 034daa6

File tree

6 files changed

+137
-10
lines changed

6 files changed

+137
-10
lines changed

core/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ dependencies {
108108
exclude(group: 'org.jetbrains', module: 'annotations')
109109
}
110110

111+
shaded 'org.awaitility:awaitility:4.1.0'
112+
111113
api "com.github.docker-java:docker-java-api:3.2.11"
112114

113115
shaded ('com.github.docker-java:docker-java-core:3.2.11') {

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.testcontainers.utility.DockerImageName;
5858
import org.testcontainers.utility.DockerLoggerFactory;
5959
import org.testcontainers.utility.DockerMachineClient;
60+
import org.testcontainers.utility.DynamicPollInterval;
6061
import org.testcontainers.utility.MountableFile;
6162
import org.testcontainers.utility.PathUtils;
6263
import org.testcontainers.utility.ResourceReaper;
@@ -84,6 +85,7 @@
8485
import java.util.List;
8586
import java.util.Map;
8687
import java.util.Map.Entry;
88+
import java.util.Objects;
8789
import java.util.Optional;
8890
import java.util.Set;
8991
import java.util.concurrent.ExecutionException;
@@ -97,6 +99,7 @@
9799
import java.util.zip.Checksum;
98100

99101
import static com.google.common.collect.Lists.newArrayList;
102+
import static org.awaitility.Awaitility.await;
100103
import static org.testcontainers.utility.CommandLine.runShellCommand;
101104

102105
/**
@@ -216,10 +219,10 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>
216219

217220
private static final Set<String> AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>();
218221
private static final RateLimiter DOCKER_CLIENT_RATE_LIMITER = RateLimiterBuilder
219-
.newBuilder()
220-
.withRate(1, TimeUnit.SECONDS)
221-
.withConstantThroughput()
222-
.build();
222+
.newBuilder()
223+
.withRate(1, TimeUnit.SECONDS)
224+
.withConstantThroughput()
225+
.build();
223226

224227
@Nullable
225228
private Map<String, String> tmpFsMapping;
@@ -425,8 +428,30 @@ private void tryStart(Instant startedAt) {
425428
// For all registered output consumers, start following as close to container startup as possible
426429
this.logConsumers.forEach(this::followOutput);
427430

431+
// Wait until inspect container returns the mapped ports
432+
containerInfo = await()
433+
.atMost(5, TimeUnit.SECONDS)
434+
.pollInterval(DynamicPollInterval.ofMillis(50))
435+
.pollInSameThread()
436+
.until(
437+
() -> dockerClient.inspectContainerCmd(containerId).exec(),
438+
inspectContainerResponse -> {
439+
Set<Integer> exposedAndMappedPorts = inspectContainerResponse
440+
.getNetworkSettings()
441+
.getPorts()
442+
.getBindings()
443+
.entrySet()
444+
.stream()
445+
.filter(it -> Objects.nonNull(it.getValue())) // filter out exposed but not yet mapped
446+
.map(Entry::getKey)
447+
.map(ExposedPort::getPort)
448+
.collect(Collectors.toSet());
449+
450+
return exposedAndMappedPorts.containsAll(exposedPorts);
451+
}
452+
);
453+
428454
// Tell subclasses that we're starting
429-
containerInfo = dockerClient.inspectContainerCmd(containerId).exec();
430455
containerIsStarting(containerInfo, reused);
431456

432457
// Wait until the container has reached the desired running state
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.testcontainers.utility;
2+
3+
import org.awaitility.pollinterval.PollInterval;
4+
5+
import java.time.Duration;
6+
import java.time.Instant;
7+
8+
/**
9+
* Awaitility {@link org.awaitility.pollinterval.PollInterval} that takes execution time into consideration,
10+
* to allow a constant poll-interval, as opposed to Awaitility's default poll-delay behaviour.
11+
*
12+
* @deprecated For internal usage only.
13+
*/
14+
@Deprecated
15+
public class DynamicPollInterval implements PollInterval {
16+
17+
final Duration interval;
18+
Instant lastTimestamp;
19+
20+
private DynamicPollInterval(Duration interval) {
21+
this.interval = interval;
22+
lastTimestamp = Instant.now();
23+
}
24+
25+
public static DynamicPollInterval of(Duration duration) {
26+
return new DynamicPollInterval(duration);
27+
}
28+
29+
public static DynamicPollInterval ofMillis(long millis) {
30+
return DynamicPollInterval.of(Duration.ofMillis(millis));
31+
}
32+
33+
@Override
34+
public Duration next(int pollCount, Duration previousDuration) {
35+
Instant now = Instant.now();
36+
Duration executionDuration = Duration.between(lastTimestamp, now);
37+
38+
Duration result = interval.minusMillis(Math.min(interval.toMillis(), executionDuration.toMillis()));
39+
lastTimestamp = now.plus(result);
40+
return result;
41+
}
42+
}

core/src/main/java/org/testcontainers/utility/ResourceReaper.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import com.github.dockerjava.api.model.Frame;
1010
import com.github.dockerjava.api.model.HostConfig;
1111
import com.github.dockerjava.api.model.Network;
12+
import com.github.dockerjava.api.model.PortBinding;
13+
import com.github.dockerjava.api.model.Ports;
1214
import com.github.dockerjava.api.model.Volume;
1315
import com.google.common.annotations.VisibleForTesting;
1416
import com.google.common.base.Throwables;
@@ -32,11 +34,13 @@
3234
import java.net.Socket;
3335
import java.net.URLEncoder;
3436
import java.nio.charset.StandardCharsets;
37+
import java.time.Duration;
3538
import java.util.AbstractMap.SimpleEntry;
3639
import java.util.ArrayList;
3740
import java.util.Collections;
3841
import java.util.List;
3942
import java.util.Map;
43+
import java.util.Objects;
4044
import java.util.Set;
4145
import java.util.concurrent.ConcurrentHashMap;
4246
import java.util.concurrent.CountDownLatch;
@@ -45,6 +49,8 @@
4549
import java.util.stream.Collectors;
4650
import java.util.stream.Stream;
4751

52+
import static org.awaitility.Awaitility.await;
53+
4854
/**
4955
* Component that responsible for container removal and automatic cleanup of dead containers at JVM shutdown.
5056
*/
@@ -96,10 +102,14 @@ public static String start(DockerClient client) {
96102
List<Bind> binds = new ArrayList<>();
97103
binds.add(new Bind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), new Volume("/var/run/docker.sock")));
98104

105+
ExposedPort ryukExposedPort = ExposedPort.tcp(8080);
99106
String ryukContainerId = client.createContainerCmd(ryukImage)
100-
.withHostConfig(new HostConfig().withAutoRemove(true))
101-
.withExposedPorts(new ExposedPort(8080))
102-
.withPublishAllPorts(true)
107+
.withHostConfig(
108+
new HostConfig()
109+
.withAutoRemove(true)
110+
.withPortBindings(new PortBinding(Ports.Binding.empty(), ryukExposedPort))
111+
)
112+
.withExposedPorts(ryukExposedPort)
103113
.withName("testcontainers-ryuk-" + DockerClientFactory.SESSION_ID)
104114
.withLabels(Collections.singletonMap(DockerClientFactory.TESTCONTAINERS_LABEL, "true"))
105115
.withBinds(binds)
@@ -125,7 +135,23 @@ public void onNext(Frame frame) {
125135

126136
ContainerState containerState = new ContainerState() {
127137

128-
final InspectContainerResponse inspectedContainer = client.inspectContainerCmd(ryukContainerId).exec();
138+
// inspect container response might initially not contain the mapped port
139+
final InspectContainerResponse inspectedContainer = await()
140+
.atMost(5, TimeUnit.SECONDS)
141+
.pollInterval(DynamicPollInterval.ofMillis(50))
142+
.pollInSameThread()
143+
.until(
144+
() -> client.inspectContainerCmd(ryukContainerId).exec(),
145+
inspectContainerResponse -> {
146+
return inspectContainerResponse
147+
.getNetworkSettings()
148+
.getPorts()
149+
.getBindings()
150+
.values()
151+
.stream()
152+
.anyMatch(Objects::nonNull);
153+
}
154+
);
129155

130156
@Override
131157
public List<Integer> getExposedPorts() {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.rnorth.ducttape.unreliables.Unreliables;
1818
import org.testcontainers.DockerClientFactory;
1919
import org.testcontainers.TestImages;
20+
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
2021
import org.testcontainers.containers.startupcheck.StartupCheckStrategy;
2122
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
2223
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -29,9 +30,11 @@
2930
import java.util.stream.Collectors;
3031

3132
import static org.assertj.core.api.Assertions.assertThatThrownBy;
33+
import static org.hamcrest.CoreMatchers.equalTo;
3234
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
3335
import static org.rnorth.visibleassertions.VisibleAssertions.assertThrows;
3436
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
37+
import static org.testcontainers.TestImages.TINY_IMAGE;
3538

3639
public class GenericContainerTest {
3740

@@ -119,6 +122,30 @@ public void shouldOnlyPublishExposedPorts() {
119122
}
120123
}
121124

125+
@Test
126+
public void shouldWaitUntilExposedPortIsMapped() {
127+
128+
ImageFromDockerfile image = new ImageFromDockerfile("publish-multiple")
129+
.withDockerfileFromBuilder(builder ->
130+
builder
131+
.from("testcontainers/helloworld:1.1.0")
132+
.expose(8080, 8081) // one additional port exposed in image
133+
.build()
134+
);
135+
136+
try (
137+
GenericContainer container = new GenericContainer<>(image)
138+
.withExposedPorts(8080)
139+
.withCreateContainerCmdModifier(it -> it.withExposedPorts(ExposedPort.tcp(8082))) // another port exposed by modifier
140+
) {
141+
142+
container.start();
143+
144+
assertEquals("Only withExposedPort should be exposed", 1, container.getExposedPorts().size());
145+
assertTrue("withExposedPort should be exposed", container.getExposedPorts().contains(8080));
146+
}
147+
}
148+
122149
static class NoopStartupCheckStrategy extends StartupCheckStrategy {
123150

124151
@Override

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.github.dockerjava.api.command.ListContainersCmd;
1111
import com.github.dockerjava.api.command.StartContainerCmd;
1212
import com.github.dockerjava.api.model.Container;
13+
import com.github.dockerjava.api.model.NetworkSettings;
1314
import com.github.dockerjava.core.command.CreateContainerCmdImpl;
1415
import com.github.dockerjava.core.command.InspectContainerCmdImpl;
1516
import com.github.dockerjava.core.command.ListContainersCmdImpl;
@@ -22,6 +23,8 @@
2223
import org.junit.runner.RunWith;
2324
import org.junit.runners.BlockJUnit4ClassRunner;
2425
import org.junit.runners.Parameterized;
26+
import org.mockito.Answers;
27+
import org.mockito.Mock;
2528
import org.mockito.Mockito;
2629
import org.mockito.stubbing.Answer;
2730
import org.rnorth.visibleassertions.VisibleAssertions;
@@ -520,7 +523,9 @@ protected Answer<StartContainerCmd> startContainerAnswer() {
520523
protected Answer<InspectContainerCmd> inspectContainerAnswer() {
521524
return invocation -> {
522525
InspectContainerCmd.Exec exec = command -> {
523-
return new InspectContainerResponse();
526+
InspectContainerResponse stubResponse = Mockito.mock(InspectContainerResponse.class, Answers.RETURNS_DEEP_STUBS);
527+
when(stubResponse.getNetworkSettings().getPorts().getBindings()).thenReturn(Collections.emptyMap());
528+
return stubResponse;
524529
};
525530
return new InspectContainerCmdImpl(exec, invocation.getArgument(0));
526531
};

0 commit comments

Comments
 (0)