Skip to content

Commit

Permalink
Add optional OkHttp transport (instead of Netty) (#710)
Browse files Browse the repository at this point in the history
* add OkHttp transport

* implement chunked response handling based on Netty implementation

* handle response errors

* do not block caller's thread

* split files, support more protocols, fix blocking call

* mark OkHttpInvocationBuilder & OkHttpWebTarget as internal classes

* re-throw ExecutionException's cause

* always close the response

* reuse OkHttpClient instance

* shade okhttp / okio, disable pooling of unix-socket based transport

* do not block caller's thread (well, again :D)

* retry on connection failure

* do not use mutable "." directory in DirectoryTarResourceTest

* replace docker-filesocket with plain AFUNIXSocket factory

* fix shaded dependencies, rename handleStreamedResponse -> executeAndStream

* fix shading

* remove isOpen check

* try jnr-unixsocket

* remove cglib

* try org.scala-sbt.ipcsocket:ipcsocket

* experimental npipe support

* fix pipe name

* fix double escaping

* shade ipcsocket

* disable read timeout

* make OkHttp opt-in-able

* fix properties file name

* fix typo

* fix Codacy warnings

* workaround docker-java (does not support npipe)

* support npipe in DockerClientConfigUtils
  • Loading branch information
bsideup committed Jun 13, 2018
1 parent dbb9267 commit 85321e1
Show file tree
Hide file tree
Showing 15 changed files with 894 additions and 16 deletions.
6 changes: 4 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
}

Expand Down
16 changes: 16 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,6 +80,7 @@ workflows:
test_all:
jobs:
- core
- okhttp
- modules-no-jdbc-test-no-selenium
- modules-jdbc-test
- selenium
15 changes: 14 additions & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ shadowJar {

mergeServiceFiles()

exclude 'org/newsclub/**'

[
'META-INF/io.netty.versions.properties',
'META-INF/NOTICE',
Expand Down Expand Up @@ -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'))
}
}

Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit 85321e1

Please sign in to comment.