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 +