net.roboconf.target.docker.internal.DockerUtils.java Source code

Java tutorial

Introduction

Here is the source code for net.roboconf.target.docker.internal.DockerUtils.java

Source

/**
 * Copyright 2015-2017 Linagora, Universit Joseph Fourier, Floralis
 *
 * The present code is developed in the scope of the joint LINAGORA -
 * Universit Joseph Fourier - Floralis research program and is designated
 * as a "Result" pursuant to the terms and conditions of the LINAGORA
 * - Universit Joseph Fourier - Floralis research program. Each copyright
 * holder of Results enumerated here above fully & independently holds complete
 * ownership of the complete Intellectual Property rights applicable to the whole
 * of said Results, and may freely exploit it in any manner which does not infringe
 * the moral rights of the other copyright holders.
 *
 * 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 net.roboconf.target.docker.internal;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.apache.commons.lang.WordUtils;
import org.ops4j.pax.url.mvn.MavenResolver;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.command.InspectContainerResponse.ContainerState;
import com.github.dockerjava.api.model.Capability;
import com.github.dockerjava.api.model.Container;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DefaultDockerClientConfig.Builder;
import com.github.dockerjava.core.DockerClientBuilder;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import net.roboconf.core.utils.UriUtils;
import net.roboconf.core.utils.Utils;
import net.roboconf.target.api.TargetException;

/**
 * @author Vincent Zurczak - Linagora
 */
public final class DockerUtils {

    /**
     * Private empty constructor.
     */
    private DockerUtils() {
        // nothing
    }

    /**
     * Verifies the Docker client configuration.
     * @param targetProperties the target properties
     * @throws TargetException if the configuration is invalid
     */
    public static void verifyDockerClient(Map<String, String> targetProperties) throws TargetException {

        String imageId = targetProperties.get(DockerHandler.IMAGE_ID);
        String generate = targetProperties.get(DockerHandler.GENERATE_IMAGE);
        if (imageId == null && !Boolean.parseBoolean(generate))
            throw new TargetException(
                    "The " + DockerHandler.IMAGE_ID + " parameter was not specified, or enable image generation.");
    }

    /**
     * Creates a Docker client from target properties.
     * @param targetProperties a non-null map
     * @return a Docker client
     * @throws TargetException if something went wrong
     */
    public static DockerClient createDockerClient(Map<String, String> targetProperties) throws TargetException {

        // Validate what needs to be validated.
        Logger logger = Logger.getLogger(DockerHandler.class.getName());
        logger.fine("Setting the target properties.");
        verifyDockerClient(targetProperties);

        String edpt = targetProperties.get(DockerHandler.ENDPOINT);
        if (Utils.isEmptyOrWhitespaces(edpt))
            edpt = "tcp://localhost:4243";

        // The configuration is straight-forward.
        Builder config = DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(edpt)
                .withRegistryUsername(targetProperties.get(DockerHandler.USER))
                .withRegistryPassword(targetProperties.get(DockerHandler.PASSWORD))
                .withRegistryEmail(targetProperties.get(DockerHandler.EMAIL))
                .withApiVersion(targetProperties.get(DockerHandler.VERSION));

        // Build the client.
        DockerClientBuilder clientBuilder = DockerClientBuilder.getInstance(config.build());

        return clientBuilder.build();
    }

    /**
     * Deletes a Docker image if it exists.
     * @param imageId the image ID (not null)
     * @param dockerClient a Docker client
     */
    public static void deleteImageIfItExists(String imageId, DockerClient dockerClient) {

        if (imageId != null) {
            List<Image> images = dockerClient.listImagesCmd().exec();
            if (findImageById(imageId, images) != null)
                dockerClient.removeImageCmd(imageId).withForce(true).exec();
        }
    }

    /**
     * Finds an image by ID or by tag.
     * @param name an image ID or a tag name (can be null)
     * @param dockerClient a Docker client (not null)
     * @return an image, or null if none matched
     */
    public static Image findImageByIdOrByTag(String name, DockerClient dockerClient) {

        Image image = null;
        if (!Utils.isEmptyOrWhitespaces(name)) {
            Logger logger = Logger.getLogger(DockerUtils.class.getName());

            List<Image> images = dockerClient.listImagesCmd().exec();
            if ((image = DockerUtils.findImageById(name, images)) != null)
                logger.fine("Found a Docker image with ID " + name);
            else if ((image = DockerUtils.findImageByTag(name, images)) != null)
                logger.fine("Found a Docker image with tag " + name);
        }

        return image;
    }

    /**
     * Finds an image by ID.
     * @param imageId the image ID (not null)
     * @param images a non-null list of images
     * @return an image, or null if none was found
     */
    public static Image findImageById(String imageId, List<Image> images) {

        Image result = null;
        for (Image img : images) {
            if (img.getId().equals(imageId)) {
                result = img;
                break;
            }
        }

        return result;
    }

    /**
     * Finds an image by tag.
     * @param imageTag the image tag (not null)
     * @param images a non-null list of images
     * @return an image, or null if none was found
     */
    public static Image findImageByTag(String imageTag, List<Image> images) {

        Image result = null;
        for (Image img : images) {
            String[] tags = img.getRepoTags();
            if (tags == null)
                continue;

            for (String s : tags) {
                if (s.contains(imageTag)) {
                    result = img;
                    break;
                }
            }
        }

        return result;
    }

    /**
     * Finds a container by ID or by name.
     * @param name the container ID or name (not null)
     * @param dockerClient a Docker client
     * @return a container, or null if none was found
     */
    public static Container findContainerByIdOrByName(String name, DockerClient dockerClient) {

        Container result = null;
        List<Container> containers = dockerClient.listContainersCmd().withShowAll(true).exec();
        for (Container container : containers) {
            List<String> names = Arrays.asList(container.getNames());

            // Docker containers are prefixed with '/'.
            // At least, those we created, since their parent is the Docker daemon.
            if (container.getId().equals(name) || names.contains("/" + name)) {
                result = container;
                break;
            }
        }

        return result;
    }

    /**
     * Gets the state of a container.
     * @param containerId the container ID
     * @param dockerClient the Docker client
     * @return a container state, or null if the container was not found
     */
    public static ContainerState getContainerState(String containerId, DockerClient dockerClient) {

        ContainerState result = null;
        try {
            InspectContainerResponse resp = dockerClient.inspectContainerCmd(containerId).exec();
            if (resp != null)
                result = resp.getState();

        } catch (Exception e) {
            // nothing
        }

        return result;
    }

    /**
     * Finds the options and tries to configure them on the creation command.
     * @param options the options (key = name, value = option value)
     * @param cmd a non-null command to create a container
     * @throws TargetException
     */
    public static void configureOptions(Map<String, String> options, CreateContainerCmd cmd)
            throws TargetException {

        Logger logger = Logger.getLogger(DockerUtils.class.getName());

        // Basically, we had two choices:
        // 1. Map our properties to the Java REST API.
        // 2. By-pass it and send our custom JSon object.
        //
        // The second option is much more complicated.
        // So, we use Java reflection and some hacks to match Docker properties
        // with the setter methods available in the API. The API remains in charge
        // of generating the right JSon objects.
        Map<String, List<String>> hackedSetterNames = new HashMap<>();

        // Remains from Docker-Java 2.x (the mechanism still works)
        //
        //      List<String> list = new ArrayList<> ();
        //      list.add( "withMemoryLimit" );
        //      hackedSetterNames.put( "withMemory", list );

        // List known types
        List<Class<?>> types = new ArrayList<>();
        types.add(String.class);
        types.add(String[].class);
        types.add(long.class);
        types.add(Long.class);
        types.add(int.class);
        types.add(Integer.class);
        types.add(boolean.class);
        types.add(Boolean.class);
        types.add(Capability[].class);

        // Deal with the options
        for (Map.Entry<String, String> entry : options.entrySet()) {
            String optionValue = entry.getValue();

            // Now, guess what option to set
            String methodName = entry.getKey().replace("-", " ").trim();
            methodName = WordUtils.capitalize(methodName);
            methodName = methodName.replace(" ", "");
            methodName = "with" + methodName;

            Method _m = null;
            for (Method m : cmd.getClass().getMethods()) {

                boolean sameMethod = methodName.equalsIgnoreCase(m.getName());
                boolean methodWithAlias = hackedSetterNames.containsKey(methodName)
                        && hackedSetterNames.get(methodName).contains(m.getName());

                if (sameMethod || methodWithAlias) {

                    // Only one parameter?
                    if (m.getParameterTypes().length != 1) {
                        logger.warning("A method was found for " + entry.getKey()
                                + " but it does not have the right number of parameters.");
                        continue;
                    }

                    // The right type?
                    if (!types.contains(m.getParameterTypes()[0])) {

                        // Since Docker-java 3.x, there are two methods to set cap-add and cap-drop.
                        // One takes an array as parameter, the other takes a list.
                        logger.warning("A method was found for " + entry.getKey()
                                + " but it does not have the right parameter type. "
                                + "Skipping it. You may want to add a feature request.");

                        continue;
                    }

                    // That's probably the right one.
                    _m = m;
                    break;
                }
            }

            // Handle errors
            if (_m == null)
                throw new TargetException(
                        "Nothing matched the " + entry.getKey() + " option in the REST API. Please, report it.");

            // Try to set the option in the REST client
            try {
                Object o = prepareParameter(optionValue, _m.getParameterTypes()[0]);
                _m.invoke(cmd, o);

            } catch (ReflectiveOperationException | IllegalArgumentException e) {
                throw new TargetException("Option " + entry.getKey() + " could not be set.");
            }
        }
    }

    /**
     * Prepares the parameter to pass it to the REST API.
     * @param rawValue the raw value, as a string
     * @param clazz the class associated with the input parameter
     * @return the object, converted to the right class
     * @throws TargetException
     */
    public static Object prepareParameter(String rawValue, Class<?> clazz) throws TargetException {

        // Simple types
        Object result;
        if (clazz == int.class || clazz == Integer.class)
            result = Integer.parseInt(rawValue);
        else if (clazz == long.class || clazz == Long.class)
            result = Long.parseLong(rawValue);
        else if (clazz == boolean.class || clazz == Boolean.class)
            result = Boolean.parseBoolean(rawValue);

        // Arrays of string
        else if (clazz == String[].class) {
            List<String> parts = Utils.splitNicely(rawValue, ",");
            result = parts.toArray(new String[parts.size()]);
        }

        // Capabilities
        else if (clazz == Capability[].class) {
            List<Capability> caps = new ArrayList<>();
            for (String s : Utils.splitNicely(rawValue, ",")) {
                try {
                    caps.add(Capability.valueOf(s));
                } catch (Exception e) {
                    throw new TargetException("Unknown capability: " + s);
                }
            }

            result = caps.toArray(new Capability[caps.size()]);
        }

        // Default: keep the string
        else
            result = rawValue;

        return result;
    }

    /**
     * Builds the command to pass to a new Docker container.
     * @param cmd the value of the docker.run.command property
     * @param messagingConfiguration the messaging configuration (not null)
     * @param applicationName the application's name
     * @param scopedInstancePath the scoped instance's path
     * @return a non-null list of arguments
     */
    public static List<String> buildRunCommand(String cmd, Map<String, String> messagingConfiguration,
            String applicationName, String scopedInstancePath) {

        // We get the custom command line (docker.run.exec property) to run and:
        // - If nothing/invalid is specified (args == null), we use the standard agent start command
        // - If an empty command is explicitly specified (args.isEmpty()), there must be a RUN line in the Dockerfile.
        // - Else we use the provided command line. We may need to inject agent & messaging configuration.
        List<String> args = parseRunExecLine(cmd);
        if (args == null) {

            // No docker.run.exec property (or invalid), fall back to the default command line.
            // Build the command line, passing the agent & messaging configuration.
            // Command line is:
            // - Agent's start.sh script
            // - messaging provider-specific configuration file,
            // - agent.application-name=<<name of the application>>
            // - agent.scoped-instance-path=<<path of the scoped instance>>
            // - agent.messaging-type=<<type of messaging>>
            // - each of the messaging configuration properties, prefixed by "msg."
            args = new ArrayList<>();
            args.add("/usr/local/roboconf-agent/start.sh");
            args.add("etc/net.roboconf.messaging." + DockerHandler.MARKER_MESSAGING_TYPE + ".cfg");
            args.add("agent.application-name=" + DockerHandler.MARKER_APPLICATION_NAME);
            args.add("agent.scoped-instance-path=" + DockerHandler.MARKER_INSTANCE_PATH);
            args.add("agent.messaging-type=" + DockerHandler.MARKER_MESSAGING_TYPE);
            args.add(DockerHandler.MARKER_MESSAGING_CONFIGURATION);
        }

        // Now proceed to argument substitution, using the special markers.
        for (int i = 0; i < args.size(); i++) {

            // The current argument, that may be substituted.
            String arg = args.get(i);

            // The string to substitute to the marker, or null if nothing to substitute.
            final String s;

            // The index (in arg) and length of the marker to replace.
            final int j, l;

            if (arg.contains(DockerHandler.MARKER_MESSAGING_TYPE)) {
                j = arg.indexOf(DockerHandler.MARKER_MESSAGING_TYPE);
                l = DockerHandler.MARKER_MESSAGING_TYPE.length();
                s = messagingConfiguration.containsKey(DockerHandler.MESSAGING_TYPE)
                        ? messagingConfiguration.get(DockerHandler.MESSAGING_TYPE)
                        : "";

            } else if (arg.contains(DockerHandler.MARKER_APPLICATION_NAME)) {
                j = arg.indexOf(DockerHandler.MARKER_APPLICATION_NAME);
                l = DockerHandler.MARKER_APPLICATION_NAME.length();
                s = applicationName;

            } else if (arg.contains(DockerHandler.MARKER_INSTANCE_PATH)) {
                j = arg.indexOf(DockerHandler.MARKER_INSTANCE_PATH);
                l = DockerHandler.MARKER_INSTANCE_PATH.length();
                s = scopedInstancePath;

            } else {
                if (arg.equals(DockerHandler.MARKER_MESSAGING_CONFIGURATION)) {

                    // A bit more special: remove the whole argument and appends all
                    // the messaging configuration, prefixed by "msg.".
                    args.remove(i);
                    for (Map.Entry<String, String> e : messagingConfiguration.entrySet()) {
                        if (DockerHandler.MESSAGING_TYPE.equals(e.getKey()))
                            continue;

                        args.add(i, "msg." + e.getKey() + '=' + e.getValue());
                        i++;
                    }

                    // We've gone one position to far...
                    i--;
                }

                // No in-string substitution.
                j = -1;
                l = 0;
                s = null;
            }

            // Proceed to in-string substitution.
            if (s != null) {
                arg = arg.substring(0, j) + s + arg.substring(j + l, arg.length());
                args.set(i, arg);
            }
        }

        return args;
    }

    /**
     * Parses the given {@code docker.run.exec} property value.
     * @param runExecLine the {@code docker.run.exec} property value.
     * @return the {@code docker.run.exec} command + arguments array.
     */
    public static List<String> parseRunExecLine(String runExecLine) {

        List<String> result = null;
        if (!Utils.isEmptyOrWhitespaces(runExecLine)) {
            try {
                Gson gson = new Gson();
                String[] array = gson.fromJson(runExecLine, String[].class);

                // The returned collection must support the remove operation!
                // Array.asList() returns an unmodifiable collection.
                result = new ArrayList<>(Arrays.asList(array));

            } catch (JsonSyntaxException e) {
                Logger logger = Logger.getLogger(DockerUtils.class.getName());
                logger.warning("Cannot parse property " + DockerHandler.RUN_EXEC + ": " + runExecLine);
                Utils.logException(logger, e);
            }
        }

        return result;
    }

    /**
     * Handles Boolean values.
     * <p>
     * Docker-java 3.x annotates state methods with "@CheckNotNull".
     * So, we must verify the state attributes are not null (why Boolean instead of boolean?!).
     * </p>
     *
     * @param bool a Boolean value
     * @return the boolean value, or <code>false</code> if it was null
     */
    public static boolean extractBoolean(Boolean bool) {
        return bool != null ? bool.booleanValue() : false;
    }

    /**
     * Downloads a remote file (supports Maven URLs).
     * <p>
     * Please, refer to Pax URL's guide for more details about Maven URLs.
     * https://ops4j1.jira.com/wiki/display/paxurl/Mvn+Protocol
     * </p>
     *
     * @param url an URL
     * @param targetFile the file where it should be saved
     * @param mavenResolver the Maven resolver
     *
     * @throws IOException
     * @throws URISyntaxException
     */
    public static void downloadRemotePackage(String url, File targetFile, MavenResolver mavenResolver)
            throws IOException, URISyntaxException {

        if (url.toLowerCase().startsWith("mvn:")) {
            if (mavenResolver == null)
                throw new IOException("Maven URLs are only resolved in Karaf at the moment.");

            File sourceFile = mavenResolver.resolve(url);
            Utils.copyStream(sourceFile, targetFile);

        } else {
            URL u = UriUtils.urlToUri(url).toURL();
            URLConnection uc = u.openConnection();
            InputStream in = null;
            try {
                in = new BufferedInputStream(uc.getInputStream());
                Utils.copyStream(in, targetFile);

            } finally {
                Utils.closeQuietly(in);
            }
        }
    }
}