Polish "Add option to customize cache volume names when building an image"

See gh-28292
This commit is contained in:
Scott Frederick
2021-10-14 14:55:04 -05:00
parent dc36346285
commit 871468931f
27 changed files with 937 additions and 157 deletions

View File

@@ -181,10 +181,15 @@ The value supplied will be passed unvalidated to Docker when creating the builde
| A list of one or more additional tags to apply to the generated image.
|
| `cacheVolumeNames`
| `buildCache`
|
| Cache volume names that should be used by the builder instead of generating random names.
| A cache containing layers created by buildpacks and used by the image building process.
| A named volume in the Docker daemon, with a name derived from the image name.
| `launchCache`
|
| A cache containing layers created by buildpacks and used by the image launching process.
| A named volume in the Docker daemon, with a name derived from the image name.
|===
@@ -384,10 +389,30 @@ The publish option can be specified on the command line as well, as shown in thi
$ gradle bootBuildImage --imageName=docker.example.com/library/my-app:v1 --publishImage
----
[[build-image.examples.caches]]
=== Builder Cache Configuration
The CNB builder caches layers that are used when building and launching an image.
By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image.
If the image name changes frequently, for example when the project version is used as a tag in the image name, then the caches can be invalidated frequently.
The cache volumes can be configured to use alternative names to give more control over cache lifecycle as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches]
----
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
.Kotlin
----
include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches]
----
[[build-image.examples.docker]]
=== Docker Configuration
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]

View File

@@ -1,23 +0,0 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{gradle-project-version}'
}
bootJar {
mainClass = 'com.example.ExampleApplication'
}
// tag::cacheVolumeNames[]
bootBuildImage {
cacheVolumeNames = [
"build": "example-build-cachevol",
"launch": "example-launch-cachevol"
]
}
// end::cacheVolumeNames[]
task bootBuildImageCacheVolumeNames {
doFirst {
bootBuildImage.cacheVolumeNames.each { type, name -> println "$type=$name" }
}
}

View File

@@ -1,21 +0,0 @@
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
// tag::cacheVolumeNames[]
tasks.getByName<BootBuildImage>("bootBuildImage") {
cacheVolumeNames = mapOf("build" to "example-build-cachevol",
"launch" to "example-launch-cachevol")
}
// end::cacheVolumeNames[]
tasks.register("bootBuildImageEnvironment") {
doFirst {
for((type, name) in tasks.getByName<BootBuildImage>("bootBuildImage").cacheVolumeNames) {
print(type + "=" + name)
}
}
}

View File

@@ -0,0 +1,30 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{gradle-project-version}'
}
bootJar {
mainClass = 'com.example.ExampleApplication'
}
// tag::caches[]
bootBuildImage {
buildCache {
volume {
name = "cache-${rootProject.name}.build"
}
}
launchCache {
volume {
name = "cache-${rootProject.name}.launch"
}
}
}
// end::caches[]
task bootBuildImageCaches {
doFirst {
bootBuildImage.buildCache.asCache().with { println "buildCache=$name" }
bootBuildImage.launchCache.asCache().with { println "launchCache=$name" }
}
}

View File

@@ -0,0 +1,28 @@
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
// tag::caches[]
tasks.getByName<BootBuildImage>("bootBuildImage") {
buildCache {
volume {
name = "cache-${rootProject.name}.build"
}
}
launchCache {
volume {
name = "cache-${rootProject.name}.launch"
}
}
}
// end::caches[]
tasks.register("bootBuildImageCaches") {
doFirst {
println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache().volume.name)
println("launchCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").launchCache.asCache().volume.name)
}
}

View File

@@ -99,7 +99,9 @@ public class BootBuildImage extends DefaultTask {
private final ListProperty<String> tags;
private Map<String, String> cacheVolumeNames = new HashMap<>();
private final CacheSpec buildCache = new CacheSpec();
private final CacheSpec launchCache = new CacheSpec();
private final DockerSpec docker = new DockerSpec();
@@ -420,41 +422,6 @@ public class BootBuildImage extends DefaultTask {
this.tags.addAll(tags);
}
/**
* Returns the cache volume names that will be used when building the image.
* @return the cache volume names
*/
@Input
@Optional
public Map<String, String> getCacheVolumeNames() {
return this.cacheVolumeNames;
}
/**
* Sets the cache volume names that will be used when building the image.
* @param cacheVolumeNames the cache volume names
*/
public void setCacheVolumeNames(Map<String, String> cacheVolumeNames) {
this.cacheVolumeNames = cacheVolumeNames;
}
/**
* Add an entry to cache volume names that will be used when building the image.
* @param type the type of the entry
* @param name the name of the entry
*/
public void cacheVolumeName(String type, String name) {
this.cacheVolumeNames.put(type, name);
}
/**
* Adds entries to cache volume names that will be used when building the image.
* @param entries the entries to add to cache volume names
*/
public void cacheVolumeNames(Map<String, String> entries) {
this.cacheVolumeNames.putAll(entries);
}
/**
* Returns the network the build container will connect to.
* @return the network
@@ -474,6 +441,62 @@ public class BootBuildImage extends DefaultTask {
this.network = network;
}
/**
* Returns the build cache that will be used when building the image.
* @return the cache
*/
@Nested
@Optional
public CacheSpec getBuildCache() {
return this.buildCache;
}
/**
* Customizes the {@link CacheSpec} for the build cache using the given
* {@code action}.
* @param action the action
*/
public void buildCache(Action<CacheSpec> action) {
action.execute(this.buildCache);
}
/**
* Customizes the {@link CacheSpec} for the build cache using the given
* {@code closure}.
* @param closure the closure
*/
public void buildCache(Closure<?> closure) {
buildCache(ConfigureUtil.configureUsing(closure));
}
/**
* Returns the launch cache that will be used when building the image.
* @return the cache
*/
@Nested
@Optional
public CacheSpec getLaunchCache() {
return this.launchCache;
}
/**
* Customizes the {@link CacheSpec} for the launch cache using the given
* {@code action}.
* @param action the action
*/
public void launchCache(Action<CacheSpec> action) {
action.execute(this.launchCache);
}
/**
* Customizes the {@link CacheSpec} for the launch cache using the given
* {@code closure}.
* @param closure the closure
*/
public void launchCache(Closure<?> closure) {
launchCache(ConfigureUtil.configureUsing(closure));
}
/**
* Returns the Docker configuration the builder will use.
* @return docker configuration.
@@ -537,7 +560,7 @@ public class BootBuildImage extends DefaultTask {
request = customizeBuildpacks(request);
request = customizeBindings(request);
request = customizeTags(request);
request = customizeCacheVolumeNames(request);
request = customizeCaches(request);
request = request.withNetwork(this.network);
return request;
}
@@ -615,9 +638,12 @@ public class BootBuildImage extends DefaultTask {
return request;
}
private BuildRequest customizeCacheVolumeNames(BuildRequest request) {
if (this.cacheVolumeNames != null && !this.cacheVolumeNames.isEmpty()) {
request = request.withCacheVolumeNames(this.cacheVolumeNames);
private BuildRequest customizeCaches(BuildRequest request) {
if (this.buildCache.asCache() != null) {
request = request.withBuildCache(this.buildCache.asCache());
}
if (this.launchCache.asCache() != null) {
request = request.withLaunchCache(this.launchCache.asCache());
}
return request;
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2021 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.tasks.bundling;
import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.tasks.Input;
import org.gradle.util.ConfigureUtil;
import org.springframework.boot.buildpack.platform.build.Cache;
/**
* Configuration for an image building cache.
*
* @author Scott Frederick
* @since 2.6.0
*/
public class CacheSpec {
private Cache cache = null;
CacheSpec() {
}
public Cache asCache() {
return this.cache;
}
/**
* Configures a volume cache using the given {@code action}.
* @param action the action
*/
public void volume(Action<VolumeCacheSpec> action) {
if (this.cache != null) {
throw new GradleException("Each image building cache can be configured only once");
}
VolumeCacheSpec spec = new VolumeCacheSpec();
action.execute(spec);
this.cache = Cache.volume(spec.getName());
}
/**
* Configures a volume cache using the given {@code closure}.
* @param closure the closure
*/
public void volume(Closure<?> closure) {
if (this.cache != null) {
throw new GradleException("Each image building cache can be configured only once");
}
volume(ConfigureUtil.configureUsing(closure));
}
/**
* Configuration for an image building cache stored in a Docker volume.
*/
public static class VolumeCacheSpec {
private String name;
/**
* Returns the name of the cache.
* @return the cache name
*/
@Input
public String getName() {
return this.name;
}
/**
* Sets the name of the cache.
* @param name the cache name
*/
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -316,6 +316,14 @@ class PackagingDocumentationTests {
.contains("urn:cnb:builder:paketo-buildpacks/java");
}
@TestTemplate
void bootBuildImageWithCaches() {
BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-caches")
.build("bootBuildImageCaches");
assertThat(result.getOutput()).containsPattern("buildCache=cache-gradle-[\\d]+.build")
.containsPattern("launchCache=cache-gradle-[\\d]+.launch");
}
protected void jarFile(File file) throws IOException {
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) {
jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));

View File

@@ -40,7 +40,10 @@ import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.FilePermissions;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
@@ -73,7 +76,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("env: BP_JVM_VERSION=8.*");
assertThat(result.getOutput()).contains("Network status: HTTP/2 200");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -91,7 +94,7 @@ class BootBuildImageIntegrationTests {
File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs");
assertThat(buildLibs.listFiles())
.containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"));
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -107,7 +110,7 @@ class BootBuildImageIntegrationTests {
File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs");
assertThat(buildLibs.listFiles())
.containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"));
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -119,7 +122,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("example/test-image-name");
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage("example/test-image-name");
removeImages("example/test-image-name");
}
@TestTemplate
@@ -131,7 +134,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("example/test-image-custom");
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage("example/test-image-custom");
removeImages("example/test-image-custom");
}
@TestTemplate
@@ -146,7 +149,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("example/test-image-cmd");
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage("example/test-image-cmd");
removeImages("example/test-image-cmd");
}
@TestTemplate
@@ -160,7 +163,7 @@ class BootBuildImageIntegrationTests {
result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).doesNotContain("Pulled builder image").doesNotContain("Pulled run image");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -173,7 +176,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Test Info buildpack building")
.contains("---> Test Info buildpack done");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -187,7 +190,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Hello World buildpack");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -202,7 +205,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Hello World buildpack");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -215,7 +218,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Test Info buildpack building")
.contains("---> Test Info buildpack done");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -232,7 +235,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("binding: certificates/test1.crt=---certificate one---");
assertThat(result.getOutput()).contains("binding: certificates/test2.crt=---certificate two---");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -246,8 +249,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
assertThat(result.getOutput()).contains("example.com/myapp:latest");
removeImage(projectName);
removeImage("example.com/myapp:latest");
removeImages(projectName, "example.com/myapp:latest");
}
@TestTemplate
@@ -260,7 +262,7 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
@@ -274,7 +276,21 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("Network status: curl failed");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImage(projectName);
removeImages(projectName);
}
@TestTemplate
void buildsImageWithVolumeCaches() throws IOException {
writeMainClass();
writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImages(projectName);
deleteVolumes("build-cache-volume", "launch-cache-volume");
}
@TestTemplate
@@ -325,6 +341,14 @@ class BootBuildImageIntegrationTests {
.containsPattern("example/Invalid-Tag-Name");
}
@TestTemplate
void failsWhenCachesAreConfiguredTwice() throws IOException {
writeMainClass();
writeLongNameResource();
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
assertThat(result.getOutput()).containsPattern("Each image building cache can be configured only once");
}
private void writeMainClass() throws IOException {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs();
@@ -447,9 +471,18 @@ class BootBuildImageIntegrationTests {
}
}
private void removeImage(String name) throws IOException {
ImageReference imageReference = ImageReference.of(name);
new DockerApi().image().remove(imageReference, false);
private void removeImages(String... names) throws IOException {
ImageApi imageApi = new DockerApi().image();
for (String name : names) {
imageApi.remove(ImageReference.of(name), false);
}
}
private void deleteVolumes(String... names) throws IOException {
VolumeApi volumeApi = new DockerApi().volume();
for (String name : names) {
volumeApi.delete(VolumeName.of(name), false);
}
}
}

View File

@@ -0,0 +1,21 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1"
buildCache {
volume {
name = "build-cache-volume"
}
}
launchCache {
volume {
name = "launch-cache-volume"
}
}
}

View File

@@ -0,0 +1,19 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1"
buildCache {
volume {
name = "build-cache-volume1"
}
volume {
name = "build-cache-volum2"
}
}
}