Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support prioritization of DockerClientProviderStrategies #362

Merged
merged 8 commits into from
Jun 21, 2017
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
- Added pre-flight checks (can be disabled with `checks.disable` configuration property) (#363)
- Removed unused Jersey dependencies (#361)
- Fixed non-POSIX fallback for file attribute reading (#371)
- Improved startup time by adding dynamic priorities to DockerClientProviderStrategy (#362)
- Added global configuration file `~/.testcontainers.properties` (#362)

## [1.3.0] - 2017-06-05
### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import com.google.common.base.Throwables;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import org.rnorth.ducttape.TimeoutException;
import org.rnorth.ducttape.ratelimits.RateLimiter;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

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

protected boolean isApplicable() {
return true;
}

/**
* @return highest to lowest priority value
*/
protected int getPriority() {
return 0;
}

protected static final Logger LOGGER = LoggerFactory.getLogger(DockerClientProviderStrategy.class);

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

for (DockerClientProviderStrategy strategy : strategies) {
try {
strategy.test();
LOGGER.info("Looking for Docker environment. Tried {}", strategy.getDescription());
return strategy;
} catch (Exception | ExceptionInInitializerError | NoClassDefFoundError e) {
@Nullable String throwableMessage = e.getMessage();
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
Throwable rootCause = Throwables.getRootCause(e);
@Nullable String rootCauseMessage = rootCause.getMessage();

String failureDescription;
if (throwableMessage != null && throwableMessage.equals(rootCauseMessage)) {
failureDescription = String.format("%s: failed with exception %s (%s)",
strategy.getClass().getSimpleName(),
e.getClass().getSimpleName(),
throwableMessage);
} else {
failureDescription = String.format("%s: failed with exception %s (%s). Root cause %s (%s)",
strategy.getClass().getSimpleName(),
e.getClass().getSimpleName(),
throwableMessage,
rootCause.getClass().getSimpleName(),
rootCauseMessage
);
}
configurationFailures.add(failureDescription);

LOGGER.debug(failureDescription);
}
}

LOGGER.error("Could not find a valid Docker environment. Please check configuration. Attempted configurations were:");
for (String failureMessage : configurationFailures) {
LOGGER.error(" " + failureMessage);
}
LOGGER.error("As no valid configuration was found, execution cannot continue");

throw new IllegalStateException("Could not find a valid Docker environment. Please see logs and check configuration");
return Stream
.concat(
Stream
.of(TestcontainersConfiguration.getInstance().getDockerClientStrategyClassName())
.filter(Objects::nonNull)
.flatMap(it -> {
try {
Class<? extends DockerClientProviderStrategy> strategyClass = (Class) Thread.currentThread().getContextClassLoader().loadClass(it);
return Stream.of(strategyClass.newInstance());
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
LOGGER.warn("Can't instantiate a strategy from " + it, e);
return Stream.empty();
}
}),
strategies
.stream()
.filter(DockerClientProviderStrategy::isApplicable)
.sorted(Comparator.comparing(DockerClientProviderStrategy::getPriority).reversed())
)
.flatMap(strategy -> {
try {
strategy.test();
LOGGER.info("Found Docker environment with {}", strategy.getDescription());

TestcontainersConfiguration.getInstance().updateGlobalConfig("docker.client.strategy", strategy.getClass().getName());

return Stream.of(strategy);
} catch (Exception | ExceptionInInitializerError | NoClassDefFoundError e) {
@Nullable String throwableMessage = e.getMessage();
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
Throwable rootCause = Throwables.getRootCause(e);
@Nullable String rootCauseMessage = rootCause.getMessage();

String failureDescription;
if (throwableMessage != null && throwableMessage.equals(rootCauseMessage)) {
failureDescription = String.format("%s: failed with exception %s (%s)",
strategy.getClass().getSimpleName(),
e.getClass().getSimpleName(),
throwableMessage);
} else {
failureDescription = String.format("%s: failed with exception %s (%s). Root cause %s (%s)",
strategy.getClass().getSimpleName(),
e.getClass().getSimpleName(),
throwableMessage,
rootCause.getClass().getSimpleName(),
rootCauseMessage
);
}
configurationFailures.add(failureDescription);

LOGGER.debug(failureDescription);
return Stream.empty();
}
})
.findAny()
.orElseThrow(() -> {
LOGGER.error("Could not find a valid Docker environment. Please check configuration. Attempted configurations were:");
for (String failureMessage : configurationFailures) {
LOGGER.error(" " + failureMessage);
}
LOGGER.error("As no valid configuration was found, execution cannot continue");

return new IllegalStateException("Could not find a valid Docker environment. Please see logs and check configuration");
});
}

/**
Expand All @@ -105,13 +147,18 @@ protected DockerClient getClientForConfig(DockerClientConfig config) {
}

protected void ping(DockerClient client, int timeoutInSeconds) {
Unreliables.retryUntilSuccess(timeoutInSeconds, TimeUnit.SECONDS, () -> {
return PING_RATE_LIMITER.getWhenReady(() -> {
LOGGER.debug("Pinging docker daemon...");
client.pingCmd().exec();
return true;
try {
Unreliables.retryUntilSuccess(timeoutInSeconds, TimeUnit.SECONDS, () -> {
return PING_RATE_LIMITER.getWhenReady(() -> {
LOGGER.debug("Pinging docker daemon...");
client.pingCmd().exec();
return true;
});
});
});
} catch (TimeoutException e) {
IOUtils.closeQuietly(client);
throw e;
}
}

public String getDockerHostIpAddress() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.testcontainers.dockerclient;

import com.github.dockerjava.core.DefaultDockerClientConfig;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.utility.CommandLine;
import org.testcontainers.utility.DockerMachineClient;

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

@Override
protected boolean isApplicable() {
return DockerMachineClient.instance().isInstalled();
}

@Override
protected int getPriority() {
return ProxiedUnixSocketClientProviderStrategy.PRIORITY - 10;
}

@Override
public void test() throws InvalidConfigurationException {

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

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

DockerMachineClient.instance().ensureMachineRunning(machineName);

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

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

config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("tcp://" + dockerDaemonIpAddress + ":2376")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,50 @@

import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.SystemUtils;

/**
* Use environment variables and system properties (as supported by the underlying DockerClient DefaultConfigBuilder)
* to try and locate a docker environment.
*/
@Slf4j
public class EnvironmentAndSystemPropertyClientProviderStrategy extends DockerClientProviderStrategy {

public static final int PRIORITY = 100;

private static final String PING_TIMEOUT_DEFAULT = "10";
private static final String PING_TIMEOUT_PROPERTY_NAME = "testcontainers.environmentprovider.timeout";

public EnvironmentAndSystemPropertyClientProviderStrategy() {
// Try using environment variables
config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
}

@Override
protected boolean isApplicable() {
return "tcp".equalsIgnoreCase(config.getDockerHost().getScheme()) || SystemUtils.IS_OS_LINUX;
}

@Override
protected int getPriority() {
return PRIORITY;
}

@Override
public void test() throws InvalidConfigurationException {

try {
// Try using environment variables
config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
client = getClientForConfig(config);

final int timeout = Integer.parseInt(System.getProperty(PING_TIMEOUT_PROPERTY_NAME, PING_TIMEOUT_DEFAULT));
ping(client, timeout);
} catch (Exception | UnsatisfiedLinkError e) {
LOGGER.error("ping failed with configuration {} due to {}", getDescription(), e.toString(), e);
log.error("ping failed with configuration {} due to {}", getDescription(), e.toString(), e);
throw new InvalidConfigurationException("ping failed");
}

LOGGER.info("Found docker client settings from environment");
log.info("Found docker client settings from environment");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
package org.testcontainers.dockerclient;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.SystemUtils;
import org.rnorth.tcpunixsocketproxy.TcpToUnixSocketProxy;

import java.io.File;

@Slf4j
public class ProxiedUnixSocketClientProviderStrategy extends UnixSocketClientProviderStrategy {

public static final int PRIORITY = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 10;

private final File socketFile = new File(DOCKER_SOCK_PATH);

@Override
public void test() throws InvalidConfigurationException {
protected boolean isApplicable() {
return !SystemUtils.IS_OS_LINUX && socketFile.exists();
}

String osName = System.getProperty("os.name").toLowerCase();
if (!osName.contains("mac") && !osName.contains("linux")) {
throw new InvalidConfigurationException("this strategy is only applicable to OS X and Linux");
}
@Override
protected int getPriority() {
return PRIORITY;
}

TcpToUnixSocketProxy proxy = new TcpToUnixSocketProxy(new File(DOCKER_SOCK_PATH));
@Override
public void test() throws InvalidConfigurationException {
TcpToUnixSocketProxy proxy = new TcpToUnixSocketProxy(socketFile);

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

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

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

proxy.stop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,33 @@

import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.SystemUtils;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

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


@Override
public void test()
throws InvalidConfigurationException {

if (!System.getProperty("os.name").toLowerCase().contains("linux")) {
throw new InvalidConfigurationException("this strategy is only applicable to Linux");
}
protected boolean isApplicable() {
return SystemUtils.IS_OS_LINUX;
}

@Override
public void test() throws InvalidConfigurationException {
try {
config = tryConfiguration(SOCKET_LOCATION);
LOGGER.info("Accessing docker with local Unix socket");
log.info("Accessing docker with local Unix socket");
} catch (Exception | UnsatisfiedLinkError e) {
throw new InvalidConfigurationException("ping failed", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import org.apache.commons.lang.SystemUtils;
import org.jetbrains.annotations.NotNull;

public class WindowsClientProviderStrategy extends DockerClientProviderStrategy {

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

@Override
protected boolean isApplicable() {
return SystemUtils.IS_OS_WINDOWS;
}

@Override
public void test() throws InvalidConfigurationException {
config = tryConfiguration("tcp://localhost:2375");
Expand Down
Loading