Skip to content

Commit

Permalink
Support prioritization of DockerClientProviderStrategies (#362)
Browse files Browse the repository at this point in the history
* Support prioritization of DockerClientProviderStrategies
* Support storing the global configuration in user's home folder. Store selected DockerClientProviderStrategy globally.
* Add changelog and priority JavaDoc
  • Loading branch information
bsideup authored and rnorth committed Jun 21, 2017
1 parent 0997c0f commit 81952c4
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 97 deletions.
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

0 comments on commit 81952c4

Please sign in to comment.