From 57413b44b4ad070f9bf79a84e90ca066b41d6a7c Mon Sep 17 00:00:00 2001 From: Qingyang Chen Date: Wed, 7 Mar 2018 16:29:52 -0500 Subject: [PATCH] Adds jib:dockercontext to export a Docker context. (#121) --- .../tools/jib/builder/BuildImageSteps.java | 35 +-- .../tools/jib/builder/EntrypointBuilder.java | 49 ++++ .../tools/jib/filesystem/DirectoryWalker.java | 58 +++++ .../tools/jib/filesystem/PathConsumer.java | 26 ++ ...psTest.java => EntrypointBuilderTest.java} | 16 +- .../jib/filesystem/DirectoryWalkerTest.java | 54 ++++ .../cloud/tools/jib/maven/BuildImageMojo.java | 83 +----- .../tools/jib/maven/DockerContextMojo.java | 238 ++++++++++++++++++ .../tools/jib/maven/ProjectProperties.java | 117 +++++++++ .../src/main/resources/DockerfileTemplate | 7 + .../jib/maven/DockerContextMojoTest.java | 123 +++++++++ .../src/test/resources/layer/a/b/bar | 1 + .../src/test/resources/layer/c/cat | 1 + jib-maven-plugin/src/test/resources/layer/foo | 1 + .../src/test/resources/sampleDockerfile | 7 + 15 files changed, 704 insertions(+), 112 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java rename jib-core/src/test/java/com/google/cloud/tools/jib/builder/{BuildImageStepsTest.java => EntrypointBuilderTest.java} (74%) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java create mode 100644 jib-maven-plugin/src/main/resources/DockerfileTemplate create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java create mode 100644 jib-maven-plugin/src/test/resources/layer/a/b/bar create mode 100644 jib-maven-plugin/src/test/resources/layer/c/cat create mode 100644 jib-maven-plugin/src/test/resources/layer/foo create mode 100644 jib-maven-plugin/src/test/resources/sampleDockerfile diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildImageSteps.java index 66b56798f1..61adc30ec9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildImageSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildImageSteps.java @@ -23,14 +23,12 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; -import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -60,6 +58,12 @@ public BuildConfiguration getBuildConfiguration() { public void run() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException { + List entrypoint = + EntrypointBuilder.makeEntrypoint( + sourceFilesConfiguration, + buildConfiguration.getJvmFlags(), + buildConfiguration.getMainClass()); + try (Timer timer = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { try (Timer timer2 = timer.subTimer("Initializing cache")) { ListeningExecutorService listeningExecutorService = @@ -145,7 +149,7 @@ public void run() authenticatePushFuture, pullBaseImageLayerFuturesFuture, buildAndCacheApplicationLayerFutures, - getEntrypoint()), + entrypoint), listeningExecutorService); timer2.lap("Setting up application layer push"); @@ -183,29 +187,6 @@ public void run() } buildConfiguration.getBuildLogger().info(""); - buildConfiguration.getBuildLogger().info("Container entrypoint set to " + getEntrypoint()); - } - - /** - * Gets the container entrypoint. - * - *

The entrypoint is {@code java -cp [classpaths] [main class]}. - */ - @VisibleForTesting - List getEntrypoint() { - List classPaths = new ArrayList<>(); - classPaths.add(sourceFilesConfiguration.getDependenciesPathOnImage() + "*"); - classPaths.add(sourceFilesConfiguration.getResourcesPathOnImage()); - classPaths.add(sourceFilesConfiguration.getClassesPathOnImage()); - - String classPathsString = String.join(":", classPaths); - - List entrypoint = new ArrayList<>(4 + buildConfiguration.getJvmFlags().size()); - entrypoint.add("java"); - entrypoint.addAll(buildConfiguration.getJvmFlags()); - entrypoint.add("-cp"); - entrypoint.add(classPathsString); - entrypoint.add(buildConfiguration.getMainClass()); - return entrypoint; + buildConfiguration.getBuildLogger().info("Container entrypoint set to " + entrypoint); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java new file mode 100644 index 0000000000..845235880c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.builder; + +import java.util.ArrayList; +import java.util.List; + +/** Builds an image entrypoint for the Java application. */ +public class EntrypointBuilder { + + /** + * Builds the container entrypoint. + * + *

The entrypoint is {@code java [jvm flags] -cp [classpaths] [main class]}. + */ + public static List makeEntrypoint( + SourceFilesConfiguration sourceFilesConfiguration, List jvmFlags, String mainClass) { + List classPaths = new ArrayList<>(); + classPaths.add(sourceFilesConfiguration.getDependenciesPathOnImage() + "*"); + classPaths.add(sourceFilesConfiguration.getResourcesPathOnImage()); + classPaths.add(sourceFilesConfiguration.getClassesPathOnImage()); + + String classPathsString = String.join(":", classPaths); + + List entrypoint = new ArrayList<>(4 + jvmFlags.size()); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-cp"); + entrypoint.add(classPathsString); + entrypoint.add(mainClass); + return entrypoint; + } + + private EntrypointBuilder() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java new file mode 100644 index 0000000000..379904db83 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.filesystem; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** Recursively applies a function to each file in a directory. */ +public class DirectoryWalker { + + private final Path rootDir; + + /** Initialize with a root directory to walk. */ + public DirectoryWalker(Path rootDir) { + if (!Files.isDirectory(rootDir)) { + throw new IllegalArgumentException("rootDir must be a directory"); + } + this.rootDir = rootDir; + } + + /** + * Walks {@link #rootDir} and applies {@code pathConsumer} to each file. Note that {@link + * #rootDir} itself is visited as well. + */ + public void walk(PathConsumer pathConsumer) throws IOException { + try (Stream fileStream = Files.walk(rootDir)) { + fileStream.forEach( + path -> { + try { + pathConsumer.accept(path); + + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java new file mode 100644 index 0000000000..ba34d0027c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.filesystem; + +import java.io.IOException; +import java.nio.file.Path; + +@FunctionalInterface +public interface PathConsumer { + + void accept(Path path) throws IOException; +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java similarity index 74% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildImageStepsTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java index 0266c335fb..c56864aa96 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildImageStepsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java @@ -16,18 +16,17 @@ package com.google.cloud.tools.jib.builder; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; -/** Tests for {@link BuildImageSteps}. More comprehensive tests are in the integration tests. */ -public class BuildImageStepsTest { +/** Tests for {@link EntrypointBuilder}. */ +public class EntrypointBuilderTest { @Test - public void testGetEntrypoint() { + public void testMakeEntrypoint() { String expectedDependenciesPath = "/app/libs/"; String expectedResourcesPath = "/app/resources/"; String expectedClassesPath = "/app/classes/"; @@ -36,7 +35,6 @@ public void testGetEntrypoint() { SourceFilesConfiguration mockSourceFilesConfiguration = Mockito.mock(SourceFilesConfiguration.class); - BuildConfiguration mockBuildConfiguration = Mockito.mock(BuildConfiguration.class); Mockito.when(mockSourceFilesConfiguration.getDependenciesPathOnImage()) .thenReturn(expectedDependenciesPath); @@ -45,9 +43,6 @@ public void testGetEntrypoint() { Mockito.when(mockSourceFilesConfiguration.getClassesPathOnImage()) .thenReturn(expectedClassesPath); - Mockito.when(mockBuildConfiguration.getJvmFlags()).thenReturn(expectedJvmFlags); - Mockito.when(mockBuildConfiguration.getMainClass()).thenReturn(expectedMainClass); - Assert.assertEquals( Arrays.asList( "java", @@ -56,8 +51,7 @@ public void testGetEntrypoint() { "-cp", "/app/libs/*:/app/resources/:/app/classes/", "SomeMainClass"), - new BuildImageSteps( - mockBuildConfiguration, mockSourceFilesConfiguration, Mockito.mock(Path.class)) - .getEntrypoint()); + EntrypointBuilder.makeEntrypoint( + mockSourceFilesConfiguration, expectedJvmFlags, expectedMainClass)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java new file mode 100644 index 0000000000..4e294fdbea --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.filesystem; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DirectoryWalker}. */ +public class DirectoryWalkerTest { + + @Test + public void testWalk() throws URISyntaxException, IOException { + Path testDir = Paths.get(Resources.getResource("layer").toURI()); + + Set walkedPaths = new HashSet<>(); + PathConsumer addToWalkedPaths = walkedPaths::add; + + new DirectoryWalker(testDir).walk(addToWalkedPaths); + + Set expectedPaths = + new HashSet<>( + Arrays.asList( + testDir, + testDir.resolve("a"), + testDir.resolve("a").resolve("b"), + testDir.resolve("a").resolve("b").resolve("bar"), + testDir.resolve("c"), + testDir.resolve("c").resolve("cat"), + testDir.resolve("foo"))); + Assert.assertEquals(expectedPaths, walkedPaths); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 457f00d56a..9d8fd014e4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -46,7 +46,6 @@ import javax.annotation.Nullable; import org.apache.http.conn.HttpHostConnectException; import org.apache.maven.execution.MavenSession; -import org.apache.maven.model.Plugin; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -55,7 +54,6 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Server; -import org.codehaus.plexus.util.xml.Xpp3Dom; /** Builds a container image. */ @Mojo(name = "build", requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) @@ -117,22 +115,19 @@ private Class getManifestTemplateClass() { public void execute() throws MojoExecutionException, MojoFailureException { validateParameters(); - // Extracts main class from 'maven-jar-plugin' configuration if available. + ProjectProperties projectProperties = new ProjectProperties(project, getLog()); + if (mainClass == null) { - Plugin mavenJarPlugin = project.getPlugin("org.apache.maven.plugins:maven-jar-plugin"); - if (mavenJarPlugin != null) { - mainClass = getMainClassFromMavenJarPlugin(mavenJarPlugin); - if (mainClass == null) { - throwMojoExecutionExceptionWithHelpMessage( - new MojoFailureException("Could not find main class specified in maven-jar-plugin"), - "add a `mainClass` configuration to jib-maven-plugin"); - } - - getLog().info("Using main class from maven-jar-plugin: " + mainClass); + mainClass = projectProperties.getMainClassFromMavenJarPlugin(); + if (mainClass == null) { + throwMojoExecutionExceptionWithHelpMessage( + new MojoFailureException("Could not find main class specified in maven-jar-plugin"), + "add a `mainClass` configuration to jib-maven-plugin"); } } - SourceFilesConfiguration sourceFilesConfiguration = getSourceFilesConfiguration(); + SourceFilesConfiguration sourceFilesConfiguration = + projectProperties.getSourceFilesConfiguration(); // Parses 'from' into image reference. ImageReference baseImage = getBaseImageReference(); @@ -275,66 +270,6 @@ private void validateParameters() throws MojoFailureException { } } - /** @return the {@link SourceFilesConfiguration} based on the current project */ - private SourceFilesConfiguration getSourceFilesConfiguration() throws MojoExecutionException { - try { - SourceFilesConfiguration sourceFilesConfiguration = - new MavenSourceFilesConfiguration(project); - - // Logs the different source files used. - getLog().info(""); - getLog().info("Containerizing application with the following files:"); - getLog().info(""); - - getLog().info("\tDependencies:"); - getLog().info(""); - sourceFilesConfiguration - .getDependenciesFiles() - .forEach(dependencyFile -> getLog().info("\t\t" + dependencyFile)); - - getLog().info("\tResources:"); - getLog().info(""); - sourceFilesConfiguration - .getResourcesFiles() - .forEach(resourceFile -> getLog().info("\t\t" + resourceFile)); - - getLog().info("\tClasses:"); - getLog().info(""); - sourceFilesConfiguration - .getClassesFiles() - .forEach(classesFile -> getLog().info("\t\t" + classesFile)); - - getLog().info(""); - - return sourceFilesConfiguration; - - } catch (IOException ex) { - throw new MojoExecutionException("Obtaining project build output files failed", ex); - } - } - - /** Gets the {@code mainClass} configuration from {@code maven-jar-plugin}. */ - @Nullable - private String getMainClassFromMavenJarPlugin(Plugin mavenJarPlugin) { - Xpp3Dom jarConfiguration = (Xpp3Dom) mavenJarPlugin.getConfiguration(); - if (jarConfiguration == null) { - return null; - } - Xpp3Dom archiveObject = jarConfiguration.getChild("archive"); - if (archiveObject == null) { - return null; - } - Xpp3Dom manifestObject = archiveObject.getChild("manifest"); - if (manifestObject == null) { - return null; - } - Xpp3Dom mainClassObject = manifestObject.getChild("mainClass"); - if (mainClassObject == null) { - return null; - } - return mainClassObject.getValue(); - } - /** @return the {@link ImageReference} parsed from {@link #from}. */ private ImageReference getBaseImageReference() throws MojoFailureException { try { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java new file mode 100644 index 0000000000..55d9bb1152 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -0,0 +1,238 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.builder.EntrypointBuilder; +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.PathConsumer; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.MoreFiles; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import javax.annotation.Nullable; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; + +/** Exports to a Docker context. This is an incubating feature. */ +@Mojo(name = "dockercontext", requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) +public class DockerContextMojo extends AbstractMojo { + + /** Copies {@code sourceFiles} to the {@code destDir} directory. */ + @VisibleForTesting + static void copyFiles(List sourceFiles, Path destDir) throws IOException { + for (Path sourceFile : sourceFiles) { + PathConsumer copyPathConsumer = + path -> { + // Creates the same path in the destDir. + Path destPath = destDir.resolve(sourceFile.getParent().relativize(path)); + if (Files.isDirectory(path)) { + Files.createDirectory(destPath); + } else { + Files.copy(path, destPath); + } + }; + + if (Files.isDirectory(sourceFile)) { + new DirectoryWalker(sourceFile).walk(copyPathConsumer); + } else { + copyPathConsumer.accept(sourceFile); + } + } + } + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Parameter( + property = "jib.dockerDir", + defaultValue = "${project.build.directory}/jib-dockercontext", + required = true + ) + private String targetDir; + + @Parameter(defaultValue = "gcr.io/distroless/java", required = true) + private String from; + + @Parameter private List jvmFlags = Collections.emptyList(); + + @Parameter private Map environment; + + @Parameter private String mainClass; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + ProjectProperties projectProperties = new ProjectProperties(project, getLog()); + + if (mainClass == null) { + mainClass = projectProperties.getMainClassFromMavenJarPlugin(); + if (mainClass == null) { + throwMojoExecutionExceptionWithHelpMessage( + new MojoFailureException("Could not find main class specified in maven-jar-plugin"), + "add a `mainClass` configuration to jib-maven-plugin"); + } + } + + createDockerContext(projectProperties); + } + + @VisibleForTesting + DockerContextMojo setJvmFlags(List jvmFlags) { + this.jvmFlags = jvmFlags; + return this; + } + + @VisibleForTesting + DockerContextMojo setMainClass(String mainClass) { + this.mainClass = mainClass; + return this; + } + + @VisibleForTesting + DockerContextMojo setTargetDir(String targetDir) { + this.targetDir = targetDir; + return this; + } + + @VisibleForTesting + DockerContextMojo setFrom(String baseImage) { + from = baseImage; + return this; + } + + @VisibleForTesting + /** Makes a {@code Dockerfile} from the {@code DockerfileTemplate}. */ + String makeDockerfile(SourceFilesConfiguration sourceFilesConfiguration) + throws IOException, URISyntaxException { + Path dockerfileTemplate = Paths.get(Resources.getResource("DockerfileTemplate").toURI()); + + String dockerfile = new String(Files.readAllBytes(dockerfileTemplate), StandardCharsets.UTF_8); + dockerfile = + dockerfile + .replace("@@BASE_IMAGE@@", from) + .replace( + "@@DEPENDENCIES_PATH_ON_IMAGE@@", + sourceFilesConfiguration.getDependenciesPathOnImage()) + .replace( + "@@RESOURCES_PATH_ON_IMAGE@@", sourceFilesConfiguration.getResourcesPathOnImage()) + .replace("@@CLASSES_PATH_ON_IMAGE@@", sourceFilesConfiguration.getClassesPathOnImage()) + .replace("@@ENTRYPOINT@@", getEntrypoint(sourceFilesConfiguration)); + return dockerfile; + } + + /** + * Gets the Dockerfile ENTRYPOINT in exec-form. + * + * @see https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example + */ + @VisibleForTesting + String getEntrypoint(SourceFilesConfiguration sourceFilesConfiguration) { + List entrypoint = + EntrypointBuilder.makeEntrypoint(sourceFilesConfiguration, jvmFlags, mainClass); + + StringBuilder entrypointString = new StringBuilder("["); + boolean firstComponent = true; + for (String entrypointComponent : entrypoint) { + if (!firstComponent) { + entrypointString.append(','); + } + + // Escapes quotes. + entrypointComponent = entrypointComponent.replaceAll("\"", Matcher.quoteReplacement("\\\"")); + + entrypointString.append('"').append(entrypointComponent).append('"'); + firstComponent = false; + } + entrypointString.append(']'); + + return entrypointString.toString(); + } + + // TODO: Move most of this to jib-core. + /** Creates the Docker context in {@link #targetDir}. */ + private void createDockerContext(ProjectProperties projectProperties) + throws MojoExecutionException, MojoFailureException { + SourceFilesConfiguration sourceFilesConfiguration = + projectProperties.getSourceFilesConfiguration(); + + try { + Path targetDirPath = Paths.get(targetDir); + + // Deletes the targetDir if it exists. + if (Files.exists(targetDirPath)) { + MoreFiles.deleteDirectoryContents(targetDirPath); + } + + Files.createDirectory(targetDirPath); + + // Creates the directories. + Path dependenciesDir = targetDirPath.resolve("libs"); + Path resourcesDIr = targetDirPath.resolve("resources"); + Path classesDir = targetDirPath.resolve("classes"); + Files.createDirectory(dependenciesDir); + Files.createDirectory(resourcesDIr); + Files.createDirectory(classesDir); + + // Copies dependencies. + copyFiles(sourceFilesConfiguration.getDependenciesFiles(), dependenciesDir); + copyFiles(sourceFilesConfiguration.getResourcesFiles(), resourcesDIr); + copyFiles(sourceFilesConfiguration.getClassesFiles(), classesDir); + + // Creates the Dockerfile. + Files.write( + targetDirPath.resolve("Dockerfile"), + makeDockerfile(sourceFilesConfiguration).getBytes(StandardCharsets.UTF_8)); + + projectProperties.getLog().info("Created Docker context at " + targetDir); + + } catch (IOException ex) { + throwMojoExecutionExceptionWithHelpMessage(ex, "check if `targetDir` is set correctly"); + + } catch (URISyntaxException ex) { + throw new MojoFailureException("Unexpected URISyntaxException", ex); + } + } + + /** + * Wraps an exception in a {@link MojoExecutionException} and provides a suggestion on how to fix + * the error. + */ + private void throwMojoExecutionExceptionWithHelpMessage( + T ex, @Nullable String suggestion) throws MojoExecutionException { + StringBuilder message = new StringBuilder("Export Docker context failed"); + if (suggestion != null) { + message.append(", perhaps you should "); + message.append(suggestion); + } + throw new MojoExecutionException(message.toString(), ex); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java new file mode 100644 index 0000000000..3da27b1bbc --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java @@ -0,0 +1,117 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import java.io.IOException; +import javax.annotation.Nullable; +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; + +/** Obtains information about a {@link MavenProject}. */ +class ProjectProperties { + + private final MavenProject project; + private final Log log; + + ProjectProperties(MavenProject project, Log log) { + this.project = project; + this.log = log; + } + + /** @return the {@link SourceFilesConfiguration} based on the current project */ + SourceFilesConfiguration getSourceFilesConfiguration() throws MojoExecutionException { + try { + SourceFilesConfiguration sourceFilesConfiguration = + new MavenSourceFilesConfiguration(project); + + // Logs the different source files used. + log.info(""); + log.info("Containerizing application with the following files:"); + log.info(""); + + log.info("\tDependencies:"); + log.info(""); + sourceFilesConfiguration + .getDependenciesFiles() + .forEach(dependencyFile -> log.info("\t\t" + dependencyFile)); + + log.info("\tResources:"); + log.info(""); + sourceFilesConfiguration + .getResourcesFiles() + .forEach(resourceFile -> log.info("\t\t" + resourceFile)); + + log.info("\tClasses:"); + log.info(""); + sourceFilesConfiguration + .getClassesFiles() + .forEach(classesFile -> log.info("\t\t" + classesFile)); + + log.info(""); + + return sourceFilesConfiguration; + + } catch (IOException ex) { + throw new MojoExecutionException("Obtaining project build output files failed", ex); + } + } + + /** Extracts main class from 'maven-jar-plugin' configuration if available. */ + @Nullable + String getMainClassFromMavenJarPlugin() { + Plugin mavenJarPlugin = project.getPlugin("org.apache.maven.plugins:maven-jar-plugin"); + if (mavenJarPlugin != null) { + String mainClass = getMainClassFromMavenJarPlugin(mavenJarPlugin); + if (mainClass != null) { + log.info("Using main class from maven-jar-plugin: " + mainClass); + return mainClass; + } + } + return null; + } + + /** Gets the {@code mainClass} configuration from {@code maven-jar-plugin}. */ + @Nullable + private String getMainClassFromMavenJarPlugin(Plugin mavenJarPlugin) { + Xpp3Dom jarConfiguration = (Xpp3Dom) mavenJarPlugin.getConfiguration(); + if (jarConfiguration == null) { + return null; + } + Xpp3Dom archiveObject = jarConfiguration.getChild("archive"); + if (archiveObject == null) { + return null; + } + Xpp3Dom manifestObject = archiveObject.getChild("manifest"); + if (manifestObject == null) { + return null; + } + Xpp3Dom mainClassObject = manifestObject.getChild("mainClass"); + if (mainClassObject == null) { + return null; + } + return mainClassObject.getValue(); + } + + /** Returns the Maven logger. */ + Log getLog() { + return log; + } +} diff --git a/jib-maven-plugin/src/main/resources/DockerfileTemplate b/jib-maven-plugin/src/main/resources/DockerfileTemplate new file mode 100644 index 0000000000..10d8d673b8 --- /dev/null +++ b/jib-maven-plugin/src/main/resources/DockerfileTemplate @@ -0,0 +1,7 @@ +FROM @@BASE_IMAGE@@ + +COPY libs @@DEPENDENCIES_PATH_ON_IMAGE@@ +COPY resources @@RESOURCES_PATH_ON_IMAGE@@ +COPY classes @@CLASSES_PATH_ON_IMAGE@@ + +ENTRYPOINT @@ENTRYPOINT@@ diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java new file mode 100644 index 0000000000..c76ac262e1 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link DockerContextMojo}. */ +@RunWith(MockitoJUnitRunner.class) +public class DockerContextMojoTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; + + @Before + public void setUpMocks() { + String expectedDependenciesPath = "/app/libs/"; + String expectedResourcesPath = "/app/resources/"; + String expectedClassesPath = "/app/classes/"; + + Mockito.when(mockSourceFilesConfiguration.getDependenciesPathOnImage()) + .thenReturn(expectedDependenciesPath); + Mockito.when(mockSourceFilesConfiguration.getResourcesPathOnImage()) + .thenReturn(expectedResourcesPath); + Mockito.when(mockSourceFilesConfiguration.getClassesPathOnImage()) + .thenReturn(expectedClassesPath); + } + + @Test + public void testGetEntrypoint() { + List expectedJvmFlags = Arrays.asList("-flag", "another\"Flag"); + String expectedMainClass = "SomeMainClass"; + + DockerContextMojo dockerContextMojo = + new DockerContextMojo().setJvmFlags(expectedJvmFlags).setMainClass(expectedMainClass); + + Assert.assertEquals( + "[\"java\",\"-flag\",\"another\\\"Flag\",\"-cp\",\"/app/libs/*:/app/resources/:/app/classes/\",\"SomeMainClass\"]", + dockerContextMojo.getEntrypoint(mockSourceFilesConfiguration)); + } + + @Test + public void testCopyFiles() throws IOException, URISyntaxException { + Path destDir = temporaryFolder.newFolder().toPath(); + Path libraryA = + Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()); + Path libraryB = + Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI()); + Path dirLayer = Paths.get(Resources.getResource("layer").toURI()); + + DockerContextMojo.copyFiles(Arrays.asList(libraryA, libraryB, dirLayer), destDir); + + assertFilesEqual(libraryA, destDir.resolve("libraryA.jar")); + assertFilesEqual(libraryB, destDir.resolve("libraryB.jar")); + Assert.assertTrue(Files.exists(destDir.resolve("layer").resolve("a").resolve("b"))); + Assert.assertTrue(Files.exists(destDir.resolve("layer").resolve("c"))); + assertFilesEqual( + dirLayer.resolve("a").resolve("b").resolve("bar"), + destDir.resolve("layer").resolve("a").resolve("b").resolve("bar")); + assertFilesEqual( + dirLayer.resolve("c").resolve("cat"), destDir.resolve("layer").resolve("c").resolve("cat")); + assertFilesEqual(dirLayer.resolve("foo"), destDir.resolve("layer").resolve("foo")); + } + + @Test + public void testMakeDockerfile() throws IOException, URISyntaxException { + Path testTargetDir = temporaryFolder.newFolder().toPath(); + + String expectedBaseImage = "somebaseimage"; + List expectedJvmFlags = Arrays.asList("-flag", "another\"Flag"); + String expectedMainClass = "SomeMainClass"; + + String dockerfile = + new DockerContextMojo() + .setTargetDir(testTargetDir.toString()) + .setFrom(expectedBaseImage) + .setJvmFlags(expectedJvmFlags) + .setMainClass(expectedMainClass) + .makeDockerfile(mockSourceFilesConfiguration); + + System.out.println(dockerfile); + + Path sampleDockerfile = Paths.get(Resources.getResource("sampleDockerfile").toURI()); + Assert.assertArrayEquals( + Files.readAllBytes(sampleDockerfile), dockerfile.getBytes(StandardCharsets.UTF_8)); + } + + private void assertFilesEqual(Path file1, Path file2) throws IOException { + Assert.assertArrayEquals(Files.readAllBytes(file1), Files.readAllBytes(file2)); + } +} diff --git a/jib-maven-plugin/src/test/resources/layer/a/b/bar b/jib-maven-plugin/src/test/resources/layer/a/b/bar new file mode 100644 index 0000000000..5716ca5987 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/layer/a/b/bar @@ -0,0 +1 @@ +bar diff --git a/jib-maven-plugin/src/test/resources/layer/c/cat b/jib-maven-plugin/src/test/resources/layer/c/cat new file mode 100644 index 0000000000..ef07ddcd0a --- /dev/null +++ b/jib-maven-plugin/src/test/resources/layer/c/cat @@ -0,0 +1 @@ +cat diff --git a/jib-maven-plugin/src/test/resources/layer/foo b/jib-maven-plugin/src/test/resources/layer/foo new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/jib-maven-plugin/src/test/resources/layer/foo @@ -0,0 +1 @@ +foo diff --git a/jib-maven-plugin/src/test/resources/sampleDockerfile b/jib-maven-plugin/src/test/resources/sampleDockerfile new file mode 100644 index 0000000000..26cc9af20b --- /dev/null +++ b/jib-maven-plugin/src/test/resources/sampleDockerfile @@ -0,0 +1,7 @@ +FROM somebaseimage + +COPY libs /app/libs/ +COPY resources /app/resources/ +COPY classes /app/classes/ + +ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/libs/*:/app/resources/:/app/classes/","SomeMainClass"]