All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.docker.client.DefaultDockerClient Maven / Gradle / Ivy

/*
 * 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.Network;
import com.spotify.docker.client.messages.NetworkConfig;
import com.spotify.docker.client.messages.NetworkCreation;
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.HashMap;
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.
   * 

* The image ID is required to tag the just loaded image since, also weirdly enough, * the pull operation with the fromSrc parameter does not support the * tag 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> CONTAINER_LIST = new GenericType>() {}; private static final GenericType> IMAGE_LIST = new GenericType>() {}; private static final GenericType> NETWORK_LIST = new GenericType>() {}; private static final GenericType> IMAGES_SEARCH_RESULT_LIST = new GenericType>() {}; private static final GenericType> REMOVED_IMAGE_LIST = new GenericType>() {}; 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 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 registryBuilder = RegistryBuilder .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 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 listImages(ListImagesParam... params) throws DockerException, InterruptedException { WebTarget resource = resource() .path("images").path("json"); final Map 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 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 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 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 BuildParam... params) throws DockerException, InterruptedException, IOException { return build(directory, null, new LoggingBuildHandler(), params); } @Override public String build(final Path directory, final String name, final BuildParam... params) throws DockerException, InterruptedException, IOException { return build(directory, name, new LoggingBuildHandler(), params); } @Override public String build(final Path directory, final ProgressHandler handler, final BuildParam... 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 BuildParam... 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 BuildParam... params) throws DockerException, InterruptedException, IOException { checkNotNull(handler, "handler"); WebTarget resource = noTimeoutResource().path("build"); for (final BuildParam param : params) { resource = resource.queryParam(param.name(), param.value()); } 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 removeImage(String image) throws DockerException, InterruptedException { return removeImage(image, false, false); } @Override public List 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(final String containerId, final String[] cmd, final ExecCreateParam... 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 (final ExecCreateParam param : params) { if (param.value().equals("true") || param.value().equals("false")) { generator.writeBooleanField(param.name(), Boolean.valueOf(param.value())); } else { generator.writeStringField(param.name(), param.value()); } } generator.writeArrayFieldStart("Cmd"); for (final String s : cmd) { generator.writeString(s); } generator.writeEndArray(); generator.writeEndObject(); generator.close(); } catch (IOException e) { throw new DockerException(e); } final 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(final String execId, final 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; } } } @Override public List listNetworks() throws DockerException, InterruptedException { final WebTarget resource = resource().path("networks"); return request(GET, NETWORK_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public Network inspectNetwork(String networkId) throws DockerException, InterruptedException { final WebTarget resource = resource().path("networks").path(networkId); try { return request(GET, Network.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NetworkNotFoundException(networkId, e); default: throw e; } } } @Override public NetworkCreation createNetwork(NetworkConfig networkConfig) throws DockerException, InterruptedException { final WebTarget resource = resource().path("networks").path("create"); return request(POST, NetworkCreation.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(networkConfig)); } @Override public void removeNetwork(String networkId) throws DockerException, InterruptedException { try { final WebTarget resource = resource().path("networks").path(networkId); request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NetworkNotFoundException(networkId, e); default: throw e; } } } @Override public void connectToNetwork(String containerId, String networkId) throws DockerException, InterruptedException { manageNetworkConnection(containerId, "connect", networkId); } @Override public void disconnectFromNetwork(String containerId, String networkId) throws DockerException, InterruptedException { manageNetworkConnection(containerId, "disconnect", networkId); } private void manageNetworkConnection(String containerId, String methodname, String networkId) throws DockerException, InterruptedException { final WebTarget resource = resource().path("networks").path(networkId).path(methodname); Map request = new HashMap<>(); request.put("Container", containerId); Response response = request(POST, Response.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(request)); switch (response.getStatus()) { case 200: return; case 404: throw new ContainerNotFoundException(containerId); case 500: throw new DockerException(response.readEntity(String.class)); } } 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 request(final String method, final GenericType 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 request(final String method, final Class 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 request(final String method, final Class 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 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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy