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

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

There is a newer version: 8.16.0
Show newest version
/*
 * Copyright (c) 2014 Spotify AB.
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.spotify.docker.client;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.io.CharStreams;

import com.spotify.docker.client.messages.Container;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ContainerCreation;
import com.spotify.docker.client.messages.ContainerExit;
import com.spotify.docker.client.messages.ContainerInfo;
import com.spotify.docker.client.messages.HostConfig;
import com.spotify.docker.client.messages.ImageInfo;
import com.spotify.docker.client.messages.ProgressMessage;
import com.spotify.docker.client.messages.RemovedImage;
import com.spotify.docker.client.messages.Version;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.UniformInterface;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.core.util.MultivaluedMapImpl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

import javax.ws.rs.core.MultivaluedMap;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.spotify.docker.client.CompressedDirectory.delete;
import static com.sun.jersey.api.client.config.ClientConfig.PROPERTY_READ_TIMEOUT;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

public class DefaultDockerClient implements DockerClient {

  private static final long CONNECT_TIMEOUT_MILLIS = SECONDS.toMillis(5);
  private static final long READ_TIMEOUT_MILLIS = SECONDS.toMillis(30);

  private static final Logger log = LoggerFactory.getLogger(DefaultDockerClient.class);

  private static final String VERSION = "v1.12";
  private static final DefaultClientConfig CLIENT_CONFIG = new DefaultClientConfig(
      ObjectMapperProvider.class,
      LogsResponseReader.class,
      ProgressResponseReader.class);

  private static final Pattern CONTAINER_NAME_PATTERN = Pattern.compile("/?[a-zA-Z0-9_-]+");

  private static final GenericType> CONTAINER_LIST =
      new GenericType>() {};

  private static final GenericType> REMOVED_IMAGE_LIST =
      new GenericType>() {};

  private final Client client;
  private final URI uri;

  public DefaultDockerClient(final String uri) {
    this(URI.create(uri));
  }

  public DefaultDockerClient(final URI uri) {
    this.uri = uri;
    this.client = new Client(new InterruptibleURLConnectionClientHandler(), CLIENT_CONFIG);
    this.client.setConnectTimeout((int) CONNECT_TIMEOUT_MILLIS);
    this.client.setReadTimeout((int) READ_TIMEOUT_MILLIS);
  }

  @Override
  public Version version() throws DockerException, InterruptedException {
    final WebResource resource = resource().path("version");
    return request(GET, Version.class, resource, resource.accept(APPLICATION_JSON_TYPE));
  }

  @Override
  public List listContainers(final ListContainersParam... params)
      throws DockerException, InterruptedException {
    final Multimap paramMap = ArrayListMultimap.create();
    for (ListContainersParam param : params) {
      paramMap.put(param.name(), param.value());
    }
    final WebResource resource = resource()
        .path("containers").path("json")
        .queryParams(multivaluedMap(paramMap));
    return request(GET, CONTAINER_LIST, resource, resource.accept(APPLICATION_JSON_TYPE));
  }

  @Override
  public ContainerCreation createContainer(final ContainerConfig config)
      throws DockerException, InterruptedException {
    return createContainer(config, null);
  }

  public static interface ExceptionPropagator {

    void propagate(UniformInterfaceException e) throws DockerException;
  }

  @Override
  public ContainerCreation createContainer(final ContainerConfig config,
                                           final String name)
      throws DockerException, InterruptedException {

    final MultivaluedMap params = new MultivaluedMapImpl();
    if (name != null) {
      checkArgument(CONTAINER_NAME_PATTERN.matcher(name).matches(),
                    "Invalid container name: \"%s\"", name);
      params.add("name", name);
    }

    try {
      final WebResource resource = resource()
          .path("containers").path("create")
          .queryParams(params);
      return request(POST, ContainerCreation.class, resource, resource
          .entity(config)
          .type(APPLICATION_JSON_TYPE)
          .accept(APPLICATION_JSON_TYPE));
    } catch (DockerRequestException e) {
      switch (e.status()) {
        case 404:
          throw new ImageNotFoundException(config.image(), e);
        default:
          throw e;
      }
    }
  }

  @Override
  public void startContainer(final String containerId)
      throws DockerException, InterruptedException {
    startContainer(containerId, HostConfig.builder().build());
  }

  @Override
  public void startContainer(final String containerId, final HostConfig hostConfig)
      throws DockerException, InterruptedException {
    checkNotNull(containerId, "containerId");
    checkNotNull(hostConfig, "hostConfig");
    try {
      final WebResource resource = resource()
          .path("containers").path(containerId).path("start");
      request(POST, resource, resource
          .type(APPLICATION_JSON_TYPE)
          .accept(APPLICATION_JSON_TYPE)
          .entity(hostConfig));
    } catch (DockerRequestException e) {
      switch (e.status()) {
        case 404:
          throw new ContainerNotFoundException(containerId, e);
        default:
          throw e;
      }
    }
  }

  @Override
  public void killContainer(final String containerId) throws DockerException, InterruptedException {
    try {
      final WebResource resource = resource().path("containers").path(containerId).path("kill");
      request(POST, resource, resource);
    } catch (UniformInterfaceException e) {
      switch (e.getResponse().getStatus()) {
        case 404:
          throw new ContainerNotFoundException(containerId, e);
        default:
          throw new DockerException(e);
      }
    }
  }

  @Override
  public ContainerExit waitContainer(final String containerId)
      throws DockerException, InterruptedException {
    try {
      final WebResource resource = resource()
          .path("containers").path(containerId).path("wait");
      // Wait forever
      resource.setProperty(PROPERTY_READ_TIMEOUT, 0);
      return request(POST, ContainerExit.class, resource, resource.accept(APPLICATION_JSON_TYPE));
    } catch (DockerRequestException e) {
      switch (e.status()) {
        case 404:
          throw new ContainerNotFoundException(containerId, e);
        default:
          throw e;
      }
    }
  }

  @Override
  public void removeContainer(final String containerId)
      throws DockerException, InterruptedException {
    removeContainer(containerId, false);
  }

  @Override
  public void removeContainer(final String containerId, final boolean removeVolumes)
      throws DockerException, InterruptedException {
    try {
      final WebResource resource = resource()
          .path("containers").path(containerId);
      request(DELETE, resource, resource
          .queryParam("v", String.valueOf(removeVolumes))
          .accept(APPLICATION_JSON_TYPE));
    } catch (UniformInterfaceException e) {
      switch (e.getResponse().getStatus()) {
        case 404:
          throw new ContainerNotFoundException(containerId);
        default:
          throw new DockerException(e);
      }
    }
  }

  @Override
  public ContainerInfo inspectContainer(final String containerId)
      throws DockerException, InterruptedException {
    try {
      final WebResource resource = resource().path("containers").path(containerId).path("json");
      return request(GET, ContainerInfo.class, resource, resource.accept(APPLICATION_JSON_TYPE));
    } catch (DockerRequestException e) {
      switch (e.status()) {
        case 404:
          throw new ContainerNotFoundException(containerId, e);
        default:
          throw e;
      }
    }
  }

  @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 {
    final ImageRef imageRef = new ImageRef(image);

    final MultivaluedMap params = new MultivaluedMapImpl();
    params.add("fromImage", imageRef.getImage());
    if (imageRef.getTag() != null) {
      params.add("tag", imageRef.getTag());
    }

    final WebResource resource = resource().path("images").path("create").queryParams(params);

    try (ProgressStream pull = request(POST, ProgressStream.class, resource,
                                       resource.accept(APPLICATION_JSON_TYPE))) {
      pull.tail(handler);
    }
  }

  @Override
  public void push(final String image) throws DockerException, InterruptedException {
    push(image, new LoggingPushHandler(image));
  }

  @Override
  public void push(final String image, final ProgressHandler handler)
      throws DockerException, InterruptedException {
    final ImageRef imageRef = new ImageRef(image);

    final MultivaluedMap params = new MultivaluedMapImpl();
    if (imageRef.getTag() != null) {
      params.add("tag", imageRef.getTag());
    }

    final WebResource resource =
        resource().path("images").path(imageRef.getImage()).path("push").queryParams(params);

    // the docker daemon requires that the X-Registry-Auth header is specified
    // with a non-empty string even if your registry doesn't use authentication
    try (ProgressStream push =
             request(POST, ProgressStream.class, resource,
                     resource.accept(APPLICATION_JSON_TYPE).header("X-Registry-Auth", "null"))) {
      push.tail(handler);
    }
  }

  @Override
  public void tag(final String image, final String name)
      throws DockerException, InterruptedException {
    final ImageRef imageRef = new ImageRef(name);

    final MultivaluedMap params = new MultivaluedMapImpl();
    params.add("repo", imageRef.getImage());
    if (imageRef.getTag() != null) {
      params.add("tag", imageRef.getTag());
    }

    final WebResource resource =
        resource().path("images").path(image).path("tag").queryParams(params);

    try {
      request(POST, resource, resource);
    } catch (DockerRequestException e) {
      switch (e.status()) {
        case 404:
          throw new ImageNotFoundException(image, e);
        default:
          throw e;
      }
    }
  }

  @Override
  public String build(final Path directory, final BuildParameter... params)
      throws DockerException, InterruptedException, IOException {
    return build(directory, null, new LoggingBuildHandler(), params);
  }

  @Override
  public String build(final Path directory, final String name, final BuildParameter... params)
      throws DockerException, InterruptedException, IOException {
    return build(directory, name, new LoggingBuildHandler(), params);
  }

  @Override
  public String build(final Path directory, final ProgressHandler handler,
                      final BuildParameter... 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 BuildParameter... params)
      throws DockerException, InterruptedException, IOException {
    checkNotNull(handler, "handler");

    final Multimap paramMap = ArrayListMultimap.create();
    for (final BuildParameter param : params) {
      paramMap.put(param.queryParam, String.valueOf(param.value));
    }
    if (name != null) {
      paramMap.put("t", name);
    }

    final WebResource resource = resource().path("build").queryParams(multivaluedMap(paramMap));
    final File compressedDirectory = CompressedDirectory.create(directory);

    try (ProgressStream build = request(POST, ProgressStream.class, resource,
                                        resource.accept(APPLICATION_JSON_TYPE)
                                            .entity(compressedDirectory, "application/tar"))) {
      String imageId = null;
      while (build.hasNextMessage()) {
        final ProgressMessage message = build.nextMessage();
        final String id = message.buildImageId();
        if (id != null) {
          imageId = id;
        }
        handler.progress(message);
      }
      return imageId;
    } finally {
      delete(compressedDirectory);
    }
  }

  @Override
  public ImageInfo inspectImage(final String image) throws DockerException, InterruptedException {
    try {
      final WebResource resource = resource().path("images").path(image).path("json");
      return request(GET, ImageInfo.class, resource, resource.accept(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 WebResource resource = resource().path("images").path(image)
          .queryParam("force", String.valueOf(force))
          .queryParam("noprune", String.valueOf(noPrune));
      return request(DELETE, REMOVED_IMAGE_LIST, resource, resource.accept(APPLICATION_JSON_TYPE));
    } catch (UniformInterfaceException e) {
      switch (e.getResponse().getStatus()) {
        case 404:
          throw new ImageNotFoundException(image);
        default:
          throw new DockerException(e);
      }
    }
  }

  @Override
  public LogStream logs(final String containerId, final LogsParameter... params)
      throws DockerException, InterruptedException {
    final Multimap paramMap = ArrayListMultimap.create();
    for (final LogsParameter param : params) {
      paramMap.put(param.name().toLowerCase(Locale.ROOT), String.valueOf(true));
    }
    final WebResource resource = resource()
        .path("containers").path(containerId).path("logs")
        .queryParams(multivaluedMap(paramMap));
    try {
      return request(GET, LogStream.class, resource,
                     resource.accept("application/vnd.docker.raw-stream"));
    } catch (DockerRequestException e) {
      switch (e.status()) {
        case 404:
          throw new ContainerNotFoundException(containerId);
        default:
          throw e;
      }
    }
  }

  private WebResource resource() {
    return client.resource(uri).path(VERSION);
  }

  private  T request(final String method, final GenericType type,
                        final WebResource resource, final WebResource.Builder request)
      throws DockerException, InterruptedException {
    try {
      return request.method(method, type);
    } catch (ClientHandlerException e) {
      throw propagate(method, resource, e);
    } catch (UniformInterfaceException e) {
      throw propagate(method, resource, e);
    }
  }

  private  T request(final String method, final Class clazz,
                        final WebResource resource, final UniformInterface request)
      throws DockerException, InterruptedException {
    try {
      return request.method(method, clazz);
    } catch (ClientHandlerException e) {
      throw propagate(method, resource, e);
    } catch (UniformInterfaceException e) {
      throw propagate(method, resource, e);
    }
  }

  private void request(final String method,
                       final WebResource resource,
                       final UniformInterface request) throws DockerException,
                                                              InterruptedException {
    try {
      request.method(method);
    } catch (ClientHandlerException e) {
      throw propagate(method, resource, e);
    } catch (UniformInterfaceException e) {
      throw propagate(method, resource, e);
    }
  }

  private DockerRequestException propagate(final String method, final WebResource resource,
                                           final UniformInterfaceException e) {
    return new DockerRequestException(method, resource.getURI(),
                                      e.getResponse().getStatus(), message(e.getResponse()),
                                      e);
  }

  private RuntimeException propagate(final String method, final WebResource resource,
                                     final ClientHandlerException e)
      throws DockerException, InterruptedException {
    final Throwable cause = e.getCause();
    if (cause instanceof SocketTimeoutException) {
      throw new DockerTimeoutException(method, resource.getURI(), e);
    } else if (cause instanceof InterruptedIOException) {
      throw new InterruptedException("Interrupted: " + method + " " + resource);
    } else {
      throw new DockerException(e);
    }
  }

  private String message(final ClientResponse response) {
    final Readable reader = new InputStreamReader(response.getEntityInputStream(), UTF_8);
    try {
      return CharStreams.toString(reader);
    } catch (IOException ignore) {
      return null;
    }
  }

  private MultivaluedMap multivaluedMap(final Multimap map) {
    final MultivaluedMap multivaluedMap = new MultivaluedMapImpl();
    for (Map.Entry e : map.entries()) {
      final String value = e.getValue();
      if (value != null) {
        multivaluedMap.add(e.getKey(), value);
      }
    }
    return multivaluedMap;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy