From cb4928ad51383866f04805c0b5c6924e0da12109 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Jan 2020 15:43:22 -0800 Subject: [PATCH] Add build-image support to the maven plugin Add a new `build-image` goal to the Maven plugin to allow Docker images to be create via using the cloud native buildpack. See gh-19830 --- .../spring-boot-maven-plugin/build.gradle | 1 + .../boot/maven/AbstractPackagerMojo.java | 199 ++++++++++++++ .../boot/maven/BuildImageMojo.java | 250 ++++++++++++++++++ .../org/springframework/boot/maven/Image.java | 88 ++++++ .../boot/maven/LayoutType.java | 68 +++++ ...oggingMainClassTimeoutWarningListener.java | 44 +++ .../boot/maven/RepackageMojo.java | 155 +---------- .../boot/maven/ImageTests.java | 107 ++++++++ 8 files changed, 760 insertions(+), 152 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle index e981387746..b7b4368d2a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -15,6 +15,7 @@ configurations { dependencies { api platform(project(':spring-boot-project:spring-boot-parent')) + api project(':spring-boot-project:spring-boot-tools:spring-boot-cloudnativebuildpack') compileOnly 'org.apache.maven.plugin-tools:maven-plugin-annotations' compileOnly 'org.sonatype.plexus:plexus-build-api' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java new file mode 100644 index 0000000000..30c56e981c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.boot.maven; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; +import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; + +import org.springframework.boot.loader.tools.Layout; +import org.springframework.boot.loader.tools.LayoutFactory; +import org.springframework.boot.loader.tools.Layouts.Expanded; +import org.springframework.boot.loader.tools.Layouts.Jar; +import org.springframework.boot.loader.tools.Layouts.LayeredJar; +import org.springframework.boot.loader.tools.Layouts.None; +import org.springframework.boot.loader.tools.Layouts.War; +import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.Packager; + +/** + * Abstract base class for classes that work with an {@link Packager}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo { + + /** + * The Maven project. + * @since 1.0.0 + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + protected MavenProject project; + + /** + * Maven project helper utils. + * @since 1.0.0 + */ + @Component + protected MavenProjectHelper projectHelper; + + /** + * The name of the main class. If not specified the first compiled class found that + * contains a 'main' method will be used. + * @since 1.0.0 + */ + @Parameter + private String mainClass; + + /** + * The type of archive (which corresponds to how the dependencies are laid out inside + * it). Possible values are JAR, LAYERED_JAR, WAR, ZIP, DIR, NONE. Defaults to a guess + * based on the archive type. + * @since 1.0.0 + */ + @Parameter(property = "spring-boot.repackage.layout") + private LayoutType layout; + + /** + * The layout factory that will be used to create the executable archive if no + * explicit layout is set. Alternative layouts implementations can be provided by 3rd + * parties. + * @since 1.5.0 + */ + @Parameter + private LayoutFactory layoutFactory; + + /** + * Exclude Spring Boot devtools from the repackaged archive. + * @since 1.3.0 + */ + @Parameter(property = "spring-boot.repackage.excludeDevtools", defaultValue = "true") + private boolean excludeDevtools = true; + + /** + * Include system scoped dependencies. + * @since 1.4.0 + */ + @Parameter(defaultValue = "false") + public boolean includeSystemScope; + + /** + * Return a {@link Packager} configured for this MOJO. + * @param

the packager type + * @param supplier a packager supplier + * @return a configured packager + */ + protected

P getConfiguredPackager(Supplier

supplier) { + P packager = supplier.get(); + packager.setLayoutFactory(this.layoutFactory); + packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog)); + packager.setMainClass(this.mainClass); + if (this.layout != null) { + getLog().info("Layout: " + this.layout); + packager.setLayout(this.layout.layout()); + } + return packager; + } + + /** + * Return {@link Libraries} that the packager can use. + * @param unpacks any libraries that require unpack + * @return the libraries to use + * @throws MojoExecutionException on execution error + */ + protected final Libraries getLibraries(Collection unpacks) throws MojoExecutionException { + Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); + Libraries libraries = new ArtifactsLibraries(artifacts, unpacks, getLog()); + return libraries; + } + + private ArtifactsFilter[] getAdditionalFilters() { + List filters = new ArrayList<>(); + if (this.excludeDevtools) { + Exclude exclude = new Exclude(); + exclude.setGroupId("org.springframework.boot"); + exclude.setArtifactId("spring-boot-devtools"); + ExcludeFilter filter = new ExcludeFilter(exclude); + filters.add(filter); + } + if (!this.includeSystemScope) { + filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM)); + } + return filters.toArray(new ArtifactsFilter[0]); + } + + /** + * Archive layout types. + */ + public enum LayoutType { + + /** + * Jar Layout. + */ + JAR(new Jar()), + + /** + * Layered Jar Layout. + */ + LAYERED_JAR(new LayeredJar()), + + /** + * War Layout. + */ + WAR(new War()), + + /** + * Zip Layout. + */ + ZIP(new Expanded()), + + /** + * Dir Layout. + */ + DIR(new Expanded()), + + /** + * No Layout. + */ + NONE(new None()); + + private final Layout layout; + + LayoutType(Layout layout) { + this.layout = layout; + } + + public Layout layout() { + return this.layout; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java new file mode 100644 index 0000000000..1632b87b27 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -0,0 +1,250 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Duration; +import java.util.Collections; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.zip.ZipEntry; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.Execute; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import org.springframework.boot.cloudnativebuildpack.build.AbstractBuildLog; +import org.springframework.boot.cloudnativebuildpack.build.BuildLog; +import org.springframework.boot.cloudnativebuildpack.build.BuildRequest; +import org.springframework.boot.cloudnativebuildpack.build.Builder; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.boot.loader.tools.EntryWriter; +import org.springframework.boot.loader.tools.ImagePackager; +import org.springframework.boot.loader.tools.Libraries; +import org.springframework.util.StringUtils; + +/** + * Package an application into a OCI image using a + * buildpack. + * + * @author Phillip Webb + * @since 2.3.0 + */ +@Mojo(name = "build-image", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) +@Execute(phase = LifecyclePhase.PACKAGE) +public class BuildImageMojo extends AbstractPackagerMojo { + + /** + * Directory containing the JAR. + * @since 2.3.0 + */ + @Parameter(defaultValue = "${project.build.directory}", required = true) + private File sourceDirectory; + + /** + * Name of the JAR. + * @since 2.3.0 + */ + @Parameter(defaultValue = "${project.build.finalName}", readonly = true) + private String finalName; + + /** + * Skip the execution. + * @since 2.3.0 + */ + @Parameter(property = "spring-boot.build-image.skip", defaultValue = "false") + private boolean skip; + + /** + * Classifier used when finding the source jar. + * @since 2.3.0 + */ + @Parameter + private String classifier; + + /** + * Image configuration operations. + * @since 2.3.0 + */ + @Parameter + private Image image; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (this.project.getPackaging().equals("pom")) { + getLog().debug("build-image goal could not be applied to pom project."); + return; + } + if (this.skip) { + getLog().debug("skipping build-image as per configuration."); + return; + } + buildImage(); + } + + private void buildImage() throws MojoExecutionException { + Libraries libraries = getLibraries(Collections.emptySet()); + try { + Builder builder = new Builder(new MojoBuildLog(this::getLog)); + BuildRequest request = getBuildRequest(libraries); + builder.build(request); + } + catch (IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } + + private BuildRequest getBuildRequest(Libraries libraries) { + Function content = (owner) -> getApplicationContent(owner, libraries); + return ((this.image != null) ? this.image : new Image()).getBuildRequest(this.project.getArtifact(), content); + } + + private TarArchive getApplicationContent(Owner owner, Libraries libraries) { + ImagePackager packager = getConfiguredPackager(() -> new ImagePackager(getJarFile())); + return new PackagedTarArchive(owner, libraries, packager); + } + + private File getJarFile() { + // We can use 'project.getArtifact().getFile()' because that was done in a + // forked lifecyle and is now null + StringBuilder name = new StringBuilder(this.finalName); + if (StringUtils.hasText(this.classifier)) { + name.append("-").append(this.classifier); + } + name.append(".jar"); + return new File(this.sourceDirectory, name.toString()); + } + + /** + * {@link BuildLog} backed by Mojo logging. + */ + private static class MojoBuildLog extends AbstractBuildLog { + + private static final long THRESHOLD = Duration.ofSeconds(2).toMillis(); + + private final Supplier log; + + MojoBuildLog(Supplier log) { + this.log = log; + } + + @Override + protected void log(String message) { + this.log.get().info(message); + } + + @Override + protected Consumer getProgressConsumer(String message) { + return new ProgressLog(message); + } + + private class ProgressLog implements Consumer { + + private final String message; + + private long last; + + ProgressLog(String message) { + this.message = message; + this.last = System.currentTimeMillis(); + } + + @Override + public void accept(TotalProgressEvent progress) { + log(progress.getPercent()); + } + + private void log(int percent) { + if (percent == 100 || (System.currentTimeMillis() - this.last) > THRESHOLD) { + MojoBuildLog.this.log.get().info(this.message + " " + percent + "%"); + this.last = System.currentTimeMillis(); + } + } + + } + + } + + /** + * Adapter class to expose the packaged jar as a {@link TarArchive}. + */ + static class PackagedTarArchive implements TarArchive { + + static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); + + private final Owner owner; + + private final Libraries libraries; + + private final ImagePackager packager; + + PackagedTarArchive(Owner owner, Libraries libraries, ImagePackager packager) { + this.owner = owner; + this.libraries = libraries; + this.packager = packager; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream); + this.packager.packageImage(this.libraries, (entry, entryWriter) -> write(entry, entryWriter, tar)); + } + + private void write(ZipEntry jarEntry, EntryWriter entryWriter, TarArchiveOutputStream tar) { + try { + TarArchiveEntry tarEntry = convert(jarEntry); + tar.putArchiveEntry(tarEntry); + if (tarEntry.isFile()) { + entryWriter.write(tar); + } + tar.closeArchiveEntry(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private TarArchiveEntry convert(ZipEntry entry) { + byte linkFlag = (entry.isDirectory()) ? TarConstants.LF_DIR : TarConstants.LF_NORMAL; + TarArchiveEntry tarEntry = new TarArchiveEntry(entry.getName(), linkFlag, true); + tarEntry.setUserId(this.owner.getUid()); + tarEntry.setGroupId(this.owner.getGid()); + tarEntry.setModTime(NORMALIZED_MOD_TIME); + if (!entry.isDirectory()) { + tarEntry.setSize(entry.getSize()); + } + return tarEntry; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java new file mode 100644 index 0000000000..de0eeb2d64 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.boot.maven; + +import java.util.Map; +import java.util.function.Function; + +import org.apache.maven.artifact.Artifact; + +import org.springframework.boot.cloudnativebuildpack.build.BuildRequest; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageName; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.util.StringUtils; + +/** + * Image configuration options. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Image { + + /** + * The name of the created image. + */ + String name; + + /** + * The builder used to create the image. + */ + String builder; + + /** + * Environment properties that should be passed to the builder. + */ + Map env; + + /** + * If the cache should be cleaned before building. + */ + boolean cleanCache; + + /** + * If verbose logging is required. + */ + boolean verboseLogging; + + BuildRequest getBuildRequest(Artifact artifact, Function applicationContent) { + return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent)); + } + + private ImageReference getOrDeduceName(Artifact artifact) { + if (StringUtils.hasText(this.name)) { + return ImageReference.of(this.name); + } + ImageName imageName = ImageName.of(artifact.getArtifactId()); + return ImageReference.of(imageName, artifact.getVersion()); + } + + private BuildRequest customize(BuildRequest request) { + if (StringUtils.hasText(this.builder)) { + request = request.withBuilder(ImageReference.of(this.builder)); + } + if (this.env != null && !this.env.isEmpty()) { + request = request.withEnv(this.env); + } + request = request.withCleanCache(this.cleanCache); + request = request.withVerboseLogging(this.verboseLogging); + return request; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java new file mode 100644 index 0000000000..7029c30bc9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.boot.maven; + +import org.springframework.boot.loader.tools.Layout; +import org.springframework.boot.loader.tools.Layouts.Expanded; +import org.springframework.boot.loader.tools.Layouts.Jar; +import org.springframework.boot.loader.tools.Layouts.None; +import org.springframework.boot.loader.tools.Layouts.War; + +/** + * Archive layout types. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public enum LayoutType { + + /** + * Jar Layout. + */ + JAR(new Jar()), + + /** + * War Layout. + */ + WAR(new War()), + + /** + * Zip Layout. + */ + ZIP(new Expanded()), + + /** + * Dir Layout. + */ + DIR(new Expanded()), + + /** + * No Layout. + */ + NONE(new None()); + + private final Layout layout; + + LayoutType(Layout layout) { + this.layout = layout; + } + + public Layout layout() { + return this.layout; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java new file mode 100644 index 0000000000..7e2b8d4756 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.boot.maven; + +import java.util.function.Supplier; + +import org.apache.maven.plugin.logging.Log; + +import org.springframework.boot.loader.tools.Packager.MainClassTimeoutWarningListener; + +/** + * {@link MainClassTimeoutWarningListener} backed by a supplied Maven {@link Log}. + * + * @author Phillip Webb + */ +class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener { + + private final Supplier log; + + LoggingMainClassTimeoutWarningListener(Supplier log) { + this.log = log; + } + + @Override + public void handleTimeoutWarning(long duration, String mainMethod) { + this.log.get().warn("Searching for the main-class is taking some time, " + + "consider using the mainClass configuration parameter"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 642cf96587..2c417df2d2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -18,38 +18,23 @@ package org.springframework.boot.maven; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Properties; -import java.util.Set; import java.util.regex.Pattern; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; 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; -import org.apache.maven.project.MavenProjectHelper; -import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; -import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.LaunchScript; -import org.springframework.boot.loader.tools.Layout; -import org.springframework.boot.loader.tools.LayoutFactory; -import org.springframework.boot.loader.tools.Layouts.Expanded; -import org.springframework.boot.loader.tools.Layouts.Jar; -import org.springframework.boot.loader.tools.Layouts.LayeredJar; -import org.springframework.boot.loader.tools.Layouts.None; -import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Repackager; -import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener; /** * Repackage existing JAR and WAR archives so that they can be executed from the command @@ -65,24 +50,10 @@ import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningL @Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) -public class RepackageMojo extends AbstractDependencyFilterMojo { +public class RepackageMojo extends AbstractPackagerMojo { private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+"); - /** - * The Maven project. - * @since 1.0.0 - */ - @Parameter(defaultValue = "${project}", readonly = true, required = true) - private MavenProject project; - - /** - * Maven project helper utils. - * @since 1.0.0 - */ - @Component - private MavenProjectHelper projectHelper; - /** * Directory containing the generated archive. * @since 1.0.0 @@ -131,32 +102,6 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { @Parameter(defaultValue = "true") private boolean attach = true; - /** - * The name of the main class. If not specified the first compiled class found that - * contains a 'main' method will be used. - * @since 1.0.0 - */ - @Parameter - private String mainClass; - - /** - * The type of archive (which corresponds to how the dependencies are laid out inside - * it). Possible values are JAR, LAYERED_JAR, WAR, ZIP, DIR, NONE. Defaults to a guess - * based on the archive type. - * @since 1.0.0 - */ - @Parameter(property = "spring-boot.repackage.layout") - private LayoutType layout; - - /** - * The layout factory that will be used to create the executable archive if no - * explicit layout is set. Alternative layouts implementations can be provided by 3rd - * parties. - * @since 1.5.0 - */ - @Parameter - private LayoutFactory layoutFactory; - /** * A list of the libraries that must be unpacked from fat jars in order to run. * Specify each library as a {@code } with a {@code } and a @@ -195,20 +140,6 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { @Parameter private Properties embeddedLaunchScriptProperties; - /** - * Exclude Spring Boot devtools from the repackaged archive. - * @since 1.3.0 - */ - @Parameter(property = "spring-boot.repackage.excludeDevtools", defaultValue = "true") - private boolean excludeDevtools = true; - - /** - * Include system scoped dependencies. - * @since 1.4.0 - */ - @Parameter(defaultValue = "false") - public boolean includeSystemScope; - @Override public void execute() throws MojoExecutionException, MojoFailureException { if (this.project.getPackaging().equals("pom")) { @@ -226,8 +157,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { Artifact source = getSourceArtifact(); File target = getTargetFile(); Repackager repackager = getRepackager(source.getFile()); - Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); - Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog()); + Libraries libraries = getLibraries(this.requiresUnpack); try { LaunchScript launchScript = getLaunchScript(); repackager.repackage(target, libraries, launchScript); @@ -274,29 +204,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { } private Repackager getRepackager(File source) { - Repackager repackager = new Repackager(source, this.layoutFactory); - repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener()); - repackager.setMainClass(this.mainClass); - if (this.layout != null) { - getLog().info("Layout: " + this.layout); - repackager.setLayout(this.layout.layout()); - } - return repackager; - } - - private ArtifactsFilter[] getAdditionalFilters() { - List filters = new ArrayList<>(); - if (this.excludeDevtools) { - Exclude exclude = new Exclude(); - exclude.setGroupId("org.springframework.boot"); - exclude.setArtifactId("spring-boot-devtools"); - ExcludeFilter filter = new ExcludeFilter(exclude); - filters.add(filter); - } - if (!this.includeSystemScope) { - filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM)); - } - return filters.toArray(new ArtifactsFilter[0]); + return getConfiguredPackager(() -> new Repackager(source)); } private LaunchScript getLaunchScript() throws IOException { @@ -361,61 +269,4 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { } } - private class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener { - - @Override - public void handleTimeoutWarning(long duration, String mainMethod) { - getLog().warn("Searching for the main-class is taking some time, " - + "consider using the mainClass configuration parameter"); - } - - } - - /** - * Archive layout types. - */ - public enum LayoutType { - - /** - * Jar Layout. - */ - JAR(new Jar()), - - /** - * Layered Jar Layout. - */ - LAYERED_JAR(new LayeredJar()), - - /** - * War Layout. - */ - WAR(new War()), - - /** - * Zip Layout. - */ - ZIP(new Expanded()), - - /** - * Dir Layout. - */ - DIR(new Expanded()), - - /** - * No Layout. - */ - NONE(new None()); - - private final Layout layout; - - LayoutType(Layout layout) { - this.layout = layout; - } - - public Layout layout() { - return this.layout; - } - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java new file mode 100644 index 0000000000..84123cf86e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.boot.maven; + +import java.util.Collections; +import java.util.function.Function; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.artifact.versioning.VersionRange; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.build.BuildRequest; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link Image}. + * + * @author Phillip Webb + */ +class ImageTests { + + @Test + void getBuildRequestWhenNameIsNullDeducesName() { + BuildRequest request = new Image().getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT"); + } + + @Test + void getBuildEquestWhenNameIsSetUsesName() { + Image image = new Image(); + image.name = "demo"; + BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/demo:latest"); + } + + @Test + void getBuildRequestWhenNoCustomizationsUsesDefaults() { + BuildRequest request = new Image().getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT"); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/cloudfoundry/cnb:0.0.43-bionic"); + assertThat(request.getEnv()).isEmpty(); + assertThat(request.isCleanCache()).isFalse(); + assertThat(request.isVerboseLogging()).isFalse(); + } + + @Test + void getBuildRequestWhenHasBuilderUsesBuilder() { + Image image = new Image(); + image.builder = "springboot/builder:2.2.x"; + BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/springboot/builder:2.2.x"); + } + + @Test + void getBuildRequestWhenHasEnvUsesEnv() { + Image image = new Image(); + image.env = Collections.singletonMap("test", "test"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.getEnv()).containsExactly(entry("test", "test")); + } + + @Test + void getBuildRequestWhenHasCleanCacheUsesCleanCache() { + Image image = new Image(); + image.cleanCache = true; + BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.isCleanCache()).isTrue(); + } + + @Test + void getBuildRequestWhenHasVerboseLoggingUsesVerboseLogging() { + Image image = new Image(); + image.verboseLogging = true; + BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent()); + assertThat(request.isVerboseLogging()).isTrue(); + } + + private Artifact createArtifact() { + return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", + "jar", null, new DefaultArtifactHandler()); + } + + private Function mockAplicationContent() { + return (owner) -> null; + } + +}