-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
DockerClientProviderStrategy.java
240 lines (212 loc) · 10.5 KB
/
DockerClientProviderStrategy.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package org.testcontainers.dockerclient;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.model.Network;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
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.dockerclient.auth.AuthDelegatingDockerClientConfig;
import org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory;
import org.testcontainers.utility.TestcontainersConfiguration;
import java.net.URI;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
/**
* Mechanism to find a viable Docker client configuration according to the host system environment.
*/
public abstract class DockerClientProviderStrategy {
protected DockerClient client;
protected DockerClientConfig config;
private String dockerHostIpAddress;
private static final RateLimiter PING_RATE_LIMITER = RateLimiterBuilder.newBuilder()
.withRate(2, TimeUnit.SECONDS)
.withConstantThroughput()
.build();
private static final AtomicBoolean FAIL_FAST_ALWAYS = new AtomicBoolean(false);
protected static final Logger LOGGER = LoggerFactory.getLogger(DockerClientProviderStrategy.class);
/**
* @throws InvalidConfigurationException if this strategy fails
*/
public abstract void test() throws InvalidConfigurationException;
/**
* @return a short textual description of the strategy
*/
public abstract String getDescription();
protected boolean isApplicable() {
return true;
}
protected boolean isPersistable() {
return true;
}
/**
* @return highest to lowest priority value
*/
protected int getPriority() {
return 0;
}
/**
* Determine the right DockerClientConfig to use for building clients by trial-and-error.
*
* @return a working DockerClientConfig, as determined by successful execution of a ping command
*/
public static DockerClientProviderStrategy getFirstValidStrategy(List<DockerClientProviderStrategy> strategies) {
if (FAIL_FAST_ALWAYS.get()) {
throw new IllegalStateException("Previous attempts to find a Docker environment failed. Will not retry. Please see logs and check configuration");
}
List<String> configurationFailures = new ArrayList<>();
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 (ClassNotFoundException e) {
LOGGER.warn("Can't instantiate a strategy from {} (ClassNotFoundException). " +
"This probably means that cached configuration refers to a client provider " +
"class that is not available in this version of Testcontainers. Other " +
"strategies will be tried instead.", it);
return Stream.empty();
} catch (InstantiationException | IllegalAccessException e) {
LOGGER.warn("Can't instantiate a strategy from {}", it, e);
return Stream.empty();
}
})
// Ignore persisted strategy if it's not persistable anymore
.filter(DockerClientProviderStrategy::isPersistable)
.peek(strategy -> LOGGER.info("Loaded {} from ~/.testcontainers.properties, will try it first", strategy.getClass().getName())),
strategies
.stream()
.filter(DockerClientProviderStrategy::isApplicable)
.sorted(Comparator.comparing(DockerClientProviderStrategy::getPriority).reversed())
)
.flatMap(strategy -> {
try {
strategy.test();
LOGGER.info("Found Docker environment with {}", strategy.getDescription());
strategy.checkOSType();
if (strategy.isPersistable()) {
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");
FAIL_FAST_ALWAYS.set(true);
return new IllegalStateException("Could not find a valid Docker environment. Please see logs and check configuration");
});
}
/**
* @return a usable, tested, Docker client configuration for the host system environment
*/
public DockerClient getClient() {
return new AuditLoggingDockerClient(client);
}
protected DockerClient getClientForConfig(DockerClientConfig config) {
return DockerClientBuilder
.getInstance(new AuthDelegatingDockerClientConfig(config))
.withDockerCmdExecFactory(new OkHttpDockerCmdExecFactory())
.build();
}
protected void ping(DockerClient client, int timeoutInSeconds) {
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 synchronized String getDockerHostIpAddress() {
if (dockerHostIpAddress == null) {
dockerHostIpAddress = resolveDockerHostIpAddress(client, config.getDockerHost());
}
return dockerHostIpAddress;
}
@VisibleForTesting
static String resolveDockerHostIpAddress(DockerClient client, URI dockerHost) {
switch (dockerHost.getScheme()) {
case "http":
case "https":
case "tcp":
return dockerHost.getHost();
case "unix":
case "npipe":
if (DockerClientConfigUtils.IN_A_CONTAINER) {
return client.inspectNetworkCmd()
.withNetworkId("bridge")
.exec()
.getIpam()
.getConfig()
.stream()
.filter(it -> it.getGateway() != null)
.findAny()
.map(Network.Ipam.Config::getGateway)
.orElse("localhost");
}
return "localhost";
default:
return null;
}
}
protected void checkOSType() {
LOGGER.debug("Checking Docker OS type for {}", this.getDescription());
String osType = client.infoCmd().exec().getOsType();
if (StringUtils.isBlank(osType)) {
LOGGER.warn("Could not determine Docker OS type");
} else if (!osType.equals("linux")) {
LOGGER.warn("{} is currently not supported", osType);
throw new InvalidConfigurationException(osType + " containers are currently not supported");
}
}
}