diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3d47f986c41db29ec6dc0d5036bf760b3a1cf366 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +target/ +.settings/ +*.iml +.project +.classpath \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ff8d4b93545876bb7066a20816f0060378ddd836 --- /dev/null +++ b/pom.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- Inherited Icy Parent POM --> + <parent> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>parent-pom-plugin</artifactId> + <version>1.0.3</version> + </parent> + + <!-- Project Information --> + <artifactId>docker4icy</artifactId> + <version>3.2.7</version> + + <packaging>jar</packaging> + + <name>Docker for Icy</name> + <description>Yes, it is finally here. A user-friendly interface to Docker, letting you run containers within your plug-ins in no time.</description> + <url>http://icy.bioimageanalysis.org/plugin/docker-for-icy/</url> + <inceptionYear>2020</inceptionYear> + + <organization> + <name>Institut Pasteur</name> + <url>https://pasteur.fr</url> + </organization> + + <licenses> + <license> + <name>GNU GPLv3</name> + <url>https://www.gnu.org/licenses/gpl-3.0.en.html</url> + <distribution>repo</distribution> + </license> + </licenses> + + <developers> + <developer> + <id>sdallongeville</id> + <name>Stéphane Dallongeville</name> + <url>https://research.pasteur.fr/fr/member/stephane-dallongeville/</url> + <roles> + <role>founder</role> + <role>lead</role> + <role>architect</role> + <role>developer</role> + <role>debugger</role> + <role>tester</role> + <role>maintainer</role> + <role>support</role> + </roles> + </developer> + </developers> + + <!-- Project properties --> + <properties> + + </properties> + + <!-- Project build configuration --> + <build> + + </build> + + <!-- List of project's dependencies --> + <dependencies> + <!-- The core of Icy --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>icy-kernel</artifactId> + </dependency> + + <dependency> + <groupId>com.github.docker-java</groupId> + <artifactId>docker-java</artifactId> + <version>3.2.7</version> + </dependency> + </dependencies> + + <!-- Icy Maven repository (to find parent POM) --> + <repositories> + <repository> + <id>icy</id> + <name>Icy's Nexus</name> + <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> + </repository> + </repositories> +</project> diff --git a/src/main/java/plugins/adufour/docker/Docker4Icy.java b/src/main/java/plugins/adufour/docker/Docker4Icy.java new file mode 100644 index 0000000000000000000000000000000000000000..35839ed783d07ebbd0f78587e478bde0afb037e2 --- /dev/null +++ b/src/main/java/plugins/adufour/docker/Docker4Icy.java @@ -0,0 +1,76 @@ +package plugins.adufour.docker; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import icy.file.FileUtil; +import icy.plugin.PluginLoader; +import icy.plugin.abstract_.Plugin; +import icy.plugin.classloader.JarClassLoader; +import icy.plugin.interface_.PluginDaemon; +import icy.util.JarUtil; + +/** + * Icy interface to <a href="https://www.docker.com/what-docker">Docker</a>. This daemon loads up + * necessary 3rd-party libraries into the class path at startup (and every time the plug-in list is + * reloaded). To interact with Docker, see the {@link DockerUtil} class. + * + * @author Alexandre Dufour + */ +public class Docker4Icy extends Plugin implements PluginDaemon +{ + /** The temporary folder where the 3rd party libraries will be extracted */ + private static final String libFolder = FileUtil.getTempDirectory() + "/Docker4Icy/"; + + @Override + public void init() + { + try + { + // 1) Unpack and load 3rd-party JAR libraries via a temporary folder + URL url = DockerUtil.class.getResource("lib"); + if (url != null) + { + String[] jarFiles = new File(url.getFile()).list(); + for (String jarFile : jarFiles) + { + String fileName = FileUtil.getFileName(jarFile); + extractResource(libFolder + fileName, DockerUtil.class.getResource("lib" + File.separator + fileName)); + ((JarClassLoader) PluginLoader.getLoader()).add(new URL("file://" + libFolder + fileName)); + } + } + else + { + String jarPath = FileUtil.getApplicationDirectory() + "/" + getDescriptor().getJarFilename(); + for (String jarFile : JarUtil.getAllFiles(jarPath, false, false)) + if (jarFile.endsWith(".jar")) + { + String fileName = FileUtil.getFileName(jarFile); + extractResource(libFolder + fileName, DockerUtil.class.getResource("lib/" + fileName)); + ((JarClassLoader) PluginLoader.getLoader()).add(libFolder + fileName); + } + } + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + @Override + public void run() + { + // Don't run. I'm too fast for you anyway. + } + + @Override + public void stop() + { + // Delete the temporary folder: + // 1) for the beauty of saving space until the next run... + // 2) To prevent potential conflicts after upgrades + FileUtil.delete(new File(libFolder), true); + } + +} diff --git a/src/main/java/plugins/adufour/docker/DockerUtil.java b/src/main/java/plugins/adufour/docker/DockerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..db7dd252e70b381d13283faefd79e3ac3599e526 --- /dev/null +++ b/src/main/java/plugins/adufour/docker/DockerUtil.java @@ -0,0 +1,168 @@ +package plugins.adufour.docker; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.model.AccessMode; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Volume; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientBuilder; +import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.core.command.PullImageResultCallback; + +/** + * Utility class to easily interface with (and run) + * <a href="https://www.docker.com/what-docker">Docker</a> within Icy, using the official + * <a href="https://github.com/docker-java/docker-java">docker-java API</a>. + * + * @author Alexandre Dufour + */ +public class DockerUtil +{ + private static DockerClient docker = DockerClientBuilder.getInstance(DefaultDockerClientConfig.createDefaultConfigBuilder().build()).build(); + + /** + * Starts a container with the specified image + * + * @param image + * the image to run (and download if necessary) + * @return the container ID + * @throws InterruptedException + */ + public static String startContainer(String image) throws InterruptedException + { + return startContainer(image, (List<Bind>) null, null); + } + + /** + * Starts a container with the specified image and virtual folder binding(s) + * + * @param image + * the image to run (and download if necessary) + * @param bindings + * Virtual bindings between host and container folders, e.g. + * <code>{"/host/a", "/docker/b"}</code>. NB: by default, these mappings are set to + * "read-write" mode. For further control, use + * {@link #startContainer(String, List, String)} + * @return the container ID + * @throws InterruptedException + */ + public static String startContainer(String image, Map<String, String> bindings) throws InterruptedException + { + return startContainer(image, bindings, null); + } + + /** + * Starts a container with the specified image and virtual folder binding(s) + * + * @param image + * the image to run (and download if necessary) + * @param bindings + * Virtual bindings between host and container folders, e.g. + * <code>new Bind("/host/a", "/docker/b", AccessMode.rw)</code> + * @return the container ID + * @throws InterruptedException + */ + public static String startContainer(String image, List<Bind> bindings) throws InterruptedException + { + return startContainer(image, bindings, null); + } + + /** + * Starts a container with the specified image and virtual folder binding(s) + * + * @param image + * the image to run (and download if necessary) + * @param bindings + * Virtual bindings between host and container folders, e.g. + * <code>{"/host/a", "/docker/b"}</code>. NB: by default, these mappings are set to + * "read-write" mode. For further control, use + * {@link #startContainer(String, List, String)} + * @param workingDirectory + * the directory where the container should start from (e.g. + * <code>"/docker/b/subdir"</code>). This is equivalent to (but faster than) starting + * the container and then calling <code>"cd /docker/b/subdir"</code> + * @return the container ID + * @throws InterruptedException + */ + public static String startContainer(String image, Map<String, String> bindings, String workingDirectory) throws InterruptedException + { + List<Bind> bindingList = null; + + if (bindings != null) + { + // Convert user mappings to Docker bindings + bindingList = new ArrayList<Bind>(bindings.size()); + for (Entry<String, String> binding : bindings.entrySet()) + bindingList.add(new Bind(binding.getKey(), new Volume(binding.getValue()), AccessMode.rw)); + } + + return startContainer(image, bindingList, workingDirectory); + } + + /** + * Starts a container with the specified image and virtual folder binding(s) + * + * @param image + * the image to run (and download if necessary) + * @param bindings + * Virtual bindings between host and container folders, e.g. + * <code>new Bind("/host/a", "/docker/b", AccessMode.rw)</code> + * @param workingDirectory + * the directory where the container should start from (e.g. + * <code>"/docker/b/subdir"</code>). This is equivalent to (but faster than) starting + * the container and then calling <code>"cd /docker/b/subdir"</code> + * @return the container ID + * @throws InterruptedException + */ + public static String startContainer(String image, List<Bind> bindings, String workingDirectory) throws InterruptedException + { + System.out.println("Fetching image " + image); + docker.pullImageCmd(image).exec(new PullImageResultCallback()).awaitCompletion(); + + System.out.println("Starting container using " + image); + + CreateContainerCmd createCommand = docker.createContainerCmd(image); + + if (bindings != null) createCommand = createCommand.withBinds(bindings); + + if (workingDirectory != null) createCommand = createCommand.withWorkingDir(workingDirectory); + + // Emulate a TTY and attach output and error streams + createCommand = createCommand.withTty(true).withAttachStdout(true).withAttachStderr(true); + + // Finally, start the container + CreateContainerResponse container = createCommand.exec(); + String containerID = container.getId(); + docker.startContainerCmd(containerID).exec(); + + return containerID; + } + + public static void stopContainer(String containerID) + { + docker.stopContainerCmd(containerID).exec(); + } + + /** + * Runs the specified command in the given container and awaits completion + * + * @param containerID + * the ID of the container where the command should run + * @param command + * the command to run (e.g. <code>"echo hello Icy world!"</code> + * @throws InterruptedException + * if the calling thread was interrupted before completion of the command + */ + public static void runCommand(String containerID, String command) throws InterruptedException + { + String cmd = docker.execCreateCmd(containerID).withAttachStdout(true).withAttachStderr(true).withTty(true).withCmd(command.split(" ")).exec().getId(); + docker.execStartCmd(cmd).exec(new ExecStartResultCallback(System.out, System.err)).awaitCompletion(); + } +}