Skip to content

Commit 0bd0b2a

Browse files
committed
Add support for building OCI images using the Gradle plugin
Closes gh-19831
1 parent bc452bc commit 0bd0b2a

File tree

12 files changed

+533
-8
lines changed

12 files changed

+533
-8
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,23 @@
3535
* Adapter class to convert a ZIP file to a {@link TarArchive}.
3636
*
3737
* @author Phillip Webb
38+
* @since 2.3.0
3839
*/
39-
class ZipFileTarArchive implements TarArchive {
40+
public class ZipFileTarArchive implements TarArchive {
4041

4142
static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli();
4243

4344
private final File zip;
4445

4546
private final Owner owner;
4647

47-
ZipFileTarArchive(File zip, Owner owner) {
48+
/**
49+
* Creates an archive from the contents of the given {@code zip}. Each entry in the
50+
* archive will be owned by the given {@code owner}.
51+
* @param zip the zip to use as a source
52+
* @param owner the owner of the tar entries
53+
*/
54+
public ZipFileTarArchive(File zip, Owner owner) {
4855
Assert.notNull(zip, "Zip must not be null");
4956
Assert.notNull(owner, "Owner must not be null");
5057
this.zip = zip;

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies {
3030

3131
asciidoctorExtensions("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.3.0.RELEASE")
3232

33+
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform"))
3334
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools"))
3435
implementation("io.spring.gradle:dependency-management-plugin")
3536
implementation("org.apache.commons:commons-compress")
@@ -38,9 +39,11 @@ dependencies {
3839
optional(platform(project(":spring-boot-project:spring-boot-dependencies")))
3940
optional("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50")
4041

41-
testImplementation("org.junit.jupiter:junit-jupiter")
42+
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
4243
testImplementation("org.assertj:assertj-core")
44+
testImplementation("org.junit.jupiter:junit-jupiter")
4345
testImplementation("org.mockito:mockito-core")
46+
testImplementation("org.testcontainers:testcontainers")
4447
}
4548

4649
gradlePlugin {

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Andy Wilkinson
2626
:api-documentation: {spring-boot-docs}/gradle-plugin/api
2727
:spring-boot-reference: {spring-boot-docs}/reference/htmlsingle
2828
:build-info-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.html
29+
:boot-build-image-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.html
2930
:boot-jar-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootJar.html
3031
:boot-war-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootWar.html
3132
:boot-run-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/run/BootRun.html

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,12 @@ The jar will then be split into layer folders which may include:
298298
* `resources`
299299
* `snapshots-dependencies`
300300
* `dependencies`
301+
302+
303+
304+
[[packaging-oci-images]]
305+
== Packaging OCI images
306+
307+
The plugin can create OCI images from executable jars using a https://buildpacks.io[buildpack].
308+
Images can be built using the `bootBuildImage` task and a local Docker installation.
309+
The task is automatically created when the `java` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`].

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring B
1515
The jar will contain everything on the runtime classpath of the main source set; classes are packaged in `BOOT-INF/classes` and jars are packaged in `BOOT-INF/lib`
1616
2. Configures the `assemble` task to depend on the `bootJar` task.
1717
3. Disables the `jar` task.
18-
4. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application.
19-
5. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task.
20-
6. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`.
21-
7. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument.
18+
4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack].
19+
5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application.
20+
6. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task.
21+
7. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`.
22+
8. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument.
2223

2324

2425

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.gradle.api.tasks.SourceSet;
3737
import org.gradle.api.tasks.compile.JavaCompile;
3838

39+
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage;
3940
import org.springframework.boot.gradle.tasks.bundling.BootJar;
4041
import org.springframework.boot.gradle.tasks.run.BootRun;
4142
import org.springframework.util.StringUtils;
@@ -65,6 +66,7 @@ public void execute(Project project) {
6566
disableJarTask(project);
6667
configureBuildTask(project);
6768
BootJar bootJar = configureBootJarTask(project);
69+
configureBootBuildImageTask(project, bootJar);
6870
configureArtifactPublication(bootJar);
6971
configureBootRunTask(project);
7072
configureUtf8Encoding(project);
@@ -94,6 +96,14 @@ private BootJar configureBootJarTask(Project project) {
9496
return bootJar;
9597
}
9698

99+
private void configureBootBuildImageTask(Project project, BootJar bootJar) {
100+
BootBuildImage buildImage = project.getTasks().create(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME,
101+
BootBuildImage.class);
102+
buildImage.setDescription("Builds an OCI image of the application using the output of the bootJar task");
103+
buildImage.setGroup(BasePlugin.BUILD_GROUP);
104+
buildImage.from(bootJar);
105+
}
106+
97107
private void configureArtifactPublication(BootJar bootJar) {
98108
ArchivePublishArtifact artifact = new ArchivePublishArtifact(bootJar);
99109
this.singlePublishedArtifact.addCandidate(artifact);

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.gradle.util.GradleVersion;
3535

3636
import org.springframework.boot.gradle.dsl.SpringBootExtension;
37+
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage;
3738
import org.springframework.boot.gradle.tasks.bundling.BootJar;
3839
import org.springframework.boot.gradle.tasks.bundling.BootWar;
3940

@@ -68,6 +69,12 @@ public class SpringBootPlugin implements Plugin<Project> {
6869
*/
6970
public static final String BOOT_WAR_TASK_NAME = "bootWar";
7071

72+
/**
73+
* The name of the default {@link BootBuildImage} task.
74+
* @since 2.3.0
75+
*/
76+
public static final String BOOT_BUILD_IMAGE_TASK_NAME = "bootBuildImage";
77+
7178
/**
7279
* The coordinates {@code (group:name:version)} of the
7380
* {@code spring-boot-dependencies} bom.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.gradle.tasks.bundling;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.function.Supplier;
24+
25+
import org.gradle.api.DefaultTask;
26+
import org.gradle.api.Project;
27+
import org.gradle.api.Task;
28+
import org.gradle.api.tasks.Input;
29+
import org.gradle.api.tasks.Optional;
30+
import org.gradle.api.tasks.TaskAction;
31+
32+
import org.springframework.boot.buildpack.platform.build.BuildRequest;
33+
import org.springframework.boot.buildpack.platform.build.Builder;
34+
import org.springframework.boot.buildpack.platform.docker.DockerException;
35+
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
36+
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
37+
import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive;
38+
import org.springframework.util.StringUtils;
39+
40+
/**
41+
* A {@link Task} for bundling an application into an OCI image using a
42+
* <a href="https://buildpacks.io">buildpack</a>.
43+
*
44+
* @author Andy Wilkinson
45+
* @since 2.3.0
46+
*/
47+
public class BootBuildImage extends DefaultTask {
48+
49+
private Supplier<File> jar;
50+
51+
private String imageName;
52+
53+
private String builder;
54+
55+
private Map<String, String> environment = new HashMap<String, String>();
56+
57+
private boolean cleanCache;
58+
59+
private boolean verboseLogging;
60+
61+
/**
62+
* Configures this task to create an image from the given {@code bootJar} task. This
63+
* task is also configured to depend upon the given task.
64+
* @param bootJar the fat jar from which the image should be created.
65+
*/
66+
public void from(BootJar bootJar) {
67+
dependsOn(bootJar);
68+
this.jar = () -> bootJar.getArchiveFile().get().getAsFile();
69+
}
70+
71+
/**
72+
* Configures this task to create an image from the given jar file.
73+
* @param jar the jar from which the image should be created.
74+
*/
75+
public void from(File jar) {
76+
this.jar = () -> jar;
77+
}
78+
79+
/**
80+
* Returns the name of the image that will be built. When {@code null}, the name will
81+
* be derived from the {@link Project Project's} {@link Project#getName() name} and
82+
* {@link Project#getVersion version}.
83+
* @return name of the image
84+
*/
85+
@Input
86+
@Optional
87+
public String getImageName() {
88+
return this.imageName;
89+
}
90+
91+
/**
92+
* Sets the name of the image that will be built.
93+
* @param imageName name of the image
94+
*/
95+
public void setImageName(String imageName) {
96+
this.imageName = imageName;
97+
}
98+
99+
/**
100+
* Returns the builder that will be used to build the image. When {@code null}, the
101+
* default builder will be used.
102+
* @return the builder
103+
*/
104+
@Input
105+
@Optional
106+
public String getBuilder() {
107+
return this.builder;
108+
}
109+
110+
/**
111+
* Sets the builder that will be used to build the image.
112+
* @param builder the builder
113+
*/
114+
public void setBuilder(String builder) {
115+
this.builder = builder;
116+
}
117+
118+
/**
119+
* Returns the environment that will be used when building the image.
120+
* @return the environment
121+
*/
122+
@Input
123+
public Map<String, String> getEnvironment() {
124+
return this.environment;
125+
}
126+
127+
/**
128+
* Sets the environment that will be used when building the image.
129+
* @param environment the environment
130+
*/
131+
public void setEnvironment(Map<String, String> environment) {
132+
this.environment = environment;
133+
}
134+
135+
/**
136+
* Add an entry to the environment that will be used when building the image.
137+
* @param name the name of the entry
138+
* @param value the value of the entry
139+
*/
140+
public void environment(String name, String value) {
141+
this.environment.put(name, value);
142+
}
143+
144+
/**
145+
* Adds entries to the environment that will be used when building the image.
146+
* @param entries the entries to add to the environment
147+
*/
148+
public void environment(Map<String, String> entries) {
149+
this.environment.putAll(entries);
150+
}
151+
152+
/**
153+
* Returns whether caches should be cleaned before packaging.
154+
* @return whether caches should be cleaned
155+
*/
156+
@Input
157+
public boolean isCleanCache() {
158+
return this.cleanCache;
159+
}
160+
161+
/**
162+
* Sets whether caches should be cleaned before packaging.
163+
* @param cleanCache {@code true} to clean the cache, otherwise {@code false}.
164+
*/
165+
public void setCleanCache(boolean cleanCache) {
166+
this.cleanCache = cleanCache;
167+
}
168+
169+
/**
170+
* Whether verbose logging should be enabled while building the image.
171+
* @return whether verbose logging should be enabled
172+
*/
173+
@Input
174+
public boolean isVerboseLogging() {
175+
return this.verboseLogging;
176+
}
177+
178+
/**
179+
* Sets whether verbose logging should be enabled while building the image.
180+
* @param verboseLogging {@code true} to enable verbose logging, otherwise
181+
* {@code false}.
182+
*/
183+
public void setVerboseLogging(boolean verboseLogging) {
184+
this.verboseLogging = verboseLogging;
185+
}
186+
187+
@TaskAction
188+
void buildImage() throws DockerException, IOException {
189+
Builder builder = new Builder();
190+
BuildRequest request = createRequest();
191+
builder.build(request);
192+
}
193+
194+
BuildRequest createRequest() {
195+
BuildRequest request = customize(
196+
BuildRequest.of(determineImageReference(), (owner) -> new ZipFileTarArchive(this.jar.get(), owner)));
197+
return request;
198+
}
199+
200+
private ImageReference determineImageReference() {
201+
if (StringUtils.hasText(this.imageName)) {
202+
return ImageReference.of(this.imageName);
203+
}
204+
ImageName imageName = ImageName.of(getProject().getName());
205+
String version = getProject().getVersion().toString();
206+
if ("unspecified".equals(version)) {
207+
return ImageReference.of(imageName);
208+
}
209+
return ImageReference.of(imageName, version);
210+
}
211+
212+
private BuildRequest customize(BuildRequest request) {
213+
if (StringUtils.hasText(this.builder)) {
214+
request = request.withBuilder(ImageReference.of(this.builder));
215+
}
216+
if (this.environment != null && !this.environment.isEmpty()) {
217+
request = request.withEnv(this.environment);
218+
}
219+
request = request.withCleanCache(this.cleanCache);
220+
request = request.withVerboseLogging(this.verboseLogging);
221+
return request;
222+
}
223+
224+
}

0 commit comments

Comments
 (0)