Commit 191dce3f authored by Scott Frederick's avatar Scott Frederick

Set Spring Boot version in ephemeral builder

This commit adds a `createdBy` structure to the metadata of the ephemeral
builder container image that identifies Spring Boot as the creator of the
image, along with the Spring Boot version.

See gh-20126
parent 97af0b2f
......@@ -44,6 +44,8 @@ public class BuildRequest {
private final ImageReference builder;
private final Creator creator;
private final Map<String, String> 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<Owner, TarArchive> applicationContent, ImageReference builder,
Map<String, String> env, boolean cleanCache, boolean verboseLogging) {
Creator creator, Map<String, String> 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<String, String> 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<String, String> 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
......
......@@ -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);
......
/*
* 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();
}
}
......@@ -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<String, String> 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<String, String> env) throws IOException {
Creator creator, Map<String, String> 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<String, String> env) throws IOException {
......
......@@ -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"));
......
......@@ -62,6 +62,8 @@ class EphemeralBuilderTests extends AbstractJsonTests {
private Map<String, String> 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");
}
......
......@@ -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<Project> {
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> {
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);
}
}
/*
* 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);
}
}
......@@ -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;
* <a href="https://buildpacks.io">buildpack</a>.
*
* @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<String, String> environment = new HashMap<String, String>();
private Map<String, String> 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;
......
......@@ -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<String, String> environment = new HashMap<String, String>();
Map<String, String> 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<String, String> environment = new HashMap<String, String>();
Map<String, String> 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<String, String> environment = new HashMap<String, String>();
Map<String, String> environment = new HashMap<>();
environment.put("ALPHA", "a");
environment.put("BRAVO", "b");
this.buildImage.environment("C", "Charlie");
......
......@@ -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<Owner, TarArchive> 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.
*/
......
/*
* 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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment