Support podman for building images

Closes gh-30196
This commit is contained in:
Scott Frederick 2022-03-11 16:39:07 -06:00
parent 7ad538cd84
commit de321b00b7
15 changed files with 227 additions and 84 deletions

View File

@ -187,7 +187,7 @@ public class DockerApi {
listener.onUpdate(event);
});
}
return inspect(reference.withDigest(digestCapture.getCapturedDigest()));
return inspect(reference);
}
finally {
listener.onFinish();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 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.
@ -61,6 +61,8 @@ public class ImageArchive implements TarArchive {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME
.withZone(ZoneOffset.UTC);
private static final String EMPTY_LAYER_NAME_PREFIX = "blank_";
private static final IOConsumer<Update> NO_UPDATES = (update) -> {
};
@ -125,6 +127,9 @@ public class ImageArchive implements TarArchive {
}
private List<LayerId> writeLayers(Layout writer) throws IOException {
for (int i = 0; i < this.existingLayers.size(); i++) {
writeEmptyLayer(writer, EMPTY_LAYER_NAME_PREFIX + i);
}
List<LayerId> writtenLayers = new ArrayList<>();
for (Layer layer : this.newLayers) {
writtenLayers.add(writeLayer(writer, layer));
@ -132,9 +137,13 @@ public class ImageArchive implements TarArchive {
return Collections.unmodifiableList(writtenLayers);
}
private void writeEmptyLayer(Layout writer, String name) throws IOException {
writer.file(name, Owner.ROOT, Content.of(""));
}
private LayerId writeLayer(Layout writer, Layer layer) throws IOException {
LayerId id = layer.getId();
writer.file("/" + id.getHash() + ".tar", Owner.ROOT, layer);
writer.file(id.getHash() + ".tar", Owner.ROOT, layer);
return id;
}
@ -144,7 +153,7 @@ public class ImageArchive implements TarArchive {
String json = this.objectMapper.writeValueAsString(config).replace("\r\n", "\n");
MessageDigest digest = MessageDigest.getInstance("SHA-256");
InspectedContent content = InspectedContent.of(Content.of(json), digest::update);
String name = "/" + LayerId.ofSha256Digest(digest.digest()).getHash() + ".json";
String name = LayerId.ofSha256Digest(digest.digest()).getHash() + ".json";
writer.file(name, Owner.ROOT, content);
return name;
}
@ -187,7 +196,7 @@ public class ImageArchive implements TarArchive {
private void writeManifest(Layout writer, String config, List<LayerId> writtenLayers) throws IOException {
ArrayNode manifest = createManifest(config, writtenLayers);
String manifestJson = this.objectMapper.writeValueAsString(manifest);
writer.file("/manifest.json", Owner.ROOT, Content.of(manifestJson));
writer.file("manifest.json", Owner.ROOT, Content.of(manifestJson));
}
private ArrayNode createManifest(String config, List<LayerId> writtenLayers) {
@ -204,7 +213,7 @@ public class ImageArchive implements TarArchive {
private ArrayNode getManifestLayers(List<LayerId> writtenLayers) {
ArrayNode layers = this.objectMapper.createArrayNode();
for (int i = 0; i < this.existingLayers.size(); i++) {
layers.add("");
layers.add(EMPTY_LAYER_NAME_PREFIX + i);
}
writtenLayers.stream().map((id) -> id.getHash() + ".tar").forEach(layers::add);
return layers;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
@ -58,6 +58,8 @@ import static org.assertj.core.api.Assertions.entry;
*/
class EphemeralBuilderTests extends AbstractJsonTests {
private static final int EXISTING_IMAGE_LAYER_COUNT = 43;
@TempDir
File temp;
@ -131,7 +133,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
void getArchiveContainsEnvLayer() throws Exception {
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, this.env, this.buildpacks);
File directory = unpack(getLayer(builder.getArchive(), 0), "env");
File directory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT), "env");
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
}
@ -154,10 +156,13 @@ class EphemeralBuilderTests extends AbstractJsonTests {
this.buildpacks = Buildpacks.of(buildpackList);
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, null, this.buildpacks);
assertBuildpackLayerContent(builder, 0, "/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml");
assertBuildpackLayerContent(builder, 1, "/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
assertBuildpackLayerContent(builder, 2, "/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
File orderDirectory = unpack(getLayer(builder.getArchive(), 3), "order");
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT,
"/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml");
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 1,
"/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 2,
"/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
File orderDirectory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT + 3), "order");
assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8)
.hasContent(content("order.toml"));
}

View File

@ -164,8 +164,7 @@ class DockerApiTests {
void pullPullsImageAndProducesEvents() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, this.pullListener);
@ -180,8 +179,7 @@ class DockerApiTests {
void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, this.pullListener, "auth token");

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 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.
@ -40,6 +40,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class ImageArchiveTests extends AbstractJsonTests {
private static final int EXISTING_IMAGE_LAYER_COUNT = 46;
@Test
void fromImageWritesToValidArchiveTar() throws Exception {
Image image = Image.of(getContent("image.json"));
@ -51,13 +53,16 @@ class ImageArchiveTests extends AbstractJsonTests {
archive.writeTo(outputStream);
try (TarArchiveInputStream tar = new TarArchiveInputStream(
new ByteArrayInputStream(outputStream.toByteArray()))) {
for (int i = 0; i < EXISTING_IMAGE_LAYER_COUNT; i++) {
TarArchiveEntry blankEntry = tar.getNextTarEntry();
assertThat(blankEntry.getName()).isEqualTo("blank_" + i);
}
TarArchiveEntry layerEntry = tar.getNextTarEntry();
byte[] layerContent = read(tar, layerEntry.getSize());
TarArchiveEntry configEntry = tar.getNextTarEntry();
byte[] configContent = read(tar, configEntry.getSize());
TarArchiveEntry manifestEntry = tar.getNextTarEntry();
byte[] manifestContent = read(tar, manifestEntry.getSize());
assertThat(tar.getNextTarEntry()).isNull();
assertExpectedLayer(layerEntry, layerContent);
assertExpectedConfig(configEntry, configContent);
assertExpectedManifest(manifestEntry, manifestContent);
@ -65,7 +70,7 @@ class ImageArchiveTests extends AbstractJsonTests {
}
private void assertExpectedLayer(TarArchiveEntry entry, byte[] content) throws Exception {
assertThat(entry.getName()).isEqualTo("/bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar");
assertThat(entry.getName()).isEqualTo("bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar");
try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) {
TarArchiveEntry contentEntry = tar.getNextTarEntry();
assertThat(contentEntry.getName()).isEqualTo("/spring/");
@ -73,14 +78,14 @@ class ImageArchiveTests extends AbstractJsonTests {
}
private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception {
assertThat(entry.getName()).isEqualTo("/682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json");
assertThat(entry.getName()).isEqualTo("682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json");
String actualJson = new String(content, StandardCharsets.UTF_8);
String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8);
JSONAssert.assertEquals(expectedJson, actualJson, false);
}
private void assertExpectedManifest(TarArchiveEntry entry, byte[] content) throws Exception {
assertThat(entry.getName()).isEqualTo("/manifest.json");
assertThat(entry.getName()).isEqualTo("manifest.json");
String actualJson = new String(content, StandardCharsets.UTF_8);
String expectedJson = StreamUtils.copyToString(getContent("image-archive-manifest.json"),
StandardCharsets.UTF_8);

View File

@ -1,53 +1,53 @@
[
{
"Config": "/682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
"Config": "682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
"Layers": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"blank_0",
"blank_1",
"blank_2",
"blank_3",
"blank_4",
"blank_5",
"blank_6",
"blank_7",
"blank_8",
"blank_9",
"blank_10",
"blank_11",
"blank_12",
"blank_13",
"blank_14",
"blank_15",
"blank_16",
"blank_17",
"blank_18",
"blank_19",
"blank_20",
"blank_21",
"blank_22",
"blank_23",
"blank_24",
"blank_25",
"blank_26",
"blank_27",
"blank_28",
"blank_29",
"blank_30",
"blank_31",
"blank_32",
"blank_33",
"blank_34",
"blank_35",
"blank_36",
"blank_37",
"blank_38",
"blank_39",
"blank_40",
"blank_41",
"blank_42",
"blank_43",
"blank_44",
"blank_45",
"bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar"
],
"RepoTags": [

View File

@ -16,7 +16,7 @@ The `bootBuildImage` task requires access to a Docker daemon.
By default, it will communicate with a Docker daemon over a local connection.
This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration.
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].
Environment variables can be set to configure the `bootBuildImage` task to use an alternative local or remote connection.
The following table shows the environment variables and their values:
|===
@ -32,8 +32,6 @@ The following table shows the environment variables and their values:
| Path to certificate and key files for HTTPS (required if `DOCKER_TLS_VERIFY=1`, ignored otherwise)
|===
On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started.
Docker daemon connection information can also be provided using `docker` properties in the plugin configuration.
The following table summarizes the available properties:
@ -416,7 +414,14 @@ 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:
[[build-image.examples.docker.minikube]]
==== Docker Configuration for minikube
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.
On Linux and macOS, environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started.
The plugin can also be configured to use the minikube daemon by providing connection details similar to those shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
@ -430,6 +435,28 @@ include::../gradle/packaging/boot-build-image-docker-host.gradle[tags=docker-hos
include::../gradle/packaging/boot-build-image-docker-host.gradle.kts[tags=docker-host]
----
[[build-image.examples.docker.podman]]
==== Docker Configuration for podman
The plugin can communicate with a https://podman.io/[podman container engine].
The plugin can be configured to use podman local connection by providing connection details similar to those shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::../gradle/packaging/boot-build-image-docker-host-podman.gradle[tags=docker-host]
----
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
.Kotlin
----
include::../gradle/packaging/boot-build-image-docker-host-podman.gradle.kts[tags=docker-host]
----
[[build-image.examples.docker.auth]]
==== Docker Configuration for Authentication
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:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]

View File

@ -0,0 +1,24 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{gradle-project-version}'
}
tasks.named("bootJar") {
mainClass = 'com.example.ExampleApplication'
}
// tag::docker-host[]
tasks.named("bootBuildImage") {
docker {
host = "unix:///run/user/1000/podman/podman.sock"
bindHostToBuilder = true
}
}
// end::docker-host[]
tasks.register("bootBuildImageDocker") {
doFirst {
println("host=${tasks.bootBuildImage.docker.host}")
println("bindHostToBuilder=${tasks.bootBuildImage.docker.bindHostToBuilder}")
}
}

View File

@ -0,0 +1,27 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
tasks.named<BootJar>("bootJar") {
mainClass.set("com.example.ExampleApplication")
}
// tag::docker-host[]
tasks.named<BootBuildImage>("bootBuildImage") {
docker {
host = "unix:///run/user/1000/podman/podman.sock"
isBindHostToBuilder = true
}
}
// end::docker-host[]
tasks.register("bootBuildImageDocker") {
doFirst {
println("host=${tasks.getByName<BootBuildImage>("bootBuildImage").docker.host}")
println("bindHostToBuilder=${tasks.getByName<BootBuildImage>("bootBuildImage").docker.isBindHostToBuilder}")
}
}

View File

@ -12,7 +12,7 @@ tasks.named("bootBuildImage") {
docker {
host = "tcp://192.168.99.100:2376"
tlsVerify = true
certPath = "/home/users/.minikube/certs"
certPath = "/home/user/.minikube/certs"
}
}
// end::docker-host[]

View File

@ -15,7 +15,7 @@ tasks.named<BootBuildImage>("bootBuildImage") {
docker {
host = "tcp://192.168.99.100:2376"
isTlsVerify = true
certPath = "/home/users/.minikube/certs"
certPath = "/home/user/.minikube/certs"
}
}
// end::docker-host[]

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
@ -279,11 +279,19 @@ class PackagingDocumentationTests {
}
@TestTemplate
void bootBuildImageWithDockerHost() {
void bootBuildImageWithDockerHostMinikube() {
BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-docker-host")
.build("bootBuildImageDocker");
assertThat(result.getOutput()).contains("host=tcp://192.168.99.100:2376").contains("tlsVerify=true")
.contains("certPath=/home/users/.minikube/certs");
.contains("certPath=/home/user/.minikube/certs");
}
@TestTemplate
void bootBuildImageWithDockerHostPodman() {
BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-docker-host-podman")
.build("bootBuildImageDocker");
assertThat(result.getOutput()).contains("host=unix:///run/user/1000/podman/podman.sock")
.contains("bindHostToBuilder=true");
}
@TestTemplate

View File

@ -25,7 +25,7 @@ The `build-image` goal requires access to a Docker daemon.
By default, it will communicate with a Docker daemon over a local connection.
This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration.
Environment variables can be set to configure the `build-image` goal to use the https://minikube.sigs.k8s.io/docs/tasks/docker_daemon/[Docker daemon provided by minikube].
Environment variables can be set to configure the `build-image` goal to use an alternative local or remote connection.
The following table shows the environment variables and their values:
|===
@ -41,8 +41,6 @@ The following table shows the environment variables and their values:
| Path to certificate and key files for HTTPS (required if `DOCKER_TLS_VERIFY=1`, ignored otherwise)
|===
On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started.
Docker daemon connection information can also be provided using `docker` parameters in the plugin configuration.
The following table summarizes the available parameters:
@ -372,13 +370,35 @@ include::../maven/packaging-oci-image/caches-pom.xml[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` parameters as shown in the following example:
[[build-image.examples.docker.minikube]]
==== Docker Configuration for minikube
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.
On Linux and macOS, environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started.
The plugin can also be configured to use the minikube daemon by providing connection details similar to those shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
----
include::../maven/packaging-oci-image/docker-remote-pom.xml[tags=docker-remote]
include::../maven/packaging-oci-image/docker-minikube-pom.xml[tags=docker-minikube]
----
[[build-image.examples.docker.podman]]
==== Docker Configuration for podman
The plugin can communicate with a https://podman.io/[podman container engine].
The plugin can be configured to use podman local connection by providing connection details similar to those shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
----
include::../maven/packaging-oci-image/docker-podman-pom.xml[tags=docker-podman]
----
[[build-image.examples.docker.auth]]
==== Docker Configuration for Authentication
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` parameters as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::docker-remote[] -->
<!-- tag::docker-minikube[] -->
<project>
<build>
<plugins>
@ -17,5 +17,5 @@
</plugins>
</build>
</project>
<!-- end::docker-remote[] -->
<!-- end::docker-minikube[] -->

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::docker-podman[] -->
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<docker>
<host>unix:///run/user/1000/podman/podman.sock</host>
<bindHostToBuilder>true</bindHostToBuilder>
</docker>
</configuration>
</plugin>
</plugins>
</build>
</project>
<!-- end::docker-podman[] -->