Skip to content

Commit 20ff3a0

Browse files
authored
Merge branch 'main' into main
2 parents 67e7ce7 + 0200a5d commit 20ff3a0

File tree

11 files changed

+245
-34
lines changed

11 files changed

+245
-34
lines changed

.github/workflows/ci-windows.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ concurrency:
4242
permissions:
4343
contents: read
4444

45+
env:
46+
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }}
47+
4548
jobs:
4649
main:
4750
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}

.github/workflows/moby-latest.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
# nightly build, at 23:59 CEST
77
- cron: '59 23 * * *'
88

9+
env:
10+
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }}
11+
912
jobs:
1013
test_docker:
1114
strategy:

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ private void tryStart() {
550550
} else {
551551
logger().error("There are no stdout/stderr logs available for the failed container");
552552
}
553+
stop();
553554
}
554555

555556
throw new ContainerLaunchException("Could not create/start container", e);
@@ -622,7 +623,14 @@ private void connectToPortForwardingNetwork(String networkMode) {
622623
.map(ContainerNetwork::getNetworkID)
623624
.ifPresent(networkId -> {
624625
if (!Arrays.asList(networkId, "none", "host").contains(networkMode)) {
625-
dockerClient.connectToNetworkCmd().withContainerId(containerId).withNetworkId(networkId).exec();
626+
com.github.dockerjava.api.model.Network network =
627+
this.dockerClient.inspectNetworkCmd().withNetworkId(networkId).exec();
628+
if (!network.getContainers().containsKey(this.containerId)) {
629+
this.dockerClient.connectToNetworkCmd()
630+
.withContainerId(this.containerId)
631+
.withNetworkId(networkId)
632+
.exec();
633+
}
626634
}
627635
});
628636
}
@@ -826,7 +834,7 @@ private void applyConfiguration(CreateContainerCmd createCommand) {
826834
withExtraHost(INTERNAL_HOST_HOSTNAME, it.getIpAddress());
827835
});
828836

829-
String[] extraHostsArray = extraHosts.stream().toArray(String[]::new);
837+
String[] extraHostsArray = extraHosts.stream().distinct().toArray(String[]::new);
830838
createCommand.withExtraHosts(extraHostsArray);
831839

832840
if (workingDirectory != null) {

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

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,6 @@ private synchronized void maybeStart() {
7777

7878
ryukContainer.start();
7979

80-
Runtime
81-
.getRuntime()
82-
.addShutdownHook(
83-
new Thread(
84-
DockerClientFactory.TESTCONTAINERS_THREAD_GROUP,
85-
() -> {
86-
this.dockerClient.killContainerCmd(this.ryukContainer.getContainerId())
87-
.withSignal("SIGTERM")
88-
.exec();
89-
}
90-
)
91-
);
92-
9380
CountDownLatch ryukScheduledLatch = new CountDownLatch(1);
9481

9582
String host = ryukContainer.getHost();

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

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
import org.junit.AfterClass;
88
import org.junit.BeforeClass;
99
import org.junit.Test;
10+
import org.junit.runner.Description;
11+
import org.junit.runners.model.Statement;
12+
import org.testcontainers.DockerClientFactory;
1013
import org.testcontainers.TestImages;
1114
import org.testcontainers.Testcontainers;
15+
import org.testcontainers.utility.TestcontainersConfiguration;
1216

17+
import java.io.IOException;
1318
import java.io.OutputStream;
1419
import java.net.InetSocketAddress;
20+
import java.util.List;
21+
import java.util.UUID;
1522

1623
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.assertj.core.api.Assumptions.assumeThat;
1725

1826
public class ExposedHostTest {
1927

@@ -33,7 +41,6 @@ public static void setUpClass() throws Exception {
3341
}
3442
}
3543
);
36-
3744
server.start();
3845
}
3946

@@ -82,6 +89,61 @@ public void testExposedHostPortOnFixedInternalPorts() {
8289
assertResponse(new GenericContainer<>(tinyContainerDef()), 81);
8390
}
8491

92+
@Test
93+
public void testExposedHostWithReusableContainerAndFixedNetworkName() throws IOException, InterruptedException {
94+
assumeThat(TestcontainersConfiguration.getInstance().environmentSupportsReuse()).isTrue();
95+
Network network = createReusableNetwork(UUID.randomUUID());
96+
Testcontainers.exposeHostPorts(server.getAddress().getPort());
97+
98+
GenericContainer<?> container = new GenericContainer<>(tinyContainerDef()).withReuse(true).withNetwork(network);
99+
container.start();
100+
101+
assertHttpResponseFromHost(container, server.getAddress().getPort());
102+
103+
PortForwardingContainer.INSTANCE.reset();
104+
Testcontainers.exposeHostPorts(server.getAddress().getPort());
105+
106+
GenericContainer<?> reusedContainer = new GenericContainer<>(tinyContainerDef())
107+
.withReuse(true)
108+
.withNetwork(network);
109+
reusedContainer.start();
110+
111+
assertThat(reusedContainer.getContainerId()).isEqualTo(container.getContainerId());
112+
assertHttpResponseFromHost(reusedContainer, server.getAddress().getPort());
113+
114+
container.stop();
115+
reusedContainer.stop();
116+
DockerClientFactory.lazyClient().removeNetworkCmd(network.getId()).exec();
117+
}
118+
119+
@Test
120+
public void testExposedHostOnFixedInternalPortsWithReusableContainerAndFixedNetworkName()
121+
throws IOException, InterruptedException {
122+
assumeThat(TestcontainersConfiguration.getInstance().environmentSupportsReuse()).isTrue();
123+
Network network = createReusableNetwork(UUID.randomUUID());
124+
Testcontainers.exposeHostPorts(ImmutableMap.of(server.getAddress().getPort(), 1234));
125+
126+
GenericContainer<?> container = new GenericContainer<>(tinyContainerDef()).withReuse(true).withNetwork(network);
127+
container.start();
128+
129+
assertHttpResponseFromHost(container, 1234);
130+
131+
PortForwardingContainer.INSTANCE.reset();
132+
Testcontainers.exposeHostPorts(ImmutableMap.of(server.getAddress().getPort(), 1234));
133+
134+
GenericContainer<?> reusedContainer = new GenericContainer<>(tinyContainerDef())
135+
.withReuse(true)
136+
.withNetwork(network);
137+
reusedContainer.start();
138+
139+
assertThat(reusedContainer.getContainerId()).isEqualTo(container.getContainerId());
140+
assertHttpResponseFromHost(reusedContainer, 1234);
141+
142+
container.stop();
143+
reusedContainer.stop();
144+
DockerClientFactory.lazyClient().removeNetworkCmd(network.getId()).exec();
145+
}
146+
85147
@SneakyThrows
86148
protected void assertResponse(GenericContainer<?> container, int port) {
87149
try {
@@ -108,4 +170,40 @@ private static class TinyContainerDef extends ContainerDef {
108170
setCommand("top");
109171
}
110172
}
173+
174+
private void assertHttpResponseFromHost(GenericContainer<?> container, int port)
175+
throws IOException, InterruptedException {
176+
String httpResponseFromHost = container
177+
.execInContainer("wget", "-O", "-", "http://host.testcontainers.internal:" + port)
178+
.getStdout();
179+
assertThat(httpResponseFromHost).isEqualTo("Hello World!");
180+
}
181+
182+
private static Network createReusableNetwork(UUID name) {
183+
String networkName = name.toString();
184+
Network network = new Network() {
185+
@Override
186+
public String getId() {
187+
return networkName;
188+
}
189+
190+
@Override
191+
public void close() {}
192+
193+
@Override
194+
public Statement apply(Statement base, Description description) {
195+
return null;
196+
}
197+
};
198+
199+
List<com.github.dockerjava.api.model.Network> networks = DockerClientFactory
200+
.lazyClient()
201+
.listNetworksCmd()
202+
.withNameFilter(networkName)
203+
.exec();
204+
if (networks.isEmpty()) {
205+
Network.builder().createNetworkCmdModifier(cmd -> cmd.withName(networkName)).build().getId();
206+
}
207+
return network;
208+
}
111209
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import com.github.dockerjava.api.DockerClient;
66
import com.github.dockerjava.api.command.InspectContainerResponse;
77
import com.github.dockerjava.api.command.InspectContainerResponse.ContainerState;
8+
import com.github.dockerjava.api.model.Container;
89
import com.github.dockerjava.api.model.ExposedPort;
910
import com.github.dockerjava.api.model.Info;
1011
import com.github.dockerjava.api.model.Ports;
12+
import com.google.common.base.MoreObjects;
13+
import com.google.common.collect.ImmutableList;
1114
import lombok.RequiredArgsConstructor;
1215
import lombok.SneakyThrows;
1316
import lombok.experimental.FieldDefaults;
@@ -28,9 +31,12 @@
2831
import org.testcontainers.utility.DockerImageName;
2932
import org.testcontainers.utility.MountableFile;
3033

34+
import java.time.Duration;
3135
import java.util.Arrays;
36+
import java.util.Collections;
3237
import java.util.List;
3338
import java.util.Map;
39+
import java.util.Optional;
3440
import java.util.concurrent.TimeUnit;
3541
import java.util.function.Predicate;
3642
import java.util.stream.Collectors;
@@ -273,6 +279,63 @@ public void shouldRespectWaitStrategy() {
273279
}
274280
}
275281

282+
@Test
283+
public void testStartupAttemptsDoesNotLeaveContainersRunningWhenWrongWaitStrategyIsUsed() {
284+
try (
285+
GenericContainer<?> container = new GenericContainer<>(TestImages.TINY_IMAGE)
286+
.withLabel("waitstrategy", "wrong")
287+
.withStartupAttempts(3)
288+
.waitingFor(
289+
Wait.forLogMessage("this text does not exist in logs", 1).withStartupTimeout(Duration.ofMillis(1))
290+
)
291+
.withCommand("tail", "-f", "/dev/null");
292+
) {
293+
assertThatThrownBy(container::start).hasStackTraceContaining("Retry limit hit with exception");
294+
}
295+
assertThat(reportLeakedContainers()).isEmpty();
296+
}
297+
298+
private static Optional<String> reportLeakedContainers() {
299+
@SuppressWarnings("resource") // Throws when close is attempted, as this is a global instance.
300+
DockerClient dockerClient = DockerClientFactory.lazyClient();
301+
302+
List<Container> containers = dockerClient
303+
.listContainersCmd()
304+
.withAncestorFilter(Collections.singletonList("alpine:3.17"))
305+
.withLabelFilter(
306+
Arrays.asList(
307+
DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL + "=" + DockerClientFactory.SESSION_ID,
308+
"waitstrategy=wrong"
309+
)
310+
)
311+
// ignore status "exited" - for example, failed containers after using `withStartupAttempts()`
312+
.withStatusFilter(Arrays.asList("created", "restarting", "running", "paused"))
313+
.exec()
314+
.stream()
315+
.collect(ImmutableList.toImmutableList());
316+
317+
if (containers.isEmpty()) {
318+
return Optional.empty();
319+
}
320+
321+
return Optional.of(
322+
String.format(
323+
"Leaked containers: %s",
324+
containers
325+
.stream()
326+
.map(container -> {
327+
return MoreObjects
328+
.toStringHelper("container")
329+
.add("id", container.getId())
330+
.add("image", container.getImage())
331+
.add("imageId", container.getImageId())
332+
.toString();
333+
})
334+
.collect(Collectors.joining(", ", "[", "]"))
335+
)
336+
);
337+
}
338+
276339
static class NoopStartupCheckStrategy extends StartupCheckStrategy {
277340

278341
@Override

docs/features/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ but does not allow starting privileged containers, you can turn off the Ryuk con
9898
9999
## Customizing client ping behaviour
100100

101-
> **client.ping.timeout = 5**
101+
> **client.ping.timeout = 10**
102102
> Specifies for how long Testcontainers will try to connect to the Docker client to obtain valid info about the client before giving up and trying next strategy, if applicable (in seconds).
103103
104104
## Customizing Docker host detection

modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.testcontainers.clickhouse;
22

33
import org.testcontainers.containers.JdbcDatabaseContainer;
4-
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
4+
import org.testcontainers.containers.wait.strategy.Wait;
55
import org.testcontainers.utility.DockerImageName;
66

77
import java.time.Duration;
@@ -56,11 +56,14 @@ public ClickHouseContainer(final DockerImageName dockerImageName) {
5656
dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME);
5757

5858
addExposedPorts(HTTP_PORT, NATIVE_PORT);
59-
this.waitStrategy =
60-
new HttpWaitStrategy()
59+
waitingFor(
60+
Wait
61+
.forHttp("/")
62+
.forPort(HTTP_PORT)
6163
.forStatusCode(200)
6264
.forResponsePredicate("Ok."::equals)
63-
.withStartupTimeout(Duration.ofMinutes(1));
65+
.withStartupTimeout(Duration.ofMinutes(1))
66+
);
6467
}
6568

6669
@Override
@@ -130,4 +133,9 @@ public ClickHouseContainer withDatabaseName(String databaseName) {
130133
this.databaseName = databaseName;
131134
return this;
132135
}
136+
137+
@Override
138+
protected void waitUntilContainerStarted() {
139+
getWaitStrategy().waitUntilReady(this);
140+
}
133141
}

0 commit comments

Comments
 (0)