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

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

There is a newer version: 8.18.4
Show newest version
/*-
 * -\-\-
 * docker-client
 * --
 * Copyright (C) 2016 Spotify AB
 * Copyright (c) 2014 Oleg Poleshuk
 * Copyright (c) 2014 CyDesign Ltd
 * Copyright (c) 2016 ThoughtWorks, Inc
 * --
 * 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 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.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonMap;
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;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;

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.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.CharStreams;
import com.google.common.net.HostAndPort;
import com.spotify.docker.client.auth.ConfigFileRegistryAuthSupplier;
import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
import com.spotify.docker.client.auth.RegistryAuthSupplier;
import com.spotify.docker.client.exceptions.BadParamException;
import com.spotify.docker.client.exceptions.ConflictException;
import com.spotify.docker.client.exceptions.ContainerNotFoundException;
import com.spotify.docker.client.exceptions.ContainerRenameConflictException;
import com.spotify.docker.client.exceptions.DockerCertificateException;
import com.spotify.docker.client.exceptions.DockerException;
import com.spotify.docker.client.exceptions.DockerRequestException;
import com.spotify.docker.client.exceptions.DockerTimeoutException;
import com.spotify.docker.client.exceptions.ExecCreateConflictException;
import com.spotify.docker.client.exceptions.ExecNotFoundException;
import com.spotify.docker.client.exceptions.ExecStartConflictException;
import com.spotify.docker.client.exceptions.ImageNotFoundException;
import com.spotify.docker.client.exceptions.NetworkNotFoundException;
import com.spotify.docker.client.exceptions.NodeNotFoundException;
import com.spotify.docker.client.exceptions.NonSwarmNodeException;
import com.spotify.docker.client.exceptions.NotFoundException;
import com.spotify.docker.client.exceptions.PermissionException;
import com.spotify.docker.client.exceptions.ServiceNotFoundException;
import com.spotify.docker.client.exceptions.TaskNotFoundException;
import com.spotify.docker.client.exceptions.UnsupportedApiVersionException;
import com.spotify.docker.client.exceptions.VolumeNotFoundException;
import com.spotify.docker.client.messages.Container;
import com.spotify.docker.client.messages.ContainerChange;
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.ContainerUpdate;
import com.spotify.docker.client.messages.Distribution;
import com.spotify.docker.client.messages.ExecCreation;
import com.spotify.docker.client.messages.ExecState;
import com.spotify.docker.client.messages.HostConfig;
import com.spotify.docker.client.messages.Image;
import com.spotify.docker.client.messages.ImageHistory;
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.NetworkConnection;
import com.spotify.docker.client.messages.NetworkCreation;
import com.spotify.docker.client.messages.ProgressMessage;
import com.spotify.docker.client.messages.RegistryAuth;
import com.spotify.docker.client.messages.RegistryConfigs;
import com.spotify.docker.client.messages.RemovedImage;
import com.spotify.docker.client.messages.ServiceCreateResponse;
import com.spotify.docker.client.messages.TopResults;
import com.spotify.docker.client.messages.Version;
import com.spotify.docker.client.messages.Volume;
import com.spotify.docker.client.messages.VolumeList;
import com.spotify.docker.client.messages.swarm.Config;
import com.spotify.docker.client.messages.swarm.ConfigCreateResponse;
import com.spotify.docker.client.messages.swarm.ConfigSpec;
import com.spotify.docker.client.messages.swarm.Node;
import com.spotify.docker.client.messages.swarm.NodeInfo;
import com.spotify.docker.client.messages.swarm.NodeSpec;
import com.spotify.docker.client.messages.swarm.Secret;
import com.spotify.docker.client.messages.swarm.SecretCreateResponse;
import com.spotify.docker.client.messages.swarm.SecretSpec;
import com.spotify.docker.client.messages.swarm.Service;
import com.spotify.docker.client.messages.swarm.ServiceSpec;
import com.spotify.docker.client.messages.swarm.Swarm;
import com.spotify.docker.client.messages.swarm.SwarmInit;
import com.spotify.docker.client.messages.swarm.SwarmJoin;
import com.spotify.docker.client.messages.swarm.SwarmSpec;
import com.spotify.docker.client.messages.swarm.Task;
import com.spotify.docker.client.messages.swarm.UnlockKey;
import com.spotify.docker.client.npipe.NpipeConnectionSocketFactory;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
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.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
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.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
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.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultDockerClient implements DockerClient, Closeable {

  /**
   * Hack: this {@link ProgressHandler} is meant to capture the image ID (or image digest in Docker
   * 1.10+) of an image being created. Weirdly enough, Docker returns the ID or digest of a newly
   * created image in the status of a progress message.
   *
   * 

The image ID/digest 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/digest, the image can be tagged * with its image name, given its ID/digest. */ private static class CreateProgressHandler implements ProgressHandler { // The length of the image hash private static final int EXPECTED_CHARACTER_NUM1 = 64; // The length of the image digest private static final int EXPECTED_CHARACTER_NUM2 = 71; private final ProgressHandler delegate; private String imageId; private CreateProgressHandler(ProgressHandler delegate) { this.delegate = delegate; } private String getImageId() { Preconditions.checkState(imageId != null, "Could not acquire image ID or digest following create"); return imageId; } @Override public void progress(ProgressMessage message) throws DockerException { delegate.progress(message); final String status = message.status(); if (status != null && (status.length() == EXPECTED_CHARACTER_NUM1 || status.length() == EXPECTED_CHARACTER_NUM2)) { imageId = message.status(); } } } /** * Hack: this {@link ProgressHandler} is meant to capture the image names * of an image being loaded. Weirdly enough, Docker returns the name of a newly * created image in the stream of a progress message. * */ private static class LoadProgressHandler implements ProgressHandler { // The length of the image hash private static final Pattern IMAGE_STREAM_PATTERN = Pattern.compile("Loaded image: (?.+)\n"); private final ProgressHandler delegate; private Set imageNames; private LoadProgressHandler(ProgressHandler delegate) { this.delegate = delegate; this.imageNames = new HashSet<>(); } private Set getImageNames() { return ImmutableSet.copyOf(imageNames); } @Override public void progress(ProgressMessage message) throws DockerException { delegate.progress(message); final String stream = message.stream(); if (stream != null) { Matcher streamMatcher = IMAGE_STREAM_PATTERN.matcher(stream); if (streamMatcher.matches()) { imageNames.add(streamMatcher.group("image")); } } } } /** * Hack: this {@link ProgressHandler} is meant to capture the image ID * of an image being built. */ private static class BuildProgressHandler implements ProgressHandler { private final ProgressHandler delegate; private String imageId; private BuildProgressHandler(ProgressHandler delegate) { this.delegate = delegate; } private String getImageId() { Preconditions.checkState(imageId != null, "Could not acquire image ID or digest following build"); return imageId; } @Override public void progress(ProgressMessage message) throws DockerException { delegate.progress(message); final String id = message.buildImageId(); if (id != null) { imageId = id; } } } // ========================================================================== private static final String UNIX_SCHEME = "unix"; private static final String NPIPE_SCHEME = "npipe"; private static final Logger log = LoggerFactory.getLogger(DefaultDockerClient.class); 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 final ClientConfig defaultConfig = new ClientConfig( ObjectMapperProvider.class, JacksonFeature.class, LogsResponseReader.class, ProgressResponseReader.class); private static final Pattern CONTAINER_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"); private static final GenericType> CONTAINER_LIST = new GenericType>() { }; private static final GenericType> CONTAINER_CHANGE_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 static final GenericType> IMAGE_HISTORY_LIST = new GenericType>() { }; private static final GenericType> SERVICE_LIST = new GenericType>() { }; private static final GenericType DISTRIBUTION = new GenericType(){ }; private static final GenericType> TASK_LIST = new GenericType>() { }; private static final GenericType> NODE_LIST = new GenericType>() { }; private static final GenericType> CONFIG_LIST = new GenericType>() { }; private static final GenericType> SECRET_LIST = new GenericType>() { }; private final Client client; private final Client noTimeoutClient; private final URI uri; private final String apiVersion; private final RegistryAuthSupplier registryAuthSupplier; private final Map headers; 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 dockerCertificatesStore The certificates to use for HTTPS. */ public DefaultDockerClient(final URI uri, final DockerCertificatesStore dockerCertificatesStore) { this(new Builder().uri(uri).dockerCertificates(dockerCertificatesStore)); } /** * Create a new client using the configuration of the builder. * * @param builder DefaultDockerClient builder */ protected DefaultDockerClient(final Builder builder) { final URI originalUri = checkNotNull(builder.uri, "uri"); checkNotNull(originalUri.getScheme(), "url has null scheme"); this.apiVersion = builder.apiVersion(); if ((builder.dockerCertificatesStore != 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 if (originalUri.getScheme().equals(NPIPE_SCHEME)) { this.uri = NpipeConnectionSocketFactory.sanitizeUri(originalUri); } else { this.uri = originalUri; } final HttpClientConnectionManager cm = getConnectionManager(builder); final HttpClientConnectionManager noTimeoutCm = getConnectionManager(builder); final RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout((int) builder.connectTimeoutMillis) .setConnectTimeout((int) builder.connectTimeoutMillis) .setSocketTimeout((int) builder.readTimeoutMillis) .build(); final ClientConfig config = updateProxy(defaultConfig, builder) .connectorProvider(new ApacheConnectorProvider()) .property(ApacheClientProperties.CONNECTION_MANAGER, cm) .property(ApacheClientProperties.REQUEST_CONFIG, requestConfig); if (builder.registryAuthSupplier == null) { this.registryAuthSupplier = new FixedRegistryAuthSupplier(); } else { this.registryAuthSupplier = builder.registryAuthSupplier; } if (builder.getRequestEntityProcessing() != null) { config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, builder.requestEntityProcessing); } this.client = ClientBuilder.newBuilder() .withConfig(config) .build(); // 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(); this.headers = new HashMap<>(builder.headers()); } private ClientConfig updateProxy(ClientConfig config, Builder builder) { if (builder.useProxy()) { final String proxyHost = System.getProperty("http.proxyHost"); if (proxyHost != null) { boolean skipProxy = false; String nonProxyHosts = System.getProperty("http.nonProxyHosts"); if (nonProxyHosts != null) { // Remove quotes, if any. Refer to https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html nonProxyHosts = StringUtils.strip(nonProxyHosts, "\""); String host = getHost(); for (String nonProxyHost : nonProxyHosts.split("\\|")) { if (host.matches(toRegExp(nonProxyHost))) { skipProxy = true; break; } } } if (!skipProxy) { String proxyPort = checkNotNull(System.getProperty("http.proxyPort"), "http.proxyPort"); config.property(ClientProperties.PROXY_URI, (!proxyHost.startsWith("http") ? "http://" : "") + proxyHost + ":" + proxyPort); final String proxyUser = System.getProperty("http.proxyUser"); if (proxyUser != null) { config.property(ClientProperties.PROXY_USERNAME, proxyUser); } final String proxyPassword = System.getProperty("http.proxyPassword"); if (proxyPassword != null) { config.property(ClientProperties.PROXY_PASSWORD, proxyPassword); } //ensure Content-Length is populated before sending request via proxy. config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); } } } return config; } private String toRegExp(String hostnameWithWildcards) { return hostnameWithWildcards.replace(".", "\\.").replace("*", ".*"); } public String getHost() { return fromNullable(uri.getHost()).or("localhost"); } private HttpClientConnectionManager getConnectionManager(Builder builder) { if (builder.uri.getScheme().equals(NPIPE_SCHEME)) { final BasicHttpClientConnectionManager bm = new BasicHttpClientConnectionManager(getSchemeRegistry(builder)); return bm; } else { 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.dockerCertificatesStore == null) { https = SSLConnectionSocketFactory.getSocketFactory(); } else { https = new SSLConnectionSocketFactory(builder.dockerCertificatesStore.sslContext(), builder.dockerCertificatesStore.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)); } if (builder.uri.getScheme().equals(NPIPE_SCHEME)) { registryBuilder.register(NPIPE_SCHEME, new NpipeConnectionSocketFactory(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 RegistryAuth registryAuth) throws DockerException, InterruptedException { final WebTarget resource = resource().path("auth"); final Response response = request(POST, Response.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(registryAuth)); 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"); resource = addParameters(resource, params); try { return request(GET, CONTAINER_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 400: throw new BadParamException(getQueryParamMap(resource), e); default: throw e; } } } private WebTarget addParameters(WebTarget resource, final Param... params) throws DockerException { final Map> filters = newHashMap(); for (final Param param : params) { if (param instanceof FilterParam) { List filterValueList; if (filters.containsKey(param.name())) { filterValueList = filters.get(param.name()); } else { filterValueList = Lists.newArrayList(); } filterValueList.add(param.value()); filters.put(param.name(), filterValueList); } else { resource = resource.queryParam(urlEncode(param.name()), urlEncode(param.value())); } } if (!filters.isEmpty()) { // 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"]}. If filters is an empty map, // urlEncodeFilters will return null and queryParam() will remove that query parameter. resource = resource.queryParam("filters", urlEncodeFilters(filters)); } return resource; } private Map getQueryParamMap(final WebTarget resource) { final String queryParams = resource.getUri().getQuery(); final Map paramsMap = Maps.newHashMap(); if (queryParams != null) { for (final String queryParam : queryParams.split("&")) { final String[] kv = queryParam.split("="); paramsMap.put(kv[0], kv[1]); } } return paramsMap; } /** * URL-encodes a string when used as a URL query parameter's value. * * @param unencoded A string that may contain characters not allowed in URL query parameters. * @return URL-encoded String * @throws DockerException if there's an UnsupportedEncodingException */ private String urlEncode(final String unencoded) throws DockerException { try { final String encode = URLEncoder.encode(unencoded, UTF_8.name()); return encode.replaceAll("\\+", "%20"); } catch (UnsupportedEncodingException e) { throw new DockerException(e); } } /** * Takes a map of filters and URL-encodes them. If the map is empty or an exception occurs, return * null. * * @param filters A map of filters. * @return String * @throws DockerException if there's an IOException */ private String urlEncodeFilters(final Map> filters) throws DockerException { try { final String unencodedFilters = objectMapper().writeValueAsString(filters); if (!unencodedFilters.isEmpty()) { return urlEncode(unencodedFilters); } } catch (IOException e) { throw new DockerException(e); } return null; } @Override public List listImages(final ListImagesParam... params) throws DockerException, InterruptedException { WebTarget resource = resource() .path("images").path("json"); resource = addParameters(resource, params); 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.debug("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); case 406: throw new DockerException("Impossible to attach. Container not running.", 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 { containerAction(containerId, action, new MultivaluedHashMap()); } private void containerAction(final String containerId, final String action, final MultivaluedMap queryParameters) throws DockerException, InterruptedException { try { WebTarget resource = resource() .path("containers").path(containerId).path(action); for (Map.Entry> queryParameter : queryParameters.entrySet()) { for (String parameterValue : queryParameter.getValue()) { resource = resource.queryParam(queryParameter.getKey(), parameterValue); } } 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"); MultivaluedMap queryParameters = new MultivaluedHashMap<>(); queryParameters.add("t", String.valueOf(secondsToWaitBeforeRestart)); containerAction(containerId, "restart", queryParameters); } @Override public void killContainer(final String containerId) throws DockerException, InterruptedException { checkNotNull(containerId, "containerId"); containerAction(containerId, "kill"); } @Override public void killContainer(final String containerId, final Signal signal) throws DockerException, InterruptedException { checkNotNull(containerId, "containerId"); MultivaluedMap queryParameters = new MultivaluedHashMap<>(); queryParameters.add("signal", signal.getName()); containerAction(containerId, "kill", queryParameters); } @Override public Distribution getDistribution(String imageName) throws DockerException, InterruptedException { checkNotNull(imageName, "containerName"); final WebTarget resource = resource().path("distribution").path(imageName).path("json"); return request(GET, DISTRIBUTION, resource, resource.request(APPLICATION_JSON_TYPE)); } @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, new RemoveContainerParam[0]); } @Deprecated @Override public void removeContainer(final String containerId, final boolean removeVolumes) throws DockerException, InterruptedException { removeContainer(containerId, RemoveContainerParam.removeVolumes(removeVolumes)); } @Override public void removeContainer(final String containerId, final RemoveContainerParam... params) throws DockerException, InterruptedException { try { WebTarget resource = resource().path("containers").path(containerId); for (final RemoveContainerParam param : params) { resource = resource.queryParam(param.name(), param.value()); } request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 400: throw new BadParamException(getQueryParamMap(resource()), e); 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"); try { return request(GET, InputStream.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); default: throw e; } } } @Override @Deprecated public InputStream copyContainer(String containerId, String path) throws DockerException, InterruptedException { final String apiVersion = version().apiVersion(); final int versionComparison = compareVersion(apiVersion, "1.24"); // Version above 1.24 if (versionComparison >= 0) { throw new UnsupportedApiVersionException(apiVersion); } final WebTarget resource = resource() .path("containers").path(containerId).path("copy"); // Internal JSON object; not worth it to create class for this final JsonNodeFactory nf = JsonNodeFactory.instance; final JsonNode params = nf.objectNode().set("Resource", nf.textNode(path)); try { return request(POST, InputStream.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE), Entity.json(params)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); default: throw e; } } } @Override public InputStream archiveContainer(String containerId, String path) throws DockerException, InterruptedException { final String apiVersion = version().apiVersion(); final int versionComparison = compareVersion(apiVersion, "1.20"); // Version below 1.20 if (versionComparison < 0) { throw new UnsupportedApiVersionException(apiVersion); } final WebTarget resource = resource() .path("containers").path(containerId).path("archive") .queryParam("path", path); try { return request(GET, InputStream.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); default: throw e; } } } @Override public TopResults topContainer(final String containerId) throws DockerException, InterruptedException { return topContainer(containerId, null); } @Override public TopResults topContainer(final String containerId, final String psArgs) throws DockerException, InterruptedException { try { WebTarget resource = resource().path("containers").path(containerId).path("top"); if (!Strings.isNullOrEmpty(psArgs)) { resource = resource.queryParam("ps_args", psArgs); } return request(GET, TopResults.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 copyToContainer(final Path directory, String containerId, String path) throws DockerException, InterruptedException, IOException { try (final CompressedDirectory compressedDirectory = CompressedDirectory.create(directory); final InputStream fileStream = Files.newInputStream(compressedDirectory.file())) { copyToContainer(fileStream, containerId, path); } } @Override public void copyToContainer(InputStream tarStream, String containerId, String path) throws DockerException, InterruptedException { final WebTarget resource = resource() .path("containers") .path(containerId) .path("archive") .queryParam("noOverwriteDirNonDir", true) .queryParam("path", path); try { request(PUT, String.class, resource, resource.request(APPLICATION_OCTET_STREAM_TYPE), Entity.entity(tarStream, "application/tar")); } catch (DockerRequestException e) { switch (e.status()) { case 400: throw new BadParamException(getQueryParamMap(resource), e); case 403: throw new PermissionException("Volume or container rootfs is marked as read-only.", e); case 404: throw new NotFoundException( String.format("Either container %s or path %s not found.", containerId, path), e); default: throw e; } } } @Override public List inspectContainerChanges(final String containerId) throws DockerException, InterruptedException { try { final WebTarget resource = resource().path("containers").path(containerId).path("changes"); return request(GET, CONTAINER_CHANGE_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); default: throw e; } } } @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); 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.debug("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 void renameContainer(final String containerId, final String name) throws DockerException, InterruptedException { WebTarget resource = resource() .path("containers").path(containerId).path("rename"); if (name == null) { throw new IllegalArgumentException("Cannot rename container to null"); } checkArgument(CONTAINER_NAME_PATTERN.matcher(name).matches(), "Invalid container name: \"%s\"", name); resource = resource.queryParam("name", name); log.info("Renaming container with id {}. New name {}.", containerId, name); try { request(POST, resource, resource.request()); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); case 409: throw new ContainerRenameConflictException(containerId, name, e); default: throw e; } } } @Override public ContainerUpdate updateContainer(final String containerId, final HostConfig config) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.22"); try { WebTarget resource = resource().path("containers").path(containerId).path("update"); return request(POST, ContainerUpdate.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(config)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId); 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 @Deprecated public void load(final String image, final InputStream imagePayload) throws DockerException, InterruptedException { create(image, imagePayload); } @Override @Deprecated public void load(final String image, final InputStream imagePayload, final ProgressHandler handler) throws DockerException, InterruptedException { create(image, imagePayload, handler); } @Override public Set load(final InputStream imagePayload) throws DockerException, InterruptedException { return load(imagePayload, new LoggingLoadHandler()); } @Override public Set load(final InputStream imagePayload, final ProgressHandler handler) throws DockerException, InterruptedException { final WebTarget resource = resource() .path("images") .path("load") .queryParam("quiet", "false"); final LoadProgressHandler loadProgressHandler = new LoadProgressHandler(handler); final Entity entity = Entity.entity(imagePayload, APPLICATION_OCTET_STREAM); try (final ProgressStream load = request(POST, ProgressStream.class, resource, resource.request(APPLICATION_JSON_TYPE), entity)) { load.tail(loadProgressHandler, POST, resource.getUri()); return loadProgressHandler.getImageNames(); } catch (IOException e) { throw new DockerException(e); } finally { IOUtils.closeQuietly(imagePayload); } } @Override public void create(final String image, final InputStream imagePayload) throws DockerException, InterruptedException { create(image, imagePayload, new LoggingPullHandler("image stream")); } @Override public void create(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); final CreateProgressHandler createProgressHandler = new CreateProgressHandler(handler); final Entity entity = Entity.entity(imagePayload, APPLICATION_OCTET_STREAM); try { requestAndTail(POST, createProgressHandler, resource, resource.request(APPLICATION_JSON_TYPE), entity); tag(createProgressHandler.getImageId(), image, true); } finally { IOUtils.closeQuietly(imagePayload); } } @Override public InputStream save(final String... images) throws DockerException, IOException, InterruptedException { WebTarget resource; if (images.length == 1) { resource = resource().path("images").path(images[0]).path("get"); } else { resource = resource().path("images").path("get"); if (images.length > 1) { for (final String image : images) { if (!isNullOrEmpty(image)) { resource = resource.queryParam("names", urlEncode(image)); } } } } return request( GET, InputStream.class, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public InputStream saveMultiple(final String... images) throws DockerException, IOException, InterruptedException { final WebTarget resource = resource().path("images").path("get"); for (final String image : images) { resource.queryParam("names", urlEncode(image)); } return request( GET, InputStream.class, resource, resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader( registryAuthSupplier.authFor(images[0]))) ); } @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, registryAuthSupplier.authFor(image), handler); } @Override public void pull(final String image, final RegistryAuth registryAuth) throws DockerException, InterruptedException { pull(image, registryAuth, new LoggingPullHandler(image)); } @Override public void pull(final String image, final RegistryAuth registryAuth, 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 { requestAndTail(POST, handler, resource, resource .request(APPLICATION_JSON_TYPE) .header("X-Registry-Auth", authHeader(registryAuth))); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ImageNotFoundException(image, e); default: throw e; } } } @Override public void push(final String image) throws DockerException, InterruptedException { push(image, new LoggingPushHandler(image)); } @Override public void push(final String image, final RegistryAuth registryAuth) throws DockerException, InterruptedException { push(image, new LoggingPushHandler(image), registryAuth); } @Override public void push(final String image, final ProgressHandler handler) throws DockerException, InterruptedException { push(image, handler, registryAuthSupplier.authFor(image)); } @Override public void push(final String image, final ProgressHandler handler, final RegistryAuth registryAuth) 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()); } try { requestAndTail(POST, handler, resource, resource.request(APPLICATION_JSON_TYPE) .header("X-Registry-Auth", authHeader(registryAuth))); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ImageNotFoundException(image, e); default: throw 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 400: throw new BadParamException(getQueryParamMap(resource), e); case 404: throw new ImageNotFoundException(image, e); case 409: throw new ConflictException(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); } @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") @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); } // Convert auth to X-Registry-Config format final RegistryConfigs registryConfigs = registryAuthSupplier.authForBuild(); final BuildProgressHandler buildHandler = new BuildProgressHandler(handler); try (final CompressedDirectory compressedDirectory = CompressedDirectory.create(directory); final InputStream fileStream = Files.newInputStream(compressedDirectory.file())) { requestAndTail(POST, buildHandler, resource, resource.request(APPLICATION_JSON_TYPE) .header("X-Registry-Config", authRegistryHeader(registryConfigs)), Entity.entity(fileStream, "application/tar")); return buildHandler.getImageId(); } } @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, e); case 409: throw new ConflictException(e); default: throw e; } } } @Override public List history(final String image) throws DockerException, InterruptedException { final WebTarget resource = resource() .path("images") .path(image) .path("history"); try { return request(GET, IMAGE_HISTORY_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ImageNotFoundException(image, e); 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 (final LogsParam param : params) { resource = resource.queryParam(param.name(), param.value()); } return getLogStream(GET, resource, containerId); } @Override public EventStream events(EventsParam... params) throws DockerException, InterruptedException { WebTarget resource = noTimeoutResource().path("events"); resource = addParameters(resource, params); try { final CloseableHttpClient client = (CloseableHttpClient) ApacheConnectorProvider .getHttpClient(noTimeoutClient); final CloseableHttpResponse response = client.execute(new HttpGet(resource.getUri())); return new EventStream(response, objectMapper()); } catch (IOException exception) { throw new DockerException(exception); } } @Override public LogStream attachContainer(final String containerId, final AttachParameter... params) throws DockerException, InterruptedException { checkNotNull(containerId, "containerId"); 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 400: throw new BadParamException(getQueryParamMap(resource), e); case 404: throw new ContainerNotFoundException(containerId); default: throw e; } } } private LogStream getServiceLogStream(final String method, final WebTarget resource, final String serviceId) 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 400: throw new BadParamException(getQueryParamMap(resource), e); case 404: throw new ServiceNotFoundException(serviceId); default: throw e; } } } @Override public ExecCreation execCreate(final String containerId, final String[] cmd, final ExecCreateParam... params) throws DockerException, InterruptedException { final ContainerInfo containerInfo = inspectContainer(containerId); if (!containerInfo.state().running()) { throw new IllegalStateException("Container " + containerId + " is not running."); } final 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); } try { return request(POST, ExecCreation.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(writer.toString())); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); case 409: throw new ExecCreateConflictException(containerId, e); default: throw e; } } } @Override public LogStream execStart(final String execId, final ExecStartParameter... params) throws DockerException, InterruptedException { final WebTarget resource = noTimeoutResource().path("exec").path(execId).path("start"); final StringWriter writer = new StringWriter(); try { final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer); generator.writeStartObject(); for (final 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, e); case 409: throw new ExecStartConflictException(execId, e); default: throw e; } } } @Override public Swarm inspectSwarm() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final WebTarget resource = resource().path("swarm"); return request(GET, Swarm.class, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public String initSwarm(final SwarmInit swarmInit) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("swarm").path("init"); return request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(swarmInit)); } catch (DockerRequestException e) { switch (e.status()) { case 400: throw new DockerException("bad parameter", e); case 500: throw new DockerException("server error", e); case 503: throw new DockerException("node is already part of a swarm", e); default: throw e; } } } @Override public void joinSwarm(final SwarmJoin swarmJoin) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("swarm").path("join"); request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(swarmJoin)); } catch (DockerRequestException e) { switch (e.status()) { case 400: throw new DockerException("bad parameter", e); case 500: throw new DockerException("server error", e); case 503: throw new DockerException("node is already part of a swarm", e); default: throw e; } } } @Override public void leaveSwarm() throws DockerException, InterruptedException { leaveSwarm(false); } @Override public void leaveSwarm(final boolean force) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("swarm").path("leave").queryParam("force", force); request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 500: throw new DockerException("server error", e); case 503: throw new DockerException("node is not part of a swarm", e); default: throw e; } } } @Override public void updateSwarm(final Long version, final boolean rotateWorkerToken, final boolean rotateManagerToken, final boolean rotateManagerUnlockKey, final SwarmSpec spec) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("swarm").path("update") .queryParam("version", version) .queryParam("rotateWorkerToken", rotateWorkerToken) .queryParam("rotateManagerToken", rotateManagerToken) .queryParam("rotateManagerUnlockKey", rotateManagerUnlockKey); request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(spec)); } catch (DockerRequestException e) { switch (e.status()) { case 400: throw new DockerException("bad parameter", e); default: throw e; } } } @Override public void updateSwarm(final Long version, final boolean rotateWorkerToken, final boolean rotateManagerToken, final SwarmSpec spec) throws DockerException, InterruptedException { updateSwarm(version, rotateWorkerToken, rotateWorkerToken, false, spec); } @Override public void updateSwarm(final Long version, final boolean rotateWorkerToken, final SwarmSpec spec) throws DockerException, InterruptedException { updateSwarm(version, rotateWorkerToken, false, false, spec); } @Override public void updateSwarm(final Long version, final SwarmSpec spec) throws DockerException, InterruptedException { updateSwarm(version, false, false, false, spec); } @Override public UnlockKey unlockKey() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("swarm").path("unlockkey"); return request(GET, UnlockKey.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 500: throw new DockerException("server error", e); case 503: throw new DockerException("node is not part of a swarm", e); default: throw e; } } } @Override public void unlock(final UnlockKey unlockKey) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("swarm").path("unlock"); request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(unlockKey)); } catch (DockerRequestException e) { switch (e.status()) { case 500: throw new DockerException("server error", e); case 503: throw new DockerException("node is not part of a swarm", e); default: throw e; } } } @Override public ServiceCreateResponse createService(ServiceSpec spec) throws DockerException, InterruptedException { return createService(spec, registryAuthSupplier.authForSwarm()); } @Override public ServiceCreateResponse createService(final ServiceSpec spec, final RegistryAuth config) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final WebTarget resource = resource().path("services").path("create"); try { return request(POST, ServiceCreateResponse.class, resource, resource.request(APPLICATION_JSON_TYPE) .header("X-Registry-Auth", authHeader(config)), Entity.json(spec)); } catch (DockerRequestException e) { switch (e.status()) { case 406: throw new DockerException("Server error or node is not part of swarm.", e); case 409: throw new DockerException("Name conflicts with an existing object.", e); default: throw e; } } } @Override public Service inspectService(final String serviceId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("services").path(serviceId); return request(GET, Service.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ServiceNotFoundException(serviceId); default: throw e; } } } @Override public void updateService(final String serviceId, final Long version, final ServiceSpec spec) throws DockerException, InterruptedException { updateService(serviceId, version, spec, registryAuthSupplier.authForSwarm()); } @Override public void updateService(final String serviceId, final Long version, final ServiceSpec spec, final RegistryAuth config) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { WebTarget resource = resource().path("services").path(serviceId).path("update"); resource = resource.queryParam("version", version); request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE) .header("X-Registry-Auth", authHeader(config)), Entity.json(spec)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ServiceNotFoundException(serviceId); default: throw e; } } } @Override public List listServices() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final WebTarget resource = resource().path("services"); return request(GET, SERVICE_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public List listServices(final Service.Criteria criteria) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final Map> filters = new HashMap<>(); if (criteria.serviceId() != null) { filters.put("id", Collections.singletonList(criteria.serviceId())); } if (criteria.serviceName() != null) { filters.put("name", Collections.singletonList(criteria.serviceName())); } final List labels = new ArrayList<>(); for (Entry input: criteria.labels().entrySet()) { if ("".equals(input.getValue())) { labels.add(input.getKey()); } else { labels.add(String.format("%s=%s", input.getKey(), input.getValue())); } } if (!labels.isEmpty()) { filters.put("label", labels); } WebTarget resource = resource().path("services"); resource = resource.queryParam("filters", urlEncodeFilters(filters)); return request(GET, SERVICE_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public void removeService(final String serviceId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("services").path(serviceId); request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ServiceNotFoundException(serviceId); default: throw e; } } } @Override public LogStream serviceLogs(String serviceId, LogsParam... params) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.25"); WebTarget resource = noTimeoutResource() .path("services").path(serviceId) .path("logs"); for (final LogsParam param : params) { resource = resource.queryParam(param.name(), param.value()); } return getServiceLogStream(GET, resource, serviceId); } @Override public Task inspectTask(final String taskId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); try { final WebTarget resource = resource().path("tasks").path(taskId); return request(GET, Task.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new TaskNotFoundException(taskId); default: throw e; } } } @Override public List listTasks() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final WebTarget resource = resource().path("tasks"); return request(GET, TASK_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public List listTasks(final Task.Criteria criteria) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final Map> filters = new HashMap<>(); if (criteria.taskId() != null) { filters.put("id", Collections.singletonList(criteria.taskId())); } if (criteria.taskName() != null) { filters.put("name", Collections.singletonList(criteria.taskName())); } if (criteria.serviceName() != null) { filters.put("service", Collections.singletonList(criteria.serviceName())); } if (criteria.nodeId() != null) { filters.put("node", Collections.singletonList(criteria.nodeId())); } if (criteria.label() != null) { filters.put("label", Collections.singletonList(criteria.label())); } if (criteria.desiredState() != null) { filters.put("desired-state", Collections.singletonList(criteria.desiredState())); } WebTarget resource = resource().path("tasks"); resource = resource.queryParam("filters", urlEncodeFilters(filters)); return request(GET, TASK_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public List listConfigs() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); final WebTarget resource = resource().path("configs"); try { return request(GET, CONFIG_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 503: throw new NonSwarmNodeException("node is not part of a swarm", e); default: throw e; } } } @Override public List listConfigs(final Config.Criteria criteria) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); final Map> filters = new HashMap<>(); if (criteria.configId() != null) { filters.put("id", Collections.singletonList(criteria.configId())); } if (criteria.label() != null) { filters.put("label", Collections.singletonList(criteria.label())); } if (criteria.name() != null) { filters.put("name", Collections.singletonList(criteria.name())); } final WebTarget resource = resource().path("configs") .queryParam("filters", urlEncodeFilters(filters)); try { return request(GET, CONFIG_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 503: throw new NonSwarmNodeException("node is not part of a swarm", e); default: throw e; } } } @Override public ConfigCreateResponse createConfig(final ConfigSpec config) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); final WebTarget resource = resource().path("configs").path("create"); try { return request(POST, ConfigCreateResponse.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(config)); } catch (final DockerRequestException ex) { switch (ex.status()) { case 503: throw new NonSwarmNodeException("Server not part of swarm.", ex); case 409: throw new ConflictException("Name conflicts with an existing object.", ex); default: throw ex; } } } @Override public Config inspectConfig(final String configId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); final WebTarget resource = resource().path("configs").path(configId); try { return request(GET, Config.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (final DockerRequestException ex) { switch (ex.status()) { case 404: throw new NotFoundException("Config " + configId + " not found.", ex); case 503: throw new NonSwarmNodeException("Config not part of swarm.", ex); default: throw ex; } } } @Override public void deleteConfig(final String configId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); final WebTarget resource = resource().path("configs").path(configId); try { request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (final DockerRequestException ex) { switch (ex.status()) { case 404: throw new NotFoundException("Config " + configId + " not found.", ex); case 503: throw new NonSwarmNodeException("Config not part of a swarm.", ex); default: throw ex; } } } @Override public void updateConfig(final String configId, final Long version, final ConfigSpec nodeSpec) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); final WebTarget resource = resource().path("configs") .path(configId) .path("update") .queryParam("version", version); try { request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(nodeSpec)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NotFoundException("Config " + configId + " not found."); case 503: throw new NonSwarmNodeException("Config not part of a swarm.", e); default: throw e; } } } @Override public List listNodes() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); WebTarget resource = resource().path("nodes"); return request(GET, NODE_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public List listNodes(Node.Criteria criteria) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final Map> filters = new HashMap<>(); if (criteria.nodeId() != null) { filters.put("id", Collections.singletonList(criteria.nodeId())); } if (criteria.label() != null) { filters.put("label", Collections.singletonList(criteria.label())); } if (criteria.membership() != null) { filters.put("membership", Collections.singletonList(criteria.membership())); } if (criteria.nodeName() != null) { filters.put("name", Collections.singletonList(criteria.nodeName())); } if (criteria.nodeRole() != null) { filters.put("role", Collections.singletonList(criteria.nodeRole())); } WebTarget resource = resource().path("nodes"); resource = resource.queryParam("filters", urlEncodeFilters(filters)); return request(GET, NODE_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public NodeInfo inspectNode(final String nodeId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); WebTarget resource = resource().path("nodes") .path(nodeId); try { return request(GET, NodeInfo.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NodeNotFoundException(nodeId); case 503: throw new NonSwarmNodeException("Node " + nodeId + " is not in a swarm", e); default: throw e; } } } @Override public void updateNode(final String nodeId, final Long version, final NodeSpec nodeSpec) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); WebTarget resource = resource().path("nodes") .path(nodeId) .path("update") .queryParam("version", version); try { request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(nodeSpec)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NodeNotFoundException(nodeId); case 503: throw new NonSwarmNodeException("Node " + nodeId + " is not a swarm node", e); default: throw e; } } } @Override public void deleteNode(final String nodeId) throws DockerException, InterruptedException { deleteNode(nodeId, false); } @Override public void deleteNode(final String nodeId, final boolean force) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.24"); final WebTarget resource = resource().path("nodes") .path(nodeId) .queryParam("force", String.valueOf(force)); try { request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NodeNotFoundException(nodeId); case 503: throw new NonSwarmNodeException("Node " + nodeId + " is not a swarm node", e); default: throw e; } } } @Override public void execResizeTty(final String execId, final Integer height, final Integer width) throws DockerException, InterruptedException { checkTtyParams(height, width); WebTarget resource = resource().path("exec").path(execId).path("resize"); if (height != null && height > 0) { resource = resource.queryParam("h", height); } if (width != null && width > 0) { resource = resource.queryParam("w", width); } try { request(POST, resource, resource.request(TEXT_PLAIN_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ExecNotFoundException(execId, e); default: throw e; } } } @Override public ExecState execInspect(final String execId) throws DockerException, InterruptedException { final 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, e); 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, e); default: throw e; } } } @Override public void resizeTty(final String containerId, final Integer height, final Integer width) throws DockerException, InterruptedException { checkTtyParams(height, width); WebTarget resource = resource().path("containers").path(containerId).path("resize"); if (height != null && height > 0) { resource = resource.queryParam("h", height); } if (width != null && width > 0) { resource = resource.queryParam("w", width); } try { request(POST, resource, resource.request(TEXT_PLAIN_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new ContainerNotFoundException(containerId, e); default: throw e; } } } private void checkTtyParams(final Integer height, final Integer width) throws BadParamException { if ((height == null && width == null) || (height != null && height == 0) || (width != null && width == 0)) { final Map paramMap = Maps.newHashMap(); paramMap.put("h", height == null ? null : height.toString()); paramMap.put("w", width == null ? null : width.toString()); throw new BadParamException(paramMap, "Either width or height must be non-null and > 0"); } } @Override public List listNetworks(final ListNetworksParam... params) throws DockerException, InterruptedException { WebTarget resource = resource().path("networks"); resource = addParameters(resource, params); 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"); try { return request(POST, NetworkCreation.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(networkConfig)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new NotFoundException("Plugin not found", e); default: throw e; } } } @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 { connectToNetwork(networkId, NetworkConnection.builder().containerId(containerId).build()); } @Override public void connectToNetwork(String networkId, NetworkConnection networkConnection) throws DockerException, InterruptedException { final WebTarget resource = resource().path("networks").path(networkId).path("connect"); try { request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(networkConnection)); } catch (DockerRequestException e) { switch (e.status()) { case 404: final String message = String.format("Container %s or network %s not found.", networkConnection.containerId(), networkId); throw new NotFoundException(message, e); default: throw e; } } } @Override public void disconnectFromNetwork(String containerId, String networkId) throws DockerException, InterruptedException { disconnectFromNetwork(containerId, networkId, false); } @Override public void disconnectFromNetwork(String containerId, String networkId, boolean force) throws DockerException, InterruptedException { final WebTarget resource = resource().path("networks").path(networkId).path("disconnect"); final Map request = new HashMap<>(); request.put("Container", containerId); request.put("Force", force); try { request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(request)); } catch (DockerRequestException e) { switch (e.status()) { case 404: final String message = String.format("Container %s or network %s not found.", containerId, networkId); throw new NotFoundException(message, e); default: throw e; } } } @Override public Volume createVolume() throws DockerException, InterruptedException { return createVolume(Volume.builder().build()); } @Override public Volume createVolume(final Volume volume) throws DockerException, InterruptedException { final WebTarget resource = resource().path("volumes").path("create"); return request(POST, Volume.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(volume)); } @Override public Volume inspectVolume(final String volumeName) throws DockerException, InterruptedException { final WebTarget resource = resource().path("volumes").path(volumeName); try { return request(GET, Volume.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new VolumeNotFoundException(volumeName, e); default: throw e; } } } @Override public void removeVolume(final Volume volume) throws DockerException, InterruptedException { removeVolume(volume.name()); } @Override public void removeVolume(final String volumeName) throws DockerException, InterruptedException { final WebTarget resource = resource().path("volumes").path(volumeName); try { request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (DockerRequestException e) { switch (e.status()) { case 404: throw new VolumeNotFoundException(volumeName, e); case 409: throw new ConflictException("Volume is in use and cannot be removed", e); default: throw e; } } } @Override public VolumeList listVolumes(ListVolumesParam... params) throws DockerException, InterruptedException { WebTarget resource = resource().path("volumes"); resource = addParameters(resource, params); return request(GET, VolumeList.class, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public List listSecrets() throws DockerException, InterruptedException { assertApiVersionIsAbove("1.25"); final WebTarget resource = resource().path("secrets"); return request(GET, SECRET_LIST, resource, resource.request(APPLICATION_JSON_TYPE)); } @Override public SecretCreateResponse createSecret(final SecretSpec secret) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.25"); final WebTarget resource = resource().path("secrets").path("create"); try { return request(POST, SecretCreateResponse.class, resource, resource.request(APPLICATION_JSON_TYPE), Entity.json(secret)); } catch (final DockerRequestException ex) { switch (ex.status()) { case 406: throw new NonSwarmNodeException("Server not part of swarm.", ex); case 409: throw new ConflictException("Name conflicts with an existing object.", ex); default: throw ex; } } } @Override public Secret inspectSecret(final String secretId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.25"); final WebTarget resource = resource().path("secrets").path(secretId); try { return request(GET, Secret.class, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (final DockerRequestException ex) { switch (ex.status()) { case 404: throw new NotFoundException("Secret " + secretId + " not found.", ex); case 406: throw new NonSwarmNodeException("Server not part of swarm.", ex); default: throw ex; } } } @Override public void deleteSecret(final String secretId) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.25"); final WebTarget resource = resource().path("secrets").path(secretId); try { request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE)); } catch (final DockerRequestException ex) { switch (ex.status()) { case 404: throw new NotFoundException("Secret " + secretId + " not found.", ex); default: throw ex; } } } 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 headers(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 headers(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 headers(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 { headers(request).async().method(method, String.class).get(); } catch (ExecutionException | MultiException e) { throw propagate(method, resource, e); } } private static class ResponseTailReader implements Callable { private final ProgressStream stream; private final ProgressHandler handler; private final String method; private final WebTarget resource; public ResponseTailReader(ProgressStream stream, ProgressHandler handler, String method, WebTarget resource) { this.stream = stream; this.handler = handler; this.method = method; this.resource = resource; } @Override public Void call() throws DockerException, InterruptedException, IOException { stream.tail(handler, method, resource.getUri()); return null; } } private void tailResponse(final String method, final Response response, final ProgressHandler handler, final WebTarget resource) throws DockerException, InterruptedException { final ExecutorService executor = Executors.newSingleThreadExecutor(); try { final ProgressStream stream = response.readEntity(ProgressStream.class); final Future future = executor.submit( new ResponseTailReader(stream, handler, method, resource)); future.get(); } catch (ExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof DockerException) { throw (DockerException)cause; } else { throw new DockerException(cause); } } finally { executor.shutdownNow(); try { response.close(); } catch (ProcessingException e) { // ignore, thrown by jnr-unixsocket when httpcomponent try to read after close // the socket is closed before this exception } } } private void requestAndTail(final String method, final ProgressHandler handler, final WebTarget resource, final Invocation.Builder request, final Entity entity) throws DockerException, InterruptedException { Response response = request(method, Response.class, resource, request, entity); tailResponse(method, response, handler, resource); } private void requestAndTail(final String method, final ProgressHandler handler, final WebTarget resource, final Invocation.Builder request) throws DockerException, InterruptedException { Response response = request(method, Response.class, resource, request); if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) { throw new DockerRequestException(method, resource.getUri(), response.getStatus(), message(response), null); } tailResponse(method, response, handler, resource); } private Invocation.Builder headers(final Invocation.Builder request) { final Set> entries = headers.entrySet(); for (final Map.Entry entry : entries) { request.header(entry.getKey(), entry.getValue()); } return request; } private RuntimeException propagate(final String method, final WebTarget resource, final Exception ex) throws DockerException, InterruptedException { Throwable cause = ex.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 (ex 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(), ex); } else if ((cause instanceof InterruptedIOException) || (cause instanceof InterruptedException)) { throw new InterruptedException("Interrupted: " + method + " " + resource); } else { throw new DockerException(ex); } } private String message(final Response response) { final Readable reader; try { reader = new InputStreamReader(response.readEntity(InputStream.class), UTF_8); } catch (IllegalStateException e) { return null; } try { return CharStreams.toString(reader); } catch (IOException ignore) { return null; } } private String authHeader(final RegistryAuth registryAuth) throws DockerException { // 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 if (registryAuth == null) { return "null"; } try { return Base64.encodeBase64String(ObjectMapperProvider .objectMapper() .writeValueAsBytes(registryAuth)); } catch (JsonProcessingException ex) { throw new DockerException("Could not encode X-Registry-Auth header", ex); } } private String authRegistryHeader(final RegistryConfigs registryConfigs) throws DockerException { if (registryConfigs == null) { return null; } try { String authRegistryJson = ObjectMapperProvider.objectMapper().writeValueAsString(registryConfigs.configs()); 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 + "}"; } return Base64.encodeBase64String(authRegistryJson.getBytes(StandardCharsets.UTF_8)); } catch (JsonProcessingException | InterruptedException ex) { throw new DockerException("Could not encode X-Registry-Config header", ex); } } private void assertApiVersionIsAbove(String minimumVersion) throws DockerException, InterruptedException { final String apiVersion = version().apiVersion(); final int versionComparison = compareVersion(apiVersion, minimumVersion); // Version above minimumVersion if (versionComparison < 0) { throw new UnsupportedApiVersionException(apiVersion); } } /** * 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 = DockerHost.endpointFromEnv(); final Path dockerCertPath = Paths.get(Iterables.find( Arrays.asList(DockerHost.certPathFromEnv(), DockerHost.configPathFromEnv(), DockerHost.defaultCertPath()), Predicates.notNull())); final Builder builder = new Builder(); final Optional certs = DockerCertificates.builder() .dockerCertPath(dockerCertPath).build(); if (endpoint.startsWith(UNIX_SCHEME + "://")) { builder.uri(endpoint); } else if (endpoint.startsWith(NPIPE_SCHEME + "://")) { builder.uri(endpoint); } else { final String stripped = endpoint.replaceAll(".*://", ""); final HostAndPort hostAndPort = HostAndPort.fromString(stripped); final String hostText = hostAndPort.getHost(); final String scheme = certs.isPresent() ? "https" : "http"; final int port = hostAndPort.getPortOrDefault(DockerHost.defaultPort()); final String address = isNullOrEmpty(hostText) ? DockerHost.defaultAddress() : hostText; builder.uri(scheme + "://" + address + ":" + port); } if (certs.isPresent()) { builder.dockerCertificates(certs.get()); } return builder; } public static class Builder { public static final String ERROR_MESSAGE = "LOGIC ERROR: DefaultDockerClient does not support being built " + "with both `registryAuth` and `registryAuthSupplier`. " + "Please build with at most one of these options."; private URI uri; private String apiVersion; private long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS; private long readTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS; private int connectionPoolSize = DEFAULT_CONNECTION_POOL_SIZE; private DockerCertificatesStore dockerCertificatesStore; private boolean dockerAuth; private boolean useProxy = true; private RegistryAuth registryAuth; private RegistryAuthSupplier registryAuthSupplier; private Map headers = new HashMap<>(); private RequestEntityProcessing requestEntityProcessing; 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 DockerCertificatesStore dockerCertificates() { return dockerCertificatesStore; } /** * Provide certificates to secure the connection to Docker. * * @param dockerCertificatesStore DockerCertificatesStore object * @return Builder */ public Builder dockerCertificates(final DockerCertificatesStore dockerCertificatesStore) { this.dockerCertificatesStore = dockerCertificatesStore; 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 boolean dockerAuth() { return dockerAuth; } /** * Allows reusing Docker auth info. * * @param dockerAuth tells if Docker auth info should be used * @return Builder * @deprecated in favor of {@link #registryAuthSupplier(RegistryAuthSupplier)} */ @Deprecated public Builder dockerAuth(final boolean dockerAuth) { this.dockerAuth = dockerAuth; return this; } public boolean useProxy() { return useProxy; } /** * Allows connecting to Docker Daemon using HTTP proxy. * * @param useProxy tells if Docker Client has to connect to docker daemon using HTTP Proxy * @return Builder */ public Builder useProxy(final boolean useProxy) { this.useProxy = useProxy; return this; } public RegistryAuth registryAuth() { return registryAuth; } /** * Set the auth parameters for pull/push requests from/to private repositories. * * @param registryAuth RegistryAuth object * @return Builder * * @deprecated in favor of {@link #registryAuthSupplier(RegistryAuthSupplier)} */ @Deprecated public Builder registryAuth(final RegistryAuth registryAuth) { if (this.registryAuthSupplier != null) { throw new IllegalStateException(ERROR_MESSAGE); } this.registryAuth = registryAuth; // stuff the static RegistryAuth into a RegistryConfigs instance to maintain what // DefaultDockerClient used to do with the RegistryAuth before we introduced the // RegistryAuthSupplier final RegistryConfigs configs = RegistryConfigs.create(singletonMap( MoreObjects.firstNonNull(registryAuth.serverAddress(), ""), registryAuth )); this.registryAuthSupplier = new FixedRegistryAuthSupplier(registryAuth, configs); return this; } public Builder registryAuthSupplier(final RegistryAuthSupplier registryAuthSupplier) { if (this.registryAuthSupplier != null) { throw new IllegalStateException(ERROR_MESSAGE); } this.registryAuthSupplier = registryAuthSupplier; return this; } /** * Adds additional headers to be sent in all requests to the Docker Remote API. */ public Builder header(String name, Object value) { headers.put(name, value); return this; } public Map headers() { return headers; } /** * Allows setting transfer encoding. CHUNKED does not send the content-length header * while BUFFERED does. * *

By default ApacheConnectorProvider uses CHUNKED mode. Some Docker API end-points * seems to fail when no content-length is specified but a body is sent. * * @param requestEntityProcessing is the requested entity processing to use when calling docker * daemon (tcp protocol). * @return Builder */ public Builder useRequestEntityProcessing( final RequestEntityProcessing requestEntityProcessing) { this.requestEntityProcessing = requestEntityProcessing; return this; } public RequestEntityProcessing getRequestEntityProcessing() { return this.requestEntityProcessing; } public DefaultDockerClient build() { // read the docker config file for auth info if nothing else was specified if (registryAuthSupplier == null) { registryAuthSupplier(new ConfigFileRegistryAuthSupplier()); } return new DefaultDockerClient(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy