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

de.gesellix.docker.engine.OkDockerClient Maven / Gradle / Ivy

There is a newer version: 2024-11-28T22-05-00
Show newest version
package de.gesellix.docker.engine;

import com.squareup.moshi.Moshi;
import de.gesellix.docker.client.filesocket.FileSocketFactory;
import de.gesellix.docker.client.filesocket.HostnameEncoder;
import de.gesellix.docker.client.filesocket.NamedPipeSocketFactory;
import de.gesellix.docker.client.filesocket.UnixSocketFactory;
import de.gesellix.docker.client.filesocket.UnixSocketFactorySupport;
import de.gesellix.docker.hijack.HijackingInterceptor;
import de.gesellix.docker.hijack.OkResponseCallback;
import de.gesellix.docker.json.CustomObjectAdapterFactory;
import de.gesellix.docker.rawstream.RawInputStream;
import de.gesellix.docker.response.JsonContentHandler;
import de.gesellix.docker.ssl.DockerSslSocket;
import de.gesellix.docker.ssl.SslSocketConfigFactory;
import de.gesellix.util.IOUtils;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okhttp3.internal.http.HttpMethod;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Proxy;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static de.gesellix.docker.client.filesocket.FileSocket.SOCKET_MARKER;
import static de.gesellix.docker.engine.RequestMethod.DELETE;
import static de.gesellix.docker.engine.RequestMethod.GET;
import static de.gesellix.docker.engine.RequestMethod.HEAD;
import static de.gesellix.docker.engine.RequestMethod.POST;
import static de.gesellix.docker.engine.RequestMethod.PUT;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * Will be replaced with the implementation from github.com/docker-client/docker-remote-api-client.
 *
 * @deprecated
 */
@Deprecated
public class OkDockerClient implements EngineClient {

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

  private final Map socketFactories = new LinkedHashMap<>();

  private final DockerClientConfig dockerClientConfig;
  private Proxy proxy;

  private final Moshi moshi;

  public OkDockerClient() {
    this(new DockerClientConfig());
  }

  public OkDockerClient(String dockerHost) {
    this(new DockerClientConfig(dockerHost));
  }

  public OkDockerClient(DockerClientConfig dockerClientConfig) {
    this(dockerClientConfig, Proxy.NO_PROXY);
  }

  public OkDockerClient(DockerClientConfig dockerClientConfig, Proxy proxy) {
    if (new UnixSocketFactorySupport().isSupported()) {
      socketFactories.put("unix", new UnixSocketFactory());
    }
    socketFactories.put("npipe", new NamedPipeSocketFactory());
    socketFactories.put("https", new SslSocketConfigFactory());

    this.dockerClientConfig = dockerClientConfig;
    this.proxy = proxy;

    this.moshi = new Moshi.Builder()
        .add(new CustomObjectAdapterFactory())
        .build();
  }

  @Override
  public EngineResponse head(Map requestConfig) {
    EngineRequest engineRequest = ensureValidRequestConfig(requestConfig, HEAD);
    return request(engineRequest);
  }

  @Override
  public EngineResponse get(Map requestConfig) {
    EngineRequest engineRequest = ensureValidRequestConfig(requestConfig, GET);
    return request(engineRequest);
  }

  @Override
  public EngineResponse put(Map requestConfig) {
    EngineRequest engineRequest = ensureValidRequestConfig(requestConfig, PUT);
    return request(engineRequest);
  }

  @Override
  public EngineResponse post(Map requestConfig) {
    EngineRequest engineRequest = ensureValidRequestConfig(requestConfig, POST);
    return request(engineRequest);
  }

  @Override
  public EngineResponse delete(Map requestConfig) {
    EngineRequest engineRequest = ensureValidRequestConfig(requestConfig, DELETE);
    return request(engineRequest);
  }

  @Override
  public WebSocket webSocket(Map requestConfig, WebSocketListener listener) {
    EngineRequest engineRequest = ensureValidRequestConfig(requestConfig, GET);
    Request.Builder requestBuilder = prepareRequest(new Request.Builder(), engineRequest);
    Request request = requestBuilder.build();

    OkHttpClient.Builder clientBuilder = prepareClient(new OkHttpClient.Builder(), engineRequest.getTimeout());
    OkHttpClient client = newClient(clientBuilder);

    return client.newWebSocket(request, listener);
  }

  @Override
  public EngineResponse request(EngineRequest requestConfig) {
    EngineRequest config = ensureValidRequestConfig(requestConfig);

    AttachConfig attachConfig = null;
    if (config.getAttach() != null) {
      Map headers = config.getHeaders();
      if (headers == null) {
        headers = new HashMap<>();
      }
      config.setHeaders(headers);
      // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
      // To hint potential proxies about connection hijacking, the Docker client sends connection upgrade headers.
      headers.put("Upgrade", "tcp");
      headers.put("Connection", "Upgrade");
      attachConfig = config.getAttach();
    }
//        boolean multiplexStreams = config.multiplexStreams

    Request.Builder requestBuilder = prepareRequest(new Request.Builder(), config);
    final Request request = requestBuilder.build();

    OkHttpClient.Builder clientBuilder = prepareClient(new OkHttpClient.Builder(), config.getTimeout());
    OkResponseCallback responseCallback = null;
    if (attachConfig != null) {
      clientBuilder.addNetworkInterceptor(new HijackingInterceptor(
          attachConfig,
          attachConfig.getStreams().getStdin() == null ? null : Okio.source(attachConfig.getStreams().getStdin()),
          attachConfig.getStreams().getStdout() == null ? null : Okio.sink(attachConfig.getStreams().getStdout())));
      responseCallback = new OkResponseCallback(attachConfig);
    }
    final OkHttpClient client = newClient(clientBuilder);

    log.debug(request.method() + " " + request.url() + " using proxy: " + client.proxy());

    Call call = client.newCall(request);
    if (responseCallback != null) {
      call.enqueue(responseCallback);
      log.debug("request enqueued");
      return new EngineResponse();
    }
    else {
      EngineResponse dockerResponse;
      try {
        Response response = call.execute();
        log.debug("response: " + response);
        dockerResponse = handleResponse(response, config);
        if (dockerResponse.getStream() == null) {
//          log.warn("closing response...");
          response.close();
        }
      }
      catch (Exception e) {
        log.error("Request failed", e);
        throw new RuntimeException("Request failed", e);
      }
      return dockerResponse;
    }
  }

  private Request.Builder prepareRequest(final Request.Builder builder, final EngineRequest config) {
    String method = config.getMethod().name();
    String contentType = config.getContentType();
    Map additionalHeaders = config.getHeaders();
    Object body = config.getBody();

    String protocol = dockerClientConfig.getScheme();
    String host = dockerClientConfig.getHost();
    int port = dockerClientConfig.getPort();

    String path = config.getPath();
    if (config.getApiVersion() != null) {
      path = config.getApiVersion() + "/" + path;
    }
    String queryAsString;
    if (config.getQuery() != null) {
      queryAsString = queryToString(config.getQuery());
    }
    else {
      queryAsString = "";
    }

    HttpUrl.Builder urlBuilder = new HttpUrl.Builder().addPathSegments(path);
    if (queryAsString != null && !queryAsString.isEmpty()) {
      urlBuilder = urlBuilder.encodedQuery(queryAsString);
//    } else {
//      urlBuilder = urlBuilder.encodedQuery(null);
    }
    HttpUrl httpUrl = createUrl(urlBuilder, protocol, host, port);

    RequestBody requestBody = createRequestBody(method, contentType, body);
    builder.method(method, requestBody).url(httpUrl).cacheControl(CacheControl.FORCE_NETWORK);
    if (additionalHeaders != null) {
      additionalHeaders.forEach(builder::header);
    }
    return builder;
  }

  private OkHttpClient.Builder prepareClient(OkHttpClient.Builder builder, int currentTimeout) {
    String protocol = dockerClientConfig.getScheme();
    switch (protocol) {
      case "unix":
        if (!socketFactories.containsKey(protocol)) {
          log.error("Unix domain socket not supported, but configured (using defaults?). Please consider changing the DOCKER_HOST environment setting to use tcp.");
          throw new IllegalStateException("Unix domain socket not supported.");
        }
        FileSocketFactory unixSocketFactory = (FileSocketFactory) socketFactories.get(protocol);
        builder
            .socketFactory(unixSocketFactory)
            .dns(unixSocketFactory)
            .build();
        break;
      case "npipe":
        FileSocketFactory npipeSocketFactory = (FileSocketFactory) socketFactories.get(protocol);
        builder
            .socketFactory(npipeSocketFactory)
            .dns(npipeSocketFactory)
            .build();
        break;
      case "https":
        String certPath = dockerClientConfig.getCertPath();
        SslSocketConfigFactory sslSocketFactory = (SslSocketConfigFactory) socketFactories.get(protocol);
        DockerSslSocket dockerSslSocket = sslSocketFactory.createDockerSslSocket(certPath);
        if (dockerSslSocket != null) {
          builder
              .sslSocketFactory(dockerSslSocket.getSslSocketFactory(), dockerSslSocket.getTrustManager())
              .build();
        }
        break;
    }
    builder.proxy(proxy);

    // do we need to disable the timeout for streaming?
    builder
        .connectTimeout(currentTimeout, MILLISECONDS)
        .readTimeout(currentTimeout, MILLISECONDS);
    return builder;
  }

  public OkHttpClient newClient(OkHttpClient.Builder clientBuilder) {
    return clientBuilder.build();
  }

  private HttpUrl createUrl(HttpUrl.Builder urlBuilder, String protocol, String host, int port) {
    HttpUrl httpUrl;
    switch (protocol) {
      case "unix":
      case "npipe":
        httpUrl = urlBuilder
            .scheme("http")
            .host(new HostnameEncoder().encode(host) + SOCKET_MARKER)
            .build();
        break;
      default:
        httpUrl = urlBuilder
            .scheme(protocol)
            .host(host)
            .port(port)
            .build();
        break;
    }
    return httpUrl;
  }

  private RequestBody createRequestBody(String method, String contentType, Object body) {
    if (body == null && HttpMethod.requiresRequestBody(method)) {
      return RequestBody.create("", MediaType.parse("application/json"));
    }

    RequestBody requestBody = null;
    if (body != null) {
      switch (contentType) {
        case "application/json":
          requestBody = RequestBody.create(moshi.adapter(Map.class).toJson((Map) body), MediaType.parse(contentType));
          break;
        case "application/octet-stream":
        default:
          Source source = Okio.source((InputStream) body);
          BufferedSource buffer = Okio.buffer(source);
          requestBody = new StreamingRequestBody(MediaType.parse(contentType), buffer);
          break;
      }
    }
    return requestBody;
  }

  public EngineResponse handleResponse(Response httpResponse, EngineRequest config) throws IOException {
    final EngineResponse response = readHeaders(httpResponse);

    if (response.getStatus().getCode() == 204) {
      if (response.getStream() != null) {
        // redirect the response body to /dev/null, since it's expected to be empty
        IOUtils.consumeToDevNull(response.getStream());
      }
      return response;
    }
    ResponseBody body = httpResponse.body();

    String mimeType = response.getMimeType();
    if (mimeType == null) {
      mimeType = "";
    }
    switch (mimeType) {
      case "application/vnd.docker.multiplexed-stream":
      case "application/vnd.docker.raw-stream":
        InputStream rawStream = new RawInputStream(body.byteStream());
        if (config.getStdout() != null) {
          log.debug("redirecting to stdout.");
          IOUtils.copy(rawStream, config.getStdout());
          response.setStream(null);
        }
        else {
          response.setStream(rawStream);
        }
        break;
      case "application/json":
        if (config.isAsync()) {
          consumeResponseBody(response, body.source(), config);
        }
        else {
          Object content = new JsonContentHandler().getContent(body.source());
          consumeResponseBody(response, content, config);
        }
        break;
      case "text/html":
      case "text/plain":
        InputStream text = body.byteStream();
        consumeResponseBody(response, text, config);
        break;
      case "application/octet-stream":
        InputStream octet = body.byteStream();
        if (config.getStdout() != null) {
          log.debug("redirecting to stdout.");
          IOUtils.copy(octet, config.getStdout());
          response.setStream(null);
        }
        else {
          log.debug("passing through via `response.stream`.");
          response.setStream(octet);
        }
        break;
      case "application/x-tar":
        if (response.getStream() != null) {
          if (config.getStdout() != null) {
            log.debug("redirecting to stdout.");
            IOUtils.copy(response.getStream(), config.getStdout());
            response.setStream(null);
          }
          else {
            log.info(response.getMimeType() + " stream won't be consumed, but is available in the response.");
          }
        }
        break;
      default:
        if (body == null || body.contentLength() == 0) {
          response.setContent(body == null ? null : body.string());
          response.setStream(null);
          return response;
        }
        log.debug("unexpected mime type '" + response.getMimeType() + "'.");
        if (body.contentLength() == -1) {
          InputStream stream = body.byteStream();
          if (config.getStdout() != null) {
            log.debug("redirecting to stdout.");
            IOUtils.copy(stream, config.getStdout());
            response.setStream(null);
          }
          else {
            log.debug("passing through via `response.stream`.");
            response.setStream(stream);
          }
        }
        else {
          log.debug("passing through via `response.content`.");
          response.setContent(body.string());
          response.setStream(null);
        }
        break;
    }

    return response;
  }

  private EngineResponse readHeaders(Response httpResponse) {
    final EngineResponse dockerResponse = new EngineResponse();

    EngineResponseStatus status = new EngineResponseStatus();
    status.setText(httpResponse.message());
    status.setCode(httpResponse.code());
    status.setSuccess(httpResponse.isSuccessful());
    dockerResponse.setStatus(status);
    log.trace("status: " + dockerResponse.getStatus());

    final Headers headers = httpResponse.headers();
    log.trace("headers: \n" + headers);
    dockerResponse.setHeaders(headers);

    String contentType = headers.get("content-type");
    dockerResponse.setContentType(contentType);

    String contentLength = headers.get("content-length");
    if (contentLength == null) {
      contentLength = "-1";
    }
    dockerResponse.setContentLength(contentLength);

    String mimeType = getMimeType(contentType);
    dockerResponse.setMimeType(mimeType);

    if (dockerResponse.getStatus().getSuccess()) {
      dockerResponse.setStream(httpResponse.body().byteStream());
    }
    else {
      dockerResponse.setStream(null);
    }
    return dockerResponse;
  }

  private void consumeResponseBody(EngineResponse response, Object content, EngineRequest config) throws IOException {
    if (content instanceof Source) {
      if (config.isAsync()) {
        response.setStream(Okio.buffer((Source) content).inputStream());
      }
      else if (config.getStdout() != null) {
        response.setStream(null);
        Okio.buffer(Okio.sink(config.getStdout())).writeAll((Source) content);
      }
      else if (response.getContentLength() != null && Integer.parseInt(response.getContentLength()) >= 0) {
        response.setStream(null);
        response.setContent(Okio.buffer((Source) content).readUtf8());
      }
      else {
        response.setStream(Okio.buffer((Source) content).inputStream());
      }
    }
    else if (content instanceof InputStream) {
      if (config.isAsync()) {
        response.setStream((InputStream) content);
      }
      else if (config.getStdout() != null) {
        IOUtils.copy((InputStream) content, config.getStdout());
        response.setStream(null);
      }
      else if (response.getContentLength() != null && Integer.parseInt(response.getContentLength()) >= 0) {
        response.setContent(IOUtils.toString((InputStream) content));
        response.setStream(null);
      }
      else {
        response.setStream((InputStream) content);
      }
    }
    else {
      response.setContent(content);
      response.setStream(null);
    }
  }

  /**
   * @see #ensureValidRequestConfig(EngineRequest)
   * @deprecated use ensureValidRequestConfig(EngineRequest)
   */
  @Deprecated
  private EngineRequest ensureValidRequestConfig(final Map config, RequestMethod method) {
    if (config == null || config.get("path") == null) {
      log.error("bad request config: " + config);
      throw new IllegalArgumentException("bad request config");
    }
    if (((String) config.get("path")).startsWith("/")) {
      config.put("path", ((String) config.get("path")).substring("/".length()));
    }
    config.put("method", method.name());

    EngineRequest engineRequest = new EngineRequest(method, (String) config.get("path"));
    engineRequest.setTimeout(config.get("timeout") == null ? 0 : (Integer) config.get("timeout"));
    engineRequest.setHeaders((Map) config.get("headers"));
    Map query = (Map) config.get("query");
    engineRequest.setQuery(coerceValuesToListOfString(query));

    engineRequest.setContentType((String) config.get("requestContentType"));
    engineRequest.setBody(config.get("body"));

    engineRequest.setAsync(config.get("async") != null && (Boolean) config.get("async"));
    engineRequest.setAttach((AttachConfig) config.get("attach"));
    engineRequest.setStdout((OutputStream) config.get("stdout"));

    engineRequest.setApiVersion((String) config.get("apiVersion"));
    return engineRequest;
  }

  private EngineRequest ensureValidRequestConfig(final EngineRequest config) {
    if (config == null || config.getPath() == null) {
      log.error("bad request config: " + config);
      throw new IllegalArgumentException("bad request config");
    }
    if ((config.getPath()).startsWith("/")) {
      config.setPath(config.getPath().substring("/".length()));
    }
    return config;
  }

  private Map> coerceValuesToListOfString(Map queryParameters) {
    if (queryParameters == null || queryParameters.isEmpty()) {
      return new HashMap<>();
    }
    return queryParameters.entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getKey, ((Map.Entry e) -> convert(e.getValue()))));
  }

  private List convert(Object value) {
    if (value instanceof String[]) {
      return Arrays.stream((String[]) value).collect(Collectors.toList());
    }
    else if (value instanceof Collection) {
      return ((Collection) value).stream()
          .map(Object::toString)
          .collect(Collectors.toList());
    }
    else if (value != null) {
      return Collections.singletonList(value.toString());
    }
    else {
      return Collections.singletonList("");
    }
  }

  public String queryToString(Map> queryParameters) {
    if (queryParameters == null || queryParameters.isEmpty()) {
      return "";
    }
    return queryParameters.entrySet().stream()
        .map((Map.Entry> e) -> {
          String key = e.getKey();
          List value = e.getValue();
          if (value != null) {
            return value.stream()
                .map((s) -> asUrlEncodedQuery(key, s))
                .collect(Collectors.joining("&"));
          }
          else {
            return asUrlEncodedQuery(key, "");
          }
        })
        .collect(Collectors.joining("&"));
  }

  private String asUrlEncodedQuery(String key, String value) {
    try {
      return URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8");
    }
    catch (UnsupportedEncodingException e) {
      log.error("Url encoding failed for key=" + key + ",value=" + value, e);
      throw new RuntimeException("Url encoding failed", e);
    }
  }

  public String getMimeType(String contentTypeHeader) {
    if (contentTypeHeader == null) {
      return null;
    }
    return contentTypeHeader.replace(" ", "").split(";")[0];
  }

  public String getCharset(String contentTypeHeader) {
    String charset = "utf-8";
    Matcher matcher = Pattern.compile("[^;]+;\\s*charset=([^;]+)(;[^;]*)*").matcher(contentTypeHeader);
    if (matcher.find()) {
      charset = matcher.group(1);
    }
    return charset;
  }

  Map getSocketFactories() {
    return socketFactories;
  }

  DockerClientConfig getDockerClientConfig() {
    return dockerClientConfig;
  }

  void setProxy(Proxy proxy) {
    this.proxy = proxy;
  }
}