From f12e684de46de1b4accc63544298dba3e31cf1f2 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 15 Jun 2020 16:09:04 +0300 Subject: [PATCH] Support jib extra directories feature Resolves: #8936 --- bom/deployment/pom.xml | 5 + docs/src/main/asciidoc/container-image.adoc | 7 ++ .../docker/deployment/DockerProcessor.java | 21 +--- .../deployment/ContainerBuilderHelper.java | 100 ++++++++++++++++++ .../image/jib/deployment/JibProcessor.java | 48 ++++++++- extensions/container-image/deployment/pom.xml | 4 + extensions/container-image/pom.xml | 1 + extensions/container-image/util/pom.xml | 15 +++ .../io/quarkus/container/util/PathsUtil.java | 31 ++++++ 9 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerBuilderHelper.java create mode 100644 extensions/container-image/util/pom.xml create mode 100644 extensions/container-image/util/src/main/java/io/quarkus/container/util/PathsUtil.java diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index 8057e4e8fe0a3..dcfd4ab4126be 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -647,6 +647,11 @@ quarkus-container-image-spi ${project.version} + + io.quarkus + quarkus-container-image-util + ${project.version} + io.quarkus quarkus-container-image-docker-deployment diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index 0b895fb105a6e..7a54789b325ad 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -34,6 +34,13 @@ NOTE: In situations where all that is needed to build a container image and no p it with the Docker daemon. This means that although Docker isn't used to build the image, it is nevertheless necessary. Also note that using this mode, the built container image *will* show up when executing `docker images`. +==== Including extra files + +There are cases when additional files (other than ones produced by the Quarkus build) need to be added to a container image. +To support these cases, Quarkus copies any file under `src/main/jib` into the built container image (which is essentially the same +idea that the Jib Maven and Gradle plugins support). +For example, the presence of `src/main/jib/foo/bar` would result in `/foo/bar` being added into the container filesystem. + [#docker] === Docker diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index 5d91520201094..b44f1aac41c18 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -1,6 +1,8 @@ package io.quarkus.container.image.docker.deployment; +import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -339,23 +341,4 @@ public Path getDockerExecutionPath() { } } - /** - * Return a Map.Entry (which is used as a Tuple) containing the main sources root as the key - * and the project root as the value - */ - private static AbstractMap.SimpleEntry findMainSourcesRoot(Path outputDirectory) { - Path currentPath = outputDirectory; - do { - Path toCheck = currentPath.resolve(Paths.get("src", "main")); - if (toCheck.toFile().exists()) { - return new AbstractMap.SimpleEntry<>(toCheck, currentPath); - } - if (Files.exists(currentPath.getParent())) { - currentPath = currentPath.getParent(); - } else { - return null; - } - } while (true); - } - } diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerBuilderHelper.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerBuilderHelper.java new file mode 100644 index 0000000000000..2769f1abc90c1 --- /dev/null +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerBuilderHelper.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 Google LLC. + * + * 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 io.quarkus.container.image.jib.deployment; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiFunction; + +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; + +/** + * Copied almost verbatim from Jib's {@code com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper} + * because the module that contains it is internal to Jib + */ +final class ContainerBuilderHelper { + + private ContainerBuilderHelper() { + } + + /** + * Returns a {@link FileEntriesLayer} for adding the extra directory to the container. + * + * @param sourceDirectory the source extra directory path + * @param targetDirectory the root directory on the container to place the files in + * @param extraDirectoryPermissions map from path on container to file permissions + * @param modificationTimeProvider file modification time provider + * @return a {@link FileEntriesLayer} for adding the extra directory to the container + * @throws IOException if walking the extra directory fails + */ + public static FileEntriesLayer extraDirectoryLayerConfiguration( + Path sourceDirectory, + AbsoluteUnixPath targetDirectory, + Map extraDirectoryPermissions, + BiFunction modificationTimeProvider) + throws IOException { + FileEntriesLayer.Builder builder = FileEntriesLayer.builder() + .setName(JavaContainerBuilder.LayerType.EXTRA_FILES.getName()); + Map pathMatchers = new LinkedHashMap<>(); + for (Map.Entry entry : extraDirectoryPermissions.entrySet()) { + pathMatchers.put( + FileSystems.getDefault().getPathMatcher("glob:" + entry.getKey()), entry.getValue()); + } + + new DirectoryWalker(sourceDirectory) + .filterRoot() + .walk( + localPath -> { + AbsoluteUnixPath pathOnContainer = targetDirectory.resolve(sourceDirectory.relativize(localPath)); + Instant modificationTime = modificationTimeProvider.apply(localPath, pathOnContainer); + FilePermissions permissions = extraDirectoryPermissions.get(pathOnContainer.toString()); + if (permissions == null) { + // Check for matching globs + Path containerPath = Paths.get(pathOnContainer.toString()); + for (Map.Entry entry : pathMatchers.entrySet()) { + if (entry.getKey().matches(containerPath)) { + builder.addEntry( + localPath, pathOnContainer, entry.getValue(), modificationTime); + return; + } + } + + // Add with default permissions + if (localPath.toFile().canExecute()) { + // make sure we can execute the file in the container + builder.addEntry(localPath, pathOnContainer, FilePermissions.fromOctalString("754"), + modificationTime); + } else { + builder.addEntry(localPath, pathOnContainer, modificationTime); + } + } else { + // Add with explicit permissions + builder.addEntry(localPath, pathOnContainer, permissions, modificationTime); + } + }); + return builder.build(); + } +} diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 66b25eb4720f9..a56997bb05360 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -1,5 +1,7 @@ package io.quarkus.container.image.jib.deployment; +import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; + import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -25,7 +27,6 @@ import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; @@ -40,6 +41,7 @@ import io.quarkus.container.spi.ContainerImageInfoBuildItem; import io.quarkus.container.spi.ContainerImageLabelBuildItem; import io.quarkus.container.spi.ContainerImagePushRequestBuildItem; +import io.quarkus.container.util.PathsUtil; import io.quarkus.deployment.Capability; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; @@ -97,6 +99,7 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji throw new IllegalArgumentException( "Package type '" + packageType + "' is not supported by the container-image-jib extension"); } + handleExtraFiles(outputTarget, jibContainerBuilder); containerize(applicationInfo, containerImageConfig, containerImage, jibContainerBuilder, pushRequest.isPresent()); @@ -108,6 +111,7 @@ public void buildFromNative(ContainerImageConfig containerImageConfig, JibConfig ContainerImageInfoBuildItem containerImage, NativeImageBuildItem nativeImage, ApplicationInfoBuildItem applicationInfo, + OutputTargetBuildItem outputTarget, Optional buildRequest, Optional pushRequest, List containerImageLabels, @@ -125,7 +129,7 @@ public void buildFromNative(ContainerImageConfig containerImageConfig, JibConfig JibContainerBuilder jibContainerBuilder = createContainerBuilderFromNative(containerImageConfig, jibConfig, nativeImage, containerImageLabels); - + handleExtraFiles(outputTarget, jibContainerBuilder); containerize(applicationInfo, containerImageConfig, containerImage, jibContainerBuilder, pushRequest.isPresent()); @@ -338,6 +342,46 @@ private JibContainerBuilder createContainerBuilderFromNative(ContainerImageConfi } } + /** + * Allow users to have custom files in {@code src/main/jib} that will be copied into the built container's file system + * in same manner as the Jib Maven and Gradle plugins do. + * For example, {@code src/main/jib/foo/bar} would add {@code /foo/bar} into the container filesystem. + * + * See: https://github.com/GoogleContainerTools/jib/blob/v0.15.0-core/docs/faq.md#can-i-add-a-custom-directory-to-the-image + */ + private void handleExtraFiles(OutputTargetBuildItem outputTarget, JibContainerBuilder jibContainerBuilder) { + Path outputDirectory = outputTarget.getOutputDirectory(); + PathsUtil.findMainSourcesRoot(outputTarget.getOutputDirectory()); + Map.Entry mainSourcesRoot = findMainSourcesRoot(outputDirectory); + if (mainSourcesRoot == null) { // this should never happen + return; + } + Path jibFilesRoot = mainSourcesRoot.getKey().resolve("jib"); + if (!jibFilesRoot.toFile().exists()) { + return; + } + + FileEntriesLayer extraFilesLayer; + try { + extraFilesLayer = ContainerBuilderHelper.extraDirectoryLayerConfiguration( + jibFilesRoot, + AbsoluteUnixPath.get("/"), + Collections.emptyMap(), + (localPath, ignored2) -> { + try { + return Files.getLastModifiedTime(localPath).toInstant(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + jibContainerBuilder.addFileEntriesLayer( + extraFilesLayer); + } catch (IOException e) { + throw new UncheckedIOException( + "Unable to add extra files in '" + jibFilesRoot.toAbsolutePath().toString() + "' to the container", e); + } + } + private Map allLabels(JibConfig jibConfig, List containerImageLabels) { if (jibConfig.labels.isEmpty() && containerImageLabels.isEmpty()) { return Collections.emptyMap(); diff --git a/extensions/container-image/deployment/pom.xml b/extensions/container-image/deployment/pom.xml index 720011e68e262..02f8c41496f74 100644 --- a/extensions/container-image/deployment/pom.xml +++ b/extensions/container-image/deployment/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-container-image-spi + + io.quarkus + quarkus-container-image-util + org.junit.jupiter diff --git a/extensions/container-image/pom.xml b/extensions/container-image/pom.xml index cd17d6f9ceff5..d2a337a0e29c4 100644 --- a/extensions/container-image/pom.xml +++ b/extensions/container-image/pom.xml @@ -17,6 +17,7 @@ deployment spi + util container-image-docker container-image-jib container-image-s2i diff --git a/extensions/container-image/util/pom.xml b/extensions/container-image/util/pom.xml new file mode 100644 index 0000000000000..b225b53052902 --- /dev/null +++ b/extensions/container-image/util/pom.xml @@ -0,0 +1,15 @@ + + + quarkus-container-image-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-container-image-util + Quarkus - Container - Image - Util + Utility classes common to the container image extensions + + diff --git a/extensions/container-image/util/src/main/java/io/quarkus/container/util/PathsUtil.java b/extensions/container-image/util/src/main/java/io/quarkus/container/util/PathsUtil.java new file mode 100644 index 0000000000000..1a6dad4c61afa --- /dev/null +++ b/extensions/container-image/util/src/main/java/io/quarkus/container/util/PathsUtil.java @@ -0,0 +1,31 @@ +package io.quarkus.container.util; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap; + +public final class PathsUtil { + + private PathsUtil() { + } + + /** + * Return a Map.Entry (which is used as a Tuple) containing the main sources root as the key + * and the project root as the value + */ + public static AbstractMap.SimpleEntry findMainSourcesRoot(Path outputDirectory) { + Path currentPath = outputDirectory; + do { + Path toCheck = currentPath.resolve(Paths.get("src", "main")); + if (toCheck.toFile().exists()) { + return new AbstractMap.SimpleEntry<>(toCheck, currentPath); + } + if (Files.exists(currentPath.getParent())) { + currentPath = currentPath.getParent(); + } else { + return null; + } + } while (true); + } +}