diff --git a/README.md b/README.md index 73c2b4e2..0bdf01dd 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Download the latest JAR or grab [via Maven][maven-search] ```java // Create a client based on DOCKER_HOST and DOCKER_CERT_PATH env vars -final DockerClient docker = new JerseyDockerClientBuilder().fromEnv().build(); +final DockerClient docker = DockerClientBuilder.fromEnv().build(); // Pull an image docker.pull("busybox"); diff --git a/pom.xml b/pom.xml index 090135f0..d4994c85 100644 --- a/pom.xml +++ b/pom.xml @@ -129,8 +129,6 @@ provided - - com.google.auth google-auth-library-oauth2-http diff --git a/src/main/java/org/mandas/docker/client/builder/BaseDockerClientBuilder.java b/src/main/java/org/mandas/docker/client/builder/BaseDockerClientBuilder.java deleted file mode 100644 index 37e18749..00000000 --- a/src/main/java/org/mandas/docker/client/builder/BaseDockerClientBuilder.java +++ /dev/null @@ -1,321 +0,0 @@ -/*- - * -\-\- - * docker-client - * -- - * Copyright (C) 2019-2020 Dimitris Mandalidis - * -- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -/-/- -*/ -package org.mandas.docker.client.builder; - -import static java.util.Arrays.asList; -import static java.util.Objects.requireNonNull; -import static java.util.Optional.ofNullable; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.mandas.docker.client.DockerHost.certPathFromEnv; -import static org.mandas.docker.client.DockerHost.configPathFromEnv; -import static org.mandas.docker.client.DockerHost.defaultAddress; -import static org.mandas.docker.client.DockerHost.defaultCertPath; -import static org.mandas.docker.client.DockerHost.defaultPort; - -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; - -import jakarta.ws.rs.client.Client; - -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.HttpClientConnectionManager; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.impl.conn.BasicHttpClientConnectionManager; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.mandas.docker.client.DefaultDockerClient; -import org.mandas.docker.client.DockerCertificates; -import org.mandas.docker.client.DockerCertificatesStore; -import org.mandas.docker.client.DockerHost; -import org.mandas.docker.client.ObjectMapperProvider; -import org.mandas.docker.client.UnixConnectionSocketFactory; -import org.mandas.docker.client.auth.ConfigFileRegistryAuthSupplier; -import org.mandas.docker.client.auth.RegistryAuthSupplier; -import org.mandas.docker.client.exceptions.DockerCertificateException; -import org.mandas.docker.client.npipe.NpipeConnectionSocketFactory; - -/** - * A convenience base class for implementing {@link DockerClientBuilder}s - * @author Dimitris Mandalidis - * @param the type of the builder - */ -public abstract class BaseDockerClientBuilder> implements DockerClientBuilder { - - protected String UNIX_SCHEME = "unix"; - protected String NPIPE_SCHEME = "npipe"; - protected long DEFAULT_CONNECT_TIMEOUT_MILLIS = SECONDS.toMillis(5); - protected long DEFAULT_READ_TIMEOUT_MILLIS = SECONDS.toMillis(30); - protected int DEFAULT_CONNECTION_POOL_SIZE = 100; - protected String ERROR_MESSAGE = "LOGIC ERROR: DefaultDockerClient does not support being built " - + "with both `registryAuth` and `registryAuthSupplier`. " - + "Please build with at most one of these options."; - protected URI uri; - protected String apiVersion; - protected long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS; - protected long readTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS; - protected int connectionPoolSize = DEFAULT_CONNECTION_POOL_SIZE; - protected DockerCertificatesStore dockerCertificatesStore; - protected boolean useProxy = true; - protected RegistryAuthSupplier registryAuthSupplier; - protected Map headers = new HashMap<>(); - protected Client client; - protected EntityProcessing entityProcessing; - - private B self() { - return (B) this; - } - - /** - * Sets or overwrites {@link #uri()} and {@link #dockerCertificates(DockerCertificatesStore)} according to the values - * present in DOCKER_HOST and DOCKER_CERT_PATH environment variables. - * - * @return Modifies a builder that can be used to further customize and then build the client. - * @throws DockerCertificateException if we could not build a DockerCertificates object - */ - @Override - public B fromEnv() throws DockerCertificateException { - final String endpoint = DockerHost.endpointFromEnv(); - final Path dockerCertPath = Paths.get(asList(certPathFromEnv(), configPathFromEnv(), defaultCertPath()) - .stream() - .filter(cert -> cert != null) - .findFirst() - .orElseThrow(() -> new NoSuchElementException("Cannot find docker certificated path"))); - - final Optional certs = DockerCertificates.builder().dockerCertPath(dockerCertPath).build(); - - if (endpoint.startsWith(UNIX_SCHEME + "://")) { - this.uri(endpoint); - } else if (endpoint.startsWith(NPIPE_SCHEME + "://")) { - this.uri(endpoint); - } else { - final String stripped = endpoint.replaceAll(".*://", ""); - final String scheme = certs.isPresent() ? "https" : "http"; - URI initialUri = URI.create(scheme + "://" + stripped); - if (initialUri.getPort() == -1 && initialUri.getHost() == null) { - initialUri = URI.create(scheme + "://" + defaultAddress() + ":" + defaultPort()); - } else if (initialUri.getHost() == null) { - initialUri = URI.create(scheme + "://" + defaultAddress()+ ":" + initialUri.getPort()); - } else if (initialUri.getPort() == -1) { - initialUri = URI.create(scheme + "://" + initialUri.getHost() + ":" + defaultPort()); - } - this.uri(initialUri); - } - - if (certs.isPresent()) { - this.dockerCertificates(certs.get()); - } - - return self(); - } - - @Override - public B uri(final URI uri) { - this.uri = uri; - return self(); - } - - /** - * Set the URI for connections to Docker. - * - * @param uri URI String for connections to Docker - * @return Builder - */ - @Override - public B uri(final String uri) { - return uri(URI.create(uri)); - } - - /** - * Set the Docker API version that will be used in the HTTP requests to Docker daemon. - * - * @param apiVersion String for Docker API version - * @return Builder - */ - @Override - public B apiVersion(final String apiVersion) { - this.apiVersion = apiVersion; - return self(); - } - - @Override - public B connectTimeoutMillis(final long connectTimeoutMillis) { - this.connectTimeoutMillis = connectTimeoutMillis; - return self(); - } - - @Override - public B readTimeoutMillis(final long readTimeoutMillis) { - this.readTimeoutMillis = readTimeoutMillis; - return self(); - } - - @Override - public B dockerCertificates(final DockerCertificatesStore dockerCertificatesStore) { - this.dockerCertificatesStore = dockerCertificatesStore; - return self(); - } - - @Override - public B connectionPoolSize(final int connectionPoolSize) { - this.connectionPoolSize = connectionPoolSize; - return self(); - } - - @Override - public B useProxy(final boolean useProxy) { - this.useProxy = useProxy; - return self(); - } - - @Override - public B registryAuthSupplier(final RegistryAuthSupplier registryAuthSupplier) { - if (this.registryAuthSupplier != null) { - throw new IllegalStateException(ERROR_MESSAGE); - } - this.registryAuthSupplier = registryAuthSupplier; - return self(); - } - - @Override - public B header(String name, Object value) { - headers.put(name, value); - return self(); - } - - @Override - public URI uri() { - return uri; - } - - @Override - public B entityProcessing(final EntityProcessing entityProcessing) { - this.entityProcessing = entityProcessing; - return self(); - } - - private String toRegExp(String hostnameWithWildcards) { - return hostnameWithWildcards.replace(".", "\\.").replace("*", ".*"); - } - - protected abstract Client createClient(); - - protected ProxyConfiguration proxyFromEnv() { - final String proxyHost = System.getProperty("http.proxyHost"); - if (proxyHost == null) { - return null; - } - - String nonProxyHosts = System.getProperty("http.nonProxyHosts"); - if (nonProxyHosts != null) { - // Remove quotes, if any. Refer to https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html - String[] nonProxy = nonProxyHosts - .replaceAll("^\\s*\"", "") - .replaceAll("\\s*\"$", "") - .split("\\|"); - String host = ofNullable(uri.getHost()).orElse("localhost"); - for (String h: nonProxy) { - if (host.matches(toRegExp(h))) { - return null; - } - } - } - - return ProxyConfiguration.builder() - .host(proxyHost) - .port(System.getProperty("http.proxyPort")) - .username(System.getProperty("http.proxyUser")) - .password(System.getProperty("http.proxyPassword")) - .build(); - } - - @Override - public DefaultDockerClient build() { - requireNonNull(uri, "uri"); - requireNonNull(uri.getScheme(), "url has null scheme"); - - if ((dockerCertificatesStore != null) && !uri.getScheme().equals("https")) { - throw new IllegalArgumentException( - "An HTTPS URI for DOCKER_HOST must be provided to use Docker client certificates"); - } - - if (uri.getScheme().startsWith(UNIX_SCHEME) || uri.getScheme().startsWith(NPIPE_SCHEME)) { - this.useProxy = false; - } - - this.client = createClient() - .register(ObjectMapperProvider.class); - - if (uri.getScheme().equals(UNIX_SCHEME)) { - this.uri = UnixConnectionSocketFactory.sanitizeUri(uri); - } else if (uri.getScheme().equals(NPIPE_SCHEME)) { - this.uri = NpipeConnectionSocketFactory.sanitizeUri(uri); - } - - // read the docker config file for auth info if nothing else was specified - if (registryAuthSupplier == null) { - registryAuthSupplier(new ConfigFileRegistryAuthSupplier()); - } - - return new DefaultDockerClient(apiVersion, registryAuthSupplier, uri, client, headers); - } - - protected HttpClientConnectionManager getConnectionManager(URI uri, Registry schemeRegistry, int connectionPoolSize) { - if (uri.getScheme().equals(NPIPE_SCHEME)) { - return new BasicHttpClientConnectionManager(schemeRegistry); - } - final PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(schemeRegistry); - // Use all available connections instead of artificially limiting ourselves to 2 per server. - cm.setMaxTotal(connectionPoolSize); - cm.setDefaultMaxPerRoute(cm.getMaxTotal()); - return cm; - } - - protected Registry getSchemeRegistry(URI uri, DockerCertificatesStore certificateStore) { - final SSLConnectionSocketFactory https; - if (dockerCertificatesStore == null) { - https = SSLConnectionSocketFactory.getSocketFactory(); - } else { - https = new SSLConnectionSocketFactory(dockerCertificatesStore.sslContext(), - dockerCertificatesStore.hostnameVerifier()); - } - - final RegistryBuilder registryBuilder = RegistryBuilder - .create() - .register("https", https) - .register("http", PlainConnectionSocketFactory.getSocketFactory()); - - if (uri.getScheme().equals(UNIX_SCHEME)) { - registryBuilder.register(UNIX_SCHEME, new UnixConnectionSocketFactory(uri)); - } - - if (uri.getScheme().equals(NPIPE_SCHEME)) { - registryBuilder.register(NPIPE_SCHEME, new NpipeConnectionSocketFactory(uri)); - } - - return registryBuilder.build(); - } -} diff --git a/src/main/java/org/mandas/docker/client/builder/DockerClientBuilder.java b/src/main/java/org/mandas/docker/client/builder/DockerClientBuilder.java index c2f881d0..3b357056 100644 --- a/src/main/java/org/mandas/docker/client/builder/DockerClientBuilder.java +++ b/src/main/java/org/mandas/docker/client/builder/DockerClientBuilder.java @@ -19,33 +19,148 @@ */ package org.mandas.docker.client.builder; +import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.mandas.docker.client.DockerHost.certPathFromEnv; +import static org.mandas.docker.client.DockerHost.configPathFromEnv; +import static org.mandas.docker.client.DockerHost.defaultAddress; +import static org.mandas.docker.client.DockerHost.defaultCertPath; +import static org.mandas.docker.client.DockerHost.defaultPort; + import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.jackson.JacksonFeature; import org.mandas.docker.client.DefaultDockerClient; +import org.mandas.docker.client.DockerCertificates; import org.mandas.docker.client.DockerCertificatesStore; +import org.mandas.docker.client.DockerHost; +import org.mandas.docker.client.ObjectMapperProvider; +import org.mandas.docker.client.UnixConnectionSocketFactory; +import org.mandas.docker.client.auth.ConfigFileRegistryAuthSupplier; import org.mandas.docker.client.auth.RegistryAuthSupplier; import org.mandas.docker.client.exceptions.DockerCertificateException; +import org.mandas.docker.client.npipe.NpipeConnectionSocketFactory; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; /** - * DockerClientBuilder is an interface which has to be implemented from clients - * when they need to use a JAXRS client implementation other than the provided Jersey - * + * Docker client builder * @author Dimitris Mandalidis - * @param the type of the builder - * @see BaseDockerClientBuilder */ -public interface DockerClientBuilder> { +public class DockerClientBuilder { - /** - * @return the URI of the Docker engine - */ - URI uri(); - - enum EntityProcessing { + public enum EntityProcessing { CHUNKED, BUFFERED; } + + private static String UNIX_SCHEME = "unix"; + private static String NPIPE_SCHEME = "npipe"; + private long DEFAULT_CONNECT_TIMEOUT_MILLIS = SECONDS.toMillis(5); + private long DEFAULT_READ_TIMEOUT_MILLIS = SECONDS.toMillis(30); + private int DEFAULT_CONNECTION_POOL_SIZE = 100; + private String ERROR_MESSAGE = "LOGIC ERROR: DefaultDockerClient does not support being built " + + "with both `registryAuth` and `registryAuthSupplier`. " + + "Please build with at most one of these options."; + private URI uri; + private String apiVersion; + private long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS; + private long readTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS; + private int connectionPoolSize = DEFAULT_CONNECTION_POOL_SIZE; + private DockerCertificatesStore dockerCertificatesStore; + private boolean useProxy = true; + private RegistryAuthSupplier registryAuthSupplier; + private Map headers = new HashMap<>(); + private Client client; + private EntityProcessing entityProcessing; + + private ClientConfig updateProxy(ClientConfig config) { + ProxyConfiguration proxyConfiguration = proxyFromEnv(); + if (proxyConfiguration == null) { + return config; + } + + String proxyHost = proxyConfiguration.host(); + + config.property(ClientProperties.PROXY_URI, (!proxyHost.startsWith("http") ? "http://" : "") + + proxyHost + ":" + proxyConfiguration.port()); + + if (proxyConfiguration.username() != null) { + config.property(ClientProperties.PROXY_USERNAME, proxyConfiguration.username()); + } + if (proxyConfiguration.password() != null) { + config.property(ClientProperties.PROXY_PASSWORD, proxyConfiguration.password()); + } + + //ensure Content-Length is populated before sending request via proxy. + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); + return config; + } + + private Client createClient() { + Registry schemeRegistry = getSchemeRegistry(uri, dockerCertificatesStore); + + final HttpClientConnectionManager cm = getConnectionManager(uri, schemeRegistry, connectionPoolSize); + + final RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout((int) connectTimeoutMillis) + .setConnectTimeout((int) connectTimeoutMillis) + .setSocketTimeout((int) readTimeoutMillis) + .build(); + + ClientConfig config = new ClientConfig(JacksonFeature.class); + + if (useProxy) { + config = updateProxy(config); + } + + config + .connectorProvider(new ApacheConnectorProvider()) + .property(ApacheClientProperties.CONNECTION_MANAGER, cm) + .property(ApacheClientProperties.CONNECTION_MANAGER_SHARED, "true") + .property(ApacheClientProperties.REQUEST_CONFIG, requestConfig); + if (entityProcessing != null) { + switch (entityProcessing) { + case BUFFERED: + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); + break; + case CHUNKED: + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); + break; + default: + throw new IllegalArgumentException("Invalid entity processing mode " + entityProcessing); + } + } + + return ClientBuilder.newBuilder() + .withConfig(config) + .build(); + } + /** * Sets or overwrites {@link #uri()} and {@link #dockerCertificates(DockerCertificatesStore)} according to the values * present in DOCKER_HOST and DOCKER_CERT_PATH environment variables. @@ -53,42 +168,65 @@ enum EntityProcessing { * @return Modifies a builder that can be used to further customize and then build the client. * @throws DockerCertificateException if we could not build a DockerCertificates object */ - B fromEnv() throws DockerCertificateException; - - B uri(URI uri); - - B uri(String uri); + public static DockerClientBuilder fromEnv() throws DockerCertificateException { + final String endpoint = DockerHost.endpointFromEnv(); + final Path dockerCertPath = Paths.get(asList(certPathFromEnv(), configPathFromEnv(), defaultCertPath()) + .stream() + .filter(cert -> cert != null) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Cannot find docker certificated path"))); - DefaultDockerClient build(); - - /** - * Adds additional headers to be sent in all requests to the Docker Remote API. - * @param name the header name - * @param value the header value - * @return this - */ - B header(String name, Object value); - - B registryAuthSupplier(RegistryAuthSupplier registryAuthSupplier); + final Optional certs = DockerCertificates.builder().dockerCertPath(dockerCertPath).build(); + + URI uri = null; + if (endpoint.startsWith(UNIX_SCHEME + "://")) { + uri = URI.create(endpoint); + } else if (endpoint.startsWith(NPIPE_SCHEME + "://")) { + uri = URI.create(endpoint); + } else { + final String stripped = endpoint.replaceAll(".*://", ""); + final String scheme = certs.isPresent() ? "https" : "http"; + URI initialUri = URI.create(scheme + "://" + stripped); + if (initialUri.getPort() == -1 && initialUri.getHost() == null) { + initialUri = URI.create(scheme + "://" + defaultAddress() + ":" + defaultPort()); + } else if (initialUri.getHost() == null) { + initialUri = URI.create(scheme + "://" + defaultAddress()+ ":" + initialUri.getPort()); + } else if (initialUri.getPort() == -1) { + initialUri = URI.create(scheme + "://" + initialUri.getHost() + ":" + defaultPort()); + } + uri = initialUri; + } + + if (certs.isPresent()) { + return new DockerClientBuilder(uri, certs.get()); + } + + return new DockerClientBuilder(uri); + } - /** - * Set the size of the connection pool for connections to Docker. Note that due to a known - * issue, DefaultDockerClient maintains two separate connection pools, each of which is capped - * at this size. Therefore, the maximum number of concurrent connections to Docker may be up to - * 2 * connectionPoolSize. - * - * @param connectionPoolSize connection pool size - * @return Builder - */ - B connectionPoolSize(int connectionPoolSize); + private DockerClientBuilder(final URI uri) { + this(uri, null); + } + + private DockerClientBuilder(final URI uri, final DockerCertificatesStore certs) { + this.uri = uri; + this.dockerCertificatesStore = certs; + } + + public DockerClientBuilder uri(final URI uri) { + this.uri = uri; + return this; + } /** - * Allows connecting to Docker Daemon using HTTP proxy. + * Set the URI for connections to Docker. * - * @param useProxy tells if Docker Client has to connect to docker daemon using HTTP Proxy + * @param uri URI String for connections to Docker * @return Builder */ - B useProxy(boolean useProxy); + public DockerClientBuilder uri(final String uri) { + return uri(URI.create(uri)); + } /** * Set the Docker API version that will be used in the HTTP requests to Docker daemon. @@ -96,7 +234,10 @@ enum EntityProcessing { * @param apiVersion String for Docker API version * @return Builder */ - B apiVersion(String apiVersion); + public DockerClientBuilder apiVersion(final String apiVersion) { + this.apiVersion = apiVersion; + return this; + } /** * Set the timeout in milliseconds until a connection to Docker is established. A timeout value @@ -105,7 +246,10 @@ enum EntityProcessing { * @param connectTimeoutMillis connection timeout to Docker daemon in milliseconds * @return Builder */ - B connectTimeoutMillis(long connectTimeoutMillis); + public DockerClientBuilder connectTimeoutMillis(final long connectTimeoutMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + return this; + } /** * Set the SO_TIMEOUT in milliseconds. This is the maximum period of inactivity between @@ -114,7 +258,10 @@ enum EntityProcessing { * @param readTimeoutMillis read timeout to Docker daemon in milliseconds * @return Builder */ - B readTimeoutMillis(long readTimeoutMillis); + public DockerClientBuilder readTimeoutMillis(final long readTimeoutMillis) { + this.readTimeoutMillis = readTimeoutMillis; + return this; + } /** * Provide certificates to secure the connection to Docker. @@ -122,7 +269,61 @@ enum EntityProcessing { * @param dockerCertificatesStore DockerCertificatesStore object * @return Builder */ - B dockerCertificates(DockerCertificatesStore dockerCertificatesStore); + public DockerClientBuilder dockerCertificates(final DockerCertificatesStore dockerCertificatesStore) { + this.dockerCertificatesStore = dockerCertificatesStore; + return this; + } + + /** + * Set the size of the connection pool for connections to Docker. Note that due to a known + * issue, DefaultDockerClient maintains two separate connection pools, each of which is capped + * at this size. Therefore, the maximum number of concurrent connections to Docker may be up to + * 2 * connectionPoolSize. + * + * @param connectionPoolSize connection pool size + * @return Builder + */ + public DockerClientBuilder connectionPoolSize(final int connectionPoolSize) { + this.connectionPoolSize = connectionPoolSize; + return this; + } + + /** + * Allows connecting to Docker Daemon using HTTP proxy. + * + * @param useProxy tells if Docker Client has to connect to docker daemon using HTTP Proxy + * @return Builder + */ + public DockerClientBuilder useProxy(final boolean useProxy) { + this.useProxy = useProxy; + return this; + } + + public DockerClientBuilder registryAuthSupplier(final RegistryAuthSupplier registryAuthSupplier) { + if (this.registryAuthSupplier != null) { + throw new IllegalStateException(ERROR_MESSAGE); + } + this.registryAuthSupplier = registryAuthSupplier; + return this; + } + + /** + * Adds additional headers to be sent in all requests to the Docker Remote API. + * @param name the header name + * @param value the header value + * @return this + */ + public DockerClientBuilder header(String name, Object value) { + headers.put(name, value); + return this; + } + + /** + * @return the URI of the Docker engine + */ + public URI uri() { + return uri; + } /** * Allows setting transfer encoding. CHUNKED does not send the content-length header @@ -135,5 +336,107 @@ enum EntityProcessing { * daemon (tcp protocol). * @return Builder */ - B entityProcessing(EntityProcessing entityProcessing); + public DockerClientBuilder entityProcessing(final EntityProcessing entityProcessing) { + this.entityProcessing = entityProcessing; + return this; + } + + private String toRegExp(String hostnameWithWildcards) { + return hostnameWithWildcards.replace(".", "\\.").replace("*", ".*"); + } + + private ProxyConfiguration proxyFromEnv() { + final String proxyHost = System.getProperty("http.proxyHost"); + if (proxyHost == null) { + return null; + } + + String nonProxyHosts = System.getProperty("http.nonProxyHosts"); + if (nonProxyHosts != null) { + // Remove quotes, if any. Refer to https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html + String[] nonProxy = nonProxyHosts + .replaceAll("^\\s*\"", "") + .replaceAll("\\s*\"$", "") + .split("\\|"); + String host = ofNullable(uri.getHost()).orElse("localhost"); + for (String h: nonProxy) { + if (host.matches(toRegExp(h))) { + return null; + } + } + } + + return ProxyConfiguration.builder() + .host(proxyHost) + .port(System.getProperty("http.proxyPort")) + .username(System.getProperty("http.proxyUser")) + .password(System.getProperty("http.proxyPassword")) + .build(); + } + + public DefaultDockerClient build() { + requireNonNull(uri, "uri"); + requireNonNull(uri.getScheme(), "url has null scheme"); + + if ((dockerCertificatesStore != null) && !uri.getScheme().equals("https")) { + throw new IllegalArgumentException( + "An HTTPS URI for DOCKER_HOST must be provided to use Docker client certificates"); + } + + if (uri.getScheme().startsWith(UNIX_SCHEME) || uri.getScheme().startsWith(NPIPE_SCHEME)) { + this.useProxy = false; + } + + this.client = createClient() + .register(ObjectMapperProvider.class); + + if (uri.getScheme().equals(UNIX_SCHEME)) { + this.uri = UnixConnectionSocketFactory.sanitizeUri(uri); + } else if (uri.getScheme().equals(NPIPE_SCHEME)) { + this.uri = NpipeConnectionSocketFactory.sanitizeUri(uri); + } + + // read the docker config file for auth info if nothing else was specified + if (registryAuthSupplier == null) { + registryAuthSupplier(new ConfigFileRegistryAuthSupplier()); + } + + return new DefaultDockerClient(apiVersion, registryAuthSupplier, uri, client, headers); + } + + private HttpClientConnectionManager getConnectionManager(URI uri, Registry schemeRegistry, int connectionPoolSize) { + if (uri.getScheme().equals(NPIPE_SCHEME)) { + return new BasicHttpClientConnectionManager(schemeRegistry); + } + final PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(schemeRegistry); + // Use all available connections instead of artificially limiting ourselves to 2 per server. + cm.setMaxTotal(connectionPoolSize); + cm.setDefaultMaxPerRoute(cm.getMaxTotal()); + return cm; + } + + private Registry getSchemeRegistry(URI uri, DockerCertificatesStore certificateStore) { + final SSLConnectionSocketFactory https; + if (dockerCertificatesStore == null) { + https = SSLConnectionSocketFactory.getSocketFactory(); + } else { + https = new SSLConnectionSocketFactory(dockerCertificatesStore.sslContext(), + dockerCertificatesStore.hostnameVerifier()); + } + + final RegistryBuilder registryBuilder = RegistryBuilder + .create() + .register("https", https) + .register("http", PlainConnectionSocketFactory.getSocketFactory()); + + if (uri.getScheme().equals(UNIX_SCHEME)) { + registryBuilder.register(UNIX_SCHEME, new UnixConnectionSocketFactory(uri)); + } + + if (uri.getScheme().equals(NPIPE_SCHEME)) { + registryBuilder.register(NPIPE_SCHEME, new NpipeConnectionSocketFactory(uri)); + } + + return registryBuilder.build(); + } } diff --git a/src/main/java/org/mandas/docker/client/builder/ProxyConfiguration.java b/src/main/java/org/mandas/docker/client/builder/ProxyConfiguration.java index c056ab87..447007d2 100644 --- a/src/main/java/org/mandas/docker/client/builder/ProxyConfiguration.java +++ b/src/main/java/org/mandas/docker/client/builder/ProxyConfiguration.java @@ -25,7 +25,7 @@ /** * Object representing a host's proxy configuration * @author Dimitris Mandalidis - * @see BaseDockerClientBuilder#proxyFromEnv() + * @see DockerClientBuilder#proxyFromEnv() */ @Immutable public interface ProxyConfiguration { diff --git a/src/main/java/org/mandas/docker/client/builder/jersey/JerseyDockerClientBuilder.java b/src/main/java/org/mandas/docker/client/builder/jersey/JerseyDockerClientBuilder.java deleted file mode 100644 index ccf80fd1..00000000 --- a/src/main/java/org/mandas/docker/client/builder/jersey/JerseyDockerClientBuilder.java +++ /dev/null @@ -1,104 +0,0 @@ -/*- - * -\-\- - * docker-client - * -- - * Copyright (C) 2019-2020 Dimitris Mandalidis - * -- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -/-/- -*/ -package org.mandas.docker.client.builder.jersey; - -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.Registry; -import org.apache.http.conn.HttpClientConnectionManager; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.glassfish.jersey.apache.connector.ApacheClientProperties; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.RequestEntityProcessing; -import org.glassfish.jersey.jackson.JacksonFeature; -import org.mandas.docker.client.builder.BaseDockerClientBuilder; -import org.mandas.docker.client.builder.ProxyConfiguration; - -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; - -public class JerseyDockerClientBuilder extends BaseDockerClientBuilder { - - private ClientConfig updateProxy(ClientConfig config) { - ProxyConfiguration proxyConfiguration = proxyFromEnv(); - if (proxyConfiguration == null) { - return config; - } - - String proxyHost = proxyConfiguration.host(); - - config.property(ClientProperties.PROXY_URI, (!proxyHost.startsWith("http") ? "http://" : "") - + proxyHost + ":" + proxyConfiguration.port()); - - if (proxyConfiguration.username() != null) { - config.property(ClientProperties.PROXY_USERNAME, proxyConfiguration.username()); - } - if (proxyConfiguration.password() != null) { - config.property(ClientProperties.PROXY_PASSWORD, proxyConfiguration.password()); - } - - //ensure Content-Length is populated before sending request via proxy. - config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); - return config; - } - - @Override - protected Client createClient() { - Registry schemeRegistry = getSchemeRegistry(uri, dockerCertificatesStore); - - final HttpClientConnectionManager cm = getConnectionManager(uri, schemeRegistry, connectionPoolSize); - - final RequestConfig requestConfig = RequestConfig.custom() - .setConnectionRequestTimeout((int) connectTimeoutMillis) - .setConnectTimeout((int) connectTimeoutMillis) - .setSocketTimeout((int) readTimeoutMillis) - .build(); - - ClientConfig config = new ClientConfig(JacksonFeature.class); - - if (useProxy) { - config = updateProxy(config); - } - - config - .connectorProvider(new ApacheConnectorProvider()) - .property(ApacheClientProperties.CONNECTION_MANAGER, cm) - .property(ApacheClientProperties.CONNECTION_MANAGER_SHARED, "true") - .property(ApacheClientProperties.REQUEST_CONFIG, requestConfig); - - if (entityProcessing != null) { - switch (entityProcessing) { - case BUFFERED: - config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); - break; - case CHUNKED: - config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); - break; - default: - throw new IllegalArgumentException("Invalid entity processing mode " + entityProcessing); - } - } - - return ClientBuilder.newBuilder() - .withConfig(config) - .build(); - } -} \ No newline at end of file diff --git a/src/test/java/org/mandas/docker/DockerClientBuilderFactory.java b/src/test/java/org/mandas/docker/DockerClientBuilderFactory.java deleted file mode 100644 index 4208d66a..00000000 --- a/src/test/java/org/mandas/docker/DockerClientBuilderFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -/*- - * -\-\- - * docker-client - * -- - * Copyright (C) 2019-2020 Dimitris Mandalidis - * -- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -/-/- -*/ -package org.mandas.docker; - -import org.mandas.docker.client.builder.DockerClientBuilder; -import org.mandas.docker.client.builder.jersey.JerseyDockerClientBuilder; - -public class DockerClientBuilderFactory { - - private DockerClientBuilderFactory() {} - - public static DockerClientBuilder newInstance() { - return new JerseyDockerClientBuilder(); - } -} diff --git a/src/test/java/org/mandas/docker/client/DefaultDockerClientTest.java b/src/test/java/org/mandas/docker/client/DefaultDockerClientTest.java index e9262493..67e4a1ff 100644 --- a/src/test/java/org/mandas/docker/client/DefaultDockerClientTest.java +++ b/src/test/java/org/mandas/docker/client/DefaultDockerClientTest.java @@ -160,7 +160,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; -import org.mandas.docker.DockerClientBuilderFactory; import org.mandas.docker.client.DockerClient.AttachParameter; import org.mandas.docker.client.DockerClient.BuildParam; import org.mandas.docker.client.DockerClient.EventsParam; @@ -183,7 +182,6 @@ import org.mandas.docker.client.messages.Container; import org.mandas.docker.client.messages.ContainerChange; import org.mandas.docker.client.messages.ContainerConfig; -import org.mandas.docker.client.messages.Healthcheck; import org.mandas.docker.client.messages.ContainerCreation; import org.mandas.docker.client.messages.ContainerExit; import org.mandas.docker.client.messages.ContainerInfo; @@ -195,6 +193,7 @@ import org.mandas.docker.client.messages.Event; import org.mandas.docker.client.messages.ExecCreation; import org.mandas.docker.client.messages.ExecState; +import org.mandas.docker.client.messages.Healthcheck; import org.mandas.docker.client.messages.HostConfig; import org.mandas.docker.client.messages.HostConfig.Bind; import org.mandas.docker.client.messages.HostConfig.Ulimit; @@ -294,7 +293,7 @@ public class DefaultDockerClientTest { @Before public void setup() throws Exception { - final DockerClientBuilder builder = DockerClientBuilderFactory.newInstance().fromEnv(); + final DockerClientBuilder builder = DockerClientBuilder.fromEnv(); // Make it easier to test no read timeout occurs by using a smaller value // Such test methods should end in 'NoTimeout' if (testName.getMethodName().endsWith("NoTimeout")) { @@ -1103,7 +1102,7 @@ public void integrationTest() throws Exception { @Test(expected = DockerException.class) public void testConnectTimeout() throws Exception { // Attempt to connect to reserved IP -> should timeout - try (final DefaultDockerClient connectTimeoutClient = DockerClientBuilderFactory.newInstance() + try (final DefaultDockerClient connectTimeoutClient = DockerClientBuilder.fromEnv() .uri("http://240.0.0.1:2375") .connectTimeoutMillis(100) .readTimeoutMillis(NO_TIMEOUT) @@ -1118,7 +1117,7 @@ public void testReadTimeout() throws Exception { // Bind and listen but do not accept -> read will time out. socket.bind(new InetSocketAddress("127.0.0.1", 0)); awaitConnectable(socket.getInetAddress(), socket.getLocalPort()); - try (final DockerClient connectTimeoutClient = DockerClientBuilderFactory.newInstance() + try (final DockerClient connectTimeoutClient = DockerClientBuilder.fromEnv() .uri("http://127.0.0.1:" + socket.getLocalPort()) .connectTimeoutMillis(NO_TIMEOUT) .readTimeoutMillis(100) @@ -1139,7 +1138,7 @@ public void testConnectionRequestTimeout() throws Exception { // Spawn and wait on many more containers than the connection pool size. // This should cause a timeout once the connection pool is exhausted. - try (final DockerClient dockerClient = DockerClientBuilderFactory.newInstance().fromEnv() + try (final DockerClient dockerClient = DockerClientBuilder.fromEnv() .connectionPoolSize(connectionPoolSize) .build()) { // Create container diff --git a/src/test/java/org/mandas/docker/client/DefaultDockerClientUnitTest.java b/src/test/java/org/mandas/docker/client/DefaultDockerClientUnitTest.java index f8c600e8..60b09568 100644 --- a/src/test/java/org/mandas/docker/client/DefaultDockerClientUnitTest.java +++ b/src/test/java/org/mandas/docker/client/DefaultDockerClientUnitTest.java @@ -65,12 +65,12 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mandas.docker.DockerClientBuilderFactory; import org.mandas.docker.client.DockerClient.Signal; import org.mandas.docker.client.auth.RegistryAuthSupplier; import org.mandas.docker.client.builder.DockerClientBuilder; import org.mandas.docker.client.builder.DockerClientBuilder.EntityProcessing; import org.mandas.docker.client.exceptions.ConflictException; +import org.mandas.docker.client.exceptions.DockerCertificateException; import org.mandas.docker.client.exceptions.DockerException; import org.mandas.docker.client.exceptions.NodeNotFoundException; import org.mandas.docker.client.exceptions.NonSwarmNodeException; @@ -142,13 +142,13 @@ public class DefaultDockerClientUnitTest { private final MockWebServer server = new MockWebServer(); - private DockerClientBuilder builder; + private DockerClientBuilder builder; @Before public void setup() throws Exception { server.start(); - builder = DockerClientBuilderFactory.newInstance(); + builder = DockerClientBuilder.fromEnv(); builder.uri(server.url("/").uri()); } @@ -158,32 +158,32 @@ public void tearDown() throws Exception { } @Test - public void testHostForUnixSocket() { - try (final DefaultDockerClient client = DockerClientBuilderFactory.newInstance() + public void testHostForUnixSocket() throws DockerCertificateException { + try (final DefaultDockerClient client = DockerClientBuilder.fromEnv() .uri("unix:///var/run/docker.sock").build()) { assertThat(client.getHost(), equalTo("localhost")); } } @Test - public void testHostForLocalHttps() { - try (final DefaultDockerClient client = DockerClientBuilderFactory.newInstance() + public void testHostForLocalHttps() throws DockerCertificateException { + try (final DefaultDockerClient client = DockerClientBuilder.fromEnv() .uri("https://localhost:2375").build()) { assertThat(client.getHost(), equalTo("localhost")); } } @Test - public void testHostForFqdnHttps() { - try (final DefaultDockerClient client = DockerClientBuilderFactory.newInstance() + public void testHostForFqdnHttps() throws DockerCertificateException { + try (final DefaultDockerClient client = DockerClientBuilder.fromEnv() .uri("https://perdu.com:2375").build()) { assertThat(client.getHost(), equalTo("perdu.com")); } } @Test - public void testHostForIpHttps() { - try (final DefaultDockerClient client = DockerClientBuilderFactory.newInstance() + public void testHostForIpHttps() throws DockerCertificateException { + try (final DefaultDockerClient client = DockerClientBuilder.fromEnv() .uri("https://192.168.53.103:2375").build()) { assertThat(client.getHost(), equalTo("192.168.53.103")); }