diff --git a/build.gradle b/build.gradle
index 8a0190349e5..edee9d34d28 100644
--- a/build.gradle
+++ b/build.gradle
@@ -72,8 +72,10 @@ subprojects {
"com.google",
"io.netty",
"org.bouncycastle",
- "org.newsclub",
- "org.zeroturnaround"
+ "org.zeroturnaround",
+ "okhttp3",
+ "okio",
+ "org.scalasbt.ipcsocket",
].each { relocate(it, "org.testcontainers.shaded.$it") }
}
diff --git a/circle.yml b/circle.yml
index a793bf8cb6a..b216656417f 100644
--- a/circle.yml
+++ b/circle.yml
@@ -14,6 +14,21 @@ jobs:
when: always
- store_test_results:
path: ~/junit
+ okhttp:
+ steps:
+ - checkout
+ - run:
+ command: |
+ echo "transport.type=okhttp" >> core/src/test/resources/testcontainers.properties
+ ./gradlew testcontainers:check
+ - run:
+ name: Save test results
+ command: |
+ mkdir -p ~/junit/
+ find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \;
+ when: always
+ - store_test_results:
+ path: ~/junit
modules-no-jdbc-test-no-selenium:
steps:
- checkout
@@ -65,6 +80,7 @@ workflows:
test_all:
jobs:
- core
+ - okhttp
- modules-no-jdbc-test-no-selenium
- modules-jdbc-test
- selenium
diff --git a/core/build.gradle b/core/build.gradle
index 167c4234910..15a0300738f 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -18,6 +18,8 @@ shadowJar {
mergeServiceFiles()
+ exclude 'org/newsclub/**'
+
[
'META-INF/io.netty.versions.properties',
'META-INF/NOTICE',
@@ -46,11 +48,13 @@ shadowJar {
include(dependency('com.google.guava:.*'))
include(dependency('io.netty:.*'))
include(dependency('org.bouncycastle:.*'))
- include(dependency('org.newsclub.*:.*'))
include(dependency('org.zeroturnaround:zt-exec'))
include(dependency('commons-lang:commons-lang'))
include(dependency('commons-io:commons-io'))
include(dependency('commons-codec:commons-codec'))
+ include(dependency('com.squareup.okhttp3:.*'))
+ include(dependency('com.squareup.okio:.*'))
+ include(dependency('org.scala-sbt.ipcsocket:ipcsocket'))
}
}
@@ -84,12 +88,21 @@ dependencies {
exclude(group: "log4j", module: "log4j")
}
+ compile "net.java.dev.jna:jna-platform:4.5.1"
+
+ shaded ('org.scala-sbt.ipcsocket:ipcsocket:1.0.0') {
+ exclude(group: "net.java.dev.jna")
+ }
+
shaded ('com.github.docker-java:docker-java:3.1.0-rc-3') {
exclude(group: 'org.glassfish.jersey.core')
exclude(group: 'org.glassfish.jersey.connectors')
exclude(group: 'log4j')
exclude(group: 'com.google.code.findbug')
+ exclude(group: 'com.kohlschutter.junixsocket')
}
+ shaded 'com.squareup.okhttp3:okhttp:3.10.0'
+
shaded 'javax.ws.rs:javax.ws.rs-api:2.0.1'
shaded 'org.zeroturnaround:zt-exec:1.8'
shaded 'commons-lang:commons-lang:2.6'
diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java
index d9c1dd2ce58..bfeabc63f8a 100644
--- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java
+++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java
@@ -52,6 +52,7 @@ public static String getDockerHostIpAddress(DockerClientConfig config) {
case "tcp":
return config.getDockerHost().getHost();
case "unix":
+ case "npipe":
if (IN_A_CONTAINER) {
return getDefaultGateway().orElse("localhost");
}
diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java
index f078aeac5fc..117acece7db 100644
--- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java
+++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java
@@ -13,6 +13,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.dockerclient.transport.TestcontainersDockerCmdExecFactory;
+import org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory;
import org.testcontainers.utility.TestcontainersConfiguration;
import java.util.ArrayList;
@@ -164,10 +165,23 @@ public DockerClient getClient() {
}
protected DockerClient getClientForConfig(DockerClientConfig config) {
- return DockerClientBuilder
- .getInstance(config)
- .withDockerCmdExecFactory(new TestcontainersDockerCmdExecFactory())
- .build();
+ DockerClientBuilder clientBuilder = DockerClientBuilder
+ .getInstance(config);
+
+ String transportType = TestcontainersConfiguration.getInstance().getTransportType();
+ if ("okhttp".equals(transportType)) {
+ clientBuilder
+ .withDockerCmdExecFactory(new OkHttpDockerCmdExecFactory());
+ } else if ("netty".equals(transportType)) {
+ clientBuilder
+ .withDockerCmdExecFactory(new TestcontainersDockerCmdExecFactory());
+ } else {
+ throw new IllegalArgumentException("Unknown transport type: " + transportType);
+ }
+
+ LOGGER.info("Will use '{}' transport", transportType);
+
+ return clientBuilder.build();
}
protected void ping(DockerClient client, int timeoutInSeconds) {
diff --git a/core/src/main/java/org/testcontainers/dockerclient/NpipeSocketClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/NpipeSocketClientProviderStrategy.java
new file mode 100644
index 00000000000..b8d85f11623
--- /dev/null
+++ b/core/src/main/java/org/testcontainers/dockerclient/NpipeSocketClientProviderStrategy.java
@@ -0,0 +1,78 @@
+package org.testcontainers.dockerclient;
+
+import com.github.dockerjava.core.DefaultDockerClientConfig;
+import com.github.dockerjava.core.DockerClientConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.SystemUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.URI;
+
+@Slf4j
+public class NpipeSocketClientProviderStrategy extends DockerClientProviderStrategy {
+
+ protected static final String DOCKER_SOCK_PATH = "//./pipe/docker_engine";
+ private static final String SOCKET_LOCATION = "npipe://" + DOCKER_SOCK_PATH;
+
+ private static final String PING_TIMEOUT_DEFAULT = "10";
+ private static final String PING_TIMEOUT_PROPERTY_NAME = "testcontainers.npipesocketprovider.timeout";
+
+ public static final int PRIORITY = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 20;
+
+ @Override
+ protected boolean isApplicable() {
+ return SystemUtils.IS_OS_WINDOWS;
+ }
+
+ @Override
+ public void test() throws InvalidConfigurationException {
+ try {
+ config = tryConfiguration();
+ log.info("Accessing docker with {}", getDescription());
+ } catch (Exception | UnsatisfiedLinkError e) {
+ throw new InvalidConfigurationException("ping failed", e);
+ }
+ }
+
+ @NotNull
+ private DockerClientConfig tryConfiguration() {
+ URI dockerHost = URI.create(SOCKET_LOCATION);
+
+ config = new DelegatingDockerClientConfig(
+ DefaultDockerClientConfig.createDefaultConfigBuilder()
+ .withDockerHost("tcp://localhost:0")
+ .withDockerTlsVerify(false)
+ .build()
+ ) {
+ @Override
+ public URI getDockerHost() {
+ return dockerHost;
+ }
+ };
+ client = getClientForConfig(config);
+
+ final int timeout = Integer.parseInt(System.getProperty(PING_TIMEOUT_PROPERTY_NAME, PING_TIMEOUT_DEFAULT));
+ ping(client, timeout);
+
+ return config;
+ }
+
+ @Override
+ public String getDescription() {
+ return "local Npipe socket (" + SOCKET_LOCATION + ")";
+ }
+
+ @Override
+ protected int getPriority() {
+ return PRIORITY;
+ }
+
+ @RequiredArgsConstructor
+ private static class DelegatingDockerClientConfig implements DockerClientConfig {
+
+ @Delegate
+ final DockerClientConfig dockerClientConfig;
+ }
+}
diff --git a/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/NamedPipeSocketFactory.java b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/NamedPipeSocketFactory.java
new file mode 100644
index 00000000000..0071dea036d
--- /dev/null
+++ b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/NamedPipeSocketFactory.java
@@ -0,0 +1,79 @@
+package org.testcontainers.dockerclient.transport.okhttp;
+
+import lombok.SneakyThrows;
+import lombok.Value;
+import org.scalasbt.ipcsocket.Win32NamedPipeSocket;
+
+import javax.net.SocketFactory;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+
+@Value
+public class NamedPipeSocketFactory extends SocketFactory {
+
+ String socketPath;
+
+ @Override
+ @SneakyThrows
+ public Socket createSocket() {
+ return new Win32NamedPipeSocket(socketPath.replace("/", "\\")) {
+
+ @Override
+ public void connect(SocketAddress endpoint, int timeout) throws IOException {
+ // Do nothing since it's not "connectable"
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new FilterInputStream(super.getInputStream()) {
+ @Override
+ public void close() throws IOException {
+ shutdownInput();
+ }
+ };
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return new FilterOutputStream(super.getOutputStream()) {
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ }
+
+ @Override
+ public void close() throws IOException {
+ shutdownOutput();
+ }
+ };
+ }
+ };
+ }
+
+ @Override
+ public Socket createSocket(String s, int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Socket createSocket(InetAddress inetAddress, int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpDockerCmdExecFactory.java b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpDockerCmdExecFactory.java
new file mode 100644
index 00000000000..08e0dde046b
--- /dev/null
+++ b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpDockerCmdExecFactory.java
@@ -0,0 +1,153 @@
+package org.testcontainers.dockerclient.transport.okhttp;
+
+import com.github.dockerjava.api.command.PingCmd;
+import com.github.dockerjava.core.AbstractDockerCmdExecFactory;
+import com.github.dockerjava.core.DockerClientConfig;
+import com.github.dockerjava.core.SSLConfig;
+import com.github.dockerjava.core.WebTarget;
+import com.github.dockerjava.core.exec.PingCmdExec;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.MultimapBuilder;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.ConnectionPool;
+import okhttp3.Dns;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.internal.Internal;
+import org.apache.commons.io.IOUtils;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509TrustManager;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class OkHttpDockerCmdExecFactory extends AbstractDockerCmdExecFactory {
+
+ private static final String SOCKET_SUFFIX = ".socket";
+
+ private OkHttpClient okHttpClient;
+
+ private HttpUrl baseUrl;
+
+ @Override
+ @SneakyThrows
+ public void init(DockerClientConfig dockerClientConfig) {
+ super.init(dockerClientConfig);
+
+ OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
+ .readTimeout(0, TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true);
+
+ URI dockerHost = dockerClientConfig.getDockerHost();
+ switch (dockerHost.getScheme()) {
+ case "unix":
+ case "npipe":
+ String socketPath = dockerHost.getPath();
+
+ if ("unix".equals(dockerHost.getScheme())) {
+ clientBuilder
+ .socketFactory(new UnixSocketFactory(socketPath));
+ } else {
+ clientBuilder
+ .socketFactory(new NamedPipeSocketFactory(socketPath));
+ }
+
+ clientBuilder
+ // Disable pooling
+ .connectionPool(new ConnectionPool(0, 1, TimeUnit.SECONDS))
+ .dns(hostname -> {
+ if (hostname.endsWith(SOCKET_SUFFIX)) {
+ return Collections.singletonList(InetAddress.getByAddress(hostname, new byte[]{0, 0, 0, 0}));
+ } else {
+ return Dns.SYSTEM.lookup(hostname);
+ }
+ });
+ default:
+ }
+
+ SSLConfig sslConfig = dockerClientConfig.getSSLConfig();
+ if (sslConfig != null) {
+ SSLContext sslContext = sslConfig.getSSLContext();
+ if (sslContext != null) {
+ clientBuilder
+ .sslSocketFactory(sslContext.getSocketFactory(), new TrustAllX509TrustManager());
+ }
+ }
+
+ okHttpClient = clientBuilder.build();
+
+ HttpUrl.Builder baseUrlBuilder;
+
+ switch (dockerHost.getScheme()) {
+ case "unix":
+ case "npipe":
+ baseUrlBuilder = new HttpUrl.Builder()
+ .scheme("http")
+ .host("docker" + SOCKET_SUFFIX);
+ break;
+ case "tcp":
+ baseUrlBuilder = new HttpUrl.Builder()
+ .scheme(sslConfig != null && sslConfig.getSSLContext() != null ? "https" : "http")
+ .host(dockerHost.getHost())
+ .port(dockerHost.getPort());
+ break;
+ default:
+ baseUrlBuilder = Internal.instance.getHttpUrlChecked(dockerHost.toString()).newBuilder();
+ }
+ baseUrl = baseUrlBuilder.build();
+ }
+
+ @Override
+ protected WebTarget getBaseResource() {
+ return new OkHttpWebTarget(
+ okHttpClient,
+ baseUrl,
+ ImmutableList.of(),
+ MultimapBuilder.hashKeys().hashSetValues().build()
+ );
+ }
+
+ @Override
+ public PingCmd.Exec createPingCmdExec() {
+ return new PingCmdExec(getBaseResource(), getDockerClientConfig()) {
+
+ @Override
+ protected Void execute(PingCmd command) {
+ WebTarget webResource = getBaseResource().path("/_ping");
+
+ // TODO contribute to docker-java, make it close the stream
+ IOUtils.closeQuietly(webResource.request().get());
+
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ }
+
+ private static class TrustAllX509TrustManager implements X509TrustManager {
+ @Override
+ public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
+
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
+
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ }
+}
diff --git a/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpInvocationBuilder.java b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpInvocationBuilder.java
new file mode 100644
index 00000000000..17727aa3c16
--- /dev/null
+++ b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpInvocationBuilder.java
@@ -0,0 +1,325 @@
+package org.testcontainers.dockerclient.transport.okhttp;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.exception.BadRequestException;
+import com.github.dockerjava.api.exception.ConflictException;
+import com.github.dockerjava.api.exception.DockerException;
+import com.github.dockerjava.api.exception.InternalServerErrorException;
+import com.github.dockerjava.api.exception.NotAcceptableException;
+import com.github.dockerjava.api.exception.NotFoundException;
+import com.github.dockerjava.api.exception.NotModifiedException;
+import com.github.dockerjava.api.exception.UnauthorizedException;
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.core.InvocationBuilder;
+import com.github.dockerjava.netty.handler.FramedResponseStreamHandler;
+import com.github.dockerjava.netty.handler.JsonResponseCallbackHandler;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.SimpleChannelInboundHandler;
+import lombok.AccessLevel;
+import lombok.SneakyThrows;
+import lombok.experimental.FieldDefaults;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.internal.connection.RealConnection;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+
+import javax.annotation.Nullable;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Slf4j
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+class OkHttpInvocationBuilder implements InvocationBuilder {
+
+ ObjectMapper objectMapper;
+
+ OkHttpClient okHttpClient;
+
+ Request.Builder requestBuilder;
+
+ public OkHttpInvocationBuilder(ObjectMapper objectMapper, OkHttpClient okHttpClient, HttpUrl httpUrl) {
+ this.objectMapper = objectMapper;
+ this.okHttpClient = okHttpClient;
+
+ requestBuilder = new Request.Builder()
+ .url(httpUrl);
+ }
+
+ @Override
+ public OkHttpInvocationBuilder accept(com.github.dockerjava.core.MediaType mediaType) {
+ return header("accept", mediaType.getMediaType());
+ }
+
+ @Override
+ public OkHttpInvocationBuilder header(String name, String value) {
+ requestBuilder.header(name, value);
+ return this;
+ }
+
+ @Override
+ @SneakyThrows
+ public void delete() {
+ Request request = requestBuilder
+ .delete()
+ .build();
+
+ execute(request).close();
+ }
+
+ @Override
+ @SneakyThrows
+ public void get(ResultCallback resultCallback) {
+ Request request = requestBuilder
+ .get()
+ .build();
+
+ executeAndStream(
+ request,
+ resultCallback,
+ new FramedResponseStreamHandler(resultCallback)
+ );
+ }
+
+ @Override
+ @SneakyThrows(IOException.class)
+ public T get(TypeReference typeReference) {
+ try (InputStream inputStream = get()) {
+ return objectMapper.readValue(inputStream, typeReference);
+ }
+ }
+
+ @Override
+ public void get(TypeReference typeReference, ResultCallback resultCallback) {
+ // FIXME
+ throw new IllegalStateException("doesn't seem to be used in docker-java");
+ }
+
+ @Override
+ @SneakyThrows
+ public InputStream post(Object entity) {
+ Request request = requestBuilder
+ .post(RequestBody.create(null, objectMapper.writeValueAsBytes(entity)))
+ .build();
+
+ return execute(request).body().byteStream();
+ }
+
+ @Override
+ @SneakyThrows
+ public T post(Object entity, TypeReference typeReference) {
+ Request request = requestBuilder
+ .post(RequestBody.create(MediaType.parse("application/json"), objectMapper.writeValueAsBytes(entity)))
+ .build();
+
+ try (Response response = execute(request)) {
+ String inputStream = response.body().string();
+ return objectMapper.readValue(inputStream, typeReference);
+ }
+ }
+
+ @Override
+ @SneakyThrows(JsonProcessingException.class)
+ public void post(Object entity, TypeReference typeReference, ResultCallback resultCallback) {
+ post(typeReference, resultCallback, new ByteArrayInputStream(objectMapper.writeValueAsBytes(entity)));
+ }
+
+ @Override
+ @SneakyThrows(IOException.class)
+ public T post(TypeReference typeReference, InputStream body) {
+ try (InputStream inputStream = post(body)) {
+ return objectMapper.readValue(inputStream, typeReference);
+ }
+ }
+
+ @Override
+ @SneakyThrows
+ public void post(Object entity, InputStream stdin, ResultCallback resultCallback) {
+ Request request = requestBuilder
+ .post(RequestBody.create(MediaType.parse("application/json"), objectMapper.writeValueAsBytes(entity)))
+ .build();
+
+ OkHttpClient okHttpClient = this.okHttpClient;
+
+ if (stdin != null) {
+ // FIXME there must be a better way of handling it
+ okHttpClient = okHttpClient.newBuilder()
+ .addNetworkInterceptor(chain -> {
+ Response response = chain.proceed(chain.request());
+ if (response.isSuccessful()) {
+ Thread thread = new Thread() {
+ @Override
+ @SneakyThrows
+ public void run() {
+ Field sinkField = RealConnection.class.getDeclaredField("sink");
+ sinkField.setAccessible(true);
+
+ try (
+ BufferedSink sink = (BufferedSink) sinkField.get(chain.connection());
+ Source source = Okio.source(stdin);
+ ) {
+ sink.writeAll(source);
+ }
+ }
+ };
+ thread.start();
+ }
+ return response;
+ })
+ .build();
+ }
+
+ executeAndStream(
+ okHttpClient,
+ request,
+ resultCallback,
+ new FramedResponseStreamHandler(resultCallback)
+ );
+ }
+
+ @Override
+ public void post(TypeReference typeReference, ResultCallback resultCallback, InputStream body) {
+ Request request = requestBuilder
+ .post(toRequestBody(body, null))
+ .build();
+
+ executeAndStream(
+ request,
+ resultCallback,
+ new JsonResponseCallbackHandler<>(typeReference, resultCallback)
+ );
+ }
+
+ @Override
+ @SneakyThrows
+ public void postStream(InputStream body) {
+ Request request = requestBuilder
+ .post(toRequestBody(body, null))
+ .build();
+
+ execute(request).close();
+ }
+
+ @Override
+ @SneakyThrows
+ public InputStream get() {
+ Request request = requestBuilder
+ .get()
+ .build();
+
+ return execute(request).body().byteStream();
+ }
+
+ @Override
+ @SneakyThrows
+ public void put(InputStream body, com.github.dockerjava.core.MediaType mediaType) {
+ Request request = requestBuilder
+ .put(toRequestBody(body, mediaType.toString()))
+ .build();
+
+ execute(request).close();
+ }
+
+ protected RequestBody toRequestBody(InputStream body, @Nullable String mediaType) {
+ return new RequestBody() {
+ @Nullable
+ @Override
+ public MediaType contentType() {
+ if (mediaType == null) {
+ return null;
+ }
+ return MediaType.parse(mediaType);
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ try(Source source = Okio.source(body)) {
+ sink.writeAll(source);
+ }
+ }
+ };
+ }
+
+ protected Response execute(Request request) {
+ return execute(okHttpClient, request);
+ }
+
+ @SneakyThrows(IOException.class)
+ protected Response execute(OkHttpClient okHttpClient, Request request) {
+ Response response = okHttpClient.newCall(request).execute();
+ if (!response.isSuccessful()) {
+ String body = response.body().string();
+ switch (response.code()) {
+ case 304:
+ throw new NotModifiedException(body);
+ case 400:
+ throw new BadRequestException(body);
+ case 401:
+ throw new UnauthorizedException(body);
+ case 404:
+ throw new NotFoundException(body);
+ case 406:
+ throw new NotAcceptableException(body);
+ case 409:
+ throw new ConflictException(body);
+ case 500:
+ throw new InternalServerErrorException(body);
+ default:
+ throw new DockerException(body, response.code());
+ }
+ } else {
+ return response;
+ }
+ }
+
+ protected void executeAndStream(Request request, ResultCallback callback, SimpleChannelInboundHandler handler) {
+ executeAndStream(okHttpClient, request, callback, handler);
+ }
+
+ protected void executeAndStream(OkHttpClient okHttpClient, Request request, ResultCallback callback, SimpleChannelInboundHandler handler) {
+ // TODO proper thread management
+ Thread thread = new Thread() {
+ @Override
+ @SneakyThrows
+ public void run() {
+ try (
+ Response response = execute(okHttpClient, request.newBuilder().tag("streaming").build());
+ BufferedSource source = response.body().source();
+ InputStream inputStream = source.inputStream();
+ ) {
+ AtomicBoolean shouldStop = new AtomicBoolean();
+ callback.onStart(() -> {
+ shouldStop.set(true);
+ response.close();
+ });
+
+ byte[] buffer = new byte[4 * 1024];
+ while (!(shouldStop.get() || source.exhausted())) {
+ int bytesReceived = inputStream.read(buffer);
+
+ handler.channelRead(null, Unpooled.wrappedBuffer(buffer, 0, bytesReceived));
+ }
+ callback.onComplete();
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ }
+ };
+
+ thread.start();
+ }
+}
diff --git a/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpWebTarget.java b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpWebTarget.java
new file mode 100644
index 00000000000..85fe8c22288
--- /dev/null
+++ b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/OkHttpWebTarget.java
@@ -0,0 +1,113 @@
+package org.testcontainers.dockerclient.transport.okhttp;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.dockerjava.core.InvocationBuilder;
+import com.github.dockerjava.core.WebTarget;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.SetMultimap;
+import lombok.SneakyThrows;
+import lombok.Value;
+import lombok.experimental.Wither;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Wither
+@Value
+class OkHttpWebTarget implements WebTarget {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ OkHttpClient okHttpClient;
+
+ HttpUrl baseUrl;
+
+ ImmutableList path;
+
+ SetMultimap queryParams;
+
+ @Override
+ @SneakyThrows
+ public InvocationBuilder request() {
+ String resource = StringUtils.join(path, "/");
+
+ if (!resource.startsWith("/")) {
+ resource = "/" + resource;
+ }
+
+ HttpUrl.Builder baseUrlBuilder = baseUrl.newBuilder()
+ .encodedPath(resource);
+
+ for (Map.Entry> queryParamEntry : queryParams.asMap().entrySet()) {
+ String key = queryParamEntry.getKey();
+ for (String paramValue : queryParamEntry.getValue()) {
+ baseUrlBuilder.addQueryParameter(key, paramValue);
+ }
+ }
+
+ return new OkHttpInvocationBuilder(
+ MAPPER,
+ okHttpClient,
+ baseUrlBuilder.build()
+ );
+ }
+
+ @Override
+ public OkHttpWebTarget path(String... components) {
+ return this.withPath(
+ ImmutableList.builder()
+ .addAll(path)
+ .add(components)
+ .build()
+ );
+ }
+
+ @Override
+ public OkHttpWebTarget resolveTemplate(String name, Object value) {
+ ImmutableList.Builder newPath = ImmutableList.builder();
+ for (String component : path) {
+ component = component.replaceAll("\\{" + name + "\\}", value.toString());
+ newPath.add(component);
+ }
+ return this.withPath(newPath.build());
+ }
+
+ @Override
+ public OkHttpWebTarget queryParam(String name, Object value) {
+ if (value == null) {
+ return this;
+ }
+
+ SetMultimap newQueryParams = HashMultimap.create(queryParams);
+ newQueryParams.put(name, value.toString());
+
+ return this.withQueryParams(newQueryParams);
+ }
+
+ @Override
+ public OkHttpWebTarget queryParamsSet(String name, Set> values) {
+ SetMultimap newQueryParams = HashMultimap.create(queryParams);
+ newQueryParams.replaceValues(name, values.stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toSet()));
+
+ return this.withQueryParams(newQueryParams);
+ }
+
+ @Override
+ @SneakyThrows(JsonProcessingException.class)
+ public OkHttpWebTarget queryParamsJsonMap(String name, Map values) {
+ if (values == null || values.isEmpty()) {
+ return this;
+ }
+
+ // when param value is JSON string
+ return queryParam(name, MAPPER.writeValueAsString(values));
+ }
+}
diff --git a/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/UnixSocketFactory.java b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/UnixSocketFactory.java
new file mode 100644
index 00000000000..c23575c2307
--- /dev/null
+++ b/core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/UnixSocketFactory.java
@@ -0,0 +1,78 @@
+package org.testcontainers.dockerclient.transport.okhttp;
+
+import lombok.SneakyThrows;
+import lombok.Value;
+import org.scalasbt.ipcsocket.UnixDomainSocket;
+
+import javax.net.SocketFactory;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+
+@Value
+public class UnixSocketFactory extends SocketFactory {
+
+ String socketPath;
+
+ @Override
+ @SneakyThrows
+ public Socket createSocket() {
+ return new UnixDomainSocket(socketPath) {
+ @Override
+ public void connect(SocketAddress endpoint, int timeout) throws IOException {
+ // Do nothing since it's not "connectable"
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new FilterInputStream(super.getInputStream()) {
+ @Override
+ public void close() throws IOException {
+ shutdownInput();
+ }
+ };
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return new FilterOutputStream(super.getOutputStream()) {
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ }
+
+ @Override
+ public void close() throws IOException {
+ shutdownOutput();
+ }
+ };
+ }
+ };
+ }
+
+ @Override
+ public Socket createSocket(String s, int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Socket createSocket(InetAddress inetAddress, int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java
index 1b5f9ce9a59..add7ad7b026 100644
--- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java
+++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java
@@ -66,6 +66,10 @@ public String getDockerClientStrategyClassName() {
return (String) properties.get("docker.client.strategy");
}
+ public String getTransportType() {
+ return properties.getProperty("transport.type", "netty");
+ }
+
@Synchronized
public boolean updateGlobalConfig(@NonNull String prop, @NonNull String value) {
try {
diff --git a/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy b/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy
index 9c8db41a429..25bbb30d725 100644
--- a/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy
+++ b/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy
@@ -2,4 +2,5 @@ org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrate
org.testcontainers.dockerclient.UnixSocketClientProviderStrategy
org.testcontainers.dockerclient.ProxiedUnixSocketClientProviderStrategy
org.testcontainers.dockerclient.DockerMachineClientProviderStrategy
-org.testcontainers.dockerclient.WindowsClientProviderStrategy
\ No newline at end of file
+org.testcontainers.dockerclient.WindowsClientProviderStrategy
+org.testcontainers.dockerclient.NpipeSocketClientProviderStrategy
diff --git a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java
index 70c366e3c80..b73bfeed46e 100644
--- a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java
+++ b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java
@@ -29,14 +29,17 @@ public void simpleRecursiveFileTest() throws TimeoutException {
final ToStringConsumer toString = new ToStringConsumer();
+ // 'src' is expected to be the project base directory, so all source code/resources should be copied in
+ File directory = new File("src");
+
GenericContainer container = new GenericContainer(
new ImageFromDockerfile()
.withDockerfileFromBuilder(builder ->
builder.from("alpine:3.3")
.copy("/tmp/foo", "/foo")
- .cmd("cat /foo/src/test/resources/test-recursive-file.txt")
+ .cmd("cat /foo/test/resources/test-recursive-file.txt")
.build()
- ).withFileFromFile("/tmp/foo", new File("."))) // '.' is expected to be the project base directory, so all source code/resources should be copied in
+ ).withFileFromFile("/tmp/foo", directory))
.withStartupCheckStrategy(new OneShotStartupCheckStrategy())
.withLogConsumer(wait.andThen(toString));
diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml
index ed0e5b2659e..42a23f5fe13 100644
--- a/core/src/test/resources/logback-test.xml
+++ b/core/src/test/resources/logback-test.xml
@@ -8,22 +8,20 @@
-
+
+
+
-
-
-
-
PROFILER
DENY
-
\ No newline at end of file
+