package org.folio.okapi.service.impl; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.http.HttpMethod; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.core.net.SocketAddress; import java.util.Iterator; import java.util.Map; import org.apache.logging.log4j.Logger; import org.folio.okapi.bean.AnyDescriptor; import org.folio.okapi.bean.EnvEntry; import org.folio.okapi.bean.LaunchDescriptor; import org.folio.okapi.bean.Ports; import org.folio.okapi.common.Config; import org.folio.okapi.common.HttpClientLegacy; import org.folio.okapi.common.Messages; import org.folio.okapi.common.OkapiLogger; import org.folio.okapi.service.ModuleHandle; import org.folio.okapi.util.TcpPortWaiting; // Docker Module. Using the Docker HTTP API. // We don't do local unix sockets. The dockerd must unfortunately be listening on localhost. // https://docs.docker.com/engine/reference/commandline/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket @java.lang.SuppressWarnings({"squid:S1192"}) public class DockerModuleHandle implements ModuleHandle { private final Logger logger = OkapiLogger.get(); private final int hostPort; private final Ports ports; private final String image; private final String[] cmd; private final String dockerUrl; private final String containerHost; private final EnvEntry[] env; private final AnyDescriptor dockerArgs; private final boolean dockerPull; private final HttpClient client; private final StringBuilder logBuffer; private int logSkip; private final String id; private final Messages messages = Messages.getInstance(); private final TcpPortWaiting tcpPortWaiting; private String containerId; private final SocketAddress socketAddress; static final String DEFAULT_DOCKER_URL = "unix:///var/run/docker.sock"; static final String DEFAULT_DOCKER_VERSION = "v1.25"; DockerModuleHandle(Vertx vertx, LaunchDescriptor desc, String id, Ports ports, String containerHost, int port, JsonObject config) { this.hostPort = port; this.ports = ports; this.id = id; this.containerHost = containerHost; this.image = desc.getDockerImage(); this.cmd = desc.getDockerCmd(); this.env = desc.getEnv(); this.dockerArgs = desc.getDockerArgs(); this.client = vertx.createHttpClient(); this.logBuffer = new StringBuilder(); this.logSkip = 0; logger.info("Docker handler with native: {}", vertx.isNativeTransportEnabled()); Boolean b = desc.getDockerPull(); this.dockerPull = b == null || b.booleanValue(); StringBuilder socketFile = new StringBuilder(); this.dockerUrl = setupDockerAddress(socketFile, Config.getSysConf("dockerUrl", DEFAULT_DOCKER_URL, config)); if (socketFile.length() > 0) { socketAddress = SocketAddress.domainSocketAddress(socketFile.toString()); } else { socketAddress = null; } tcpPortWaiting = new TcpPortWaiting(vertx, containerHost, port); if (desc.getWaitIterations() != null) { tcpPortWaiting.setMaxIterations(desc.getWaitIterations()); } } static String setupDockerAddress(StringBuilder socketAddress, String u) { if (u.startsWith("tcp://")) { u = "http://" + u.substring(6); } else if (u.startsWith("unix://")) { socketAddress.append(u.substring(7)); u = "http://localhost"; } while (u.endsWith("/")) { u = u.substring(0, u.length() - 1); } return u + "/" + DEFAULT_DOCKER_VERSION; } private void handle204(HttpClientResponse res, String msg, Handler<AsyncResult<Void>> future) { Buffer body = Buffer.buffer(); res.handler(body::appendBuffer); res.endHandler(d -> { if (res.statusCode() == 204) { future.handle(Future.succeededFuture()); } else { String m = msg + " HTTP error " + res.statusCode() + "\n" + body.toString(); logger.error(m); future.handle(Future.failedFuture(m)); } }); } private HttpClientRequest request(HttpMethod method, String url, Handler<HttpClientResponse> response) { if (socketAddress != null) { return HttpClientLegacy.requestAbs(client, method, socketAddress, dockerUrl + url, response); } else { return HttpClientLegacy.requestAbs(client, method, dockerUrl + url, response); } } void postUrl(String url, String msg, Handler<AsyncResult<Void>> future) { HttpClientRequest req = request(HttpMethod.POST, url, res -> handle204(res, msg, future)); req.exceptionHandler(d -> future.handle(Future.failedFuture(d.getCause()))); req.end(); } void deleteUrl(String url, String msg, Handler<AsyncResult<Void>> future) { HttpClientRequest req = request(HttpMethod.DELETE, url, res -> handle204(res, msg, future)); req.exceptionHandler(d -> future.handle(Future.failedFuture(d.getCause()))); req.end(); } private void startContainer(Handler<AsyncResult<Void>> future) { logger.info("start container {} for image {}", containerId, image); postUrl("/containers/" + containerId + "/start", "startContainer", future); } private void stopContainer(Handler<AsyncResult<Void>> future) { logger.info("stop container {} image {}", containerId, image); postUrl("/containers/" + containerId + "/stop", "stopContainer", future); } private void deleteContainer(Handler<AsyncResult<Void>> future) { logger.info("delete container {} image {}", containerId, image); deleteUrl("/containers/" + containerId, "deleteContainer", future); } private void logHandler(Buffer b) { if (logSkip == 0 && b.getByte(0) < 3) { logSkip = 8; } if (b.length() > logSkip) { logBuffer.append(b.getString(logSkip, b.length())); logSkip = 0; } else { logSkip = logSkip - b.length(); } if (logBuffer.length() > 0 && logBuffer.charAt(logBuffer.length() - 1) == '\n') { logger.info("{} {}", () -> id, () -> logBuffer.substring(0, logBuffer.length() - 1)); logBuffer.setLength(0); } } private void getContainerLog(Handler<AsyncResult<Void>> future) { final String url = "/containers/" + containerId + "/logs?stderr=1&stdout=1&follow=1"; HttpClientRequest req = request(HttpMethod.GET, url, res -> { if (res.statusCode() == 200) { // stream OK. Continue other work but keep fetching! // remove 8 bytes of binary data and final newline res.handler(this::logHandler); tcpPortWaiting.waitReady(null, future); } else { String m = "getContainerLog HTTP error " + res.statusCode(); logger.error(m); future.handle(Future.failedFuture(m)); } }); req.exceptionHandler(d -> future.handle(Future.failedFuture(d.getCause()))); req.end(); } void getUrl(String url, Handler<AsyncResult<JsonObject>> future) { HttpClientRequest req = request(HttpMethod.GET, url, res -> { Buffer body = Buffer.buffer(); res.exceptionHandler(d -> { logger.warn("{}: {}", url, d.getMessage()); future.handle(Future.failedFuture(url + ": " + d.getMessage())); }); res.handler(body::appendBuffer); res.endHandler(d -> { if (res.statusCode() == 200) { JsonObject b = body.toJsonObject(); logger.info(b.encodePrettily()); future.handle(Future.succeededFuture(b)); } else { String m = url + " HTTP error " + res.statusCode() + "\n" + body.toString(); logger.error(m); future.handle(Future.failedFuture(m)); } }); }); req.exceptionHandler(e -> { Throwable cause = e.getCause() == null ? e : e.getCause(); String msg = url + ": " + e.getMessage() + " - " + cause.getClass().getName(); logger.warn(msg); future.handle(Future.failedFuture(msg)); }); req.end(); } private void getImage(Handler<AsyncResult<JsonObject>> future) { getUrl("/images/" + image + "/json", future); } private void pullImage(Handler<AsyncResult<Void>> future) { logger.info("pull image {}", image); postUrlJson("/images/create?fromImage=" + image, "pullImage", "", future); } void postUrlJson(String url, String msg, String doc, Handler<AsyncResult<Void>> future) { HttpClientRequest req = request(HttpMethod.POST, url, res -> { Buffer body = Buffer.buffer(); res.exceptionHandler(d -> future.handle(Future.failedFuture(d.getCause()))); res.handler(body::appendBuffer); res.endHandler(d -> { if (res.statusCode() >= 200 && res.statusCode() <= 201) { containerId = body.toJsonObject().getString("Id"); future.handle(Future.succeededFuture()); } else { String m = msg + " HTTP error " + res.statusCode() + "\n" + body.toString(); logger.error(m); future.handle(Future.failedFuture(m)); } }); }); req.exceptionHandler(d -> future.handle(Future.failedFuture(d.getCause()))); req.putHeader("Content-Type", "application/json"); req.end(doc); } private void createContainer(int exposedPort, Handler<AsyncResult<Void>> future) { logger.info("create container from image {}", image); JsonObject j = new JsonObject(); j.put("AttachStdin", Boolean.FALSE); j.put("AttachStdout", Boolean.TRUE); j.put("AttachStderr", Boolean.TRUE); j.put("StopSignal", "SIGTERM"); if (env != null) { JsonArray a = new JsonArray(); for (EnvEntry nv : env) { a.add(nv.getName() + "=" + nv.getValue()); } j.put("env", a); } j.put("Image", image); JsonObject hp = new JsonObject().put("HostPort", Integer.toString(hostPort)); JsonArray ep = new JsonArray().add(hp); JsonObject pb = new JsonObject(); pb.put(exposedPort + "/tcp", ep); JsonObject hc = new JsonObject(); hc.put("PortBindings", pb); hc.put("PublishAllPorts", Boolean.FALSE); j.put("HostConfig", hc); if (this.cmd != null && this.cmd.length > 0) { JsonArray a = new JsonArray(); for (String cmdElement : cmd) { a.add(cmdElement); } j.put("Cmd", a); } if (dockerArgs != null) { for (Map.Entry<String, Object> entry : dockerArgs.properties().entrySet()) { j.put(entry.getKey(), entry.getValue()); } } String doc = j.encodePrettily(); doc = doc.replace("%p", Integer.toString(hostPort)).replace("%c", containerHost); logger.info("createContainer {}", doc); postUrlJson("/containers/create", "createContainer", doc, future); } private int getExposedPort(JsonObject b) { JsonObject config = b.getJsonObject("Config"); if (config == null) { throw (new IllegalArgumentException(messages.getMessage("11302"))); } JsonObject exposedPorts = config.getJsonObject("ExposedPorts"); if (exposedPorts == null) { throw (new IllegalArgumentException(messages.getMessage("11301"))); } int exposedPort = 0; Iterator<Map.Entry<String, Object>> iterator = exposedPorts.iterator(); while (iterator.hasNext()) { Map.Entry<String, Object> next = iterator.next(); String key = next.getKey(); String port = key.split("/")[0]; if (exposedPort == 0) { exposedPort = Integer.valueOf(port); } } return exposedPort; } private void prepareContainer(Handler<AsyncResult<Void>> startFuture) { getImage(res1 -> { if (res1.failed()) { logger.warn("getImage failed 1 : {}", res1.cause().getMessage()); startFuture.handle(Future.failedFuture(res1.cause())); return; } if (hostPort == 0) { startFuture.handle(Future.failedFuture(messages.getMessage("11300"))); return; } int exposedPort; try { exposedPort = getExposedPort(res1.result()); } catch (Exception ex) { startFuture.handle(Future.failedFuture(ex)); return; } createContainer(exposedPort, res2 -> { if (res2.failed()) { startFuture.handle(res2); return; } startContainer(res3 -> { if (res3.failed()) { deleteContainer(x -> startFuture.handle(res3)); return; } getContainerLog(res4 -> { if (res4.failed()) { this.stop(x -> startFuture.handle(res4)); return; } startFuture.handle(res4); }); }); }); }); } @Override public void start(Handler<AsyncResult<Void>> startFuture) { if (dockerPull) { pullImage(res -> prepareContainer(startFuture)); } else { prepareContainer(startFuture); } } @Override public void stop(Handler<AsyncResult<Void>> stopFuture) { stopContainer(res -> { if (res.failed()) { stopFuture.handle(Future.failedFuture(res.cause())); } else { ports.free(hostPort); deleteContainer(stopFuture); } }); } }