Merge branch '2.7.x'

Closes gh-31559
This commit is contained in:
Scott Frederick 2022-06-30 14:58:48 -05:00
commit e49597c1ae
8 changed files with 441 additions and 17 deletions

View File

@ -107,7 +107,8 @@ public class Builder {
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
assertStackIdsMatch(runImage, builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata);
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
@ -143,8 +144,10 @@ public class Builder {
+ "' does not match builder stack '" + builderImageStackId + "'");
}
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata);
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
BuildpackLayersMetadata buildpackLayersMetadata) {
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata,
buildpackLayersMetadata);
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
}
@ -245,9 +248,13 @@ public class Builder {
private final BuilderMetadata builderMetadata;
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
private final BuildpackLayersMetadata buildpackLayersMetadata;
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
BuildpackLayersMetadata buildpackLayersMetadata) {
this.imageFetcher = imageFetcher;
this.builderMetadata = builderMetadata;
this.buildpackLayersMetadata = buildpackLayersMetadata;
}
@Override
@ -255,6 +262,11 @@ public class Builder {
return this.builderMetadata.getBuildpacks();
}
@Override
public BuildpackLayersMetadata getBuildpackLayersMetadata() {
return this.buildpackLayersMetadata;
}
@Override
public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException {
return this.imageFetcher.fetchImage(imageType, reference);

View File

@ -0,0 +1,194 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import org.springframework.boot.buildpack.platform.json.MappedObject;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Buildpack layers metadata information.
*
* @author Scott Frederick
*/
final class BuildpackLayersMetadata extends MappedObject {
private static final String LABEL_NAME = "io.buildpacks.buildpack.layers";
private final Buildpacks buildpacks;
private BuildpackLayersMetadata(JsonNode node) {
super(node, MethodHandles.lookup());
this.buildpacks = Buildpacks.fromJson(getNode());
}
/**
* Return the metadata details of a buildpack with the given ID and version.
* @param id the buildpack ID
* @param version the buildpack version
* @return the buildpack details or {@code null} if a buildpack with the given ID and
* version does not exist in the metadata
*/
BuildpackLayerDetails getBuildpack(String id, String version) {
return this.buildpacks.getBuildpack(id, version);
}
/**
* Create a {@link BuildpackLayersMetadata} from an image.
* @param image the source image
* @return the buildpack layers metadata
* @throws IOException on IO error
*/
static BuildpackLayersMetadata fromImage(Image image) throws IOException {
Assert.notNull(image, "Image must not be null");
return fromImageConfig(image.getConfig());
}
/**
* Create a {@link BuildpackLayersMetadata} from image config.
* @param imageConfig the source image config
* @return the buildpack layers metadata
* @throws IOException on IO error
*/
static BuildpackLayersMetadata fromImageConfig(ImageConfig imageConfig) throws IOException {
Assert.notNull(imageConfig, "ImageConfig must not be null");
String json = imageConfig.getLabels().get(LABEL_NAME);
Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '"
+ StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'");
return fromJson(json);
}
/**
* Create a {@link BuildpackLayersMetadata} from JSON.
* @param json the source JSON
* @return the buildpack layers metadata
* @throws IOException on IO error
*/
static BuildpackLayersMetadata fromJson(String json) throws IOException {
return fromJson(SharedObjectMapper.get().readTree(json));
}
/**
* Create a {@link BuildpackLayersMetadata} from JSON.
* @param node the source JSON
* @return the buildpack layers metadata
*/
static BuildpackLayersMetadata fromJson(JsonNode node) {
return new BuildpackLayersMetadata(node);
}
private static class Buildpacks {
private final Map<String, BuildpackVersions> buildpacks = new HashMap<>();
private BuildpackLayerDetails getBuildpack(String id, String version) {
if (this.buildpacks.containsKey(id)) {
return this.buildpacks.get(id).getBuildpack(version);
}
return null;
}
private void addBuildpackVersions(String id, BuildpackVersions versions) {
this.buildpacks.put(id, versions);
}
private static Buildpacks fromJson(JsonNode node) {
Buildpacks buildpacks = new Buildpacks();
node.fields().forEachRemaining((field) -> buildpacks.addBuildpackVersions(field.getKey(),
BuildpackVersions.fromJson(field.getValue())));
return buildpacks;
}
}
private static class BuildpackVersions {
private final Map<String, BuildpackLayerDetails> versions = new HashMap<>();
private BuildpackLayerDetails getBuildpack(String version) {
return this.versions.get(version);
}
private void addBuildpackVersion(String version, BuildpackLayerDetails details) {
this.versions.put(version, details);
}
private static BuildpackVersions fromJson(JsonNode node) {
BuildpackVersions versions = new BuildpackVersions();
node.fields().forEachRemaining((field) -> versions.addBuildpackVersion(field.getKey(),
BuildpackLayerDetails.fromJson(field.getValue())));
return versions;
}
}
static final class BuildpackLayerDetails extends MappedObject {
private final String name;
private final String homepage;
private final String layerDiffId;
private BuildpackLayerDetails(JsonNode node) {
super(node, MethodHandles.lookup());
this.name = valueAt("/name", String.class);
this.homepage = valueAt("/homepage", String.class);
this.layerDiffId = valueAt("/layerDiffID", String.class);
}
/**
* Return the buildpack name.
* @return the name
*/
String getName() {
return this.name;
}
/**
* Return the buildpack homepage address.
* @return the homepage address
*/
String getHomepage() {
return this.homepage;
}
/**
* Return the buildpack layer {@code diffID}.
* @return the layer {@code diffID}
*/
String getLayerDiffId() {
return this.layerDiffId;
}
private static BuildpackLayerDetails fromJson(JsonNode node) {
return new BuildpackLayerDetails(node);
}
}
}

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.
@ -34,6 +34,8 @@ interface BuildpackResolverContext {
List<BuildpackMetadata> getBuildpackMetadata();
BuildpackLayersMetadata getBuildpackLayersMetadata();
/**
* Retrieve an image.
* @param reference the image reference

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.
@ -28,10 +28,12 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.springframework.boot.buildpack.platform.build.BuildpackLayersMetadata.BuildpackLayerDetails;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.docker.type.LayerId;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.util.StreamUtils;
@ -59,13 +61,28 @@ final class ImageBuildpack implements Buildpack {
Image image = context.fetchImage(reference, ImageType.BUILDPACK);
BuildpackMetadata buildpackMetadata = BuildpackMetadata.fromImage(image);
this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata);
this.exportedLayers = new ExportedLayers(context, reference);
if (!buildpackExistsInBuilder(context, image.getLayers())) {
this.exportedLayers = new ExportedLayers(context, reference);
}
else {
this.exportedLayers = null;
}
}
catch (IOException | DockerEngineException ex) {
throw new IllegalArgumentException("Error pulling buildpack image '" + reference + "'", ex);
}
}
private boolean buildpackExistsInBuilder(BuildpackResolverContext context, List<LayerId> imageLayers) {
BuildpackLayerDetails buildpackLayerDetails = context.getBuildpackLayersMetadata()
.getBuildpack(this.coordinates.getId(), this.coordinates.getVersion());
if (buildpackLayerDetails != null) {
String layerDiffId = buildpackLayerDetails.getLayerDiffId();
return imageLayers.stream().map(LayerId::toString).anyMatch((layerId) -> layerId.equals(layerDiffId));
}
return false;
}
@Override
public BuildpackCoordinates getCoordinates() {
return this.coordinates;
@ -73,7 +90,9 @@ final class ImageBuildpack implements Buildpack {
@Override
public void apply(IOConsumer<Layer> layers) throws IOException {
this.exportedLayers.apply(layers);
if (this.exportedLayers != null) {
this.exportedLayers.apply(layers);
}
}
/**

View File

@ -0,0 +1,97 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BuildpackLayersMetadata}.
*
* @author Scott Frederick
*/
class BuildpackLayersMetadataTests extends AbstractJsonTests {
@Test
void fromImageLoadsMetadata() throws IOException {
Image image = Image.of(getContent("buildpack-image.json"));
BuildpackLayersMetadata metadata = BuildpackLayersMetadata.fromImage(image);
assertThat(metadata.getBuildpack("example/hello-moon", "0.0.3")).extracting("homepage", "layerDiffId")
.containsExactly("https://github.com/example/tree/main/buildpacks/hello-moon",
"sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2");
assertThat(metadata.getBuildpack("example/hello-world", "0.0.2")).extracting("homepage", "layerDiffId")
.containsExactly("https://github.com/example/tree/main/buildpacks/hello-world",
"sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940");
assertThat(metadata.getBuildpack("example/hello-world", "version-does-not-exist")).isNull();
assertThat(metadata.getBuildpack("id-does-not-exist", "9.9.9")).isNull();
}
@Test
void fromImageWhenImageIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackLayersMetadata.fromImage(null))
.withMessage("Image must not be null");
}
@Test
void fromImageWhenImageConfigIsNullThrowsException() {
Image image = mock(Image.class);
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackLayersMetadata.fromImage(image))
.withMessage("ImageConfig must not be null");
}
@Test
void fromImageConfigWhenLabelIsMissingThrowsException() {
Image image = mock(Image.class);
ImageConfig imageConfig = mock(ImageConfig.class);
given(image.getConfig()).willReturn(imageConfig);
given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a"));
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackLayersMetadata.fromImage(image))
.withMessage("No 'io.buildpacks.buildpack.layers' label found in image config labels 'alpha'");
}
@Test
void fromJsonLoadsMetadata() throws IOException {
BuildpackLayersMetadata metadata = BuildpackLayersMetadata
.fromJson(getContentAsString("buildpack-layers-metadata.json"));
assertThat(metadata.getBuildpack("example/hello-moon", "0.0.3")).extracting("name", "homepage", "layerDiffId")
.containsExactly("Example hello-moon buildpack",
"https://github.com/example/tree/main/buildpacks/hello-moon",
"sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2");
assertThat(metadata.getBuildpack("example/hello-world", "0.0.1")).extracting("name", "homepage", "layerDiffId")
.containsExactly("Example hello-world buildpack",
"https://github.com/example/tree/main/buildpacks/hello-world",
"sha256:1c90e0b80d92555a0523c9ee6500845328fc39ba9dca9d30a877ff759ffbff28");
assertThat(metadata.getBuildpack("example/hello-world", "0.0.2")).extracting("name", "homepage", "layerDiffId")
.containsExactly("Example hello-world buildpack",
"https://github.com/example/tree/main/buildpacks/hello-world",
"sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940");
assertThat(metadata.getBuildpack("example/hello-world", "version-does-not-exist")).isNull();
assertThat(metadata.getBuildpack("id-does-not-exist", "9.9.9")).isNull();
}
}

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.
@ -84,6 +84,7 @@ class BuildpackResolversTests extends AbstractJsonTests {
void resolveAllWithImageBuildpackReferenceReturnsExpectedBuildpack() throws IOException {
Image image = Image.of(getContent("buildpack-image.json"));
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}"));
given(resolverContext.fetchImage(any(), any())).willReturn(image);
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest");
Buildpacks buildpacks = BuildpackResolvers.resolveAll(resolverContext, Collections.singleton(reference));

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.
@ -64,29 +64,31 @@ class ImageBuildpackTests extends AbstractJsonTests {
}
@Test
void resolveWhenFullyQualifiedReferenceReturnsBuilder() throws Exception {
void resolveWhenFullyQualifiedReferenceReturnsBuildpack() throws Exception {
Image image = Image.of(getContent("buildpack-image.json"));
ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}"));
given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any());
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:1.0.0");
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertHasExpectedLayers(buildpack);
assertAppliesExpectedLayers(buildpack);
}
@Test
void resolveWhenUnqualifiedReferenceReturnsBuilder() throws Exception {
void resolveWhenUnqualifiedReferenceReturnsBuildpack() throws Exception {
Image image = Image.of(getContent("buildpack-image.json"));
ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}"));
given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any());
BuildpackReference reference = BuildpackReference.of("example/buildpack1:1.0.0");
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertHasExpectedLayers(buildpack);
assertAppliesExpectedLayers(buildpack);
}
@Test
@ -94,12 +96,13 @@ class ImageBuildpackTests extends AbstractJsonTests {
Image image = Image.of(getContent("buildpack-image.json"));
ImageReference imageReference = ImageReference.of("example/buildpack1:latest");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}"));
given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any());
BuildpackReference reference = BuildpackReference.of("example/buildpack1");
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertHasExpectedLayers(buildpack);
assertAppliesExpectedLayers(buildpack);
}
@Test
@ -108,12 +111,28 @@ class ImageBuildpackTests extends AbstractJsonTests {
String digest = "sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
ImageReference imageReference = ImageReference.of("example/buildpack1@" + digest);
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}"));
given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any());
BuildpackReference reference = BuildpackReference.of("example/buildpack1@" + digest);
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertHasExpectedLayers(buildpack);
assertAppliesExpectedLayers(buildpack);
}
@Test
void resolveWhenBuildpackExistsInBuilderSkipsLayers() throws Exception {
Image image = Image.of(getContent("buildpack-image.json"));
ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata())
.willReturn(BuildpackLayersMetadata.fromJson(getContentAsString("buildpack-layers-metadata.json")));
given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any());
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:1.0.0");
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertAppliesNoLayers(buildpack);
}
@Test
@ -181,7 +200,7 @@ class ImageBuildpackTests extends AbstractJsonTests {
tarOut.closeArchiveEntry();
}
private void assertHasExpectedLayers(Buildpack buildpack) throws IOException {
private void assertAppliesExpectedLayers(Buildpack buildpack) throws IOException {
List<ByteArrayOutputStream> layers = new ArrayList<>();
buildpack.apply((layer) -> {
ByteArrayOutputStream out = new ByteArrayOutputStream();
@ -208,4 +227,14 @@ class ImageBuildpackTests extends AbstractJsonTests {
TarArchiveEntry.DEFAULT_FILE_MODE));
}
private void assertAppliesNoLayers(Buildpack buildpack) throws IOException {
List<ByteArrayOutputStream> layers = new ArrayList<>();
buildpack.apply((layer) -> {
ByteArrayOutputStream out = new ByteArrayOutputStream();
layer.writeTo(out);
layers.add(out);
});
assertThat(layers).isEmpty();
}
}

View File

@ -0,0 +1,70 @@
{
"example/hello-moon": {
"0.0.3": {
"api": "0.2",
"stacks": [
{
"id": "io.buildpacks.stacks.alpine"
},
{
"id": "io.buildpacks.stacks.bionic"
}
],
"name": "Example hello-moon buildpack",
"layerDiffID": "sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2",
"homepage": "https://github.com/example/tree/main/buildpacks/hello-moon"
}
},
"example/hello-universe": {
"0.0.1": {
"api": "0.2",
"order": [
{
"group": [
{
"id": "example/hello-world",
"version": "0.0.2"
},
{
"id": "example/hello-moon",
"version": "0.0.2"
}
]
}
],
"name": "Example hello-universe buildpack",
"layerDiffID": "sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69",
"homepage": "https://github.com/example/tree/main/buildpacks/hello-universe"
}
},
"example/hello-world": {
"0.0.1": {
"api": "0.2",
"stacks": [
{
"id": "io.buildpacks.stacks.alpine"
},
{
"id": "io.buildpacks.stacks.bionic"
}
],
"name": "Example hello-world buildpack",
"layerDiffID": "sha256:1c90e0b80d92555a0523c9ee6500845328fc39ba9dca9d30a877ff759ffbff28",
"homepage": "https://github.com/example/tree/main/buildpacks/hello-world"
},
"0.0.2": {
"api": "0.2",
"stacks": [
{
"id": "io.buildpacks.stacks.alpine"
},
{
"id": "io.buildpacks.stacks.bionic"
}
],
"name": "Example hello-world buildpack",
"layerDiffID": "sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940",
"homepage": "https://github.com/example/tree/main/buildpacks/hello-world"
}
}
}