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

org.mandas.docker.client.DefaultDockerClient Maven / Gradle / Ivy

There is a newer version: 8.0.3
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
 * Copyright (C) 9/2019 - 2020 Dimitris Mandalidis
 * --
 * 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 org.mandas.docker.client;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.unmodifiableSet;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static jakarta.ws.rs.HttpMethod.DELETE;
import static jakarta.ws.rs.HttpMethod.GET;
import static jakarta.ws.rs.HttpMethod.POST;
import static jakarta.ws.rs.HttpMethod.PUT;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
import static org.mandas.docker.client.ObjectMapperProvider.objectMapper;
import static org.mandas.docker.client.VersionCompare.compareVersion;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
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.util.ArrayList;
import java.util.Base64;
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 jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.ResponseProcessingException;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status.Family;

import org.apache.commons.compress.utils.IOUtils;
import org.mandas.docker.client.auth.RegistryAuthSupplier;
import org.mandas.docker.client.exceptions.BadParamException;
import org.mandas.docker.client.exceptions.ConflictException;
import org.mandas.docker.client.exceptions.ContainerNotFoundException;
import org.mandas.docker.client.exceptions.ContainerRenameConflictException;
import org.mandas.docker.client.exceptions.DockerException;
import org.mandas.docker.client.exceptions.DockerRequestException;
import org.mandas.docker.client.exceptions.DockerTimeoutException;
import org.mandas.docker.client.exceptions.ExecCreateConflictException;
import org.mandas.docker.client.exceptions.ExecNotFoundException;
import org.mandas.docker.client.exceptions.ExecStartConflictException;
import org.mandas.docker.client.exceptions.ImageNotFoundException;
import org.mandas.docker.client.exceptions.NetworkNotFoundException;
import org.mandas.docker.client.exceptions.NodeNotFoundException;
import org.mandas.docker.client.exceptions.NonSwarmNodeException;
import org.mandas.docker.client.exceptions.NotFoundException;
import org.mandas.docker.client.exceptions.PermissionException;
import org.mandas.docker.client.exceptions.ServiceNotFoundException;
import org.mandas.docker.client.exceptions.TaskNotFoundException;
import org.mandas.docker.client.exceptions.UnsupportedApiVersionException;
import org.mandas.docker.client.exceptions.VolumeNotFoundException;
import org.mandas.docker.client.messages.Container;
import org.mandas.docker.client.messages.ContainerChange;
import org.mandas.docker.client.messages.ContainerConfig;
import org.mandas.docker.client.messages.ContainerCreation;
import org.mandas.docker.client.messages.ContainerExit;
import org.mandas.docker.client.messages.ContainerInfo;
import org.mandas.docker.client.messages.ContainerStats;
import org.mandas.docker.client.messages.ContainerUpdate;
import org.mandas.docker.client.messages.Distribution;
import org.mandas.docker.client.messages.ExecCreation;
import org.mandas.docker.client.messages.ExecState;
import org.mandas.docker.client.messages.HostConfig;
import org.mandas.docker.client.messages.Image;
import org.mandas.docker.client.messages.ImageHistory;
import org.mandas.docker.client.messages.ImageInfo;
import org.mandas.docker.client.messages.ImageSearchResult;
import org.mandas.docker.client.messages.Info;
import org.mandas.docker.client.messages.Network;
import org.mandas.docker.client.messages.NetworkConfig;
import org.mandas.docker.client.messages.NetworkConnection;
import org.mandas.docker.client.messages.NetworkCreation;
import org.mandas.docker.client.messages.ProgressMessage;
import org.mandas.docker.client.messages.RegistryAuth;
import org.mandas.docker.client.messages.RegistryConfigs;
import org.mandas.docker.client.messages.RemovedImage;
import org.mandas.docker.client.messages.ServiceCreateResponse;
import org.mandas.docker.client.messages.TopResults;
import org.mandas.docker.client.messages.Version;
import org.mandas.docker.client.messages.Volume;
import org.mandas.docker.client.messages.VolumeList;
import org.mandas.docker.client.messages.swarm.Config;
import org.mandas.docker.client.messages.swarm.ConfigCreateResponse;
import org.mandas.docker.client.messages.swarm.ConfigSpec;
import org.mandas.docker.client.messages.swarm.Node;
import org.mandas.docker.client.messages.swarm.NodeInfo;
import org.mandas.docker.client.messages.swarm.NodeSpec;
import org.mandas.docker.client.messages.swarm.Secret;
import org.mandas.docker.client.messages.swarm.SecretCreateResponse;
import org.mandas.docker.client.messages.swarm.SecretSpec;
import org.mandas.docker.client.messages.swarm.Service;
import org.mandas.docker.client.messages.swarm.ServiceSpec;
import org.mandas.docker.client.messages.swarm.Swarm;
import org.mandas.docker.client.messages.swarm.SwarmInit;
import org.mandas.docker.client.messages.swarm.SwarmJoin;
import org.mandas.docker.client.messages.swarm.SwarmSpec;
import org.mandas.docker.client.messages.swarm.Task;
import org.mandas.docker.client.messages.swarm.UnlockKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;

public class DefaultDockerClient implements DockerClient {

  /**
   * 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() { if (imageId == null) { throw new IllegalStateException("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 unmodifiableSet(new HashSet<>(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() { if (imageId == null) { throw new IllegalStateException("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 Logger log = LoggerFactory.getLogger(DefaultDockerClient.class); static final long NO_TIMEOUT = 0; 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 URI uri; private final String apiVersion; private final RegistryAuthSupplier registryAuthSupplier; private final Map headers; /** * Create a new client using the configuration of the builder. * @param apiVersion the specific API version of the Docker engine * @param authSupplier credentials supplier for the docker registry * @param uri the Docker engine URI * @param client the JAX-RS client for issuing the calls to the docker engine * @param headers a custom set of HTTP headers */ public DefaultDockerClient(String apiVersion, RegistryAuthSupplier authSupplier, URI uri, Client client, Map headers) { this.apiVersion = apiVersion; this.registryAuthSupplier = authSupplier; this.uri = uri; this.client = client; this.headers = new HashMap<>(headers); } @Override public String getHost() { return ofNullable(uri.getHost()).orElse("localhost"); } @Override public void close() { client.close(); } @Override public String ping() throws DockerException, InterruptedException { final WebTarget resource = resource().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"); try (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 { WebTarget target = resource; final Map> filters = new HashMap<>(); for (final Param param : params) { if (param instanceof FilterParam) { List filterValueList; if (filters.containsKey(param.name())) { filterValueList = filters.get(param.name()); } else { filterValueList = new ArrayList<>(); } filterValueList.add(param.value()); filters.put(param.name(), filterValueList); } else { target = target.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. target = target.queryParam("filters", urlEncodeFilters(filters)); } return target; } private Map getQueryParamMap(final WebTarget resource) { final String queryParams = resource.getUri().getQuery(); final Map paramsMap = new HashMap<>(); 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) { if (!CONTAINER_NAME_PATTERN.matcher(name).matches()) { throw new IllegalArgumentException(String.format("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); case 409: throw new ConflictException(String.format("Container %s already exists", name), e); default: throw e; } } } @Override public void startContainer(final String containerId) throws DockerException, InterruptedException { requireNonNull(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 { 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); } } try (final Response response = 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 { requireNonNull(containerId, "containerId"); containerAction(containerId, "pause"); } @Override public void unpauseContainer(final String containerId) throws DockerException, InterruptedException { requireNonNull(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 { requireNonNull(containerId, "containerId"); MultivaluedMap queryParameters = new MultivaluedHashMap<>(); queryParameters.add("t", String.valueOf(secondsToWaitBeforeRestart)); containerAction(containerId, "restart", queryParameters); } @Override public void killContainer(final String containerId) throws DockerException, InterruptedException { requireNonNull(containerId, "containerId"); containerAction(containerId, "kill"); } @Override public void killContainer(final String containerId, final Signal signal) throws DockerException, InterruptedException { requireNonNull(containerId, "containerId"); MultivaluedMap queryParameters = new MultivaluedHashMap<>(); queryParameters.add("signal", signal.getName()); containerAction(containerId, "kill", queryParameters); } @Override public Distribution getDistribution(String imageName) throws DockerException, InterruptedException { requireNonNull(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 { final WebTarget resource = resource() .path("containers").path(containerId).path("stop") .queryParam("t", String.valueOf(secondsToWaitBeforeKilling)); try (final Response response = 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 = resource() .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]); } @Override public void removeContainer(final String containerId, final RemoveContainerParam... params) throws DockerException, InterruptedException { WebTarget resource = resource().path("containers").path(containerId); for (final RemoveContainerParam param : params) { resource = resource.queryParam(param.name(), param.value()); } try (final Response response = 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); case 409: throw new ConflictException(String.format("Container %s is still runnning", 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 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 (psArgs != null && !"".equals(psArgs.trim())) { 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 { requireNonNull(containerId, "containerId"); requireNonNull(repo, "repo"); requireNonNull(config, "containerConfig"); WebTarget resource = resource() .path("commit") .queryParam("container", containerId) .queryParam("repo", repo); if (author != null && !"".equals(author.trim())) { resource = resource.queryParam("author", author); } if (comment != null && !"".equals(comment.trim())) { resource = resource.queryParam("comment", comment); } if (tag != null && !"".equals(tag.trim())) { 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"); } if (!CONTAINER_NAME_PATTERN.matcher(name).matches()) { throw new IllegalArgumentException(String.format("Invalid container name: \"%s\"", name)); } resource = resource.queryParam("name", name); log.info("Renaming container with id {}. New name {}.", containerId, name); try (final Response response = 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 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 InputStream inputStream = request(POST, InputStream.class, resource, resource.request(APPLICATION_JSON_TYPE), entity); ProgressStream load = new ProgressStream(inputStream)) { 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 (image != null && !"".equals(image.trim())) { 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 (final Response response = 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); } @Override public String build(final Path directory, final String name, final String dockerfile, final ProgressHandler handler, final BuildParam... params) throws DockerException, InterruptedException, IOException { requireNonNull(handler, "handler"); WebTarget resource = resource().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 = resource() .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 = resource().path("events"); resource = addParameters(resource, params); InputStream stream = request(GET, InputStream.class, resource, resource.request(MediaType.APPLICATION_JSON)); return new EventStream(stream, objectMapper()); } @Override public LogStream attachContainer(final String containerId, final AttachParameter... params) throws DockerException, InterruptedException { requireNonNull(containerId, "containerId"); WebTarget resource = resource() .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"); InputStream inputStream = request(method, InputStream.class, resource, request); return DefaultLogStream.create(inputStream); } 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"); InputStream inputStream = request(method, InputStream.class, resource, request); return DefaultLogStream.create(inputStream); } 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(); } 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 = resource() .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(); } catch (IOException e) { throw new DockerException(e); } try { InputStream inputStream = request(POST, InputStream.class, resource, resource.request("application/vnd.docker.raw-stream"), Entity.json(writer.toString())); return DefaultLogStream.create(inputStream); } 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"); final WebTarget resource = resource().path("services").path(serviceId); try (final Response response = 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 = resource() .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 { return listConfigs(null); } @Override public List listConfigs(final Config.Criteria criteria) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.30"); WebTarget resource = resource().path("configs"); if (criteria != null) { 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())); } resource = resource.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 (final Response response = 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 (final Response response = 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 (final Response response = 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 (final Response response = 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 = new HashMap<>(); 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 { final WebTarget resource = resource().path("networks").path(networkId); try (final Response response = 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 (final Response response = 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 { return listSecrets(null); } @Override public List listSecrets(final Secret.Criteria criteria) throws DockerException, InterruptedException { assertApiVersionIsAbove("1.25"); WebTarget resource = resource().path("secrets"); if (criteria != null) { final Map> filters = new HashMap<>(); if (criteria.id() != null) { filters.put("id", Collections.singletonList(criteria.id())); } if (criteria.label() != null) { filters.put("label", Collections.singletonList(criteria.label())); } if (criteria.name() != null) { filters.put("name", Collections.singletonList(criteria.name())); } resource = resource.queryParam("filters", urlEncodeFilters(filters)); } try { return request(GET, SECRET_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 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 (final Response response = 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() { return resource(client); } private WebTarget resource(Client client) { final WebTarget target = client.target(uri); if (apiVersion != null && !"".equals(apiVersion.trim())) { 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).method(method, type); } catch (Exception 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).method(method, clazz); } catch (Exception 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).method(method, entity, clazz); } catch (Exception e) { throw propagate(method, resource, e); } } private Response request(final String method, final WebTarget resource, final Invocation.Builder request) throws DockerException, InterruptedException { try { Response response = headers(request).method(method); Family family = response.getStatusInfo().getFamily(); if (family == Family.CLIENT_ERROR || family == Family.SERVER_ERROR) { int status = response.getStatus(); String responseBody = extractDockerResponse(response); response.close(); throw new DockerRequestException(method, resource.getUri(), status, responseBody, null); } return response; } catch (DockerRequestException dre) { throw dre; } catch (Exception 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 (InputStream inputStream = response.readEntity(InputStream.class); ProgressStream stream = new ProgressStream(inputStream)) { 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; } throw new DockerException(cause); } catch (IOException e) { throw new DockerException(e); } 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 log.error("Error", e); } } } private void requestAndTail(final String method, final ProgressHandler handler, final WebTarget resource, final Invocation.Builder request, final Entity entity) throws DockerException, InterruptedException { try (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 { try (Response response = request(method, Response.class, resource, request)) { if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) { throw new DockerRequestException(method, resource.getUri(), response.getStatus(), extractDockerResponse(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; if (cause instanceof ResponseProcessingException) { try (Response response = ((ResponseProcessingException) cause).getResponse()) { throw new DockerRequestException(method, resource.getUri(), response.getStatus(), extractDockerResponse(response), cause); } } else if (cause instanceof WebApplicationException) { try (Response response = ((WebApplicationException) cause).getResponse()) { throw new DockerRequestException(method, resource.getUri(), response.getStatus(), extractDockerResponse(response), cause); } } 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 (cause instanceof InterruptedIOException) { throw new DockerTimeoutException(method, resource.getUri(), ex); } else if (cause instanceof InterruptedException) { throw new InterruptedException("Interrupted: " + method + " " + resource); } else { throw new DockerException(ex); } } private String extractDockerResponse(Response response) { try { return response.readEntity(String.class); } catch (IllegalStateException | ProcessingException e) { log.warn("Cannot extract response message", e); 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 new String(Base64.getEncoder().encode(ObjectMapperProvider .objectMapper() .writeValueAsBytes(registryAuth)), UTF_8); } 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 = new StringBuilder("{\"configs\":").append(authRegistryJson).append('}').toString(); } else if (versionComparison == 0) { // Version equal 1.19 authRegistryJson = new StringBuilder("{\"auths\":").append(authRegistryJson).append('}').toString(); } return new String(Base64.getEncoder().encode(authRegistryJson.getBytes(StandardCharsets.UTF_8)), 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy