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
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
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 java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
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 +43,14 @@ public abstract class DockerClientProviderStrategy {
*/
public abstract String getDescription();

protected boolean isApplicable() {
return true;
}

protected int getPriority() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth mentioning in a comment how the priority is used (i.e. highest to lowest) for avoidance of doubt.

return 0;
}

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

/**
Expand All @@ -49,45 +61,51 @@ 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 strategies.stream()
.filter(DockerClientProviderStrategy::isApplicable)
.sorted(Comparator.comparing(DockerClientProviderStrategy::getPriority).reversed())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only slight concern with determining the order at run time is that it means there's no one place you can look in the code to see the sequence. Now people will have to look at the implementations of all the strategies to figure out what order they'll run in.

Having said that, this does allow us to do dynamic prioritisation. So, e.g. as I mentioned on Slack, caching last known good config somewhere. Is that the kind of thing you had in mind too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore that - the PR title gives it away 🤦‍♂️

👍 !

.flatMap(strategy -> {
try {
strategy.test();
LOGGER.info("Found Docker environment with {}", strategy.getDescription());
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 +123,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.info("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