diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 16af015d62c..8b24eba140e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -44,6 +44,8 @@ public class BuildRequest { private final ImageReference builder; + private final Creator creator; + private final Map env; private final boolean cleanCache; @@ -59,13 +61,15 @@ public class BuildRequest { this.env = Collections.emptyMap(); this.cleanCache = false; this.verboseLogging = false; + this.creator = Creator.withVersion(""); } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, - Map env, boolean cleanCache, boolean verboseLogging) { + Creator creator, Map env, boolean cleanCache, boolean verboseLogging) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; + this.creator = creator; this.env = env; this.cleanCache = cleanCache; this.verboseLogging = verboseLogging; @@ -78,7 +82,18 @@ public class BuildRequest { */ public BuildRequest withBuilder(ImageReference builder) { Assert.notNull(builder, "Builder must not be null"); - return new BuildRequest(this.name, this.applicationContent, builder.inTaggedForm(), this.env, this.cleanCache, + return new BuildRequest(this.name, this.applicationContent, builder.inTaggedForm(), this.creator, this.env, + this.cleanCache, this.verboseLogging); + } + + /** + * Return a new {@link BuildRequest} with an updated builder. + * @param creator the new {@code Creator} to use + * @return an updated build request + */ + public BuildRequest withCreator(Creator creator) { + Assert.notNull(creator, "Creator must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, creator, this.env, this.cleanCache, this.verboseLogging); } @@ -93,8 +108,8 @@ public class BuildRequest { Assert.hasText(value, "Value must not be empty"); Map env = new LinkedHashMap<>(this.env); env.put(name, value); - return new BuildRequest(this.name, this.applicationContent, this.builder, Collections.unmodifiableMap(env), - this.cleanCache, this.verboseLogging); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, + Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging); } /** @@ -106,7 +121,7 @@ public class BuildRequest { Assert.notNull(env, "Env must not be null"); Map updatedEnv = new LinkedHashMap<>(this.env); updatedEnv.putAll(env); - return new BuildRequest(this.name, this.applicationContent, this.builder, + return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging); } @@ -116,7 +131,7 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withCleanCache(boolean cleanCache) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.env, cleanCache, + return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, this.env, cleanCache, this.verboseLogging); } @@ -126,8 +141,8 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withVerboseLogging(boolean verboseLogging) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.env, this.cleanCache, - verboseLogging); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, this.env, + this.cleanCache, verboseLogging); } /** @@ -157,6 +172,14 @@ public class BuildRequest { return this.builder; } + /** + * Return the {@link Creator} the builder should use. + * @return the {@code Creator} + */ + public Creator getCreator() { + return this.creator; + } + /** * Return any env variable that should be passed to the builder. * @return the builder env diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 7fe3ec4e4bc..fd3a51469e9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -66,7 +66,8 @@ public class Builder { ImageReference runImageReference = getRunImageReference(builderMetadata.getStack()); Image runImage = pullRunImage(request, runImageReference); assertHasExpectedStackId(runImage, stackId); - EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getEnv()); + EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(), + request.getEnv()); this.docker.image().load(builder.getArchive(), UpdateListener.none()); try { executeLifecycle(request, runImageReference, builder); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Creator.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Creator.java new file mode 100644 index 00000000000..d4d38148a38 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Creator.java @@ -0,0 +1,66 @@ +/* + * 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.buildpack.platform.build; + +import org.springframework.util.Assert; + +/** + * Identifying information about the tooling that created a builder. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public class Creator { + + private final String version; + + Creator(String version) { + this.version = version; + } + + /** + * Return the name of the builder creator. + * @return the name + */ + public String getName() { + return "Spring Boot"; + } + + /** + * Return the version of the builder creator. + * @return the version + */ + public String getVersion() { + return this.version; + } + + /** + * Create a new {@code Creator} using the provided version. + * @param version the creator version + * @return a new creator instance + */ + public static Creator withVersion(String version) { + Assert.notNull(version, "Version must not be null"); + return new Creator(version); + } + + @Override + public String toString() { + return getName() + " version " + getVersion(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java index 0546c018e52..5b1b43659ed 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java @@ -42,17 +42,20 @@ class EphemeralBuilder { private final ImageArchive archive; + private final Creator creator; + /** * Create a new {@link EphemeralBuilder} instance. * @param buildOwner the build owner * @param builderImage the image * @param builderMetadata the builder metadata + * @param creator the builder creator * @param env the builder env * @throws IOException on IO error */ - EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, + EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, Creator creator, Map env) throws IOException { - this(Clock.systemUTC(), buildOwner, builderImage, builderMetadata, env); + this(Clock.systemUTC(), buildOwner, builderImage, builderMetadata, creator, env); } /** @@ -61,13 +64,15 @@ class EphemeralBuilder { * @param buildOwner the build owner * @param builderImage the image * @param builderMetadata the builder metadata + * @param creator the builder creator * @param env the builder env * @throws IOException on IO error */ EphemeralBuilder(Clock clock, BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, - Map env) throws IOException { + Creator creator, Map env) throws IOException { ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm(); this.buildOwner = buildOwner; + this.creator = creator; this.builderMetadata = builderMetadata.copy(this::updateMetadata); this.archive = ImageArchive.from(builderImage, (update) -> { update.withUpdatedConfig(this.builderMetadata::attachTo); @@ -80,7 +85,7 @@ class EphemeralBuilder { } private void updateMetadata(BuilderMetadata.Update update) { - update.withCreatedBy("Spring Boot", "dev"); + update.withCreatedBy(this.creator.getName(), this.creator.getVersion()); } private Layer getEnvLayer(Map env) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index aa46044884e..9a74d4e7aab 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -42,6 +42,7 @@ import static org.assertj.core.api.Assertions.entry; * Tests for {@link BuildRequest}. * * @author Phillip Webb + * @author Scott Frederick */ public class BuildRequestTests { @@ -97,6 +98,16 @@ public class BuildRequestTests { assertThat(request.getBuilder().toString()).isEqualTo("docker.io/spring/builder:latest"); } + @Test + void withCreatorUpdatesCreator() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCreator = request.withCreator(Creator.withVersion("1.0.0")); + assertThat(request.getCreator().getName()).isEqualTo("Spring Boot"); + assertThat(request.getCreator().getVersion()).isEqualTo(""); + assertThat(withCreator.getCreator().getName()).isEqualTo("Spring Boot"); + assertThat(withCreator.getCreator().getVersion()).isEqualTo("1.0.0"); + } + @Test void withEnvAddsEnvEntry() throws IOException { BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java index 60b682b8437..04f3304503d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java @@ -62,6 +62,8 @@ class EphemeralBuilderTests extends AbstractJsonTests { private Map env; + private Creator creator = Creator.withVersion("dev"); + @BeforeEach void setup() throws Exception { this.image = Image.of(getContent("image.json")); @@ -71,23 +73,24 @@ class EphemeralBuilderTests extends AbstractJsonTests { @Test void getNameHasRandomName() throws Exception { - EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); - EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); + EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest"); assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString()); } @Test void getArchiveHasCreatedByConfig() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); ImageConfig config = builder.getArchive().getImageConfig(); BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config); assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot"); + assertThat(ephemeralMetadata.getCreatedBy().getVersion()).isEqualTo("dev"); } @Test void getArchiveHasTag() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); ImageReference tag = builder.getArchive().getTag(); assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest"); } @@ -95,13 +98,14 @@ class EphemeralBuilderTests extends AbstractJsonTests { @Test void getArchiveHasCreateDate() throws Exception { Clock clock = Clock.fixed(Instant.now(), ZoneOffset.UTC); - EphemeralBuilder builder = new EphemeralBuilder(clock, this.owner, this.image, this.metadata, this.env); + EphemeralBuilder builder = new EphemeralBuilder(clock, this.owner, this.image, this.metadata, this.creator, + this.env); assertThat(builder.getArchive().getCreateDate()).isEqualTo(Instant.now(clock)); } @Test void getArchiveContainsEnvLayer() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); File folder = unpack(getLayer(builder.getArchive(), 0), "env"); assertThat(new File(folder, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index e01163a8274..167ec72d684 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -16,15 +16,8 @@ package org.springframework.boot.gradle.plugin; -import java.io.File; -import java.io.IOException; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLConnection; import java.util.Arrays; import java.util.List; -import java.util.jar.Attributes; -import java.util.jar.JarFile; import org.gradle.api.GradleException; import org.gradle.api.Plugin; @@ -45,11 +38,12 @@ import org.springframework.boot.gradle.tasks.bundling.BootWar; * @author Dave Syer * @author Andy Wilkinson * @author Danny Hyun + * @author Scott Frederick * @since 1.2.7 */ public class SpringBootPlugin implements Plugin { - private static final String SPRING_BOOT_VERSION = determineSpringBootVersion(); + private static final String SPRING_BOOT_VERSION = VersionExtractor.forClass(DependencyManagementPluginAction.class); /** * The name of the {@link Configuration} that contains Spring Boot archives. @@ -135,29 +129,4 @@ public class SpringBootPlugin implements Plugin { project.getGradle().buildFinished((buildResult) -> unresolvedDependenciesAnalyzer.buildFinished(project)); } - private static String determineSpringBootVersion() { - String implementationVersion = DependencyManagementPluginAction.class.getPackage().getImplementationVersion(); - if (implementationVersion != null) { - return implementationVersion; - } - URL codeSourceLocation = DependencyManagementPluginAction.class.getProtectionDomain().getCodeSource() - .getLocation(); - try { - URLConnection connection = codeSourceLocation.openConnection(); - if (connection instanceof JarURLConnection) { - return getImplementationVersion(((JarURLConnection) connection).getJarFile()); - } - try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { - return getImplementationVersion(jarFile); - } - } - catch (Exception ex) { - return null; - } - } - - private static String getImplementationVersion(JarFile jarFile) throws IOException { - return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/VersionExtractor.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/VersionExtractor.java new file mode 100644 index 00000000000..42b7a7d5f5f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/VersionExtractor.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.gradle.plugin; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +/** + * Extracts version information for a Class. + * + * @author Andy Wilkinson + * @author Scott Frederick + * @since 2.3.0 + */ +public final class VersionExtractor { + + private VersionExtractor() { + } + + /** + * Return the version information for the provided {@link Class}. + * @param cls the Class to retrieve the version for + * @return the version, or {@code null} if a version can not be extracted + */ + public static String forClass(Class cls) { + String implementationVersion = cls.getPackage().getImplementationVersion(); + if (implementationVersion != null) { + return implementationVersion; + } + URL codeSourceLocation = cls.getProtectionDomain().getCodeSource().getLocation(); + try { + URLConnection connection = codeSourceLocation.openConnection(); + if (connection instanceof JarURLConnection) { + return getImplementationVersion(((JarURLConnection) connection).getJarFile()); + } + try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { + return getImplementationVersion(jarFile); + } + } + catch (Exception ex) { + return null; + } + } + + private static String getImplementationVersion(JarFile jarFile) throws IOException { + return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 1dec4c91f57..adbb22a79ef 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -30,10 +30,12 @@ import org.gradle.api.tasks.TaskAction; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.docker.DockerException; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive; +import org.springframework.boot.gradle.plugin.VersionExtractor; import org.springframework.util.StringUtils; /** @@ -41,6 +43,7 @@ import org.springframework.util.StringUtils; * buildpack. * * @author Andy Wilkinson + * @author Scott Frederick * @since 2.3.0 */ public class BootBuildImage extends DefaultTask { @@ -51,7 +54,7 @@ public class BootBuildImage extends DefaultTask { private String builder; - private Map environment = new HashMap(); + private Map environment = new HashMap<>(); private boolean cleanCache; @@ -210,6 +213,10 @@ public class BootBuildImage extends DefaultTask { if (this.environment != null && !this.environment.isEmpty()) { request = request.withEnv(this.environment); } + String springBootVersion = VersionExtractor.forClass(BootBuildImage.class); + if (StringUtils.hasText(springBootVersion)) { + request = request.withCreator(Creator.withVersion(springBootVersion)); + } request = request.withCleanCache(this.cleanCache); request = request.withVerboseLogging(this.verboseLogging); return request; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index ac5574c42bb..6f08ab5f5ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link BootBuildImage}. * * @author Andy Wilkinson + * @author Scott Frederick */ class BootBuildImageTests { @@ -81,6 +82,13 @@ class BootBuildImageTests { assertThat(request.getName().getDigest()).isNull(); } + @Test + void springBootVersionDefaultValueIsUsed() { + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getCreator().getName()).isEqualTo("Spring Boot"); + assertThat(request.getCreator().getVersion()).isEqualTo(""); + } + @Test void whenIndividualEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() { this.buildImage.environment("ALPHA", "a"); @@ -91,7 +99,7 @@ class BootBuildImageTests { @Test void whenEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() { - Map environment = new HashMap(); + Map environment = new HashMap<>(); environment.put("ALPHA", "a"); environment.put("BRAVO", "b"); this.buildImage.environment(environment); @@ -101,7 +109,7 @@ class BootBuildImageTests { @Test void whenTheEnvironmentIsSetItIsIncludedInTheRequest() { - Map environment = new HashMap(); + Map environment = new HashMap<>(); environment.put("ALPHA", "a"); environment.put("BRAVO", "b"); this.buildImage.setEnvironment(environment); @@ -111,7 +119,7 @@ class BootBuildImageTests { @Test void whenTheEnvironmentIsSetItReplacesAnyExistingEntriesAndIsIncludedInTheRequest() { - Map environment = new HashMap(); + Map environment = new HashMap<>(); environment.put("ALPHA", "a"); environment.put("BRAVO", "b"); this.buildImage.environment("C", "Charlie"); 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 index 795af32b975..de29bc59494 100644 --- 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 @@ -41,6 +41,7 @@ import org.springframework.boot.buildpack.platform.build.AbstractBuildLog; import org.springframework.boot.buildpack.platform.build.BuildLog; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; @@ -53,6 +54,7 @@ import org.springframework.util.StringUtils; * Package an application into a OCI image using a buildpack. * * @author Phillip Webb + * @author Scott Frederick * @since 2.3.0 */ @Mojo(name = "build-image", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, @@ -124,7 +126,8 @@ public class BuildImageMojo extends AbstractPackagerMojo { private BuildRequest getBuildRequest(Libraries libraries) { Function content = (owner) -> getApplicationContent(owner, libraries); - return ((this.image != null) ? this.image : new Image()).getBuildRequest(this.project.getArtifact(), content); + Image image = (this.image != null) ? this.image : new Image(); + return customize(image.getBuildRequest(this.project.getArtifact(), content)); } private TarArchive getApplicationContent(Owner owner, Libraries libraries) { @@ -143,6 +146,14 @@ public class BuildImageMojo extends AbstractPackagerMojo { return new File(this.sourceDirectory, name.toString()); } + private BuildRequest customize(BuildRequest request) { + String springBootVersion = VersionExtractor.forClass(BuildImageMojo.class); + if (StringUtils.hasText(springBootVersion)) { + request = request.withCreator(Creator.withVersion(springBootVersion)); + } + return request; + } + /** * {@link BuildLog} backed by Mojo logging. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/VersionExtractor.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/VersionExtractor.java new file mode 100644 index 00000000000..41462ebf5ad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/VersionExtractor.java @@ -0,0 +1,67 @@ +/* + * 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.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +/** + * Extracts version information for a Class. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +final class VersionExtractor { + + private VersionExtractor() { + } + + /** + * Return the version information for the provided {@link Class}. + * @param cls the Class to retrieve the version for + * @return the version, or {@code null} if a version can not be extracted + */ + static String forClass(Class cls) { + String implementationVersion = cls.getPackage().getImplementationVersion(); + if (implementationVersion != null) { + return implementationVersion; + } + URL codeSourceLocation = cls.getProtectionDomain().getCodeSource().getLocation(); + try { + URLConnection connection = codeSourceLocation.openConnection(); + if (connection instanceof JarURLConnection) { + return getImplementationVersion(((JarURLConnection) connection).getJarFile()); + } + try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { + return getImplementationVersion(jarFile); + } + } + catch (Exception ex) { + return null; + } + } + + private static String getImplementationVersion(JarFile jarFile) throws IOException { + return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); + } + +}