Package org.jolokia.docker.maven.access

Source Code of org.jolokia.docker.maven.access.DockerAccessWithHttpClient$ChunkedCallback

package org.jolokia.docker.maven.access;

import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.SSLContext;

import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jolokia.docker.maven.util.*;
import org.json.*;

/**
* Implementation using <a href="http://hc.apache.org/">Apache HttpComponents</a> for accessing remotely
* the docker host.
*
* The design goal here is to provide only the functionality required for this plugin in order to make
* it as robust as possible agains docker API changes (which happen quite frequently). That's also
* the reason, why no framework like JAX-RS or docker-java is used so that the dependencies are kept low.
*
* Of course, it's a bit more manual work, but it's worth the effort (as long as the Docker API functionality
* required is not to much).
*
* @author roland
* @since 26.03.14
*/
public class DockerAccessWithHttpClient implements DockerAccess {

    // Used Docker API
    static private final String DOCKER_API_VERSION = "v1.10";
    public static final String HEADER_ACCEPT = "Accept";
    public static final String HEADER_ACCEPT_ALL = "*/*";

    // Logging
    private final LogHandler log;

    // Base Docker URL
    private final String baseUrl;

    // HttpClient to use
    private final HttpClient client;

    /**
     * Create a new access for the given URL
     * @param baseUrl base URL for accessing the docker Daemon
     * @param certPath used to build up a keystore with the given keys and certificates found in this directory
     * @param log a log handler for printing out logging information
     */
    public DockerAccessWithHttpClient(String baseUrl, String certPath, LogHandler log) throws DockerAccessException {
        this.baseUrl = stripSlash(baseUrl) + "/" + DOCKER_API_VERSION;
        this.log = log;
        this.client = createHttpClient(certPath);
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasImage(String image) throws DockerAccessException {
        Matcher matcher = Pattern.compile("^(.*?):([^:]+)?$").matcher(image);
        String base = matcher.matches() ? matcher.group(1) : image;

        HttpUriRequest req = newGet(baseUrl + "/images/json?filter=" + base);
        HttpResponse resp = request(req);
        checkReturnCode("Checking for image '" + image + "'", resp, 200);
        JSONArray array = asJsonArray(resp);
        return array.length() > 0;
    }

    /** {@inheritDoc} */
    @Override
    public String createContainer(String image, String command, Set<Integer> ports, Map<String, String> env) throws DockerAccessException {
        HttpUriRequest post = newPost(baseUrl + "/containers/create", getContainerConfig(image, ports, command, env));
        HttpResponse resp = request(post);
        checkReturnCode("Creating container for image '" + image + "'", resp, 201);
        JSONObject json = asJsonObject(resp);
        logWarnings(json);
        return json.getString("Id");
    }

    @Override
    public String getContainerName(String id) throws DockerAccessException {
        HttpUriRequest req = newGet(baseUrl + "/containers/" + encode(id) + "/json");
        HttpResponse  resp = request(req);
        checkReturnCode("Getting information about container '" + id + "'", resp, 200);
        JSONObject json = asJsonObject(resp);
        return json.getString("Name");
    }

    /** {@inheritDoc} */
    @Override
    public void startContainer(String containerId, PortMapping portMapping,List<String> volumesFrom, List<String> links)
            throws DockerAccessException {
        HttpUriRequest req = newPost(baseUrl + "/containers/" + encode(containerId) + "/start",
                                     getStartConfig(portMapping, volumesFrom, links));
        HttpResponse resp = request(req);
        checkReturnCode("Starting container with id " + containerId, resp, 204);
    }

    /** {@inheritDoc} */
    @Override
    public void stopContainer(String containerId) throws DockerAccessException {
        HttpUriRequest req = newPost(baseUrl + "/containers/" + encode(containerId) + "/stop", null);
        HttpResponse  resp = request(req);
        checkReturnCode("Stopping container with id " + containerId, resp, 204, 304);
    }

    /** {@inheritDoc} */
    @Override
    public void buildImage(String image, File dockerArchive) throws DockerAccessException {
        String buildUrl = baseUrl + "/build?rm=true" + (image != null ? "&t=" + encode(image) : "");
        HttpPost post = new HttpPost(buildUrl);
        post.setEntity(new FileEntity(dockerArchive));
        processBuildResponse(image, request(post));
    }

    /** {@inheritDoc} */
    @Override
    public Map<Integer, Integer> queryContainerPortMapping(String containerId) throws DockerAccessException {
        HttpUriRequest req = newGet(baseUrl + "/containers/" + encode(containerId) + "/json");
        HttpResponse resp = request(req);
        checkReturnCode("Getting container information for " + containerId, resp, 200);
        return extractPorts(asJsonObject(resp));
    }

    /** {@inheritDoc} */
    @Override
    public List<String> getContainersForImage(String image) throws DockerAccessException {
        List<String> ret = new ArrayList<>();
        HttpUriRequest req = newGet(baseUrl + "/containers/json?limit=100");
        HttpResponse resp = request(req);
        checkReturnCode("Fetching container information", resp, 200);
        JSONArray configs = asJsonArray(resp);
        for (int i = 0; i < configs.length(); i ++) {
            JSONObject config = configs.getJSONObject(i);
            String containerImage = config.getString("Image");

            if (image.equals(containerImage)) {
                ret.add(config.getString("Id"));
            }
        }
        return ret;
    }

    @Override
    public String getLogs(final String containerId) throws DockerAccessException {
        HttpUriRequest get = newGet(baseUrl + "/containers/" + encode(containerId) + "/logs?stdout=1&stderr=1");

        HttpResponse resp = request(get);
        checkReturnCode("Getting log for '" + containerId + "' ",resp,200);
        return asString(resp);
    }

    /** {@inheritDoc} */
    @Override
    public void removeContainer(String containerId) throws DockerAccessException {
        HttpUriRequest req = newDelete(baseUrl + "/containers/" + encode(containerId));
        HttpResponse  resp = request(req);
        checkReturnCode("Stopping container with id " + containerId, resp, 204);
    }

    /** {@inheritDoc} */
    @Override
    public void pullImage(String image,AuthConfig authConfig) throws DockerAccessException {
        ImageName name = new ImageName(image);
        String pullUrl = baseUrl + "/images/create?fromImage=" + encode(name.getRepository());
        pullUrl = addTag(pullUrl, name);
        pullUrl = addRegistry(pullUrl,name);
        pullOrPushImage(image,pullUrl,"pulling",authConfig);
    }

    /** {@inheritDoc} */
    @Override
    public void pushImage(String image, AuthConfig authConfig) throws DockerAccessException {
        ImageName name = new ImageName(image);
        String pushUrl = baseUrl + "/images/" + encode(name.getRepositoryWithRegistry()) + "/push";
        pushUrl = addTag(pushUrl, name);
        pullOrPushImage(image,pushUrl,"pushing",authConfig);
    }

    /** {@inheritDoc} */
    @Override
    public boolean removeImage(String image, boolean ... forceOpt) throws DockerAccessException {
        boolean force = forceOpt != null && forceOpt.length > 0 && forceOpt[0];
        HttpUriRequest req = newDelete(baseUrl + "/images/" + image + (force ? "?force=1" : ""));
        HttpResponse resp = request(req);
        checkReturnCode("Removing image " + image, resp, 200, 404);
        if (log.isDebugEnabled()) {
            logRemoveResponse(asJsonArray(resp));
        }
        return resp.getStatusLine().getStatusCode() == 200;
    }

    // ---------------
    // Lifecycle methods not needed here
    /** {@inheritDoc} */
    @Override
    public void start() {}


    /** {@inheritDoc} */
    @Override
    public void shutdown() {}

    // ====================================================================================================

    // HttpClient to use
    private HttpClient createHttpClient(String certPath) throws DockerAccessException {
        HttpClientBuilder builder = HttpClients.custom();
        addSslSupportIfNeeded(builder,certPath);

        // TODO: Tune client if needed (e.g. add pooling factoring .....
        // But I think, that's not really required.

        return builder.build();
    }

    // Lookup a keystore and add it to the client
    private void addSslSupportIfNeeded(HttpClientBuilder builder, String certPath) throws DockerAccessException {
        if (certPath != null) {
            try {
                KeyStore keyStore = KeyStoreUtil.createDockerKeyStore(certPath);

                SSLContext sslContext =
                        SSLContexts.custom()
                                   .useTLS()
                                   .loadKeyMaterial(keyStore,"docker".toCharArray())
                                   .loadTrustMaterial(keyStore)
                                   .build();
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
                builder.setSSLSocketFactory(sslsf);
            } catch (IOException | GeneralSecurityException e) {
                throw new DockerAccessException("Cannot read keys and/or certs from " + certPath + ": " + e,e);
            }
        }

    }

    // -----------------------
    // Request related methods
    private HttpUriRequest newGet(String url) {
        return addDefaultHeaders(new HttpGet(url));
    }

    private HttpUriRequest newPost(String url, String body) {
        HttpPost post = new HttpPost(url);
        if (body != null) {
            post.setEntity(new StringEntity(body, Charset.defaultCharset()));
        }
        return addDefaultHeaders(post);
    }

    private HttpUriRequest newDelete(String url) {
        return addDefaultHeaders(new HttpDelete(url));
    }

    private HttpUriRequest addDefaultHeaders(HttpUriRequest req) {
        req.addHeader(HEADER_ACCEPT, HEADER_ACCEPT_ALL);
        req.addHeader("Content-Type", "application/json");
        return req;
    }

    private HttpResponse request(HttpUriRequest req) throws DockerAccessException {
        try {
            return client.execute(req);
        } catch (IOException e) {
            throw new DockerAccessException("Cannot send req " + req + ": " + e,e);
        }
    }

    // -----------------------
    // Serialization stuff

    private JSONArray asJsonArray(HttpResponse resp) throws DockerAccessException {
        try {
            return new JSONArray(EntityUtils.toString(resp.getEntity()));
        } catch (IOException e) {
            throw new DockerAccessException("Cannot read content from response " + resp,e);
        }
    }

    private JSONObject asJsonObject(HttpResponse resp) throws DockerAccessException {
        try {
            return new JSONObject(EntityUtils.toString(resp.getEntity()));
        } catch (IOException e) {
            throw new DockerAccessException("Cannot read content from response as JSON object " + resp,e);
        }
    }

    private String asString(HttpResponse resp) throws DockerAccessException {
        try {
            return EntityUtils.toString(resp.getEntity());
        } catch (IOException e) {
            throw new DockerAccessException("Cannot read content from response as string " + resp,e);
        }
    }
    private String encode(String param) {
        try {
            return URLEncoder.encode(param,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            // wont happen
            return URLEncoder.encode(param);
        }
    }


    // ===========================================================================================================
    // Preparation for performing requests

    private Map<Integer, Integer> extractPorts(JSONObject info) {
        JSONObject networkSettings = info.getJSONObject("NetworkSettings");
        if (networkSettings != null) {
            JSONObject ports = networkSettings.getJSONObject("Ports");
            if (ports != null) {
                return createPortMapping(ports);
            }
        }
        return Collections.emptyMap();
    }

    private Map<Integer, Integer> createPortMapping(JSONObject ports) {
        Map<Integer, Integer> portMapping = new HashMap<>();
        for (Object portSpecO : ports.keySet()) {
            String portSpec = portSpecO.toString();
            if (!ports.isNull(portSpec)) {
                JSONArray hostSpecs = ports.getJSONArray(portSpec);
                parseHostSpecsAndUpdateMapping(portMapping, hostSpecs, portSpec);
            }
        }
        return portMapping;
    }

    private void parseHostSpecsAndUpdateMapping(Map<Integer, Integer> portMapping, JSONArray hostSpecs, String portSpec) {
        if (hostSpecs != null && hostSpecs.length() > 0) {
            // We take only the first
            JSONObject hostSpec = hostSpecs.getJSONObject(0);
            Object hostPortO = hostSpec.get("HostPort");
            if (hostPortO != null) {
                parsePortSpecAndUpdateMapping(portMapping, hostPortO, portSpec);
            }
        }
    }

    private void parsePortSpecAndUpdateMapping(Map<Integer, Integer> portMapping, Object hostPort, String portSpec) {
        try {
            Integer hostP = (Integer.parseInt(hostPort.toString()));
            int idx = portSpec.indexOf('/');
            String p = idx > 0 ? portSpec.substring(0, idx) : portSpec;
            Integer containerPort = Integer.parseInt(p);
            portMapping.put(containerPort, hostP);
        } catch (NumberFormatException exp) {
            log.warn("Cannot parse " + hostPort + " or " + portSpec + " as a port number. Ignoring in mapping");
        }
    }

    private String getContainerConfig(String image, Set<Integer> ports, String command, Map<String, String> env) {
        JSONObject ret = new JSONObject();
        ret.put("Image", image);
        if (ports != null && ports.size() > 0) {
            JSONObject exposedPorts = new JSONObject();
            for (Integer port : ports) {
                exposedPorts.put(port.toString() + "/tcp", new JSONObject());
            }
            ret.put("ExposedPorts", exposedPorts);
        }
        if (command != null) {
            JSONArray a = new JSONArray();
            for (String s : EnvUtil.splitWOnSpaceWithEscape(command)) {
                a.put(s);
            }
            ret.put("Cmd",a);
        }
        if (env != null && env.size() > 0) {
            JSONArray a = new JSONArray();
            for (Map.Entry<String,String> entry : env.entrySet()) {
                a.put(entry.getKey() + "=" + entry.getValue());
            }
            ret.put("Env", a);
        }
        log.debug("Container create config: " + ret.toString());
        return ret.toString();
    }

    private String getStartConfig(PortMapping portMapping, List<String> volumesFrom, List<String> links) {
        JSONObject ret = new JSONObject();
        if (portMapping != null && !portMapping.isEmpty()) {
            ret.put("PortBindings", portMapping.toDockerConfig());
        }
        if (volumesFrom != null) {
            ret.put("VolumesFrom", new JSONArray(volumesFrom));
        }
        if (links != null) {
            ret.put("Links", new JSONArray(links));
        }
        log.debug("Container start config: " + ret.toString());
        return ret.toString();
    }

    // ======================================================================================================

    private void dump(HttpResponse resp) {
        String body = null;
        try {
            body = EntityUtils.toString(resp.getEntity());
            log.debug("<<<< " + (body != null ? body : "[empty]"));
        } catch (IOException e) {
            log.error("<<<< Error while deserializing response " + resp + ": " + e);
        }
    }

    private void dump(HttpUriRequest req) {
        try {
            log.debug(">>>> " + req.getURI());
            Header[] headers = req.getAllHeaders();
            if (headers != null) {
                for (Header h : headers) {
                    log.debug("|||| " + h.getName() + "=" + h.getValue());
                }
            }
            if (req.getMethod() == "POST") {
                HttpPost post = (HttpPost) req;
                log.debug("---- " + (post.getEntity() != null ? EntityUtils.toString(post.getEntity()) : "[empty]"));
            }
        } catch (IOException e) {
            log.error("<<<< Error while deserializing response " + req + ": " + e);
        }
    }

    private void pullOrPushImage(String image, String uri, String what, AuthConfig authConfig)
            throws DockerAccessException {
        try {
            HttpPost post = new HttpPost(uri);
            if (authConfig != null) {
                post.addHeader("X-Registry-Auth",authConfig.toHeaderValue());
            }

            processPullOrPushResponse(image, client.execute(post), what);
        } catch (IOException e) {
            throw new DockerAccessException("Error while " + what + " " + image + ": ",e);
        }
    }

    private String addTag(String url, ImageName name) {
        return addQueryParam(url, "tag", name.getTag());
    }

    private String addRegistry(String url, ImageName name) {
        return addQueryParam(url,"registry",name.getRegistry());
    }

    private String addQueryParam(String url, String param, String value) {
        if (value != null) {
            return url + (url.contains("?") ? "&" : "?") + param + "=" + encode(value);
        } else {
            return url;
        }
    }

    private void processPullOrPushResponse(final String image, HttpResponse resp, final String action)
            throws DockerAccessException {
        processChunkedResponse(resp, new ChunkedCallback() {

            private boolean downloadInProgress = false;

            @Override
      public void process(JSONObject json) {
                if (json.has("progressDetail")) {
                    JSONObject details = json.getJSONObject("progressDetail");
                    if (details.has("total")) {
                        if (!downloadInProgress) {
                            log.progressStart(details.getInt("total"));
                        }
                        log.progressUpdate(details.getInt("current"));
                        downloadInProgress = true;
                        return;
                    }
                      
                    if (downloadInProgress) {
                      log.progressFinished();
                    }
                    downloadInProgress = false;
                }
                if (json.has("error")) {
                    String msg = json.getString("error").trim();
                    String details = json.getJSONObject("errorDetail").getString("message").trim();
                    log.error("!!! " + msg + (msg.equals(details) ? "" : "(" + details + ")"));
                } else {
                    log.info("... " + (json.has("id") ? json.getString("id") + ": " : "") + json.getString("status"));
                }
            }

            @Override
      public String getErrorMessage(StatusLine status) {
                return "Error while " + action + " image '" + image + "' (code: " + status.getStatusCode() + ", " + status.getReasonPhrase() + ")";
            }
        });
    }

    private void processBuildResponse(final String image, org.apache.http.HttpResponse resp) throws DockerAccessException {

        processChunkedResponse(resp, new ChunkedCallback() {
            @Override
      public void process(JSONObject json) {
                if (json.has("error")) {
                    log.error("Error building image: " + json.get("error"));
                    if (json.has("errorDetail")) {
                        JSONObject details = json.getJSONObject("errorDetail");
                        log.error(details.getString("message"));
                    }
                } else if (json.has("stream")) {
                    String message = json.getString("stream");
                    log.debug(trim(message));
                } else if (json.has("status")) {
                    String status = trim(json.getString("status"));
                    String id = json.has("id") ? json.getString("id") : null;
                    if (status.matches("^.*(Download|Pulling).*")) {
                        log.info("  " + (id != null ? id + " " : "") + status);
                    }
                }
            }

            private String trim(String message) {
                while (message.endsWith("\n")) {
                    message = message.substring(0,message.length() - 1);
                }
                return message;
            }

            @Override
      public String getErrorMessage(StatusLine status) {
                return "Error while building image '" + image + "' (code: " + status.getStatusCode()
                       + ", " + status.getReasonPhrase() + ")";
            }
        });

    }

    private void processChunkedResponse(org.apache.http.HttpResponse resp, ChunkedCallback cb) throws DockerAccessException {
        try {
            InputStream is = resp.getEntity().getContent();
            int len;
            int size = 8129;
            byte[] buf = new byte[size];
            // Data comes in chunkwise
            while ((len = is.read(buf, 0, size)) != -1) {
                String txt = new String(buf,0,len,"UTF-8");
                try {
                    JSONObject json = new JSONObject(txt);
                    cb.process(json);
                } catch (JSONException exp) {
                    log.warn("Couldn't parse answer chunk '" + txt + "': " + exp);
                }
            }
            StatusLine status = resp.getStatusLine();
            if (status.getStatusCode() != 200) {
                throw new DockerAccessException(cb.getErrorMessage(status));
            }
        } catch (IOException e) {
            throw new DockerAccessException("Cannot process chunk response: " + e,e);
        }
    }

    private void logWarnings(JSONObject body) {
        Object warningsObj = body.get("Warnings");
        if (warningsObj != JSONObject.NULL) {
            JSONArray warnings = (JSONArray) warningsObj;
            for (int i = 0; i < warnings.length(); i++) {
                log.warn(warnings.getString(i));
            }
        }
    }

    private void checkReturnCode(String msg, HttpResponse resp, int ... expectedCodes) throws DockerAccessException {
        StatusLine status = resp.getStatusLine();
        int statusCode = status.getStatusCode();
        for (int code : expectedCodes) {
            if (statusCode == code) {
                return;
            }
        }
        throw new DockerAccessException("Error while calling docker: " + msg + " (code: " + statusCode + ")");
    }

    private String stripSlash(String url) {
        String ret = url;
        while (ret.endsWith("/")) {
            ret = ret.substring(0, ret.length() - 1);
        }
        return ret;
    }

    // Callback for processing response chunks
    private void logRemoveResponse(JSONArray logElements) {
        for (int i = 0; i < logElements.length(); i++) {
            JSONObject entry = logElements.getJSONObject(i);
            for (Object key : entry.keySet()) {
                log.debug(key + ": " + entry.get(key.toString()));
            }
        }
    }

    // ================================================================================================

    private interface ChunkedCallback {
        void process(JSONObject json);
        String getErrorMessage(StatusLine status);
    }
}
TOP

Related Classes of org.jolokia.docker.maven.access.DockerAccessWithHttpClient$ChunkedCallback

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.