Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version-spring-boot}'
}

tasks.named("bootJar") {
mainClass = 'com.example.ExampleApplication'
}

// tag::caches[]
tasks.named("bootBuildImage") {
buildCache {
image {
name = "docker.io/library/${rootProject.name}:build"
}
}
}
// end::caches[]

tasks.register("bootBuildImageCaches") {
doFirst {
bootBuildImage.buildCache.asCache().with { println "buildCache=$name" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

plugins {
java
id("org.springframework.boot") version "{version-spring-boot}"
}

// tag::caches[]
tasks.named<BootBuildImage>("bootBuildImage") {
buildCache {
image {
name.set("docker.io/library/${rootProject.name}:build")
}
}
}
// end::caches[]

tasks.register("bootBuildImageCaches") {
doFirst {
println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache()?.image?.name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,24 @@ include::example$packaging/boot-build-image-bind-caches.gradle.kts[tags=caches]
----
======

The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example:

[tabs]
======
Groovy::
+
[source,groovy,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-build-image-image-caches.gradle[tags=caches]
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-build-image-image-caches.gradle.kts[tags=caches]
----
======



[[build-image.examples.docker]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ public void bind(Action<BindCacheSpec> action) {
this.cache = Cache.bind(spec.getSource().get());
}

/**
* Configures an image cache using the given {@code action}.
* @param action the action
*/
public void image(Action<ImageCacheSpec> action) {
if (this.cache != null) {
throw new GradleException("Each image building cache can be configured only once");
}
ImageCacheSpec spec = this.objectFactory.newInstance(ImageCacheSpec.class);
action.execute(spec);
this.cache = Cache.image(spec.getName().get());
}

/**
* Configuration for an image building cache stored in a Docker volume.
*/
Expand Down Expand Up @@ -102,4 +115,18 @@ public abstract static class BindCacheSpec {

}

/**
* Configuration for an image building cache stored in an image.
*/
public abstract static class ImageCacheSpec {

/**
* Returns the name of the cache image.
* @return the cache image name
*/
@Input
public abstract Property<String> getName();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,13 @@ void bootBuildImageWithBindCaches() {
.containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch");
}

@TestTemplate
void bootBuildImageWithImageCaches() {
BuildResult result = this.gradleBuild.script(Examples.DIR + "packaging/boot-build-image-image-caches")
.build("bootBuildImageCaches");
assertThat(result.getOutput()).containsPattern("buildCache=docker.io/library/gradle-[\\d]+:build");
}

protected void jarFile(File file) throws IOException {
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) {
jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::caches[] -->
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<buildCache>
<image>
<name>docker.io/library/${project.artifactId}:build</name>
</image>
</buildCache>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
<!-- end::caches[] -->
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,13 @@ The caches and the build workspace can be configured to use bind mounts instead
include::example$packaging-oci-image/bind-caches-pom.xml[tags=caches]
----

The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example:

[source,xml,indent=0,subs="verbatim,attributes"]
----
include::example$packaging-oci-image/image-caches-pom.xml[tags=caches]
----



[[build-image.examples.docker]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ public void setBind(BindCacheInfo info) {
this.cache = Cache.bind(source);
}

public void setImage(ImageCacheInfo info) {
Assert.state(this.cache == null, "Each image building cache can be configured only once");
String name = info.getName();
Assert.state(name != null, "'name' must not be null");
this.cache = Cache.image(name);
}

@Nullable Cache asCache() {
return this.cache;
}
Expand All @@ -68,6 +75,12 @@ static CacheInfo fromBind(BindCacheInfo cacheInfo) {
return new CacheInfo(Cache.bind(source));
}

static CacheInfo fromImage(ImageCacheInfo cacheInfo) {
String name = cacheInfo.getName();
Assert.state(name != null, "'name' must not be null");
return new CacheInfo(Cache.image(name));
}

/**
* Encapsulates configuration of an image building cache stored in a volume.
*/
Expand Down Expand Up @@ -116,4 +129,28 @@ void setSource(@Nullable String source) {

}

/**
* Encapsulates configuration of an image building cache stored in an image.
*/
public static class ImageCacheInfo {

private @Nullable String name;

public ImageCacheInfo() {
}

ImageCacheInfo(String name) {
this.name = name;
}

public @Nullable String getName() {
return this.name;
}

void setName(@Nullable String name) {
this.name = name;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.boot.maven.CacheInfo.BindCacheInfo;
import org.springframework.boot.maven.CacheInfo.ImageCacheInfo;
import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;

Expand Down Expand Up @@ -245,6 +247,23 @@ void getBuildRequestWhenHasLaunchCacheBindUsesCache() {
assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir"));
}

@Test
void getBuildRequestWhenHasBuildCacheImageUsesCache() {
Image image = new Image();
image.buildCache = CacheInfo.fromImage(new ImageCacheInfo("build-cache-image"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBuildCache()).isEqualTo(Cache.image("build-cache-image"));
}

@Test
void getBuildRequestWhenHasLaunchCacheImageThrowsException() {
Image image = new Image();
image.launchCache = CacheInfo.fromImage(new ImageCacheInfo("launch-cache-image"));
assertThatIllegalArgumentException()
.isThrownBy(() -> image.getBuildRequest(createArtifact(), mockApplicationContent()))
.withMessage("Launch cache must not be an image cache");
}

@Test
void getBuildRequestWhenHasCreatedDateUsesCreatedDate() {
Image image = new Image();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ public BuildRequest withBuildCache(Cache buildCache) {
*/
public BuildRequest withLaunchCache(Cache launchCache) {
Assert.notNull(launchCache, "'launchCache' must not be null");
Assert.isNull(launchCache.getImage(), "Launch cache must not be an image cache");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ public enum Format {
/**
* A cache stored as a bind mount.
*/
BIND("bind mount");
BIND("bind mount"),

/**
* A cache stored as an image.
*/
IMAGE("image");

private final String description;

Expand Down Expand Up @@ -81,6 +86,14 @@ public String getDescription() {
return (this.format.equals(Format.BIND)) ? (Bind) this : null;
}

/**
* Return the details of the cache if it is an image cache.
* @return the cache, or {@code null} if it is not an image cache
*/
public @Nullable Image getImage() {
return (this.format.equals(Format.IMAGE)) ? (Image) this : null;
}

/**
* Create a new {@code Cache} that uses a volume with the provided name.
* @param name the cache volume name
Expand Down Expand Up @@ -111,6 +124,16 @@ public static Cache bind(String source) {
return new Bind(source);
}

/**
* Create a new {@code Cache} that uses an image with the provided name.
* @param name the cache image name
* @return a new cache instance
*/
public static Cache image(String name) {
Assert.notNull(name, "'name' must not be null");
return new Image(name);
}

@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
Expand Down Expand Up @@ -222,4 +245,49 @@ public String toString() {

}

/**
* Details of a cache stored in an image.
*/
public static class Image extends Cache {

private final String name;

Image(String name) {
super(Format.IMAGE);
this.name = name;
}

public String getName() {
return this.name;
}

@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (!super.equals(obj)) {
return false;
}
Image other = (Image) obj;
return Objects.equals(this.name, other.name);
}

@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.name);
return result;
}

@Override
public String toString() {
return this.format.getDescription() + " '" + this.name + "'";
}

}

}
Loading
Loading