Java tutorial
/** * 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); } } } }