Skip to content

Commit 81952c4

Browse files
bsideuprnorth
authored andcommitted
Support prioritization of DockerClientProviderStrategies (#362)
* Support prioritization of DockerClientProviderStrategies * Support storing the global configuration in user's home folder. Store selected DockerClientProviderStrategy globally. * Add changelog and priority JavaDoc
1 parent 0997c0f commit 81952c4

8 files changed

+248
-97
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
77
- Added pre-flight checks (can be disabled with `checks.disable` configuration property) (#363)
88
- Removed unused Jersey dependencies (#361)
99
- Fixed non-POSIX fallback for file attribute reading (#371)
10+
- Improved startup time by adding dynamic priorities to DockerClientProviderStrategy (#362)
11+
- Added global configuration file `~/.testcontainers.properties` (#362)
1012

1113
## [1.3.0] - 2017-06-05
1214
### Fixed

core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55
import com.github.dockerjava.core.DockerClientConfig;
66
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
77
import com.google.common.base.Throwables;
8+
import org.apache.commons.io.IOUtils;
89
import org.jetbrains.annotations.Nullable;
10+
import org.rnorth.ducttape.TimeoutException;
911
import org.rnorth.ducttape.ratelimits.RateLimiter;
1012
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
1113
import org.rnorth.ducttape.unreliables.Unreliables;
1214
import org.slf4j.Logger;
1315
import org.slf4j.LoggerFactory;
16+
import org.testcontainers.utility.TestcontainersConfiguration;
1417

1518
import java.util.ArrayList;
19+
import java.util.Comparator;
1620
import java.util.List;
21+
import java.util.Objects;
1722
import java.util.concurrent.TimeUnit;
23+
import java.util.stream.Stream;
1824

1925
/**
2026
* Mechanism to find a viable Docker client configuration according to the host system environment.
@@ -39,6 +45,17 @@ public abstract class DockerClientProviderStrategy {
3945
*/
4046
public abstract String getDescription();
4147

48+
protected boolean isApplicable() {
49+
return true;
50+
}
51+
52+
/**
53+
* @return highest to lowest priority value
54+
*/
55+
protected int getPriority() {
56+
return 0;
57+
}
58+
4259
protected static final Logger LOGGER = LoggerFactory.getLogger(DockerClientProviderStrategy.class);
4360

4461
/**
@@ -49,45 +66,70 @@ public abstract class DockerClientProviderStrategy {
4966
public static DockerClientProviderStrategy getFirstValidStrategy(List<DockerClientProviderStrategy> strategies) {
5067
List<String> configurationFailures = new ArrayList<>();
5168

52-
for (DockerClientProviderStrategy strategy : strategies) {
53-
try {
54-
strategy.test();
55-
LOGGER.info("Looking for Docker environment. Tried {}", strategy.getDescription());
56-
return strategy;
57-
} catch (Exception | ExceptionInInitializerError | NoClassDefFoundError e) {
58-
@Nullable String throwableMessage = e.getMessage();
59-
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
60-
Throwable rootCause = Throwables.getRootCause(e);
61-
@Nullable String rootCauseMessage = rootCause.getMessage();
62-
63-
String failureDescription;
64-
if (throwableMessage != null && throwableMessage.equals(rootCauseMessage)) {
65-
failureDescription = String.format("%s: failed with exception %s (%s)",
66-
strategy.getClass().getSimpleName(),
67-
e.getClass().getSimpleName(),
68-
throwableMessage);
69-
} else {
70-
failureDescription = String.format("%s: failed with exception %s (%s). Root cause %s (%s)",
71-
strategy.getClass().getSimpleName(),
72-
e.getClass().getSimpleName(),
73-
throwableMessage,
74-
rootCause.getClass().getSimpleName(),
75-
rootCauseMessage
76-
);
77-
}
78-
configurationFailures.add(failureDescription);
79-
80-
LOGGER.debug(failureDescription);
81-
}
82-
}
83-
84-
LOGGER.error("Could not find a valid Docker environment. Please check configuration. Attempted configurations were:");
85-
for (String failureMessage : configurationFailures) {
86-
LOGGER.error(" " + failureMessage);
87-
}
88-
LOGGER.error("As no valid configuration was found, execution cannot continue");
89-
90-
throw new IllegalStateException("Could not find a valid Docker environment. Please see logs and check configuration");
69+
return Stream
70+
.concat(
71+
Stream
72+
.of(TestcontainersConfiguration.getInstance().getDockerClientStrategyClassName())
73+
.filter(Objects::nonNull)
74+
.flatMap(it -> {
75+
try {
76+
Class<? extends DockerClientProviderStrategy> strategyClass = (Class) Thread.currentThread().getContextClassLoader().loadClass(it);
77+
return Stream.of(strategyClass.newInstance());
78+
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
79+
LOGGER.warn("Can't instantiate a strategy from " + it, e);
80+
return Stream.empty();
81+
}
82+
}),
83+
strategies
84+
.stream()
85+
.filter(DockerClientProviderStrategy::isApplicable)
86+
.sorted(Comparator.comparing(DockerClientProviderStrategy::getPriority).reversed())
87+
)
88+
.flatMap(strategy -> {
89+
try {
90+
strategy.test();
91+
LOGGER.info("Found Docker environment with {}", strategy.getDescription());
92+
93+
TestcontainersConfiguration.getInstance().updateGlobalConfig("docker.client.strategy", strategy.getClass().getName());
94+
95+
return Stream.of(strategy);
96+
} catch (Exception | ExceptionInInitializerError | NoClassDefFoundError e) {
97+
@Nullable String throwableMessage = e.getMessage();
98+
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
99+
Throwable rootCause = Throwables.getRootCause(e);
100+
@Nullable String rootCauseMessage = rootCause.getMessage();
101+
102+
String failureDescription;
103+
if (throwableMessage != null && throwableMessage.equals(rootCauseMessage)) {
104+
failureDescription = String.format("%s: failed with exception %s (%s)",
105+
strategy.getClass().getSimpleName(),
106+
e.getClass().getSimpleName(),
107+
throwableMessage);
108+
} else {
109+
failureDescription = String.format("%s: failed with exception %s (%s). Root cause %s (%s)",
110+
strategy.getClass().getSimpleName(),
111+
e.getClass().getSimpleName(),
112+
throwableMessage,
113+
rootCause.getClass().getSimpleName(),
114+
rootCauseMessage
115+
);
116+
}
117+
configurationFailures.add(failureDescription);
118+
119+
LOGGER.debug(failureDescription);
120+
return Stream.empty();
121+
}
122+
})
123+
.findAny()
124+
.orElseThrow(() -> {
125+
LOGGER.error("Could not find a valid Docker environment. Please check configuration. Attempted configurations were:");
126+
for (String failureMessage : configurationFailures) {
127+
LOGGER.error(" " + failureMessage);
128+
}
129+
LOGGER.error("As no valid configuration was found, execution cannot continue");
130+
131+
return new IllegalStateException("Could not find a valid Docker environment. Please see logs and check configuration");
132+
});
91133
}
92134

93135
/**
@@ -105,13 +147,18 @@ protected DockerClient getClientForConfig(DockerClientConfig config) {
105147
}
106148

107149
protected void ping(DockerClient client, int timeoutInSeconds) {
108-
Unreliables.retryUntilSuccess(timeoutInSeconds, TimeUnit.SECONDS, () -> {
109-
return PING_RATE_LIMITER.getWhenReady(() -> {
110-
LOGGER.debug("Pinging docker daemon...");
111-
client.pingCmd().exec();
112-
return true;
150+
try {
151+
Unreliables.retryUntilSuccess(timeoutInSeconds, TimeUnit.SECONDS, () -> {
152+
return PING_RATE_LIMITER.getWhenReady(() -> {
153+
LOGGER.debug("Pinging docker daemon...");
154+
client.pingCmd().exec();
155+
return true;
156+
});
113157
});
114-
});
158+
} catch (TimeoutException e) {
159+
IOUtils.closeQuietly(client);
160+
throw e;
161+
}
115162
}
116163

117164
public String getDockerHostIpAddress() {

core/src/main/java/org/testcontainers/dockerclient/DockerMachineClientProviderStrategy.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.testcontainers.dockerclient;
22

33
import com.github.dockerjava.core.DefaultDockerClientConfig;
4+
import lombok.extern.slf4j.Slf4j;
45
import org.testcontainers.utility.CommandLine;
56
import org.testcontainers.utility.DockerMachineClient;
67

@@ -13,10 +14,21 @@
1314
/**
1415
* Use Docker machine (if available on the PATH) to locate a Docker environment.
1516
*/
17+
@Slf4j
1618
public class DockerMachineClientProviderStrategy extends DockerClientProviderStrategy {
1719
private static final String PING_TIMEOUT_DEFAULT = "30";
1820
private static final String PING_TIMEOUT_PROPERTY_NAME = "testcontainers.dockermachineprovider.timeout";
1921

22+
@Override
23+
protected boolean isApplicable() {
24+
return DockerMachineClient.instance().isInstalled();
25+
}
26+
27+
@Override
28+
protected int getPriority() {
29+
return ProxiedUnixSocketClientProviderStrategy.PRIORITY - 10;
30+
}
31+
2032
@Override
2133
public void test() throws InvalidConfigurationException {
2234

@@ -28,13 +40,13 @@ public void test() throws InvalidConfigurationException {
2840
checkArgument(machineNameOptional.isPresent(), "docker-machine is installed but no default machine could be found");
2941
String machineName = machineNameOptional.get();
3042

31-
LOGGER.info("Found docker-machine, and will use machine named {}", machineName);
43+
log.info("Found docker-machine, and will use machine named {}", machineName);
3244

3345
DockerMachineClient.instance().ensureMachineRunning(machineName);
3446

3547
String dockerDaemonIpAddress = DockerMachineClient.instance().getDockerDaemonIpAddress(machineName);
3648

37-
LOGGER.info("Docker daemon IP address for docker machine {} is {}", machineName, dockerDaemonIpAddress);
49+
log.info("Docker daemon IP address for docker machine {} is {}", machineName, dockerDaemonIpAddress);
3850

3951
config = DefaultDockerClientConfig.createDefaultConfigBuilder()
4052
.withDockerHost("tcp://" + dockerDaemonIpAddress + ":2376")

core/src/main/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategy.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,50 @@
22

33
import com.github.dockerjava.core.DefaultDockerClientConfig;
44
import com.github.dockerjava.core.DockerClientConfig;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.apache.commons.lang.SystemUtils;
57

68
/**
79
* Use environment variables and system properties (as supported by the underlying DockerClient DefaultConfigBuilder)
810
* to try and locate a docker environment.
911
*/
12+
@Slf4j
1013
public class EnvironmentAndSystemPropertyClientProviderStrategy extends DockerClientProviderStrategy {
14+
15+
public static final int PRIORITY = 100;
16+
1117
private static final String PING_TIMEOUT_DEFAULT = "10";
1218
private static final String PING_TIMEOUT_PROPERTY_NAME = "testcontainers.environmentprovider.timeout";
1319

20+
public EnvironmentAndSystemPropertyClientProviderStrategy() {
21+
// Try using environment variables
22+
config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
23+
}
24+
25+
@Override
26+
protected boolean isApplicable() {
27+
return "tcp".equalsIgnoreCase(config.getDockerHost().getScheme()) || SystemUtils.IS_OS_LINUX;
28+
}
29+
30+
@Override
31+
protected int getPriority() {
32+
return PRIORITY;
33+
}
34+
1435
@Override
1536
public void test() throws InvalidConfigurationException {
1637

1738
try {
18-
// Try using environment variables
19-
config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
2039
client = getClientForConfig(config);
2140

2241
final int timeout = Integer.parseInt(System.getProperty(PING_TIMEOUT_PROPERTY_NAME, PING_TIMEOUT_DEFAULT));
2342
ping(client, timeout);
2443
} catch (Exception | UnsatisfiedLinkError e) {
25-
LOGGER.error("ping failed with configuration {} due to {}", getDescription(), e.toString(), e);
44+
log.error("ping failed with configuration {} due to {}", getDescription(), e.toString(), e);
2645
throw new InvalidConfigurationException("ping failed");
2746
}
2847

29-
LOGGER.info("Found docker client settings from environment");
48+
log.info("Found docker client settings from environment");
3049
}
3150

3251
@Override

core/src/main/java/org/testcontainers/dockerclient/ProxiedUnixSocketClientProviderStrategy.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
11
package org.testcontainers.dockerclient;
22

3+
import lombok.extern.slf4j.Slf4j;
4+
import org.apache.commons.lang.SystemUtils;
35
import org.rnorth.tcpunixsocketproxy.TcpToUnixSocketProxy;
46

57
import java.io.File;
68

9+
@Slf4j
710
public class ProxiedUnixSocketClientProviderStrategy extends UnixSocketClientProviderStrategy {
811

12+
public static final int PRIORITY = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 10;
13+
14+
private final File socketFile = new File(DOCKER_SOCK_PATH);
15+
916
@Override
10-
public void test() throws InvalidConfigurationException {
17+
protected boolean isApplicable() {
18+
return !SystemUtils.IS_OS_LINUX && socketFile.exists();
19+
}
1120

12-
String osName = System.getProperty("os.name").toLowerCase();
13-
if (!osName.contains("mac") && !osName.contains("linux")) {
14-
throw new InvalidConfigurationException("this strategy is only applicable to OS X and Linux");
15-
}
21+
@Override
22+
protected int getPriority() {
23+
return PRIORITY;
24+
}
1625

17-
TcpToUnixSocketProxy proxy = new TcpToUnixSocketProxy(new File(DOCKER_SOCK_PATH));
26+
@Override
27+
public void test() throws InvalidConfigurationException {
28+
TcpToUnixSocketProxy proxy = new TcpToUnixSocketProxy(socketFile);
1829

1930
try {
2031
int proxyPort = proxy.start().getPort();
2132

2233
config = tryConfiguration("tcp://localhost:" + proxyPort);
2334

24-
LOGGER.info("Accessing unix domain socket via TCP proxy (" + DOCKER_SOCK_PATH + " via localhost:" + proxyPort + ")");
35+
log.debug("Accessing unix domain socket via TCP proxy (" + DOCKER_SOCK_PATH + " via localhost:" + proxyPort + ")");
2536
} catch (Exception e) {
2637

2738
proxy.stop();

core/src/main/java/org/testcontainers/dockerclient/UnixSocketClientProviderStrategy.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,33 @@
22

33
import com.github.dockerjava.core.DefaultDockerClientConfig;
44
import com.github.dockerjava.core.DockerClientConfig;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.apache.commons.lang.SystemUtils;
57
import org.jetbrains.annotations.NotNull;
68

79
import java.io.IOException;
810
import java.nio.file.Files;
911
import java.nio.file.Path;
1012
import java.nio.file.Paths;
1113

14+
@Slf4j
1215
public class UnixSocketClientProviderStrategy extends DockerClientProviderStrategy {
1316
protected static final String DOCKER_SOCK_PATH = "/var/run/docker.sock";
1417
private static final String SOCKET_LOCATION = "unix://" + DOCKER_SOCK_PATH;
1518
private static final int SOCKET_FILE_MODE_MASK = 0xc000;
1619
private static final String PING_TIMEOUT_DEFAULT = "10";
1720
private static final String PING_TIMEOUT_PROPERTY_NAME = "testcontainers.unixsocketprovider.timeout";
1821

19-
2022
@Override
21-
public void test()
22-
throws InvalidConfigurationException {
23-
24-
if (!System.getProperty("os.name").toLowerCase().contains("linux")) {
25-
throw new InvalidConfigurationException("this strategy is only applicable to Linux");
26-
}
23+
protected boolean isApplicable() {
24+
return SystemUtils.IS_OS_LINUX;
25+
}
2726

27+
@Override
28+
public void test() throws InvalidConfigurationException {
2829
try {
2930
config = tryConfiguration(SOCKET_LOCATION);
30-
LOGGER.info("Accessing docker with local Unix socket");
31+
log.info("Accessing docker with local Unix socket");
3132
} catch (Exception | UnsatisfiedLinkError e) {
3233
throw new InvalidConfigurationException("ping failed", e);
3334
}

core/src/main/java/org/testcontainers/dockerclient/WindowsClientProviderStrategy.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
import com.github.dockerjava.core.DefaultDockerClientConfig;
44
import com.github.dockerjava.core.DockerClientConfig;
5+
import org.apache.commons.lang.SystemUtils;
56
import org.jetbrains.annotations.NotNull;
67

78
public class WindowsClientProviderStrategy extends DockerClientProviderStrategy {
89

910
private static final int PING_TIMEOUT_DEFAULT = 5;
1011
private static final String PING_TIMEOUT_PROPERTY_NAME = "testcontainers.windowsprovider.timeout";
1112

13+
@Override
14+
protected boolean isApplicable() {
15+
return SystemUtils.IS_OS_WINDOWS;
16+
}
17+
1218
@Override
1319
public void test() throws InvalidConfigurationException {
1420
config = tryConfiguration("tcp://localhost:2375");

0 commit comments

Comments
 (0)