com.spotify.docker.client.DefaultDockerClient.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.docker.client.DefaultDockerClient.java

Source

/*
 * Copyright (c) 2014 Spotify AB.
 * Copyright (c) 2014 Oleg Poleshuk.
 * Copyright (c) 2014 CyDesign Ltd.
 *
 * 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 com.spotify.docker.client;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.io.CharStreams;
import com.google.common.net.HostAndPort;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.spotify.docker.client.messages.AuthConfig;
import com.spotify.docker.client.messages.AuthRegistryConfig;
import com.spotify.docker.client.messages.Container;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ContainerCreation;
import com.spotify.docker.client.messages.ContainerExit;
import com.spotify.docker.client.messages.ContainerInfo;
import com.spotify.docker.client.messages.ContainerStats;
import com.spotify.docker.client.messages.ExecState;
import com.spotify.docker.client.messages.Image;
import com.spotify.docker.client.messages.ImageInfo;
import com.spotify.docker.client.messages.ImageSearchResult;
import com.spotify.docker.client.messages.Info;
import com.spotify.docker.client.messages.ProgressMessage;
import com.spotify.docker.client.messages.RemovedImage;
import com.spotify.docker.client.messages.Version;

import org.apache.commons.compress.utils.IOUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
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.PoolingHttpClientConnectionManager;
import org.glassfish.hk2.api.MultiException;
import org.glassfish.jersey.apache.connector.ApacheClientProperties;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.StringWriter;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.ResponseProcessingException;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Maps.newHashMap;
import static com.spotify.docker.client.ObjectMapperProvider.objectMapper;
import static com.spotify.docker.client.VersionCompare.compareVersion;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.HttpMethod.PUT;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;

public class DefaultDockerClient implements DockerClient, Closeable {

    /**
     * Hack: this {@link ProgressHandler} is meant to capture the image ID of an
     * image being loaded. Weirdly enough, Docker returns the ID of a newly created image
     * in the status of a progress message.
     * <p>
     * The image ID is required to tag the just loaded image since, also weirdly enough,
     * the pull operation with the <code>fromSrc</code> parameter does not support the 
     * <code>tag</code> parameter. By retrieving the ID, the image can be tagged with its 
     * image name, given its ID.
     */
    private static class LoadProgressHandler implements ProgressHandler {

        private static final int EXPECTED_CHARACTER_NUM = 64;

        private final ProgressHandler delegate;

        private String imageId;

        private LoadProgressHandler(ProgressHandler delegate) {
            this.delegate = delegate;
        }

        private String getImageId() {
            Preconditions.checkState(imageId != null, "Could not acquire image ID following load");
            return imageId;
        }

        @Override
        public void progress(ProgressMessage message) throws DockerException {
            delegate.progress(message);
            if (message.status() != null && message.status().length() == EXPECTED_CHARACTER_NUM) {
                imageId = message.status();
            }
        }

    }

    // ==========================================================================

    public static final String DEFAULT_UNIX_ENDPOINT = "unix:///var/run/docker.sock";
    public static final String DEFAULT_HOST = "localhost";
    public static final int DEFAULT_PORT = 2375;

    private static final String UNIX_SCHEME = "unix";

    private static final Logger log = LoggerFactory.getLogger(DefaultDockerClient.class);

    public static final long NO_TIMEOUT = 0;

    private static final long DEFAULT_CONNECT_TIMEOUT_MILLIS = SECONDS.toMillis(5);
    private static final long DEFAULT_READ_TIMEOUT_MILLIS = SECONDS.toMillis(30);
    private static final int DEFAULT_CONNECTION_POOL_SIZE = 100;

    private static final ClientConfig DEFAULT_CONFIG = new ClientConfig(ObjectMapperProvider.class,
            JacksonFeature.class, LogsResponseReader.class, ProgressResponseReader.class);

    private static final Pattern CONTAINER_NAME_PATTERN = Pattern.compile("/?[a-zA-Z0-9_-]+");

    private static final GenericType<List<Container>> CONTAINER_LIST = new GenericType<List<Container>>() {
    };

    private static final GenericType<List<Image>> IMAGE_LIST = new GenericType<List<Image>>() {
    };

    private static final GenericType<List<ImageSearchResult>> IMAGES_SEARCH_RESULT_LIST = new GenericType<List<ImageSearchResult>>() {
    };

    private static final GenericType<List<RemovedImage>> REMOVED_IMAGE_LIST = new GenericType<List<RemovedImage>>() {
    };

    private final Client client;
    private final Client noTimeoutClient;

    private final URI uri;
    private final String apiVersion;
    private final AuthConfig authConfig;

    Client getClient() {
        return client;
    }

    Client getNoTimeoutClient() {
        return noTimeoutClient;
    }

    /**
     * Create a new client with default configuration.
     * @param uri The docker rest api uri.
     */
    public DefaultDockerClient(final String uri) {
        this(URI.create(uri.replaceAll("^unix:///", "unix://localhost/")));
    }

    /**
     * Create a new client with default configuration.
     * @param uri The docker rest api uri.
     */
    public DefaultDockerClient(final URI uri) {
        this(new Builder().uri(uri));
    }

    /**
     * Create a new client with default configuration.
     * @param uri The docker rest api uri.
     * @param dockerCertificates The certificates to use for HTTPS.
     */
    public DefaultDockerClient(final URI uri, final DockerCertificates dockerCertificates) {
        this(new Builder().uri(uri).dockerCertificates(dockerCertificates));
    }

    /**
     * Create a new client using the configuration of the builder.
     *
     * @param builder DefaultDockerClient builder
     */
    protected DefaultDockerClient(final Builder builder) {
        URI originalUri = checkNotNull(builder.uri, "uri");
        this.apiVersion = builder.apiVersion();

        if ((builder.dockerCertificates != null) && !originalUri.getScheme().equals("https")) {
            throw new IllegalArgumentException(
                    "An HTTPS URI for DOCKER_HOST must be provided to use Docker client certificates");
        }

        if (originalUri.getScheme().equals(UNIX_SCHEME)) {
            this.uri = UnixConnectionSocketFactory.sanitizeUri(originalUri);
        } else {
            this.uri = originalUri;
        }

        final PoolingHttpClientConnectionManager cm = getConnectionManager(builder);
        final PoolingHttpClientConnectionManager noTimeoutCm = getConnectionManager(builder);

        final RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout((int) builder.connectTimeoutMillis)
                .setConnectTimeout((int) builder.connectTimeoutMillis)
                .setSocketTimeout((int) builder.readTimeoutMillis).build();

        final ClientConfig config = DEFAULT_CONFIG.connectorProvider(new ApacheConnectorProvider())
                .property(ApacheClientProperties.CONNECTION_MANAGER, cm)
                .property(ApacheClientProperties.REQUEST_CONFIG, requestConfig);

        this.authConfig = builder.authConfig;

        this.client = ClientBuilder.newClient(config);

        // ApacheConnector doesn't respect per-request timeout settings.
        // Workaround: instead create a client with infinite read timeout,
        // and use it for waitContainer, stopContainer, attachContainer, logs, and build
        final RequestConfig noReadTimeoutRequestConfig = RequestConfig.copy(requestConfig)
                .setSocketTimeout((int) NO_TIMEOUT).build();
        this.noTimeoutClient = ClientBuilder.newBuilder().withConfig(config)
                .property(ApacheClientProperties.CONNECTION_MANAGER, noTimeoutCm)
                .property(ApacheClientProperties.REQUEST_CONFIG, noReadTimeoutRequestConfig).build();
    }

    public String getHost() {
        return fromNullable(uri.getHost()).or("localhost");
    }

    private PoolingHttpClientConnectionManager getConnectionManager(Builder builder) {
        final PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
                getSchemeRegistry(builder));

        // Use all available connections instead of artificially limiting ourselves to 2 per server.
        cm.setMaxTotal(builder.connectionPoolSize);
        cm.setDefaultMaxPerRoute(cm.getMaxTotal());

        return cm;
    }

    private Registry<ConnectionSocketFactory> getSchemeRegistry(final Builder builder) {
        final SSLConnectionSocketFactory https;
        if (builder.dockerCertificates == null) {
            https = SSLConnectionSocketFactory.getSocketFactory();
        } else {
            https = new SSLConnectionSocketFactory(builder.dockerCertificates.sslContext(),
                    builder.dockerCertificates.hostnameVerifier());
        }

        final RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder
                .<ConnectionSocketFactory>create().register("https", https)
                .register("http", PlainConnectionSocketFactory.getSocketFactory());

        if (builder.uri.getScheme().equals(UNIX_SCHEME)) {
            registryBuilder.register(UNIX_SCHEME, new UnixConnectionSocketFactory(builder.uri));
        }

        return registryBuilder.build();
    }

    @Override
    public void close() {
        client.close();
        noTimeoutClient.close();
    }

    @Override
    public String ping() throws DockerException, InterruptedException {
        final WebTarget resource = client.target(uri).path("_ping");
        return request(GET, String.class, resource, resource.request());
    }

    @Override
    public Version version() throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("version");
        return request(GET, Version.class, resource, resource.request(APPLICATION_JSON_TYPE));
    }

    @Override
    public int auth(final AuthConfig authConfig) throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("auth");
        final Response response = request(POST, Response.class, resource, resource.request(APPLICATION_JSON_TYPE),
                Entity.json(authConfig));
        return response.getStatus();
    }

    @Override
    public Info info() throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("info");
        return request(GET, Info.class, resource, resource.request(APPLICATION_JSON_TYPE));
    }

    @Override
    public List<Container> listContainers(final ListContainersParam... params)
            throws DockerException, InterruptedException {
        WebTarget resource = resource().path("containers").path("json");

        for (ListContainersParam param : params) {
            resource = resource.queryParam(param.name(), param.value());
        }

        return request(GET, CONTAINER_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
    }

    @Override
    public List<Image> listImages(ListImagesParam... params) throws DockerException, InterruptedException {
        WebTarget resource = resource().path("images").path("json");

        final Map<String, String> filters = newHashMap();
        for (ListImagesParam param : params) {
            if (param instanceof ListImagesFilterParam) {
                filters.put(param.name(), param.value());
            } else {
                resource = resource.queryParam(param.name(), param.value());
            }
        }

        // If filters were specified, we must put them in a JSON object and pass them using the
        // 'filters' query param like this: filters={"dangling":["true"]}
        try {
            if (!filters.isEmpty()) {
                final StringWriter writer = new StringWriter();
                final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer);
                generator.writeStartObject();
                for (Map.Entry<String, String> entry : filters.entrySet()) {
                    generator.writeArrayFieldStart(entry.getKey());
                    generator.writeString(entry.getValue());
                    generator.writeEndArray();
                }
                generator.writeEndObject();
                generator.close();
                // We must URL encode the string, otherwise Jersey chokes on the double-quotes in the json.
                final String encoded = URLEncoder.encode(writer.toString(), UTF_8.name());
                resource = resource.queryParam("filters", encoded);
            }
        } catch (IOException e) {
            throw new DockerException(e);
        }

        return request(GET, IMAGE_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
    }

    @Override
    public ContainerCreation createContainer(final ContainerConfig config)
            throws DockerException, InterruptedException {
        return createContainer(config, null);
    }

    @Override
    public ContainerCreation createContainer(final ContainerConfig config, final String name)
            throws DockerException, InterruptedException {
        WebTarget resource = resource().path("containers").path("create");

        if (name != null) {
            checkArgument(CONTAINER_NAME_PATTERN.matcher(name).matches(), "Invalid container name: \"%s\"", name);
            resource = resource.queryParam("name", name);
        }

        log.info("Creating container with ContainerConfig: {}", config);

        try {
            return request(POST, ContainerCreation.class, resource, resource.request(APPLICATION_JSON_TYPE),
                    Entity.json(config));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ImageNotFoundException(config.image(), e);
            default:
                throw e;
            }
        }
    }

    @Override
    public void startContainer(final String containerId) throws DockerException, InterruptedException {
        checkNotNull(containerId, "containerId");

        log.info("Starting container with Id: {}", containerId);

        containerAction(containerId, "start");
    }

    private void containerAction(final String containerId, final String action)
            throws DockerException, InterruptedException {
        try {
            final WebTarget resource = resource().path("containers").path(containerId).path(action);
            request(POST, resource, resource.request());
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public void pauseContainer(final String containerId) throws DockerException, InterruptedException {
        checkNotNull(containerId, "containerId");
        containerAction(containerId, "pause");
    }

    @Override
    public void unpauseContainer(final String containerId) throws DockerException, InterruptedException {
        checkNotNull(containerId, "containerId");
        containerAction(containerId, "unpause");
    }

    @Override
    public void restartContainer(String containerId) throws DockerException, InterruptedException {
        restartContainer(containerId, 10);
    }

    @Override
    public void restartContainer(String containerId, int secondsToWaitBeforeRestart)
            throws DockerException, InterruptedException {
        checkNotNull(containerId, "containerId");
        checkNotNull(secondsToWaitBeforeRestart, "secondsToWait");
        try {
            final WebTarget resource = resource().path("containers").path(containerId).path("restart")
                    .queryParam("t", String.valueOf(secondsToWaitBeforeRestart));
            request(POST, resource, resource.request());
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public void killContainer(final String containerId) throws DockerException, InterruptedException {
        containerAction(containerId, "kill");
    }

    @Override
    public void stopContainer(final String containerId, final int secondsToWaitBeforeKilling)
            throws DockerException, InterruptedException {
        try {
            final WebTarget resource = noTimeoutResource().path("containers").path(containerId).path("stop")
                    .queryParam("t", String.valueOf(secondsToWaitBeforeKilling));
            request(POST, resource, resource.request());
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 304: // already stopped, so we're cool
                return;
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public ContainerExit waitContainer(final String containerId) throws DockerException, InterruptedException {
        try {
            final WebTarget resource = noTimeoutResource().path("containers").path(containerId).path("wait");
            // Wait forever
            return request(POST, ContainerExit.class, resource, resource.request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public void removeContainer(final String containerId) throws DockerException, InterruptedException {
        removeContainer(containerId, false);
    }

    @Override
    public void removeContainer(final String containerId, final boolean removeVolumes)
            throws DockerException, InterruptedException {
        try {
            final WebTarget resource = resource().path("containers").path(containerId);
            request(DELETE, resource,
                    resource.queryParam("v", String.valueOf(removeVolumes)).request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public InputStream exportContainer(String containerId) throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("containers").path(containerId).path("export");
        return request(GET, InputStream.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE));
    }

    @Override
    public InputStream copyContainer(String containerId, String path) throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("containers").path(containerId).path("copy");

        // Internal JSON object; not worth it to create class for this
        JsonNodeFactory nf = JsonNodeFactory.instance;
        final JsonNode params = nf.objectNode().set("Resource", nf.textNode(path));

        return request(POST, InputStream.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE),
                Entity.json(params));
    }

    @Override
    public void copyToContainer(final Path directory, String containerId, String path)
            throws DockerException, InterruptedException, IOException {
        final WebTarget resource = resource().path("containers").path(containerId).path("archive")
                .queryParam("noOverwriteDirNonDir", true).queryParam("path", path);

        CompressedDirectory compressedDirectory = CompressedDirectory.create(directory);

        final InputStream fileStream = Files.newInputStream(compressedDirectory.file());

        request(PUT, String.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE),
                Entity.entity(fileStream, "application/tar"));
    }

    @Override
    public ContainerInfo inspectContainer(final String containerId) throws DockerException, InterruptedException {
        try {
            final WebTarget resource = resource().path("containers").path(containerId).path("json");
            return request(GET, ContainerInfo.class, resource, resource.request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public ContainerCreation commitContainer(final String containerId, final String repo, final String tag,
            final ContainerConfig config, final String comment, final String author)
            throws DockerException, InterruptedException {

        checkNotNull(containerId, "containerId");
        checkNotNull(repo, "repo");
        checkNotNull(config, "containerConfig");

        WebTarget resource = resource().path("commit").queryParam("container", containerId).queryParam("repo", repo)
                .queryParam("comment", comment);

        if (!isNullOrEmpty(author)) {
            resource = resource.queryParam("author", author);
        }
        if (!isNullOrEmpty(comment)) {
            resource = resource.queryParam("comment", comment);
        }
        if (!isNullOrEmpty(tag)) {
            resource = resource.queryParam("tag", tag);
        }

        log.info("Committing container id: {} to repository: {} with ContainerConfig: {}", containerId, repo,
                config);

        try {
            return request(POST, ContainerCreation.class, resource, resource.request(APPLICATION_JSON_TYPE),
                    Entity.json(config));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public List<ImageSearchResult> searchImages(final String term) throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("images").path("search").queryParam("term", term);
        return request(GET, IMAGES_SEARCH_RESULT_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
    }

    @Override
    public void load(final String image, final InputStream imagePayload)
            throws DockerException, InterruptedException {
        load(image, imagePayload, new LoggingPullHandler("image stream"));
    }

    @Override
    public void load(final String image, final InputStream imagePayload, final AuthConfig authConfig,
            final ProgressHandler handler) throws DockerException, InterruptedException {
        load(image, imagePayload, handler);
    }

    @Override
    public void load(final String image, final InputStream imagePayload, final AuthConfig authConfig)
            throws DockerException, InterruptedException {
        load(image, imagePayload, authConfig, new LoggingPullHandler("image stream"));
    }

    @Override
    public void load(final String image, final InputStream imagePayload, final ProgressHandler handler)
            throws DockerException, InterruptedException {

        WebTarget resource = resource().path("images").path("create");

        resource = resource.queryParam("fromSrc", "-").queryParam("tag", image);

        LoadProgressHandler loadProgressHandler = new LoadProgressHandler(handler);
        Entity<InputStream> entity = Entity.entity(imagePayload, MediaType.APPLICATION_OCTET_STREAM);
        try (ProgressStream load = request(POST, ProgressStream.class, resource,
                resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader(authConfig)),
                entity)) {
            load.tail(loadProgressHandler, POST, resource.getUri());
            tag(loadProgressHandler.getImageId(), image, true);
        } catch (IOException e) {
            throw new DockerException(e);
        } finally {
            IOUtils.closeQuietly(imagePayload);
        }
    }

    @Override
    public InputStream save(final String image) throws DockerException, IOException, InterruptedException {
        return save(image, authConfig);
    }

    @Override
    public InputStream save(final String image, final AuthConfig authConfig)
            throws DockerException, IOException, InterruptedException {
        WebTarget resource = resource().path("images").path(image).path("get");

        return request(GET, InputStream.class, resource,
                resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader(authConfig)));
    }

    @Override
    public void pull(final String image) throws DockerException, InterruptedException {
        pull(image, new LoggingPullHandler(image));
    }

    @Override
    public void pull(final String image, final ProgressHandler handler)
            throws DockerException, InterruptedException {
        pull(image, authConfig, handler);
    }

    @Override
    public void pull(final String image, final AuthConfig authConfig) throws DockerException, InterruptedException {
        pull(image, authConfig, new LoggingPullHandler(image));
    }

    @Override
    public void pull(final String image, final AuthConfig authConfig, final ProgressHandler handler)
            throws DockerException, InterruptedException {
        final ImageRef imageRef = new ImageRef(image);

        WebTarget resource = resource().path("images").path("create");

        resource = resource.queryParam("fromImage", imageRef.getImage());
        if (imageRef.getTag() != null) {
            resource = resource.queryParam("tag", imageRef.getTag());
        }

        try (ProgressStream pull = request(POST, ProgressStream.class, resource,
                resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader(authConfig)))) {
            pull.tail(handler, POST, resource.getUri());
        } catch (IOException e) {
            throw new DockerException(e);
        }
    }

    @Override
    public void push(final String image) throws DockerException, InterruptedException {
        push(image, new LoggingPushHandler(image));
    }

    @Override
    public void push(final String image, final ProgressHandler handler)
            throws DockerException, InterruptedException {
        final ImageRef imageRef = new ImageRef(image);

        WebTarget resource = resource().path("images").path(imageRef.getImage()).path("push");

        if (imageRef.getTag() != null) {
            resource = resource.queryParam("tag", imageRef.getTag());
        }

        // the docker daemon requires that the X-Registry-Auth header is specified
        // with a non-empty string even if your registry doesn't use authentication
        try (ProgressStream push = request(POST, ProgressStream.class, resource,
                resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader()))) {
            push.tail(handler, POST, resource.getUri());
        } catch (IOException e) {
            throw new DockerException(e);
        }
    }

    @Override
    public void tag(final String image, final String name) throws DockerException, InterruptedException {
        tag(image, name, false);
    }

    @Override
    public void tag(final String image, final String name, final boolean force)
            throws DockerException, InterruptedException {
        final ImageRef imageRef = new ImageRef(name);

        WebTarget resource = resource().path("images").path(image).path("tag");

        resource = resource.queryParam("repo", imageRef.getImage());
        if (imageRef.getTag() != null) {
            resource = resource.queryParam("tag", imageRef.getTag());
        }

        if (force) {
            resource = resource.queryParam("force", true);
        }

        try {
            request(POST, resource, resource.request());
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ImageNotFoundException(image, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public String build(final Path directory, final BuildParameter... params)
            throws DockerException, InterruptedException, IOException {
        return build(directory, null, new LoggingBuildHandler(), params);
    }

    @Override
    public String build(final Path directory, final String name, final BuildParameter... params)
            throws DockerException, InterruptedException, IOException {
        return build(directory, name, new LoggingBuildHandler(), params);
    }

    @Override
    public String build(final Path directory, final ProgressHandler handler, final BuildParameter... params)
            throws DockerException, InterruptedException, IOException {
        return build(directory, null, handler, params);
    }

    @Override
    public String build(final Path directory, final String name, final ProgressHandler handler,
            final BuildParameter... params) throws DockerException, InterruptedException, IOException {
        return build(directory, name, null, handler, params);
    }

    @Override
    public String build(final Path directory, final String name, final String dockerfile,
            final ProgressHandler handler, final BuildParameter... params)
            throws DockerException, InterruptedException, IOException {
        checkNotNull(handler, "handler");

        WebTarget resource = noTimeoutResource().path("build");

        for (final BuildParameter param : params) {
            resource = resource.queryParam(param.buildParamName, String.valueOf(param.buildParamValue));
        }
        if (name != null) {
            resource = resource.queryParam("t", name);
        }
        if (dockerfile != null) {
            resource = resource.queryParam("dockerfile", dockerfile);
        }

        log.debug("Auth Config {}", authConfig);

        // Convert auth to X-Registry-Config format
        AuthRegistryConfig authRegistryConfig;
        if (authConfig == null) {
            authRegistryConfig = AuthRegistryConfig.EMPTY;
        } else {
            authRegistryConfig = new AuthRegistryConfig(authConfig.serverAddress(), authConfig.username(),
                    authConfig.password(), authConfig.email(), authConfig.serverAddress());
        }

        try (final CompressedDirectory compressedDirectory = CompressedDirectory.create(directory);
                final InputStream fileStream = Files.newInputStream(compressedDirectory.file());
                final ProgressStream build = request(POST, ProgressStream.class, resource,
                        resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Config",
                                authRegistryHeader(authRegistryConfig)),
                        Entity.entity(fileStream, "application/tar"))) {

            String imageId = null;
            while (build.hasNextMessage(POST, resource.getUri())) {
                final ProgressMessage message = build.nextMessage(POST, resource.getUri());
                final String id = message.buildImageId();
                if (id != null) {
                    imageId = id;
                }
                handler.progress(message);
            }
            return imageId;
        }
    }

    @Override
    public ImageInfo inspectImage(final String image) throws DockerException, InterruptedException {
        try {
            final WebTarget resource = resource().path("images").path(image).path("json");
            return request(GET, ImageInfo.class, resource, resource.request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ImageNotFoundException(image, e);
            default:
                throw e;
            }
        }
    }

    @Override
    public List<RemovedImage> removeImage(String image) throws DockerException, InterruptedException {
        return removeImage(image, false, false);
    }

    @Override
    public List<RemovedImage> removeImage(String image, boolean force, boolean noPrune)
            throws DockerException, InterruptedException {
        try {
            final WebTarget resource = resource().path("images").path(image)
                    .queryParam("force", String.valueOf(force)).queryParam("noprune", String.valueOf(noPrune));
            return request(DELETE, REMOVED_IMAGE_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ImageNotFoundException(image);
            default:
                throw e;
            }
        }
    }

    @Override
    public LogStream logs(final String containerId, final LogsParam... params)
            throws DockerException, InterruptedException {
        WebTarget resource = noTimeoutResource().path("containers").path(containerId).path("logs");

        for (LogsParam param : params) {
            resource = resource.queryParam(param.name(), param.value());
        }

        return getLogStream(GET, resource, containerId);
    }

    @Override
    public LogStream attachContainer(final String containerId, final AttachParameter... params)
            throws DockerException, InterruptedException {
        WebTarget resource = noTimeoutResource().path("containers").path(containerId).path("attach");

        for (final AttachParameter param : params) {
            resource = resource.queryParam(param.name().toLowerCase(Locale.ROOT), String.valueOf(true));
        }

        return getLogStream(POST, resource, containerId);
    }

    private LogStream getLogStream(final String method, final WebTarget resource, final String containerId)
            throws DockerException, InterruptedException {
        try {
            final Invocation.Builder request = resource.request("application/vnd.docker.raw-stream");
            return request(method, LogStream.class, resource, request);
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId);
            default:
                throw e;
            }
        }
    }

    @Override
    public String execCreate(String containerId, String[] cmd, ExecParameter... params)
            throws DockerException, InterruptedException {
        WebTarget resource = resource().path("containers").path(containerId).path("exec");

        final StringWriter writer = new StringWriter();
        try {
            final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer);
            generator.writeStartObject();

            for (ExecParameter param : params) {
                generator.writeBooleanField(param.getName(), true);
            }

            generator.writeArrayFieldStart("Cmd");
            for (String s : cmd) {
                generator.writeString(s);
            }
            generator.writeEndArray();

            generator.writeEndObject();
            generator.close();
        } catch (IOException e) {
            throw new DockerException(e);
        }

        String response;
        try {
            response = request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE),
                    Entity.json(writer.toString()));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId);
            default:
                throw e;
            }
        }

        try {
            JsonNode json = objectMapper().readTree(response);
            return json.findValue("Id").textValue();
        } catch (IOException e) {
            throw new DockerException(e);
        }
    }

    @Override
    public LogStream execStart(String execId, ExecStartParameter... params)
            throws DockerException, InterruptedException {
        WebTarget resource = resource().path("exec").path(execId).path("start");

        final StringWriter writer = new StringWriter();
        try {
            final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer);
            generator.writeStartObject();

            for (ExecStartParameter param : params) {
                generator.writeBooleanField(param.getName(), true);
            }

            generator.writeEndObject();
            generator.close();
        } catch (IOException e) {
            throw new DockerException(e);
        }

        try {
            return request(POST, LogStream.class, resource, resource.request("application/vnd.docker.raw-stream"),
                    Entity.json(writer.toString()));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ExecNotFoundException(execId);
            default:
                throw e;
            }
        }
    }

    @Override
    public ExecState execInspect(final String execId) throws DockerException, InterruptedException {
        WebTarget resource = resource().path("exec").path(execId).path("json");

        try {
            return request(GET, ExecState.class, resource, resource.request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ExecNotFoundException(execId);
            default:
                throw e;
            }
        }
    }

    @Override
    public ContainerStats stats(final String containerId) throws DockerException, InterruptedException {
        final WebTarget resource = resource().path("containers").path(containerId).path("stats")
                .queryParam("stream", "0");

        try {
            return request(GET, ContainerStats.class, resource, resource.request(APPLICATION_JSON_TYPE));
        } catch (DockerRequestException e) {
            switch (e.status()) {
            case 404:
                throw new ContainerNotFoundException(containerId);
            default:
                throw e;
            }
        }
    }

    private WebTarget resource() {
        final WebTarget target = client.target(uri);
        if (!isNullOrEmpty(apiVersion)) {
            return target.path(apiVersion);
        }
        return target;
    }

    private WebTarget noTimeoutResource() {
        final WebTarget target = noTimeoutClient.target(uri);
        if (!isNullOrEmpty(apiVersion)) {
            return target.path(apiVersion);
        }
        return target;
    }

    private <T> T request(final String method, final GenericType<T> type, final WebTarget resource,
            final Invocation.Builder request) throws DockerException, InterruptedException {
        try {
            return request.async().method(method, type).get();
        } catch (ExecutionException | MultiException e) {
            throw propagate(method, resource, e);
        }
    }

    private <T> T request(final String method, final Class<T> clazz, final WebTarget resource,
            final Invocation.Builder request) throws DockerException, InterruptedException {
        try {
            return request.async().method(method, clazz).get();
        } catch (ExecutionException | MultiException e) {
            throw propagate(method, resource, e);
        }
    }

    private <T> T request(final String method, final Class<T> clazz, final WebTarget resource,
            final Invocation.Builder request, final Entity<?> entity) throws DockerException, InterruptedException {
        try {
            return request.async().method(method, entity, clazz).get();
        } catch (ExecutionException | MultiException e) {
            throw propagate(method, resource, e);
        }
    }

    private void request(final String method, final WebTarget resource, final Invocation.Builder request)
            throws DockerException, InterruptedException {
        try {
            request.async().method(method, String.class).get();
        } catch (ExecutionException | MultiException e) {
            throw propagate(method, resource, e);
        }
    }

    private RuntimeException propagate(final String method, final WebTarget resource, final Exception e)
            throws DockerException, InterruptedException {
        Throwable cause = e.getCause();

        // Sometimes e is a org.glassfish.hk2.api.MultiException
        // which contains the cause we're actually interested in.
        // So we unpack it here.
        if (e instanceof MultiException) {
            cause = cause.getCause();
        }

        Response response = null;
        if (cause instanceof ResponseProcessingException) {
            response = ((ResponseProcessingException) cause).getResponse();
        } else if (cause instanceof WebApplicationException) {
            response = ((WebApplicationException) cause).getResponse();
        } else if ((cause instanceof ProcessingException) && (cause.getCause() != null)) {
            // For a ProcessingException, The exception message or nested Throwable cause SHOULD contain
            // additional information about the reason of the processing failure.
            cause = cause.getCause();
        }

        if (response != null) {
            throw new DockerRequestException(method, resource.getUri(), response.getStatus(), message(response),
                    cause);
        } else if ((cause instanceof SocketTimeoutException) || (cause instanceof ConnectTimeoutException)) {
            throw new DockerTimeoutException(method, resource.getUri(), e);
        } else if ((cause instanceof InterruptedIOException) || (cause instanceof InterruptedException)) {
            throw new InterruptedException("Interrupted: " + method + " " + resource);
        } else {
            throw new DockerException(e);
        }
    }

    private String message(final Response response) {
        final Readable reader = new InputStreamReader(response.readEntity(InputStream.class), UTF_8);
        try {
            return CharStreams.toString(reader);
        } catch (IOException ignore) {
            return null;
        }
    }

    private String authHeader() throws DockerException {
        return authHeader(authConfig);
    }

    private String authHeader(final AuthConfig authConfig) throws DockerException {
        if (authConfig == null) {
            return "null";
        }
        try {
            return Base64.encodeAsString(ObjectMapperProvider.objectMapper().writeValueAsString(authConfig));
        } catch (JsonProcessingException ex) {
            throw new DockerException("Could not encode X-Registry-Auth header", ex);
        }
    }

    private String authRegistryHeader(final AuthRegistryConfig authRegistryConfig) throws DockerException {
        if (authRegistryConfig == null) {
            return "null";
        }
        try {
            String authRegistryJson = ObjectMapperProvider.objectMapper().writeValueAsString(authRegistryConfig);

            final String apiVersion = version().apiVersion();
            final int versionComparison = compareVersion(apiVersion, "1.19");

            // Version below 1.19
            if (versionComparison < 0) {
                authRegistryJson = "{\"configs\":" + authRegistryJson + "}";
            } else if (versionComparison == 0) {
                // Version equal 1.19
                authRegistryJson = "{\"auths\":" + authRegistryJson + "}";
            }

            log.debug("Registry Config Json {}", authRegistryJson);
            String authRegistryEncoded = Base64.encodeAsString(authRegistryJson);
            log.debug("Registry Config Encoded {}", authRegistryEncoded);
            return authRegistryEncoded;
        } catch (JsonProcessingException | InterruptedException ex) {
            throw new DockerException("Could not encode X-Registry-Config header", ex);
        }
    }

    /**
     * Create a new {@link DefaultDockerClient} builder.
     * @return Returns a builder that can be used to further customize and then build the client.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Create a new {@link DefaultDockerClient} builder prepopulated with values loaded
     * from the DOCKER_HOST and DOCKER_CERT_PATH environment variables.
     * @return Returns a builder that can be used to further customize and then build the client.
     * @throws DockerCertificateException if we could not build a DockerCertificates object
     */
    public static Builder fromEnv() throws DockerCertificateException {
        final String endpoint = fromNullable(getenv("DOCKER_HOST")).or(defaultEndpoint());
        final Path dockerCertPath = Paths.get(fromNullable(getenv("DOCKER_CERT_PATH")).or(defaultCertPath()));

        final Builder builder = new Builder();

        final Optional<DockerCertificates> certs = DockerCertificates.builder().dockerCertPath(dockerCertPath)
                .build();

        if (endpoint.startsWith(UNIX_SCHEME + "://")) {
            builder.uri(endpoint);
        } else {
            final String stripped = endpoint.replaceAll(".*://", "");
            final HostAndPort hostAndPort = HostAndPort.fromString(stripped);
            final String hostText = hostAndPort.getHostText();
            final String scheme = certs.isPresent() ? "https" : "http";

            final int port = hostAndPort.getPortOrDefault(DEFAULT_PORT);
            final String address = isNullOrEmpty(hostText) ? DEFAULT_HOST : hostText;

            builder.uri(scheme + "://" + address + ":" + port);
        }

        if (certs.isPresent()) {
            builder.dockerCertificates(certs.get());
        }

        return builder;
    }

    private static String defaultEndpoint() {
        if (getProperty("os.name").equalsIgnoreCase("linux")) {
            return DEFAULT_UNIX_ENDPOINT;
        } else {
            return DEFAULT_HOST + ":" + DEFAULT_PORT;
        }
    }

    private static String defaultCertPath() {
        return Paths.get(getProperty("user.home"), ".docker").toString();
    }

    public static class Builder {

        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 DockerCertificates dockerCertificates;
        private AuthConfig authConfig;

        public URI uri() {
            return uri;
        }

        public Builder uri(final URI uri) {
            this.uri = uri;
            return this;
        }

        /**
         * Set the URI for connections to Docker.
         *
         * @param uri URI String for connections to Docker
         * @return Builder
         */
        public Builder 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
         */
        public Builder apiVersion(final String apiVersion) {
            this.apiVersion = apiVersion;
            return this;
        }

        public String apiVersion() {
            return apiVersion;
        }

        public long connectTimeoutMillis() {
            return connectTimeoutMillis;
        }

        /**
         * Set the timeout in milliseconds until a connection to Docker is established.
         * A timeout value of zero is interpreted as an infinite timeout.
         *
         * @param connectTimeoutMillis connection timeout to Docker daemon in milliseconds
         * @return Builder
         */
        public Builder connectTimeoutMillis(final long connectTimeoutMillis) {
            this.connectTimeoutMillis = connectTimeoutMillis;
            return this;
        }

        public long readTimeoutMillis() {
            return readTimeoutMillis;
        }

        /**
         * Set the SO_TIMEOUT in milliseconds. This is the maximum period of inactivity
         * between receiving two consecutive data packets from Docker.
         *
         * @param readTimeoutMillis read timeout to Docker daemon in milliseconds
         * @return Builder
         */
        public Builder readTimeoutMillis(final long readTimeoutMillis) {
            this.readTimeoutMillis = readTimeoutMillis;
            return this;
        }

        public DockerCertificates dockerCertificates() {
            return dockerCertificates;
        }

        /**
         * Provide certificates to secure the connection to Docker.
         *
         * @param dockerCertificates DockerCertificates object
         * @return Builder
         */
        public Builder dockerCertificates(final DockerCertificates dockerCertificates) {
            this.dockerCertificates = dockerCertificates;
            return this;
        }

        public int connectionPoolSize() {
            return connectionPoolSize;
        }

        /**
         * 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 Builder connectionPoolSize(final int connectionPoolSize) {
            this.connectionPoolSize = connectionPoolSize;
            return this;
        }

        public AuthConfig authConfig() {
            return authConfig;
        }

        /**
         * Set the auth parameters for pull/push requests from/to private repositories.
         *
         * @param authConfig AuthConfig object
         * @return Builder
         */
        public Builder authConfig(final AuthConfig authConfig) {
            this.authConfig = authConfig;
            return this;
        }

        public DefaultDockerClient build() {
            return new DefaultDockerClient(this);
        }
    }

}