/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.docker.client;
import com.google.common.io.CharStreams;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.che.api.core.util.FileCleaner;
import org.eclipse.che.api.core.util.ValueHolder;
import org.eclipse.che.commons.json.JsonHelper;
import org.eclipse.che.commons.json.JsonNameConvention;
import org.eclipse.che.commons.json.JsonParseException;
import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.commons.lang.TarUtils;
import org.eclipse.che.commons.lang.ws.rs.ExtMediaType;
import org.eclipse.che.plugin.docker.client.connection.CloseConnectionInputStream;
import org.eclipse.che.plugin.docker.client.connection.DockerConnection;
import org.eclipse.che.plugin.docker.client.connection.DockerConnectionFactory;
import org.eclipse.che.plugin.docker.client.connection.DockerResponse;
import org.eclipse.che.plugin.docker.client.dto.AuthConfigs;
import org.eclipse.che.plugin.docker.client.json.ContainerCommited;
import org.eclipse.che.plugin.docker.client.json.ContainerConfig;
import org.eclipse.che.plugin.docker.client.json.ContainerCreated;
import org.eclipse.che.plugin.docker.client.json.ContainerExitStatus;
import org.eclipse.che.plugin.docker.client.json.ContainerInfo;
import org.eclipse.che.plugin.docker.client.json.ContainerProcesses;
import org.eclipse.che.plugin.docker.client.json.ContainerResource;
import org.eclipse.che.plugin.docker.client.json.Event;
import org.eclipse.che.plugin.docker.client.json.ExecConfig;
import org.eclipse.che.plugin.docker.client.json.ExecCreated;
import org.eclipse.che.plugin.docker.client.json.ExecInfo;
import org.eclipse.che.plugin.docker.client.json.ExecStart;
import org.eclipse.che.plugin.docker.client.json.Filters;
import org.eclipse.che.plugin.docker.client.json.HostConfig;
import org.eclipse.che.plugin.docker.client.json.Image;
import org.eclipse.che.plugin.docker.client.json.ImageInfo;
import org.eclipse.che.plugin.docker.client.json.ProgressStatus;
import org.eclipse.che.plugin.docker.client.json.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.core.MediaType;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper;
import static javax.ws.rs.core.Response.Status.CREATED;
import static javax.ws.rs.core.Response.Status.NOT_MODIFIED;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.Status.OK;
/**
* Client for docker API.
*
* @author andrew00x
* @author Alexander Garagatyi
* @author Anton Korneta
*/
@Singleton
public class DockerConnector {
private static final Logger LOG = LoggerFactory.getLogger(DockerConnector.class);
private final URI dockerDaemonUri;
private final InitialAuthConfig initialAuthConfig;
private final ExecutorService executor;
private final DockerConnectionFactory connectionFactory;
@Inject
public DockerConnector(DockerConnectorConfiguration connectorConfiguration,
DockerConnectionFactory connectionFactory) {
this.dockerDaemonUri = connectorConfiguration.getDockerDaemonUri();
this.initialAuthConfig = connectorConfiguration.getAuthConfigs();
this.connectionFactory = connectionFactory;
executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder()
.setNameFormat("DockerApiConnector-%d")
.setDaemon(true)
.build());
}
/**
* Gets system-wide information.
*
* @return system-wide information
* @throws IOException
*/
public org.eclipse.che.plugin.docker.client.json.SystemInfo getSystemInfo() throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/info")) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), org.eclipse.che.plugin.docker.client.json.SystemInfo.class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
/**
* Gets docker version.
*
* @return information about version docker
* @throws IOException
*/
public Version getVersion() throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/version")) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), Version.class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
/**
* Lists docker images.
*
* @return list of docker images
* @throws IOException
*/
public Image[] listImages() throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/images/json")) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), Image[].class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
/**
* Builds new docker image from specified dockerfile.
*
* @param repository
* full repository name to be applied to newly created image
* @param progressMonitor
* ProgressMonitor for images creation process
* @param authConfigs
* Authentication configuration for private registries. Can be null.
* @param files
* files that are needed for creation docker images (e.g. file of directories used in ADD instruction in Dockerfile), one of
* them must be Dockerfile.
* @return image id
* @throws IOException
* @throws InterruptedException
* if build process was interrupted
*/
public String buildImage(String repository,
ProgressMonitor progressMonitor,
AuthConfigs authConfigs,
boolean doForcePull,
File... files) throws IOException, InterruptedException {
final File tar = Files.createTempFile(null, ".tar").toFile();
try {
createTarArchive(tar, files);
return buildImage(repository, tar, progressMonitor, authConfigs, doForcePull);
} finally {
FileCleaner.addFile(tar);
}
}
/**
* Builds new docker image from specified tar archive that must contain Dockerfile.
*
* @param repository
* full repository name to be applied to newly created image
* @param tar
* archived files that are needed for creation docker images (e.g. file of directories used in ADD instruction in Dockerfile).
* One of them must be Dockerfile.
* @param progressMonitor
* ProgressMonitor for images creation process
* @param authConfigs
* Authentication configuration for private registries. Can be null.
* @return image id
* @throws IOException
* @throws InterruptedException
* if build process was interrupted
*/
protected String buildImage(String repository,
File tar,
final ProgressMonitor progressMonitor,
AuthConfigs authConfigs,
boolean doForcePull) throws IOException, InterruptedException {
return doBuildImage(repository, tar, progressMonitor, dockerDaemonUri, authConfigs, doForcePull);
}
/**
* Gets detailed information about docker image.
*
* @param image
* id or full repository name of docker image
* @return detailed information about {@code image}
* @throws IOException
*/
public ImageInfo inspectImage(String image) throws IOException {
return doInspectImage(image, dockerDaemonUri);
}
protected ImageInfo doInspectImage(String image, URI dockerDaemonUri) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/images/" + image + "/json")) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ImageInfo.class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
public void removeImage(String image, boolean force) throws IOException {
doRemoveImage(image, force, dockerDaemonUri);
}
public void tag(String image, String repository, String tag) throws IOException {
doTag(image, repository, tag, dockerDaemonUri);
}
public void push(String repository,
String tag,
String registry,
final ProgressMonitor progressMonitor) throws IOException, InterruptedException {
doPush(repository, tag, registry, progressMonitor, dockerDaemonUri);
}
/**
* See <a href="https://docs.docker.com/reference/api/docker_remote_api_v1.16/#create-an-image">Docker remote API # Create an
* image</a>.
* To pull from private registry use registry.address:port/image as image. This is not documented.
*
* @throws IOException
* @throws InterruptedException
*/
public void pull(String image,
String tag,
String registry,
ProgressMonitor progressMonitor) throws IOException, InterruptedException {
doPull(image, tag, registry, progressMonitor, dockerDaemonUri);
}
public ContainerCreated createContainer(ContainerConfig containerConfig, String containerName) throws IOException {
return doCreateContainer(containerConfig, containerName, dockerDaemonUri);
}
public void startContainer(String container, HostConfig hostConfig) throws IOException {
doStartContainer(container, hostConfig, dockerDaemonUri);
}
/**
* Stops container.
*
* @param container
* container identifier, either id or name
* @param timeout
* time to wait for the container to stop before killing it
* @param timeunit
* time unit of the timeout parameter
* @throws IOException
*/
public void stopContainer(String container, long timeout, TimeUnit timeunit) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/" + container + "/stop")
.query("t", timeunit.toSeconds(timeout))
.headers(headers)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (!(NO_CONTENT.getStatusCode() == status || NOT_MODIFIED.getStatusCode() == status)) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
}
}
/**
* Kills running container Kill a running container using specified signal.
*
* @param container
* container identifier, either id or name
* @param signal
* code of signal, e.g. 9 in case of SIGKILL
* @throws IOException
*/
public void killContainer(String container, int signal) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/" + container + "/kill")
.query("signal", signal)
.headers(headers)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (NO_CONTENT.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
}
}
/**
* Kills container with SIGKILL signal.
*
* @param container
* container identifier, either id or name
* @throws IOException
*/
public void killContainer(String container) throws IOException {
killContainer(container, 9);
}
/**
* Removes container.
*
* @param container
* container identifier, either id or name
* @param force
* if {@code true} kills the running container then remove it
* @param removeVolumes
* if {@code true} removes volumes associated to the container
* @throws IOException
*/
public void removeContainer(String container, boolean force, boolean removeVolumes) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("DELETE")
.path("/containers/" + container)
.query("force", force ? 1 : 0)
.query("v", removeVolumes ? 1 : 0)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (NO_CONTENT.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
}
}
/**
* Blocks until {@code container} stops, then returns the exit code
*
* @param container
* container identifier, either id or name
* @return exit code
* @throws IOException
*/
public int waitContainer(String container) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/" + container + "/wait")
.headers(headers)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ContainerExitStatus.class).getStatusCode();
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
/**
* Gets detailed information about docker container.
*
* @param container
* id of container
* @return detailed information about {@code container}
* @throws IOException
*/
public ContainerInfo inspectContainer(String container) throws IOException {
return doInspectContainer(container, dockerDaemonUri);
}
protected ContainerInfo doInspectContainer(String container, URI dockerDaemonUri) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/containers/" + container + "/json")) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ContainerInfo.class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
/**
* Attaches to the container with specified id.
*
* @param container
* id of container
* @param containerLogsProcessor
* output for container logs
* @param stream
* if {@code true} then get 'live' stream from container. Typically need to run this method in separate thread, if {@code
* stream} is {@code true} since this method blocks until container is running.
* @throws java.io.IOException
*/
public void attachContainer(String container, MessageProcessor<LogMessage> containerLogsProcessor, boolean stream) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/" + container + "/attach")
.query("stream", (stream ? 1 : 0))
.query("logs", (stream ? 0 : 1))
.query("stdout", 1)
.query("stderr", 1)
.headers(headers)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
try (InputStream responseStream = response.getInputStream()) {
new LogMessagePumper(responseStream, containerLogsProcessor).start();
}
}
}
public String commit(String container, String repository, String tag, String comment, String author) throws IOException {
// todo: pause container
return doCommit(container, repository, tag, comment, author, dockerDaemonUri);
}
/**
* Copies file or directory {@code path} from {@code container} to the {code hostPath}.
*
* @param container
* container id
* @param path
* path to file or directory inside container
* @param hostPath
* path to the directory on host filesystem
* @throws IOException
* @deprecated since 1.20 docker api in favor of the {@link #getResource(String, String)}
* and {@link #putResource(String, String, InputStream, boolean) putResource}
*/
@Deprecated
public void copy(String container, String path, File hostPath) throws IOException {
final String entity = JsonHelper.toJson(new ContainerResource().withResource(path), FIRST_LETTER_LOWERCASE);
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.APPLICATION_JSON));
headers.add(Pair.of("Content-Length", entity.getBytes().length));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path(String.format("/containers/%s/copy", container))
.headers(headers)
.entity(entity)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
// TarUtils uses apache commons compress library for working with tar archive and it fails
// (e.g. doesn't unpack all files from archive in case of coping directory) when we try to use stream from docker remote API.
// Docker sends tar contents as sequence of chunks and seems that causes problems for apache compress library.
// The simplest solution is spool content to temporary file and then unpack it to destination folder.
final Path spoolFilePath = Files.createTempFile("docker-copy-spool-", ".tar");
try (InputStream is = response.getInputStream()) {
Files.copy(is, spoolFilePath, StandardCopyOption.REPLACE_EXISTING);
try (InputStream tarStream = Files.newInputStream(spoolFilePath)) {
TarUtils.untar(tarStream, hostPath);
}
} finally {
FileCleaner.addFile(spoolFilePath.toFile());
}
}
}
public Exec createExec(String container, boolean detach, String... cmd) throws IOException {
final ExecConfig execConfig = new ExecConfig().withCmd(cmd);
if (!detach) {
execConfig.withAttachStderr(true).withAttachStdout(true);
}
final List<Pair<String, ?>> headers = new ArrayList<>(2);
final String entity = JsonHelper.toJson(execConfig, FIRST_LETTER_LOWERCASE);
headers.add(Pair.of("Content-Type", MediaType.APPLICATION_JSON));
headers.add(Pair.of("Content-Length", entity.getBytes().length));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/" + container + "/exec")
.headers(headers)
.entity(entity)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (status / 100 != 2) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return new Exec(cmd, parseResponseStreamAndClose(response.getInputStream(), ExecCreated.class).getId());
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
public void startExec(String execId, MessageProcessor<LogMessage> execOutputProcessor) throws IOException {
final ExecStart execStart = new ExecStart().withDetach(execOutputProcessor == null);
final String entity = JsonHelper.toJson(execStart, FIRST_LETTER_LOWERCASE);
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.APPLICATION_JSON));
headers.add(Pair.of("Content-Length", entity.getBytes().length));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/exec/" + execId + "/start")
.headers(headers)
.entity(entity)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
// According to last doc (https://docs.docker.com/reference/api/docker_remote_api_v1.15/#exec-start) status must be 201 but
// in fact docker API returns 200 or 204 status.
if (status / 100 != 2) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
if (status != NO_CONTENT.getStatusCode() && execOutputProcessor != null) {
try (InputStream responseStream = response.getInputStream()) {
new LogMessagePumper(responseStream, execOutputProcessor).start();
}
}
}
}
/**
* Gets detailed information about exec
*
* @return detailed information about {@code execId}
* @throws IOException
*/
public ExecInfo getExecInfo(String execId) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/exec/" + execId + "/json")) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ExecInfo.class);
} catch (Exception e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
public ContainerProcesses top(String container, String... psArgs) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
final DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/containers/" + container + "/top")
.headers(headers);
if (psArgs != null && psArgs.length != 0) {
StringBuilder psArgsQueryBuilder = new StringBuilder();
for (int i = 0, l = psArgs.length; i < l; i++) {
if (i > 0) {
psArgsQueryBuilder.append('+');
}
psArgsQueryBuilder.append(URLEncoder.encode(psArgs[i], "UTF-8"));
}
connection.query("ps_args", psArgsQueryBuilder.toString());
}
try {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ContainerProcesses.class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
} finally {
connection.close();
}
}
/**
* Gets files from the specified container.
*
* @param container
* container id
* @param sourcePath
* path to file or directory inside specified container
* @return stream of resources from the specified container filesystem, with retention connection
* @throws IOException
* when problems occurs with docker api calls
* @apiNote this method implements 1.20 docker API and requires docker not less than 1.8.0 version
*/
public InputStream getResource(String container, String sourcePath) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/containers/" + container + "/archive")
.query("path", sourcePath)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (status != OK.getStatusCode()) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return new CloseConnectionInputStream(response.getInputStream(), connection);
}
}
/**
* Puts files into specified container.
*
* @param container
* container id
* @param targetPath
* path to file or directory inside specified container
* @param sourceStream
* stream of files from source container
* @param noOverwriteDirNonDir
* If "false" then it will be an error if unpacking the given content would cause
* an existing directory to be replaced with a non-directory or other resource and vice versa.
* @throws IOException
* when problems occurs with docker api calls, or during file system operations
* @apiNote this method implements 1.20 docker API and requires docker not less than 1.8 version
*/
public void putResource(String container,
String targetPath,
InputStream sourceStream,
boolean noOverwriteDirNonDir) throws IOException {
File tarFile;
long length;
try (InputStream sourceData = sourceStream) {
Path tarFilePath = Files.createTempFile("compressed-resources", ".tar");
tarFile = tarFilePath.toFile();
length = Files.copy(sourceData, tarFilePath, StandardCopyOption.REPLACE_EXISTING);
}
List<Pair<String, ?>> headers = Arrays.asList(Pair.of("Content-Type", ExtMediaType.APPLICATION_X_TAR),
Pair.of("Content-Length", length));
try (InputStream tarStream = new BufferedInputStream(new FileInputStream(tarFile));
DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("PUT")
.path("/containers/" + container + "/archive")
.query("path", targetPath)
.query("noOverwriteDirNonDir", noOverwriteDirNonDir ? 0 : 1)
.headers(headers)
.entity(tarStream)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (status != OK.getStatusCode()) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
} finally {
FileCleaner.addFile(tarFile);
}
}
/**
* Get docker events.
* Parameter {@code untilSecond} does nothing if {@code sinceSecond} is 0.<br>
* If {@code untilSecond} and {@code sinceSecond} are 0 method gets new events only (streaming mode).<br>
* If {@code untilSecond} and {@code sinceSecond} are not 0 (but less that current date)
* methods get events that were generated between specified dates.<br>
* If {@code untilSecond} is 0 but {@code sinceSecond} is not method gets old events and streams new ones.<br>
* If {@code sinceSecond} is 0 no old events will be got.<br>
* With some connection implementations method can fail due to connection timeout in streaming mode.
*
* @param sinceSecond
* UNIX date in seconds. allow omit events created before specified date.
* @param untilSecond
* UNIX date in seconds. allow omit events created after specified date.
* @param filters
* filter of needed events. Available filters: {@code event=<string>}
* {@code image=<string>} {@code container=<string>}
* @param messageProcessor
* processor of all found events that satisfy specified parameters
* @throws IOException
*/
public void getEvents(long sinceSecond,
long untilSecond,
Filters filters,
MessageProcessor<Event> messageProcessor) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("GET")
.path("/events")) {
if (sinceSecond != 0) {
connection.query("since", sinceSecond);
}
if (untilSecond != 0) {
connection.query("until", untilSecond);
}
if (filters != null) {
connection.query("filters", urlPathSegmentEscaper().escape(JsonHelper.toJson(filters.getFilters())));
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
try (InputStream responseStream = response.getInputStream()) {
new MessagePumper<>(new JsonMessageReader<>(responseStream, Event.class), messageProcessor).start();
}
}
}
/**
* Builds new docker image from specified tar archive that must contain Dockerfile.
*
* @param repository
* full repository name to be applied to newly created image
* @param tar
* archived files that are needed for creation docker images (e.g. file of directories used in ADD instruction in Dockerfile).
* One of them must be Dockerfile.
* @param progressMonitor
* ProgressMonitor for images creation process
* @param dockerDaemonUri
* Uri for remote access to docker API
* @param authConfigs
* Authentication configuration for private registries. Can be null.
* @return image id
* @throws IOException
* @throws InterruptedException
* if build process was interrupted
*/
protected String doBuildImage(String repository,
File tar,
final ProgressMonitor progressMonitor,
URI dockerDaemonUri,
AuthConfigs authConfigs,
boolean doForcePull) throws IOException, InterruptedException {
if (authConfigs == null) {
authConfigs = initialAuthConfig.getAuthConfigs();
}
final List<Pair<String, ?>> headers = new ArrayList<>(3);
headers.add(Pair.of("Content-Type", "application/x-compressed-tar"));
headers.add(Pair.of("Content-Length", tar.length()));
headers.add(Pair.of("X-Registry-Config", Base64.encodeBase64String(JsonHelper.toJson(authConfigs).getBytes())));
try (InputStream tarInput = new FileInputStream(tar);
DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/build")
.query("rm", 1)
.query("pull", doForcePull)
.headers(headers)
.entity(tarInput)) {
if (repository != null) {
connection.query("t", repository);
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
try (InputStream responseStream = response.getInputStream()) {
JsonMessageReader<ProgressStatus> progressReader = new JsonMessageReader<>(responseStream, ProgressStatus.class);
final ValueHolder<IOException> errorHolder = new ValueHolder<>();
final ValueHolder<String> imageIdHolder = new ValueHolder<>();
// Here do some trick to be able interrupt build process. Basically for now it is not possible interrupt docker daemon while
// it's building images but here we need just be able to close connection to the unix socket. Thread is blocking while read
// from the socket stream so need one more thread that is able to close socket. In this way we can release thread that is
// blocking on i/o.
final Runnable runnable = new Runnable() {
@Override
public void run() {
try {
ProgressStatus progressStatus;
while ((progressStatus = progressReader.next()) != null) {
final String buildImageId = getBuildImageId(progressStatus);
if (buildImageId != null) {
imageIdHolder.set(buildImageId);
}
progressMonitor.updateProgress(progressStatus);
}
} catch (IOException e) {
errorHolder.set(e);
}
synchronized (this) {
notify();
}
}
};
executor.execute(runnable);
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (runnable) {
runnable.wait();
}
final IOException ioe = errorHolder.get();
if (ioe != null) {
throw ioe;
}
if (imageIdHolder.get() == null) {
throw new IOException("Docker image build failed");
}
return imageIdHolder.get();
}
}
}
protected void doRemoveImage(String image, boolean force, URI dockerDaemonUri) throws IOException {
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("DELETE")
.path("/images/" + image)
.query("force", force ? 1 : 0)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
}
}
protected void doTag(String image, String repository, String tag, URI dockerDaemonUri) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(3);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/images/" + image + "/tag")
.query("repo", repository)
.query("force", 0)
.headers(headers)) {
if (tag != null) {
connection.query("tag", tag);
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (status / 100 != 2) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
}
}
protected void doPush(final String repository,
final String tag,
final String registry,
final ProgressMonitor progressMonitor,
final URI dockerDaemonUri) throws IOException, InterruptedException {
final List<Pair<String, ?>> headers = new ArrayList<>(3);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
headers.add(Pair.of("X-Registry-Auth", initialAuthConfig.getAuthConfigHeader()));
final String fullRepo = registry != null ? registry + "/" + repository : repository;
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/images/" + fullRepo + "/push")
.headers(headers)) {
if (tag != null) {
connection.query("tag", tag);
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
try (InputStream responseStream = response.getInputStream()) {
JsonMessageReader<ProgressStatus> progressReader = new JsonMessageReader<>(responseStream, ProgressStatus.class);
final ValueHolder<IOException> errorHolder = new ValueHolder<>();
//it is necessary to track errors during the push, this is useful in the case when docker API returns status 200 OK,
//but in fact we have an error (e.g docker registry is not accessible but we are trying to push).
final ValueHolder<String> exceptionHolder = new ValueHolder<>();
// Here do some trick to be able interrupt push process. Basically for now it is not possible interrupt docker daemon while
// it's pushing images but here we need just be able to close connection to the unix socket. Thread is blocking while read
// from the socket stream so need one more thread that is able to close socket. In this way we can release thread that is
// blocking on i/o.
final Runnable runnable = new Runnable() {
@Override
public void run() {
try {
ProgressStatus progressStatus;
while ((progressStatus = progressReader.next()) != null && exceptionHolder.get() == null) {
progressMonitor.updateProgress(progressStatus);
if (progressStatus.getError() != null) {
exceptionHolder.set(progressStatus.getError());
}
}
} catch (IOException e) {
errorHolder.set(e);
}
synchronized (this) {
notify();
}
}
};
executor.execute(runnable);
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (runnable) {
runnable.wait();
}
if (exceptionHolder.get() != null) {
throw new DockerException(exceptionHolder.get(), 500);
}
final IOException ioe = errorHolder.get();
if (ioe != null) {
throw ioe;
}
}
}
}
protected String doCommit(String container,
String repository,
String tag,
String comment,
String author,
URI dockerDaemonUri) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.APPLICATION_JSON));
final String entity = "{}";
headers.add(Pair.of("Content-Length", entity.getBytes().length));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/commit")
.query("container", container)
.query("repo", repository)
.headers(headers)
.entity(entity)) {
if (tag != null) {
connection.query("tag", tag);
}
if (comment != null) {
connection.query("comment", URLEncoder.encode(comment, "UTF-8"));
}
if (comment != null) {
connection.query("author", URLEncoder.encode(author, "UTF-8"));
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (CREATED.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ContainerCommited.class).getId();
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
protected void doPull(String image,
String tag,
String registry,
final ProgressMonitor progressMonitor,
URI dockerDaemonUri) throws IOException, InterruptedException {
final List<Pair<String, ?>> headers = new ArrayList<>(3);
headers.add(Pair.of("Content-Type", MediaType.TEXT_PLAIN));
headers.add(Pair.of("Content-Length", 0));
headers.add(Pair.of("X-Registry-Auth", initialAuthConfig.getAuthConfigHeader()));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/images/create")
.query("fromImage", registry != null ? registry + "/" + image : image)
.headers(headers)) {
if (tag != null) {
connection.query("tag", tag);
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (OK.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
try (InputStream responseStream = response.getInputStream()) {
JsonMessageReader<ProgressStatus> progressReader = new JsonMessageReader<>(responseStream, ProgressStatus.class);
final ValueHolder<IOException> errorHolder = new ValueHolder<>();
// Here do some trick to be able interrupt pull process. Basically for now it is not possible interrupt docker daemon while
// it's pulling images but here we need just be able to close connection to the unix socket. Thread is blocking while read
// from the socket stream so need one more thread that is able to close socket. In this way we can release thread that is
// blocking on i/o.
final Runnable runnable = new Runnable() {
@Override
public void run() {
try {
ProgressStatus progressStatus;
while ((progressStatus = progressReader.next()) != null) {
progressMonitor.updateProgress(progressStatus);
}
} catch (IOException e) {
errorHolder.set(e);
}
synchronized (this) {
notify();
}
}
};
executor.execute(runnable);
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (runnable) {
runnable.wait();
}
final IOException ioe = errorHolder.get();
if (ioe != null) {
throw ioe;
}
}
}
}
protected ContainerCreated doCreateContainer(ContainerConfig containerConfig,
String containerName,
URI dockerDaemonUri) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.APPLICATION_JSON));
final String entity = JsonHelper.toJson(containerConfig, FIRST_LETTER_LOWERCASE);
headers.add(Pair.of("Content-Length", entity.getBytes().length));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/create")
.headers(headers)
.entity(entity)) {
if (containerName != null) {
connection.query("name", containerName);
}
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (CREATED.getStatusCode() != status) {
throw new DockerException(getDockerExceptionMessage(response), status);
}
return parseResponseStreamAndClose(response.getInputStream(), ContainerCreated.class);
} catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
protected void doStartContainer(String container,
HostConfig hostConfig,
URI dockerDaemonUri) throws IOException {
final List<Pair<String, ?>> headers = new ArrayList<>(2);
headers.add(Pair.of("Content-Type", MediaType.APPLICATION_JSON));
final String entity = hostConfig == null ? "{}" : JsonHelper.toJson(hostConfig, FIRST_LETTER_LOWERCASE);
headers.add(Pair.of("Content-Length", entity.getBytes().length));
try (DockerConnection connection = connectionFactory.openConnection(dockerDaemonUri)
.method("POST")
.path("/containers/" + container + "/start")
.headers(headers)
.entity(entity)) {
final DockerResponse response = connection.request();
final int status = response.getStatus();
if (!(NO_CONTENT.getStatusCode() == status || NOT_MODIFIED.getStatusCode() == status)) {
final String errorMessage = getDockerExceptionMessage(response);
if (OK.getStatusCode() == status) {
// docker API 1.20 returns 200 with warning message about usage of loopback docker backend
LOG.warn(errorMessage);
} else {
throw new DockerException(errorMessage, status);
}
}
}
}
private String getBuildImageId(ProgressStatus progressStatus) {
final String stream = progressStatus.getStream();
if (stream != null && stream.startsWith("Successfully built ")) {
int endSize = 19;
while (endSize < stream.length() && Character.digit(stream.charAt(endSize), 16) != -1) {
endSize++;
}
return stream.substring(19, endSize);
}
return null;
}
private <T> T parseResponseStreamAndClose(InputStream inputStream, Class<T> clazz) throws IOException, JsonParseException {
try (InputStream responseStream = inputStream) {
return JsonHelper.fromJson(responseStream,
clazz,
null,
FIRST_LETTER_LOWERCASE);
}
}
private String getDockerExceptionMessage(DockerResponse response) throws IOException {
try (InputStream is = response.getInputStream()) {
return "Error response from docker API, status: " +
response.getStatus() +
", message: " +
CharStreams.toString(new InputStreamReader(is));
}
}
// Unfortunately we can't use generated DTO here.
// Docker uses uppercase in first letter in names of json objects, e.g. {"Id":"123"} instead of {"id":"123"}
protected static JsonNameConvention FIRST_LETTER_LOWERCASE = new JsonNameConvention() {
@Override
public String toJsonName(String javaName) {
return Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
}
@Override
public String toJavaName(String jsonName) {
return Character.toLowerCase(jsonName.charAt(0)) + jsonName.substring(1);
}
};
private void createTarArchive(File tar, File... files) throws IOException {
TarUtils.tarFiles(tar, 0, files);
}
}