com.spotify.docker.client.DefaultDockerClient Maven / Gradle / Ivy
/*
* Copyright (c) 2014 Spotify AB.
* Copyright (c) 2014 Oleg Poleshuk.
* Copyright (c) 2014 CyDesign Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.spotify.docker.client;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.io.CharStreams;
import com.google.common.net.HostAndPort;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.spotify.docker.client.messages.AuthConfig;
import com.spotify.docker.client.messages.AuthRegistryConfig;
import com.spotify.docker.client.messages.Container;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ContainerCreation;
import com.spotify.docker.client.messages.ContainerExit;
import com.spotify.docker.client.messages.ContainerInfo;
import com.spotify.docker.client.messages.ContainerStats;
import com.spotify.docker.client.messages.ExecState;
import com.spotify.docker.client.messages.Image;
import com.spotify.docker.client.messages.ImageInfo;
import com.spotify.docker.client.messages.ImageSearchResult;
import com.spotify.docker.client.messages.Info;
import com.spotify.docker.client.messages.Network;
import com.spotify.docker.client.messages.NetworkConfig;
import com.spotify.docker.client.messages.NetworkCreation;
import com.spotify.docker.client.messages.ProgressMessage;
import com.spotify.docker.client.messages.RemovedImage;
import com.spotify.docker.client.messages.Version;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.glassfish.hk2.api.MultiException;
import org.glassfish.jersey.apache.connector.ApacheClientProperties;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.StringWriter;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.ResponseProcessingException;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Maps.newHashMap;
import static com.spotify.docker.client.ObjectMapperProvider.objectMapper;
import static com.spotify.docker.client.VersionCompare.compareVersion;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.HttpMethod.PUT;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
public class DefaultDockerClient implements DockerClient, Closeable {
/**
* Hack: this {@link ProgressHandler} is meant to capture the image ID of an
* image being loaded. Weirdly enough, Docker returns the ID of a newly created image
* in the status of a progress message.
*
* The image ID is required to tag the just loaded image since, also weirdly enough,
* the pull operation with the fromSrc
parameter does not support the
* tag
parameter. By retrieving the ID, the image can be tagged with its
* image name, given its ID.
*/
private static class LoadProgressHandler implements ProgressHandler {
private static final int EXPECTED_CHARACTER_NUM = 64;
private final ProgressHandler delegate;
private String imageId;
private LoadProgressHandler(ProgressHandler delegate) {
this.delegate = delegate;
}
private String getImageId() {
Preconditions.checkState(imageId != null, "Could not acquire image ID following load");
return imageId;
}
@Override
public void progress(ProgressMessage message) throws DockerException {
delegate.progress(message);
if (message.status() != null && message.status().length() == EXPECTED_CHARACTER_NUM) {
imageId = message.status();
}
}
}
// ==========================================================================
public static final String DEFAULT_UNIX_ENDPOINT = "unix:///var/run/docker.sock";
public static final String DEFAULT_HOST = "localhost";
public static final int DEFAULT_PORT = 2375;
private static final String UNIX_SCHEME = "unix";
private static final Logger log = LoggerFactory.getLogger(DefaultDockerClient.class);
public static final long NO_TIMEOUT = 0;
private static final long DEFAULT_CONNECT_TIMEOUT_MILLIS = SECONDS.toMillis(5);
private static final long DEFAULT_READ_TIMEOUT_MILLIS = SECONDS.toMillis(30);
private static final int DEFAULT_CONNECTION_POOL_SIZE = 100;
private static final ClientConfig DEFAULT_CONFIG = new ClientConfig(
ObjectMapperProvider.class,
JacksonFeature.class,
LogsResponseReader.class,
ProgressResponseReader.class);
private static final Pattern CONTAINER_NAME_PATTERN = Pattern.compile("/?[a-zA-Z0-9_-]+");
private static final GenericType> CONTAINER_LIST =
new GenericType>() {};
private static final GenericType> IMAGE_LIST =
new GenericType>() {};
private static final GenericType> NETWORK_LIST =
new GenericType>() {};
private static final GenericType> IMAGES_SEARCH_RESULT_LIST =
new GenericType>() {};
private static final GenericType> REMOVED_IMAGE_LIST =
new GenericType>() {};
private final Client client;
private final Client noTimeoutClient;
private final URI uri;
private final String apiVersion;
private final AuthConfig authConfig;
Client getClient() {
return client;
}
Client getNoTimeoutClient() {
return noTimeoutClient;
}
/**
* Create a new client with default configuration.
* @param uri The docker rest api uri.
*/
public DefaultDockerClient(final String uri) {
this(URI.create(uri.replaceAll("^unix:///", "unix://localhost/")));
}
/**
* Create a new client with default configuration.
* @param uri The docker rest api uri.
*/
public DefaultDockerClient(final URI uri) {
this(new Builder().uri(uri));
}
/**
* Create a new client with default configuration.
* @param uri The docker rest api uri.
* @param dockerCertificates The certificates to use for HTTPS.
*/
public DefaultDockerClient(final URI uri, final DockerCertificates dockerCertificates) {
this(new Builder().uri(uri).dockerCertificates(dockerCertificates));
}
/**
* Create a new client using the configuration of the builder.
*
* @param builder DefaultDockerClient builder
*/
protected DefaultDockerClient(final Builder builder) {
URI originalUri = checkNotNull(builder.uri, "uri");
this.apiVersion = builder.apiVersion();
if ((builder.dockerCertificates != null) && !originalUri.getScheme().equals("https")) {
throw new IllegalArgumentException(
"An HTTPS URI for DOCKER_HOST must be provided to use Docker client certificates");
}
if (originalUri.getScheme().equals(UNIX_SCHEME)) {
this.uri = UnixConnectionSocketFactory.sanitizeUri(originalUri);
} else {
this.uri = originalUri;
}
final PoolingHttpClientConnectionManager cm = getConnectionManager(builder);
final PoolingHttpClientConnectionManager noTimeoutCm = getConnectionManager(builder);
final RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout((int) builder.connectTimeoutMillis)
.setConnectTimeout((int) builder.connectTimeoutMillis)
.setSocketTimeout((int) builder.readTimeoutMillis)
.build();
final ClientConfig config = DEFAULT_CONFIG
.connectorProvider(new ApacheConnectorProvider())
.property(ApacheClientProperties.CONNECTION_MANAGER, cm)
.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig);
this.authConfig = builder.authConfig;
this.client = ClientBuilder.newClient(config);
// ApacheConnector doesn't respect per-request timeout settings.
// Workaround: instead create a client with infinite read timeout,
// and use it for waitContainer, stopContainer, attachContainer, logs, and build
final RequestConfig noReadTimeoutRequestConfig = RequestConfig.copy(requestConfig)
.setSocketTimeout((int) NO_TIMEOUT)
.build();
this.noTimeoutClient = ClientBuilder.newBuilder()
.withConfig(config)
.property(ApacheClientProperties.CONNECTION_MANAGER, noTimeoutCm)
.property(ApacheClientProperties.REQUEST_CONFIG, noReadTimeoutRequestConfig)
.build();
}
public String getHost() {
return fromNullable(uri.getHost()).or("localhost");
}
private PoolingHttpClientConnectionManager getConnectionManager(Builder builder) {
final PoolingHttpClientConnectionManager cm =
new PoolingHttpClientConnectionManager(getSchemeRegistry(builder));
// Use all available connections instead of artificially limiting ourselves to 2 per server.
cm.setMaxTotal(builder.connectionPoolSize);
cm.setDefaultMaxPerRoute(cm.getMaxTotal());
return cm;
}
private Registry getSchemeRegistry(final Builder builder) {
final SSLConnectionSocketFactory https;
if (builder.dockerCertificates == null) {
https = SSLConnectionSocketFactory.getSocketFactory();
} else {
https = new SSLConnectionSocketFactory(builder.dockerCertificates.sslContext(),
builder.dockerCertificates.hostnameVerifier());
}
final RegistryBuilder registryBuilder = RegistryBuilder
.create()
.register("https", https)
.register("http", PlainConnectionSocketFactory.getSocketFactory());
if (builder.uri.getScheme().equals(UNIX_SCHEME)) {
registryBuilder.register(UNIX_SCHEME, new UnixConnectionSocketFactory(builder.uri));
}
return registryBuilder.build();
}
@Override
public void close() {
client.close();
noTimeoutClient.close();
}
@Override
public String ping() throws DockerException, InterruptedException {
final WebTarget resource = client.target(uri).path("_ping");
return request(GET, String.class, resource, resource.request());
}
@Override
public Version version() throws DockerException, InterruptedException {
final WebTarget resource = resource().path("version");
return request(GET, Version.class, resource, resource.request(APPLICATION_JSON_TYPE));
}
@Override
public int auth(final AuthConfig authConfig) throws DockerException, InterruptedException {
final WebTarget resource = resource().path("auth");
final Response response =
request(POST, Response.class, resource, resource.request(APPLICATION_JSON_TYPE),
Entity.json(authConfig));
return response.getStatus();
}
@Override
public Info info() throws DockerException, InterruptedException {
final WebTarget resource = resource().path("info");
return request(GET, Info.class, resource, resource.request(APPLICATION_JSON_TYPE));
}
@Override
public List listContainers(final ListContainersParam... params)
throws DockerException, InterruptedException {
WebTarget resource = resource()
.path("containers").path("json");
for (ListContainersParam param : params) {
resource = resource.queryParam(param.name(), param.value());
}
return request(GET, CONTAINER_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
}
@Override
public List listImages(ListImagesParam... params)
throws DockerException, InterruptedException {
WebTarget resource = resource()
.path("images").path("json");
final Map filters = newHashMap();
for (ListImagesParam param : params) {
if (param instanceof ListImagesFilterParam) {
filters.put(param.name(), param.value());
} else {
resource = resource.queryParam(param.name(), param.value());
}
}
// If filters were specified, we must put them in a JSON object and pass them using the
// 'filters' query param like this: filters={"dangling":["true"]}
try {
if (!filters.isEmpty()) {
final StringWriter writer = new StringWriter();
final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer);
generator.writeStartObject();
for (Map.Entry entry : filters.entrySet()) {
generator.writeArrayFieldStart(entry.getKey());
generator.writeString(entry.getValue());
generator.writeEndArray();
}
generator.writeEndObject();
generator.close();
// We must URL encode the string, otherwise Jersey chokes on the double-quotes in the json.
final String encoded = URLEncoder.encode(writer.toString(), UTF_8.name());
resource = resource.queryParam("filters", encoded);
}
} catch (IOException e) {
throw new DockerException(e);
}
return request(GET, IMAGE_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
}
@Override
public ContainerCreation createContainer(final ContainerConfig config)
throws DockerException, InterruptedException {
return createContainer(config, null);
}
@Override
public ContainerCreation createContainer(final ContainerConfig config, final String name)
throws DockerException, InterruptedException {
WebTarget resource = resource()
.path("containers").path("create");
if (name != null) {
checkArgument(CONTAINER_NAME_PATTERN.matcher(name).matches(),
"Invalid container name: \"%s\"", name);
resource = resource.queryParam("name", name);
}
log.info("Creating container with ContainerConfig: {}", config);
try {
return request(POST, ContainerCreation.class, resource, resource
.request(APPLICATION_JSON_TYPE), Entity.json(config));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ImageNotFoundException(config.image(), e);
default:
throw e;
}
}
}
@Override
public void startContainer(final String containerId)
throws DockerException, InterruptedException {
checkNotNull(containerId, "containerId");
log.info("Starting container with Id: {}", containerId);
containerAction(containerId, "start");
}
private void containerAction(final String containerId, final String action)
throws DockerException, InterruptedException {
try {
final WebTarget resource = resource()
.path("containers").path(containerId).path(action);
request(POST, resource, resource.request());
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public void pauseContainer(final String containerId)
throws DockerException, InterruptedException {
checkNotNull(containerId, "containerId");
containerAction(containerId, "pause");
}
@Override
public void unpauseContainer(final String containerId)
throws DockerException, InterruptedException {
checkNotNull(containerId, "containerId");
containerAction(containerId, "unpause");
}
@Override
public void restartContainer(String containerId) throws DockerException, InterruptedException {
restartContainer(containerId, 10);
}
@Override
public void restartContainer(String containerId, int secondsToWaitBeforeRestart)
throws DockerException, InterruptedException {
checkNotNull(containerId, "containerId");
checkNotNull(secondsToWaitBeforeRestart, "secondsToWait");
try {
final WebTarget resource = resource().path("containers").path(containerId)
.path("restart")
.queryParam("t", String.valueOf(secondsToWaitBeforeRestart));
request(POST, resource, resource.request());
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public void killContainer(final String containerId) throws DockerException, InterruptedException {
containerAction(containerId, "kill");
}
@Override
public void stopContainer(final String containerId, final int secondsToWaitBeforeKilling)
throws DockerException, InterruptedException {
try {
final WebTarget resource = noTimeoutResource()
.path("containers").path(containerId).path("stop")
.queryParam("t", String.valueOf(secondsToWaitBeforeKilling));
request(POST, resource, resource.request());
} catch (DockerRequestException e) {
switch (e.status()) {
case 304: // already stopped, so we're cool
return;
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public ContainerExit waitContainer(final String containerId)
throws DockerException, InterruptedException {
try {
final WebTarget resource = noTimeoutResource()
.path("containers").path(containerId).path("wait");
// Wait forever
return request(POST, ContainerExit.class, resource,
resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public void removeContainer(final String containerId)
throws DockerException, InterruptedException {
removeContainer(containerId, false);
}
@Override
public void removeContainer(final String containerId, final boolean removeVolumes)
throws DockerException, InterruptedException {
try {
final WebTarget resource = resource()
.path("containers").path(containerId);
request(DELETE, resource, resource
.queryParam("v", String.valueOf(removeVolumes))
.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public InputStream exportContainer(String containerId)
throws DockerException, InterruptedException {
final WebTarget resource = resource()
.path("containers").path(containerId).path("export");
return request(GET, InputStream.class, resource,
resource.request(APPLICATION_OCTET_STREAM_TYPE));
}
@Override
public InputStream copyContainer(String containerId, String path)
throws DockerException, InterruptedException {
final WebTarget resource = resource()
.path("containers").path(containerId).path("copy");
// Internal JSON object; not worth it to create class for this
JsonNodeFactory nf = JsonNodeFactory.instance;
final JsonNode params = nf.objectNode().set("Resource", nf.textNode(path));
return request(POST, InputStream.class, resource,
resource.request(APPLICATION_OCTET_STREAM_TYPE),
Entity.json(params));
}
@Override
public void copyToContainer(final Path directory, String containerId, String path)
throws DockerException, InterruptedException, IOException {
final WebTarget resource = resource()
.path("containers")
.path(containerId)
.path("archive")
.queryParam("noOverwriteDirNonDir", true)
.queryParam("path", path);
CompressedDirectory compressedDirectory
= CompressedDirectory.create(directory);
final InputStream fileStream =
Files.newInputStream(compressedDirectory.file());
request(PUT, String.class, resource,
resource.request(APPLICATION_OCTET_STREAM_TYPE),
Entity.entity(fileStream, "application/tar"));
}
@Override
public ContainerInfo inspectContainer(final String containerId)
throws DockerException, InterruptedException {
try {
final WebTarget resource = resource().path("containers").path(containerId).path("json");
return request(GET, ContainerInfo.class, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public ContainerCreation commitContainer(final String containerId,
final String repo,
final String tag,
final ContainerConfig config,
final String comment,
final String author)
throws DockerException, InterruptedException {
checkNotNull(containerId, "containerId");
checkNotNull(repo, "repo");
checkNotNull(config, "containerConfig");
WebTarget resource = resource()
.path("commit")
.queryParam("container", containerId)
.queryParam("repo", repo)
.queryParam("comment", comment);
if (!isNullOrEmpty(author)) {
resource = resource.queryParam("author", author);
}
if (!isNullOrEmpty(comment)) {
resource = resource.queryParam("comment", comment);
}
if (!isNullOrEmpty(tag)) {
resource = resource.queryParam("tag", tag);
}
log.info("Committing container id: {} to repository: {} with ContainerConfig: {}", containerId,
repo, config);
try {
return request(POST, ContainerCreation.class, resource, resource
.request(APPLICATION_JSON_TYPE), Entity.json(config));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId, e);
default:
throw e;
}
}
}
@Override
public List searchImages(final String term)
throws DockerException, InterruptedException {
final WebTarget resource = resource().path("images").path("search")
.queryParam("term", term);
return request(GET, IMAGES_SEARCH_RESULT_LIST, resource,
resource.request(APPLICATION_JSON_TYPE));
}
@Override
public void load(final String image, final InputStream imagePayload)
throws DockerException, InterruptedException {
load(image, imagePayload, new LoggingPullHandler("image stream"));
}
@Override
public void load(final String image, final InputStream imagePayload,
final AuthConfig authConfig, final ProgressHandler handler)
throws DockerException, InterruptedException {
load(image, imagePayload, handler);
}
@Override
public void load(final String image, final InputStream imagePayload,
final AuthConfig authConfig)
throws DockerException, InterruptedException {
load(image, imagePayload, authConfig, new LoggingPullHandler("image stream"));
}
@Override
public void load(final String image, final InputStream imagePayload,
final ProgressHandler handler)
throws DockerException, InterruptedException {
WebTarget resource = resource().path("images").path("create");
resource = resource
.queryParam("fromSrc", "-")
.queryParam("tag", image);
LoadProgressHandler loadProgressHandler = new LoadProgressHandler(handler);
Entity entity = Entity.entity(imagePayload, MediaType.APPLICATION_OCTET_STREAM);
try (ProgressStream load =
request(POST, ProgressStream.class, resource,
resource
.request(APPLICATION_JSON_TYPE)
.header("X-Registry-Auth", authHeader(authConfig)), entity)) {
load.tail(loadProgressHandler, POST, resource.getUri());
tag(loadProgressHandler.getImageId(), image, true);
} catch (IOException e) {
throw new DockerException(e);
} finally {
IOUtils.closeQuietly(imagePayload);
}
}
@Override
public InputStream save(final String image)
throws DockerException, IOException, InterruptedException {
return save(image, authConfig);
}
@Override
public InputStream save(final String image, final AuthConfig authConfig)
throws DockerException, IOException, InterruptedException {
WebTarget resource = resource().path("images").path(image).path("get");
return request(
GET,
InputStream.class,
resource,
resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader(authConfig))
);
}
@Override
public void pull(final String image) throws DockerException, InterruptedException {
pull(image, new LoggingPullHandler(image));
}
@Override
public void pull(final String image, final ProgressHandler handler)
throws DockerException, InterruptedException {
pull(image, authConfig, handler);
}
@Override
public void pull(final String image, final AuthConfig authConfig)
throws DockerException, InterruptedException {
pull(image, authConfig, new LoggingPullHandler(image));
}
@Override
public void pull(final String image, final AuthConfig authConfig, final ProgressHandler handler)
throws DockerException, InterruptedException {
final ImageRef imageRef = new ImageRef(image);
WebTarget resource = resource().path("images").path("create");
resource = resource.queryParam("fromImage", imageRef.getImage());
if (imageRef.getTag() != null) {
resource = resource.queryParam("tag", imageRef.getTag());
}
try (ProgressStream pull =
request(POST, ProgressStream.class, resource,
resource
.request(APPLICATION_JSON_TYPE)
.header("X-Registry-Auth", authHeader(authConfig)))) {
pull.tail(handler, POST, resource.getUri());
} catch (IOException e) {
throw new DockerException(e);
}
}
@Override
public void push(final String image) throws DockerException, InterruptedException {
push(image, new LoggingPushHandler(image));
}
@Override
public void push(final String image, final ProgressHandler handler)
throws DockerException, InterruptedException {
final ImageRef imageRef = new ImageRef(image);
WebTarget resource = resource().path("images").path(imageRef.getImage()).path("push");
if (imageRef.getTag() != null) {
resource = resource.queryParam("tag", imageRef.getTag());
}
// the docker daemon requires that the X-Registry-Auth header is specified
// with a non-empty string even if your registry doesn't use authentication
try (ProgressStream push =
request(POST, ProgressStream.class, resource,
resource.request(APPLICATION_JSON_TYPE)
.header("X-Registry-Auth", authHeader()))) {
push.tail(handler, POST, resource.getUri());
} catch (IOException e) {
throw new DockerException(e);
}
}
@Override
public void tag(final String image, final String name)
throws DockerException, InterruptedException {
tag(image, name, false);
}
@Override
public void tag(final String image, final String name, final boolean force)
throws DockerException, InterruptedException {
final ImageRef imageRef = new ImageRef(name);
WebTarget resource = resource().path("images").path(image).path("tag");
resource = resource.queryParam("repo", imageRef.getImage());
if (imageRef.getTag() != null) {
resource = resource.queryParam("tag", imageRef.getTag());
}
if (force) {
resource = resource.queryParam("force", true);
}
try {
request(POST, resource, resource.request());
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ImageNotFoundException(image, e);
default:
throw e;
}
}
}
@Override
public String build(final Path directory, final BuildParam... params)
throws DockerException, InterruptedException, IOException {
return build(directory, null, new LoggingBuildHandler(), params);
}
@Override
public String build(final Path directory, final String name, final BuildParam... params)
throws DockerException, InterruptedException, IOException {
return build(directory, name, new LoggingBuildHandler(), params);
}
@Override
public String build(final Path directory, final ProgressHandler handler,
final BuildParam... params)
throws DockerException, InterruptedException, IOException {
return build(directory, null, handler, params);
}
@Override
public String build(final Path directory, final String name, final ProgressHandler handler,
final BuildParam... params)
throws DockerException, InterruptedException, IOException {
return build(directory, name, null, handler, params);
}
@Override
public String build(final Path directory, final String name, final String dockerfile,
final ProgressHandler handler, final BuildParam... params)
throws DockerException, InterruptedException, IOException {
checkNotNull(handler, "handler");
WebTarget resource = noTimeoutResource().path("build");
for (final BuildParam param : params) {
resource = resource.queryParam(param.name(), param.value());
}
if (name != null) {
resource = resource.queryParam("t", name);
}
if (dockerfile != null) {
resource = resource.queryParam("dockerfile", dockerfile);
}
log.debug("Auth Config {}", authConfig);
// Convert auth to X-Registry-Config format
AuthRegistryConfig authRegistryConfig;
if (authConfig == null) {
authRegistryConfig = AuthRegistryConfig.EMPTY;
} else {
authRegistryConfig = new AuthRegistryConfig(authConfig.serverAddress(),
authConfig.username(),
authConfig.password(),
authConfig.email(),
authConfig.serverAddress());
}
try (final CompressedDirectory compressedDirectory = CompressedDirectory.create(directory);
final InputStream fileStream = Files.newInputStream(compressedDirectory.file());
final ProgressStream build =
request(POST, ProgressStream.class, resource,
resource.request(APPLICATION_JSON_TYPE)
.header("X-Registry-Config",
authRegistryHeader(authRegistryConfig)),
Entity.entity(fileStream, "application/tar"))) {
String imageId = null;
while (build.hasNextMessage(POST, resource.getUri())) {
final ProgressMessage message = build.nextMessage(POST, resource.getUri());
final String id = message.buildImageId();
if (id != null) {
imageId = id;
}
handler.progress(message);
}
return imageId;
}
}
@Override
public ImageInfo inspectImage(final String image) throws DockerException, InterruptedException {
try {
final WebTarget resource = resource().path("images").path(image).path("json");
return request(GET, ImageInfo.class, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ImageNotFoundException(image, e);
default:
throw e;
}
}
}
@Override
public List removeImage(String image)
throws DockerException, InterruptedException {
return removeImage(image, false, false);
}
@Override
public List removeImage(String image, boolean force, boolean noPrune)
throws DockerException, InterruptedException {
try {
final WebTarget resource = resource().path("images").path(image)
.queryParam("force", String.valueOf(force))
.queryParam("noprune", String.valueOf(noPrune));
return request(DELETE, REMOVED_IMAGE_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ImageNotFoundException(image);
default:
throw e;
}
}
}
@Override
public LogStream logs(final String containerId, final LogsParam... params)
throws DockerException, InterruptedException {
WebTarget resource = noTimeoutResource()
.path("containers").path(containerId)
.path("logs");
for (LogsParam param : params) {
resource = resource.queryParam(param.name(), param.value());
}
return getLogStream(GET, resource, containerId);
}
@Override
public LogStream attachContainer(final String containerId, final AttachParameter... params)
throws DockerException, InterruptedException {
WebTarget resource = noTimeoutResource()
.path("containers").path(containerId)
.path("attach");
for (final AttachParameter param : params) {
resource = resource.queryParam(param.name().toLowerCase(Locale.ROOT),
String.valueOf(true));
}
return getLogStream(POST, resource, containerId);
}
private LogStream getLogStream(final String method, final WebTarget resource,
final String containerId)
throws DockerException, InterruptedException {
try {
final Invocation.Builder request = resource.request("application/vnd.docker.raw-stream");
return request(method, LogStream.class, resource, request);
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId);
default:
throw e;
}
}
}
@Override
public String execCreate(final String containerId,
final String[] cmd,
final ExecCreateParam... params)
throws DockerException, InterruptedException {
WebTarget resource = resource().path("containers").path(containerId).path("exec");
final StringWriter writer = new StringWriter();
try {
final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer);
generator.writeStartObject();
for (final ExecCreateParam param : params) {
if (param.value().equals("true") || param.value().equals("false")) {
generator.writeBooleanField(param.name(), Boolean.valueOf(param.value()));
} else {
generator.writeStringField(param.name(), param.value());
}
}
generator.writeArrayFieldStart("Cmd");
for (final String s : cmd) {
generator.writeString(s);
}
generator.writeEndArray();
generator.writeEndObject();
generator.close();
} catch (IOException e) {
throw new DockerException(e);
}
final String response;
try {
response = request(POST, String.class, resource, resource.request(APPLICATION_JSON_TYPE),
Entity.json(writer.toString()));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId);
default:
throw e;
}
}
try {
JsonNode json = objectMapper().readTree(response);
return json.findValue("Id").textValue();
} catch (IOException e) {
throw new DockerException(e);
}
}
@Override
public LogStream execStart(final String execId, final ExecStartParameter... params)
throws DockerException, InterruptedException {
WebTarget resource = resource().path("exec").path(execId).path("start");
final StringWriter writer = new StringWriter();
try {
final JsonGenerator generator = objectMapper().getFactory().createGenerator(writer);
generator.writeStartObject();
for (ExecStartParameter param : params) {
generator.writeBooleanField(param.getName(), true);
}
generator.writeEndObject();
generator.close();
} catch (IOException e) {
throw new DockerException(e);
}
try {
return request(POST, LogStream.class, resource,
resource.request("application/vnd.docker.raw-stream"),
Entity.json(writer.toString()));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ExecNotFoundException(execId);
default:
throw e;
}
}
}
@Override
public ExecState execInspect(final String execId) throws DockerException, InterruptedException {
WebTarget resource = resource().path("exec").path(execId).path("json");
try {
return request(GET, ExecState.class, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ExecNotFoundException(execId);
default:
throw e;
}
}
}
@Override
public ContainerStats stats(final String containerId)
throws DockerException, InterruptedException {
final WebTarget resource = resource().path("containers").path(containerId).path("stats")
.queryParam("stream", "0");
try {
return request(GET, ContainerStats.class, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new ContainerNotFoundException(containerId);
default:
throw e;
}
}
}
@Override
public List listNetworks() throws DockerException, InterruptedException {
final WebTarget resource = resource().path("networks");
return request(GET, NETWORK_LIST, resource, resource.request(APPLICATION_JSON_TYPE));
}
@Override
public Network inspectNetwork(String networkId) throws DockerException, InterruptedException {
final WebTarget resource = resource().path("networks").path(networkId);
try {
return request(GET, Network.class, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new NetworkNotFoundException(networkId, e);
default:
throw e;
}
}
}
@Override
public NetworkCreation createNetwork(NetworkConfig networkConfig)
throws DockerException, InterruptedException {
final WebTarget resource = resource().path("networks").path("create");
return request(POST, NetworkCreation.class, resource, resource.request(APPLICATION_JSON_TYPE),
Entity.json(networkConfig));
}
@Override
public void removeNetwork(String networkId) throws DockerException, InterruptedException {
try {
final WebTarget resource = resource().path("networks").path(networkId);
request(DELETE, resource, resource.request(APPLICATION_JSON_TYPE));
} catch (DockerRequestException e) {
switch (e.status()) {
case 404:
throw new NetworkNotFoundException(networkId, e);
default:
throw e;
}
}
}
@Override
public void connectToNetwork(String containerId, String networkId)
throws DockerException, InterruptedException {
manageNetworkConnection(containerId, "connect", networkId);
}
@Override
public void disconnectFromNetwork(String containerId, String networkId)
throws DockerException, InterruptedException {
manageNetworkConnection(containerId, "disconnect", networkId);
}
private void manageNetworkConnection(String containerId, String methodname, String networkId)
throws DockerException, InterruptedException {
final WebTarget resource = resource().path("networks").path(networkId).path(methodname);
Map request = new HashMap<>();
request.put("Container", containerId);
Response response =
request(POST, Response.class, resource, resource.request(APPLICATION_JSON_TYPE),
Entity.json(request));
switch (response.getStatus()) {
case 200:
return;
case 404:
throw new ContainerNotFoundException(containerId);
case 500:
throw new DockerException(response.readEntity(String.class));
}
}
private WebTarget resource() {
final WebTarget target = client.target(uri);
if (!isNullOrEmpty(apiVersion)) {
return target.path(apiVersion);
}
return target;
}
private WebTarget noTimeoutResource() {
final WebTarget target = noTimeoutClient.target(uri);
if (!isNullOrEmpty(apiVersion)) {
return target.path(apiVersion);
}
return target;
}
private T request(final String method, final GenericType type,
final WebTarget resource, final Invocation.Builder request)
throws DockerException, InterruptedException {
try {
return request.async().method(method, type).get();
} catch (ExecutionException | MultiException e) {
throw propagate(method, resource, e);
}
}
private T request(final String method, final Class clazz,
final WebTarget resource, final Invocation.Builder request)
throws DockerException, InterruptedException {
try {
return request.async().method(method, clazz).get();
} catch (ExecutionException | MultiException e) {
throw propagate(method, resource, e);
}
}
private T request(final String method, final Class clazz,
final WebTarget resource, final Invocation.Builder request,
final Entity> entity)
throws DockerException, InterruptedException {
try {
return request.async().method(method, entity, clazz).get();
} catch (ExecutionException | MultiException e) {
throw propagate(method, resource, e);
}
}
private void request(final String method,
final WebTarget resource,
final Invocation.Builder request)
throws DockerException, InterruptedException {
try {
request.async().method(method, String.class).get();
} catch (ExecutionException | MultiException e) {
throw propagate(method, resource, e);
}
}
private RuntimeException propagate(final String method, final WebTarget resource,
final Exception e)
throws DockerException, InterruptedException {
Throwable cause = e.getCause();
// Sometimes e is a org.glassfish.hk2.api.MultiException
// which contains the cause we're actually interested in.
// So we unpack it here.
if (e instanceof MultiException) {
cause = cause.getCause();
}
Response response = null;
if (cause instanceof ResponseProcessingException) {
response = ((ResponseProcessingException) cause).getResponse();
} else if (cause instanceof WebApplicationException) {
response = ((WebApplicationException) cause).getResponse();
} else if ((cause instanceof ProcessingException) && (cause.getCause() != null)) {
// For a ProcessingException, The exception message or nested Throwable cause SHOULD contain
// additional information about the reason of the processing failure.
cause = cause.getCause();
}
if (response != null) {
throw new DockerRequestException(method, resource.getUri(), response.getStatus(),
message(response), cause);
} else if ((cause instanceof SocketTimeoutException) ||
(cause instanceof ConnectTimeoutException)) {
throw new DockerTimeoutException(method, resource.getUri(), e);
} else if ((cause instanceof InterruptedIOException)
|| (cause instanceof InterruptedException)) {
throw new InterruptedException("Interrupted: " + method + " " + resource);
} else {
throw new DockerException(e);
}
}
private String message(final Response response) {
final Readable reader = new InputStreamReader(response.readEntity(InputStream.class), UTF_8);
try {
return CharStreams.toString(reader);
} catch (IOException ignore) {
return null;
}
}
private String authHeader() throws DockerException {
return authHeader(authConfig);
}
private String authHeader(final AuthConfig authConfig) throws DockerException {
if (authConfig == null) {
return "null";
}
try {
return Base64.encodeAsString(ObjectMapperProvider
.objectMapper()
.writeValueAsString(authConfig));
} catch (JsonProcessingException ex) {
throw new DockerException("Could not encode X-Registry-Auth header", ex);
}
}
private String authRegistryHeader(final AuthRegistryConfig authRegistryConfig)
throws DockerException {
if (authRegistryConfig == null) {
return "null";
}
try {
String authRegistryJson =
ObjectMapperProvider.objectMapper().writeValueAsString(authRegistryConfig);
final String apiVersion = version().apiVersion();
final int versionComparison = compareVersion(apiVersion, "1.19");
// Version below 1.19
if (versionComparison < 0) {
authRegistryJson = "{\"configs\":" + authRegistryJson + "}";
} else if (versionComparison == 0) {
// Version equal 1.19
authRegistryJson = "{\"auths\":" + authRegistryJson + "}";
}
log.debug("Registry Config Json {}", authRegistryJson);
String authRegistryEncoded = Base64.encodeAsString(authRegistryJson);
log.debug("Registry Config Encoded {}", authRegistryEncoded);
return authRegistryEncoded;
} catch (JsonProcessingException | InterruptedException ex) {
throw new DockerException("Could not encode X-Registry-Config header", ex);
}
}
/**
* Create a new {@link DefaultDockerClient} builder.
* @return Returns a builder that can be used to further customize and then build the client.
*/
public static Builder builder() {
return new Builder();
}
/**
* Create a new {@link DefaultDockerClient} builder prepopulated with values loaded
* from the DOCKER_HOST and DOCKER_CERT_PATH environment variables.
* @return Returns a builder that can be used to further customize and then build the client.
* @throws DockerCertificateException if we could not build a DockerCertificates object
*/
public static Builder fromEnv() throws DockerCertificateException {
final String endpoint = fromNullable(getenv("DOCKER_HOST")).or(defaultEndpoint());
final Path dockerCertPath = Paths.get(fromNullable(getenv("DOCKER_CERT_PATH"))
.or(defaultCertPath()));
final Builder builder = new Builder();
final Optional certs = DockerCertificates.builder()
.dockerCertPath(dockerCertPath).build();
if (endpoint.startsWith(UNIX_SCHEME + "://")) {
builder.uri(endpoint);
} else {
final String stripped = endpoint.replaceAll(".*://", "");
final HostAndPort hostAndPort = HostAndPort.fromString(stripped);
final String hostText = hostAndPort.getHostText();
final String scheme = certs.isPresent() ? "https" : "http";
final int port = hostAndPort.getPortOrDefault(DEFAULT_PORT);
final String address = isNullOrEmpty(hostText) ? DEFAULT_HOST : hostText;
builder.uri(scheme + "://" + address + ":" + port);
}
if (certs.isPresent()) {
builder.dockerCertificates(certs.get());
}
return builder;
}
private static String defaultEndpoint() {
if (getProperty("os.name").equalsIgnoreCase("linux")) {
return DEFAULT_UNIX_ENDPOINT;
} else {
return DEFAULT_HOST + ":" + DEFAULT_PORT;
}
}
private static String defaultCertPath() {
return Paths.get(getProperty("user.home"), ".docker").toString();
}
public static class Builder {
private URI uri;
private String apiVersion;
private long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS;
private long readTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS;
private int connectionPoolSize = DEFAULT_CONNECTION_POOL_SIZE;
private DockerCertificates dockerCertificates;
private AuthConfig authConfig;
public URI uri() {
return uri;
}
public Builder uri(final URI uri) {
this.uri = uri;
return this;
}
/**
* Set the URI for connections to Docker.
*
* @param uri URI String for connections to Docker
* @return Builder
*/
public Builder uri(final String uri) {
return uri(URI.create(uri));
}
/**
* Set the Docker API version that will be used in the HTTP requests to Docker daemon.
*
* @param apiVersion String for Docker API version
* @return Builder
*/
public Builder apiVersion(final String apiVersion) {
this.apiVersion = apiVersion;
return this;
}
public String apiVersion() {
return apiVersion;
}
public long connectTimeoutMillis() {
return connectTimeoutMillis;
}
/**
* Set the timeout in milliseconds until a connection to Docker is established.
* A timeout value of zero is interpreted as an infinite timeout.
*
* @param connectTimeoutMillis connection timeout to Docker daemon in milliseconds
* @return Builder
*/
public Builder connectTimeoutMillis(final long connectTimeoutMillis) {
this.connectTimeoutMillis = connectTimeoutMillis;
return this;
}
public long readTimeoutMillis() {
return readTimeoutMillis;
}
/**
* Set the SO_TIMEOUT in milliseconds. This is the maximum period of inactivity
* between receiving two consecutive data packets from Docker.
*
* @param readTimeoutMillis read timeout to Docker daemon in milliseconds
* @return Builder
*/
public Builder readTimeoutMillis(final long readTimeoutMillis) {
this.readTimeoutMillis = readTimeoutMillis;
return this;
}
public DockerCertificates dockerCertificates() {
return dockerCertificates;
}
/**
* Provide certificates to secure the connection to Docker.
*
* @param dockerCertificates DockerCertificates object
* @return Builder
*/
public Builder dockerCertificates(final DockerCertificates dockerCertificates) {
this.dockerCertificates = dockerCertificates;
return this;
}
public int connectionPoolSize() {
return connectionPoolSize;
}
/**
* Set the size of the connection pool for connections to Docker. Note that due to
* a known issue, DefaultDockerClient maintains two separate connection pools, each
* of which is capped at this size. Therefore, the maximum number of concurrent
* connections to Docker may be up to 2 * connectionPoolSize.
*
* @param connectionPoolSize connection pool size
* @return Builder
*/
public Builder connectionPoolSize(final int connectionPoolSize) {
this.connectionPoolSize = connectionPoolSize;
return this;
}
public AuthConfig authConfig() {
return authConfig;
}
/**
* Set the auth parameters for pull/push requests from/to private repositories.
*
* @param authConfig AuthConfig object
* @return Builder
*/
public Builder authConfig(final AuthConfig authConfig) {
this.authConfig = authConfig;
return this;
}
public DefaultDockerClient build() {
return new DefaultDockerClient(this);
}
}
}