Skip to content

Commit de321b0

Browse files
Support podman for building images
Closes gh-30196
1 parent 7ad538c commit de321b0

File tree

15 files changed

+227
-84
lines changed

15 files changed

+227
-84
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent>
187187
listener.onUpdate(event);
188188
});
189189
}
190-
return inspect(reference.withDigest(digestCapture.getCapturedDigest()));
190+
return inspect(reference);
191191
}
192192
finally {
193193
listener.onFinish();

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,8 @@ public class ImageArchive implements TarArchive {
6161
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME
6262
.withZone(ZoneOffset.UTC);
6363

64+
private static final String EMPTY_LAYER_NAME_PREFIX = "blank_";
65+
6466
private static final IOConsumer<Update> NO_UPDATES = (update) -> {
6567
};
6668

@@ -125,16 +127,23 @@ private void write(Layout writer) throws IOException {
125127
}
126128

127129
private List<LayerId> writeLayers(Layout writer) throws IOException {
130+
for (int i = 0; i < this.existingLayers.size(); i++) {
131+
writeEmptyLayer(writer, EMPTY_LAYER_NAME_PREFIX + i);
132+
}
128133
List<LayerId> writtenLayers = new ArrayList<>();
129134
for (Layer layer : this.newLayers) {
130135
writtenLayers.add(writeLayer(writer, layer));
131136
}
132137
return Collections.unmodifiableList(writtenLayers);
133138
}
134139

140+
private void writeEmptyLayer(Layout writer, String name) throws IOException {
141+
writer.file(name, Owner.ROOT, Content.of(""));
142+
}
143+
135144
private LayerId writeLayer(Layout writer, Layer layer) throws IOException {
136145
LayerId id = layer.getId();
137-
writer.file("/" + id.getHash() + ".tar", Owner.ROOT, layer);
146+
writer.file(id.getHash() + ".tar", Owner.ROOT, layer);
138147
return id;
139148
}
140149

@@ -144,7 +153,7 @@ private String writeConfig(Layout writer, List<LayerId> writtenLayers) throws IO
144153
String json = this.objectMapper.writeValueAsString(config).replace("\r\n", "\n");
145154
MessageDigest digest = MessageDigest.getInstance("SHA-256");
146155
InspectedContent content = InspectedContent.of(Content.of(json), digest::update);
147-
String name = "/" + LayerId.ofSha256Digest(digest.digest()).getHash() + ".json";
156+
String name = LayerId.ofSha256Digest(digest.digest()).getHash() + ".json";
148157
writer.file(name, Owner.ROOT, content);
149158
return name;
150159
}
@@ -187,7 +196,7 @@ private JsonNode createRootFs(List<LayerId> writtenLayers) {
187196
private void writeManifest(Layout writer, String config, List<LayerId> writtenLayers) throws IOException {
188197
ArrayNode manifest = createManifest(config, writtenLayers);
189198
String manifestJson = this.objectMapper.writeValueAsString(manifest);
190-
writer.file("/manifest.json", Owner.ROOT, Content.of(manifestJson));
199+
writer.file("manifest.json", Owner.ROOT, Content.of(manifestJson));
191200
}
192201

193202
private ArrayNode createManifest(String config, List<LayerId> writtenLayers) {
@@ -204,7 +213,7 @@ private ArrayNode createManifest(String config, List<LayerId> writtenLayers) {
204213
private ArrayNode getManifestLayers(List<LayerId> writtenLayers) {
205214
ArrayNode layers = this.objectMapper.createArrayNode();
206215
for (int i = 0; i < this.existingLayers.size(); i++) {
207-
layers.add("");
216+
layers.add(EMPTY_LAYER_NAME_PREFIX + i);
208217
}
209218
writtenLayers.stream().map((id) -> id.getHash() + ".tar").forEach(layers::add);
210219
return layers;

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,6 +58,8 @@
5858
*/
5959
class EphemeralBuilderTests extends AbstractJsonTests {
6060

61+
private static final int EXISTING_IMAGE_LAYER_COUNT = 43;
62+
6163
@TempDir
6264
File temp;
6365

@@ -131,7 +133,7 @@ void getArchiveHasFixedCreateDate() throws Exception {
131133
void getArchiveContainsEnvLayer() throws Exception {
132134
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
133135
this.creator, this.env, this.buildpacks);
134-
File directory = unpack(getLayer(builder.getArchive(), 0), "env");
136+
File directory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT), "env");
135137
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
136138
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
137139
}
@@ -154,10 +156,13 @@ void getArchiveContainsBuildpackLayers() throws Exception {
154156
this.buildpacks = Buildpacks.of(buildpackList);
155157
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
156158
this.creator, null, this.buildpacks);
157-
assertBuildpackLayerContent(builder, 0, "/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml");
158-
assertBuildpackLayerContent(builder, 1, "/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
159-
assertBuildpackLayerContent(builder, 2, "/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
160-
File orderDirectory = unpack(getLayer(builder.getArchive(), 3), "order");
159+
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT,
160+
"/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml");
161+
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 1,
162+
"/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
163+
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 2,
164+
"/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
165+
File orderDirectory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT + 3), "order");
161166
assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8)
162167
.hasContent(content("order.toml"));
163168
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ void pullWhenListenerIsNullThrowsException() {
164164
void pullPullsImageAndProducesEvents() throws Exception {
165165
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
166166
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
167-
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
168-
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
167+
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
169168
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
170169
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
171170
Image image = this.api.pull(reference, this.pullListener);
@@ -180,8 +179,7 @@ void pullPullsImageAndProducesEvents() throws Exception {
180179
void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
181180
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
182181
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
183-
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
184-
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
182+
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
185183
given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json"));
186184
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
187185
Image image = this.api.pull(reference, this.pullListener, "auth token");

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,6 +40,8 @@
4040
*/
4141
class ImageArchiveTests extends AbstractJsonTests {
4242

43+
private static final int EXISTING_IMAGE_LAYER_COUNT = 46;
44+
4345
@Test
4446
void fromImageWritesToValidArchiveTar() throws Exception {
4547
Image image = Image.of(getContent("image.json"));
@@ -51,36 +53,39 @@ void fromImageWritesToValidArchiveTar() throws Exception {
5153
archive.writeTo(outputStream);
5254
try (TarArchiveInputStream tar = new TarArchiveInputStream(
5355
new ByteArrayInputStream(outputStream.toByteArray()))) {
56+
for (int i = 0; i < EXISTING_IMAGE_LAYER_COUNT; i++) {
57+
TarArchiveEntry blankEntry = tar.getNextTarEntry();
58+
assertThat(blankEntry.getName()).isEqualTo("blank_" + i);
59+
}
5460
TarArchiveEntry layerEntry = tar.getNextTarEntry();
5561
byte[] layerContent = read(tar, layerEntry.getSize());
5662
TarArchiveEntry configEntry = tar.getNextTarEntry();
5763
byte[] configContent = read(tar, configEntry.getSize());
5864
TarArchiveEntry manifestEntry = tar.getNextTarEntry();
5965
byte[] manifestContent = read(tar, manifestEntry.getSize());
60-
assertThat(tar.getNextTarEntry()).isNull();
6166
assertExpectedLayer(layerEntry, layerContent);
6267
assertExpectedConfig(configEntry, configContent);
6368
assertExpectedManifest(manifestEntry, manifestContent);
6469
}
6570
}
6671

6772
private void assertExpectedLayer(TarArchiveEntry entry, byte[] content) throws Exception {
68-
assertThat(entry.getName()).isEqualTo("/bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar");
73+
assertThat(entry.getName()).isEqualTo("bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar");
6974
try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) {
7075
TarArchiveEntry contentEntry = tar.getNextTarEntry();
7176
assertThat(contentEntry.getName()).isEqualTo("/spring/");
7277
}
7378
}
7479

7580
private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception {
76-
assertThat(entry.getName()).isEqualTo("/682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json");
81+
assertThat(entry.getName()).isEqualTo("682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json");
7782
String actualJson = new String(content, StandardCharsets.UTF_8);
7883
String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8);
7984
JSONAssert.assertEquals(expectedJson, actualJson, false);
8085
}
8186

8287
private void assertExpectedManifest(TarArchiveEntry entry, byte[] content) throws Exception {
83-
assertThat(entry.getName()).isEqualTo("/manifest.json");
88+
assertThat(entry.getName()).isEqualTo("manifest.json");
8489
String actualJson = new String(content, StandardCharsets.UTF_8);
8590
String expectedJson = StreamUtils.copyToString(getContent("image-archive-manifest.json"),
8691
StandardCharsets.UTF_8);

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
11
[
22
{
3-
"Config": "/682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
3+
"Config": "682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
44
"Layers": [
5-
"",
6-
"",
7-
"",
8-
"",
9-
"",
10-
"",
11-
"",
12-
"",
13-
"",
14-
"",
15-
"",
16-
"",
17-
"",
18-
"",
19-
"",
20-
"",
21-
"",
22-
"",
23-
"",
24-
"",
25-
"",
26-
"",
27-
"",
28-
"",
29-
"",
30-
"",
31-
"",
32-
"",
33-
"",
34-
"",
35-
"",
36-
"",
37-
"",
38-
"",
39-
"",
40-
"",
41-
"",
42-
"",
43-
"",
44-
"",
45-
"",
46-
"",
47-
"",
48-
"",
49-
"",
50-
"",
5+
"blank_0",
6+
"blank_1",
7+
"blank_2",
8+
"blank_3",
9+
"blank_4",
10+
"blank_5",
11+
"blank_6",
12+
"blank_7",
13+
"blank_8",
14+
"blank_9",
15+
"blank_10",
16+
"blank_11",
17+
"blank_12",
18+
"blank_13",
19+
"blank_14",
20+
"blank_15",
21+
"blank_16",
22+
"blank_17",
23+
"blank_18",
24+
"blank_19",
25+
"blank_20",
26+
"blank_21",
27+
"blank_22",
28+
"blank_23",
29+
"blank_24",
30+
"blank_25",
31+
"blank_26",
32+
"blank_27",
33+
"blank_28",
34+
"blank_29",
35+
"blank_30",
36+
"blank_31",
37+
"blank_32",
38+
"blank_33",
39+
"blank_34",
40+
"blank_35",
41+
"blank_36",
42+
"blank_37",
43+
"blank_38",
44+
"blank_39",
45+
"blank_40",
46+
"blank_41",
47+
"blank_42",
48+
"blank_43",
49+
"blank_44",
50+
"blank_45",
5151
"bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar"
5252
],
5353
"RepoTags": [

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The `bootBuildImage` task requires access to a Docker daemon.
1616
By default, it will communicate with a Docker daemon over a local connection.
1717
This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration.
1818

19-
Environment variables can be set to configure the `bootBuildImage` task to use the https://minikube.sigs.k8s.io/docs/tasks/docker_daemon/[Docker daemon provided by minikube].
19+
Environment variables can be set to configure the `bootBuildImage` task to use an alternative local or remote connection.
2020
The following table shows the environment variables and their values:
2121

2222
|===
@@ -32,8 +32,6 @@ The following table shows the environment variables and their values:
3232
| Path to certificate and key files for HTTPS (required if `DOCKER_TLS_VERIFY=1`, ignored otherwise)
3333
|===
3434

35-
On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started.
36-
3735
Docker daemon connection information can also be provided using `docker` properties in the plugin configuration.
3836
The following table summarizes the available properties:
3937

@@ -416,7 +414,14 @@ include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches]
416414
[[build-image.examples.docker]]
417415
=== Docker Configuration
418416

419-
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:
417+
[[build-image.examples.docker.minikube]]
418+
==== Docker Configuration for minikube
419+
420+
The plugin can communicate with the https://minikube.sigs.k8s.io/docs/tasks/docker_daemon/[Docker daemon provided by minikube] instead of the default local connection.
421+
422+
On Linux and macOS, environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started.
423+
424+
The plugin can also be configured to use the minikube daemon by providing connection details similar to those shown in the following example:
420425

421426
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
422427
.Groovy
@@ -430,6 +435,28 @@ include::../gradle/packaging/boot-build-image-docker-host.gradle[tags=docker-hos
430435
include::../gradle/packaging/boot-build-image-docker-host.gradle.kts[tags=docker-host]
431436
----
432437

438+
[[build-image.examples.docker.podman]]
439+
==== Docker Configuration for podman
440+
441+
The plugin can communicate with a https://podman.io/[podman container engine].
442+
443+
The plugin can be configured to use podman local connection by providing connection details similar to those shown in the following example:
444+
445+
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
446+
.Groovy
447+
----
448+
include::../gradle/packaging/boot-build-image-docker-host-podman.gradle[tags=docker-host]
449+
----
450+
451+
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
452+
.Kotlin
453+
----
454+
include::../gradle/packaging/boot-build-image-docker-host-podman.gradle.kts[tags=docker-host]
455+
----
456+
457+
[[build-image.examples.docker.auth]]
458+
==== Docker Configuration for Authentication
459+
433460
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.builderRegistry` properties as shown in the following example:
434461

435462
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '{gradle-project-version}'
4+
}
5+
6+
tasks.named("bootJar") {
7+
mainClass = 'com.example.ExampleApplication'
8+
}
9+
10+
// tag::docker-host[]
11+
tasks.named("bootBuildImage") {
12+
docker {
13+
host = "unix:///run/user/1000/podman/podman.sock"
14+
bindHostToBuilder = true
15+
}
16+
}
17+
// end::docker-host[]
18+
19+
tasks.register("bootBuildImageDocker") {
20+
doFirst {
21+
println("host=${tasks.bootBuildImage.docker.host}")
22+
println("bindHostToBuilder=${tasks.bootBuildImage.docker.bindHostToBuilder}")
23+
}
24+
}

0 commit comments

Comments
 (0)