Skip to content

Commit

Permalink
Support podman for building images
Browse files Browse the repository at this point in the history
  • Loading branch information
scottfrederick committed Mar 11, 2022
1 parent 7ad538c commit de321b0
Show file tree
Hide file tree
Showing 15 changed files with 227 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent>
listener.onUpdate(event);
});
}
return inspect(reference.withDigest(digestCapture.getCapturedDigest()));
return inspect(reference);
}
finally {
listener.onFinish();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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) -> {
};

Expand Down Expand Up @@ -125,16 +127,23 @@ private void write(Layout writer) throws IOException {
}

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));
}
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;
}

Expand All @@ -144,7 +153,7 @@ private String writeConfig(Layout writer, List<LayerId> writtenLayers) throws IO
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;
}
Expand Down Expand Up @@ -187,7 +196,7 @@ private JsonNode createRootFs(List<LayerId> writtenLayers) {
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) {
Expand All @@ -204,7 +213,7 @@ private ArrayNode createManifest(String config, List<LayerId> writtenLayers) {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -58,6 +58,8 @@
*/
class EphemeralBuilderTests extends AbstractJsonTests {

private static final int EXISTING_IMAGE_LAYER_COUNT = 43;

@TempDir
File temp;

Expand Down Expand Up @@ -131,7 +133,7 @@ void getArchiveHasFixedCreateDate() throws Exception {
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("");
}
Expand All @@ -154,10 +156,13 @@ void getArchiveContainsBuildpackLayers() throws Exception {
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"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ void pullWhenListenerIsNullThrowsException() {
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);
Expand All @@ -180,8 +179,7 @@ void pullPullsImageAndProducesEvents() throws Exception {
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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -40,6 +40,8 @@
*/
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"));
Expand All @@ -51,36 +53,39 @@ void fromImageWritesToValidArchiveTar() throws Exception {
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);
}
}

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/");
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:

|===
Expand All @@ -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:

Expand Down Expand Up @@ -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
Expand All @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
@@ -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}")
}
}
Loading

0 comments on commit de321b0

Please sign in to comment.