diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 7945b55c10a..504448abccd 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -106,17 +106,25 @@ public class Builder { this.log.start(request); validateBindings(request.getBindings()); PullPolicy pullPolicy = request.getPullPolicy(); - ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(), - pullPolicy, request.getImagePlatform()); - Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder()); + ImagePlatform platform = request.getImagePlatform(); + boolean specifiedPlatform = request.getImagePlatform() != null; + DockerRegistryAuthentication registryAuthentication = this.dockerConfiguration.builderRegistryAuthentication(); + ImageFetcher imageFetcher = new ImageFetcher(registryAuthentication, pullPolicy); + Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder(), platform); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); request = withRunImageIfNeeded(request, builderMetadata); Assert.state(request.getRunImage() != null, "'request.getRunImage()' must not be null"); - Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage()); + platform = (platform != null) ? platform : ImagePlatform.from(builderImage); + Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform); + if (specifiedPlatform && runImage.getPrimaryDigest() != null) { + request = request.withRunImage(request.getRunImage().withDigest(runImage.getPrimaryDigest())); + runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform); + } assertStackIdsMatch(runImage, builderImage); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage); - Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata); + Buildpacks buildpacks = getBuildpacks(request, imageFetcher, platform, builderMetadata, + buildpackLayersMetadata); EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), builderMetadata, request.getCreator(), request.getEnv(), buildpacks); executeLifecycle(request, ephemeralBuilder); @@ -160,9 +168,9 @@ public class Builder { } } - private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata, - BuildpackLayersMetadata buildpackLayersMetadata) { - BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata, + private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, ImagePlatform platform, + BuilderMetadata builderMetadata, BuildpackLayersMetadata buildpackLayersMetadata) { + BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, platform, builderMetadata, buildpackLayersMetadata); return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks()); } @@ -225,49 +233,74 @@ public class Builder { private final PullPolicy pullPolicy; - private @Nullable ImagePlatform defaultPlatform; - - ImageFetcher(@Nullable DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy, - @Nullable ImagePlatform platform) { + ImageFetcher(@Nullable DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy) { this.registryAuthentication = registryAuthentication; this.pullPolicy = pullPolicy; - this.defaultPlatform = platform; } - Image fetchImage(ImageType type, ImageReference reference) throws IOException { + Image fetchImage(ImageType type, ImageReference reference, @Nullable ImagePlatform platform) + throws IOException { Assert.notNull(type, "'type' must not be null"); Assert.notNull(reference, "'reference' must not be null"); if (this.pullPolicy == PullPolicy.ALWAYS) { - return checkPlatformMismatch(pullImage(reference, type), reference); + return pullImageAndCheckForPlatformMismatch(type, reference, platform); } try { - return checkPlatformMismatch(Builder.this.docker.image().inspect(reference), reference); + Image image = Builder.this.docker.image().inspect(reference, platform); + return checkPlatformMismatch(image, reference, platform); } catch (DockerEngineException ex) { if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) { - return checkPlatformMismatch(pullImage(reference, type), reference); + return pullImageAndCheckForPlatformMismatch(type, reference, platform); } throw ex; } } - private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { - TotalProgressPullListener listener = new TotalProgressPullListener( - Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType)); - String authHeader = authHeader(this.registryAuthentication, reference); - Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader); - Builder.this.log.pulledImage(image, imageType); - if (this.defaultPlatform == null) { - this.defaultPlatform = ImagePlatform.from(image); + private Image pullImageAndCheckForPlatformMismatch(ImageType type, ImageReference reference, + @Nullable ImagePlatform platform) throws IOException { + try { + Image image = pullImage(reference, type, platform); + return checkPlatformMismatch(image, reference, platform); } + catch (DockerEngineException ex) { + // Try to throw our own exception for consistent log output. Matching + // on the message is a little brittle, but it doesn't matter too much + // if it fails as the original exception is still enough to stop the build + if (platform != null && ex.getMessage() != null + && ex.getMessage().contains("does not provide the specified platform")) { + throwAsPlatformMismatchException(type, reference, platform, ex); + } + throw ex; + } + } + + private void throwAsPlatformMismatchException(ImageType type, ImageReference reference, ImagePlatform platform, + @Nullable Throwable cause) throws IOException { + try { + Image image = pullImage(reference, type, null); + throw new PlatformMismatchException(reference, platform, ImagePlatform.from(image), cause); + } + catch (DockerEngineException ex) { + } + } + + private Image pullImage(ImageReference reference, ImageType imageType, @Nullable ImagePlatform platform) + throws IOException { + TotalProgressPullListener listener = new TotalProgressPullListener( + Builder.this.log.pullingImage(reference, platform, imageType)); + String authHeader = authHeader(this.registryAuthentication, reference); + Image image = Builder.this.docker.image().pull(reference, platform, listener, authHeader); + Builder.this.log.pulledImage(image, imageType); return image; } - private Image checkPlatformMismatch(Image image, ImageReference imageReference) { - if (this.defaultPlatform != null) { - ImagePlatform imagePlatform = ImagePlatform.from(image); - if (!imagePlatform.equals(this.defaultPlatform)) { - throw new PlatformMismatchException(imageReference, this.defaultPlatform, imagePlatform); + private Image checkPlatformMismatch(Image image, ImageReference reference, + @Nullable ImagePlatform requestedPlatform) { + if (requestedPlatform != null) { + ImagePlatform actualPlatform = ImagePlatform.from(image); + if (!actualPlatform.equals(requestedPlatform)) { + throw new PlatformMismatchException(reference, requestedPlatform, actualPlatform, null); } } return image; @@ -278,9 +311,9 @@ public class Builder { private static final class PlatformMismatchException extends RuntimeException { private PlatformMismatchException(ImageReference imageReference, ImagePlatform requestedPlatform, - ImagePlatform actualPlatform) { + ImagePlatform actualPlatform, @Nullable Throwable cause) { super("Image platform mismatch detected. The configured platform '%s' is not supported by the image '%s'. Requested platform '%s' but got '%s'" - .formatted(requestedPlatform, imageReference, requestedPlatform, actualPlatform)); + .formatted(requestedPlatform, imageReference, requestedPlatform, actualPlatform), cause); } } @@ -326,13 +359,16 @@ public class Builder { private final ImageFetcher imageFetcher; + private final ImagePlatform platform; + private final BuilderMetadata builderMetadata; private final BuildpackLayersMetadata buildpackLayersMetadata; - BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata, + BuilderResolverContext(ImageFetcher imageFetcher, ImagePlatform platform, BuilderMetadata builderMetadata, BuildpackLayersMetadata buildpackLayersMetadata) { this.imageFetcher = imageFetcher; + this.platform = platform; this.builderMetadata = builderMetadata; this.buildpackLayersMetadata = buildpackLayersMetadata; } @@ -349,7 +385,7 @@ public class Builder { @Override public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException { - return this.imageFetcher.fetchImage(imageType, reference); + return this.imageFetcher.fetchImage(imageType, reference, this.platform); } @Override diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 407d5f9298c..f645cb9b2ae 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -66,6 +66,8 @@ public class DockerApi { static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41); + static final ApiVersion PLATFORM_INSPECT_API_VERSION = ApiVersion.of(1, 49); + static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0); static final String API_VERSION_HEADER_NAME = "API-Version"; @@ -237,7 +239,7 @@ public class DockerApi { listener.onUpdate(event); }); } - return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference); + return inspect(reference, platform); } finally { listener.onFinish(); @@ -339,17 +341,36 @@ public class DockerApi { * @throws IOException on IO error */ public Image inspect(ImageReference reference) throws IOException { - return inspect(API_VERSION, reference); + return inspect(reference, null); } - private Image inspect(ApiVersion apiVersion, ImageReference reference) throws IOException { + /** + * Inspect an image. + * @param reference the image reference + * @param platform the platform (os/architecture/variant) of the image to inspect. + * Ignored on older versions of Docker. + * @return the image from the local repository + * @throws IOException on IO error + * @since 3.4.12 + */ + public Image inspect(ImageReference reference, @Nullable ImagePlatform platform) throws IOException { + // The Docker documentation is incomplete but platform parameters + // are supported since 1.49 (see https://github.com/moby/moby/pull/49586) Assert.notNull(reference, "'reference' must not be null"); - URI imageUri = buildUrl(apiVersion, "/images/" + reference + "/json"); - try (Response response = http().get(imageUri)) { + URI inspectUrl = inspectUrl(reference, platform); + try (Response response = http().get(inspectUrl)) { return Image.of(response.getContent()); } } + private URI inspectUrl(ImageReference reference, @Nullable ImagePlatform platform) { + String path = "/images/" + reference + "/json"; + if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) { + return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson()); + } + return buildUrl(path); + } + public void tag(ImageReference sourceReference, ImageReference targetReference) throws IOException { Assert.notNull(sourceReference, "'sourceReference' must not be null"); Assert.notNull(targetReference, "'targetReference' must not be null"); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImagePlatform.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImagePlatform.java index 571eca32dc9..34f0374cc5b 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImagePlatform.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImagePlatform.java @@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A platform specification for a Docker image. @@ -102,4 +103,24 @@ public class ImagePlatform { return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant()); } + /** + * Return a JSON-encoded representation of this platform. + * @return the JSON string + */ + public String toJson() { + StringBuilder json = new StringBuilder("{"); + json.append(jsonPair("os", this.os)); + if (StringUtils.hasText(this.architecture)) { + json.append(",").append(jsonPair("architecture", this.architecture)); + } + if (StringUtils.hasText(this.variant)) { + json.append(",").append(jsonPair("variant", this.variant)); + } + return json.append("}").toString(); + } + + private String jsonPair(String name, String value) { + return "\"%s\":\"%s\"".formatted(name, value); + } + } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java index 0ad0c67d5d7..75aebe51005 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java @@ -22,11 +22,13 @@ import java.lang.invoke.MethodHandles; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -52,6 +54,8 @@ public class Image extends MappedObject { private final @Nullable String created; + private final @Nullable Descriptor descriptor; + Image(JsonNode node) { super(node, MethodHandles.lookup()); this.digests = childrenAt("/RepoDigests", JsonNode::asString); @@ -61,6 +65,9 @@ public class Image extends MappedObject { this.architecture = valueAt("/Architecture", String.class); this.variant = valueAt("/Variant", String.class); this.created = valueAt("/Created", String.class); + JsonNode descriptorNode = getNode().path("Descriptor"); + this.descriptor = (descriptorNode.isMissingNode() || descriptorNode.isNull()) ? null + : new Descriptor(descriptorNode); } private List extractLayers(String @Nullable [] layers) { @@ -126,6 +133,35 @@ public class Image extends MappedObject { return this.created; } + /** + * Return the descriptor for this image as reported by Docker Engine inspect. + * @return the image descriptor or {@code null} + */ + public @Nullable Descriptor getDescriptor() { + return this.descriptor; + } + + /** + * Return the primary digest of the image or {@code null}. Checks the + * {@code Descriptor.digest} first, falling back to {@code RepoDigest}. + * @return the primary digest or {@code null} + * @since 3.4.12 + */ + public @Nullable String getPrimaryDigest() { + if (this.descriptor != null && StringUtils.hasText(this.descriptor.getDigest())) { + return this.descriptor.getDigest(); + } + if (!CollectionUtils.isEmpty(this.digests)) { + try { + String digest = this.digests.get(0); + return (digest != null) ? ImageReference.of(digest).getDigest() : null; + } + catch (RuntimeException ex) { + } + } + return null; + } + /** * Create a new {@link Image} instance from the specified JSON content. * @param content the JSON content @@ -136,4 +172,24 @@ public class Image extends MappedObject { return of(content, Image::new); } + /** + * Descriptor details as reported in the {@code Docker inspect} response. + * + * @since 3.4.12 + */ + public final class Descriptor extends MappedObject { + + private final String digest; + + Descriptor(JsonNode node) { + super(node, MethodHandles.lookup()); + this.digest = Objects.requireNonNull(valueAt("/digest", String.class)); + } + + public String getDigest() { + return this.digest; + } + + } + } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index 11bf2a166e3..56ea417fd9e 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -78,6 +78,9 @@ class BuilderTests { private static final ImageReference BASE_CNB = ImageReference.of("docker.io/cloudfoundry/run:base-cnb"); + private static final ImageReference PLATFORM_CNB = ImageReference + .of("docker.io/cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad"); + @Test @SuppressWarnings("NullAway") // Test null check void createWhenLogIsNullThrowsException() { @@ -278,8 +281,8 @@ class BuilderTests { .willAnswer(withPulledImage(builderImage)); given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); - given(docker.image().inspect(eq(DEFAULT_BUILDER))).willReturn(builderImage); - given(docker.image().inspect(eq(BASE_CNB))).willReturn(runImage); + given(docker.image().inspect(eq(DEFAULT_BUILDER), any())).willReturn(builderImage); + given(docker.image().inspect(eq(BASE_CNB), any())).willReturn(runImage); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER); builder.build(request); @@ -291,7 +294,7 @@ class BuilderTests { assertThat(tag).isNotNull(); then(docker.image()).should().remove(tag, true); then(docker.image()).should(never()).pull(any(), any(), any()); - then(docker.image()).should(times(2)).inspect(any()); + then(docker.image()).should(times(2)).inspect(any(), any()); } @Test @@ -330,11 +333,11 @@ class BuilderTests { .willAnswer(withPulledImage(builderImage)); given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); - given(docker.image().inspect(eq(DEFAULT_BUILDER))) + given(docker.image().inspect(eq(DEFAULT_BUILDER), any())) .willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null, null)) .willReturn(builderImage); - given(docker.image().inspect(eq(BASE_CNB))) + given(docker.image().inspect(eq(BASE_CNB), any())) .willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null, null)) .willReturn(runImage); @@ -348,7 +351,7 @@ class BuilderTests { ImageReference tag = archive.getValue().getTag(); assertThat(tag).isNotNull(); then(docker.image()).should().remove(tag, true); - then(docker.image()).should(times(2)).inspect(any()); + then(docker.image()).should(times(2)).inspect(any(), any()); then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull()); } @@ -425,6 +428,8 @@ class BuilderTests { given(docker.image().pull(eq(DEFAULT_BUILDER), eq(platform), any(), isNull())) .willAnswer(withPulledImage(builderImage)); given(docker.image().pull(eq(BASE_CNB), eq(platform), any(), isNull())).willAnswer(withPulledImage(runImage)); + given(docker.image().pull(eq(PLATFORM_CNB), eq(platform), any(), isNull())) + .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withImagePlatform("linux/arm64/v1"); builder.build(request); @@ -433,6 +438,7 @@ class BuilderTests { ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); then(docker.image()).should().pull(eq(DEFAULT_BUILDER), eq(platform), any(), isNull()); then(docker.image()).should().pull(eq(BASE_CNB), eq(platform), any(), isNull()); + then(docker.image()).should().pull(eq(PLATFORM_CNB), eq(platform), any(), isNull()); then(docker.image()).should().load(archive.capture(), any()); ImageReference tag = archive.getValue().getTag(); assertThat(tag).isNotNull(); diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 9006aee724a..2e26f480cc3 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -23,6 +23,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; @@ -88,20 +90,25 @@ class DockerApiTests { private static final String API_URL = "/v" + DockerApi.API_VERSION; - private static final String PLATFORM_API_URL = "/v" + DockerApi.PLATFORM_API_VERSION; - public static final String PING_URL = "/_ping"; private static final String IMAGES_URL = API_URL + "/images"; - private static final String PLATFORM_IMAGES_URL = PLATFORM_API_URL + "/images"; + private static final String PLATFORM_IMAGES_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/images"; + + private static final String PLATFORM_INSPECT_IMAGES_URL = "/v" + DockerApi.PLATFORM_INSPECT_API_VERSION + "/images"; private static final String CONTAINERS_URL = API_URL + "/containers"; - private static final String PLATFORM_CONTAINERS_URL = PLATFORM_API_URL + "/containers"; + private static final String PLATFORM_CONTAINERS_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/containers"; private static final String VOLUMES_URL = API_URL + "/volumes"; + private static final ImagePlatform LINUX_ARM64_PLATFORM = ImagePlatform.of("linux/arm64/v1"); + + private static final String ENCODED_LINUX_ARM64_PLATFORM_JSON = URLEncoder.encode(LINUX_ARM64_PLATFORM.toJson(), + StandardCharsets.UTF_8); + @Mock @SuppressWarnings("NullAway.Init") private HttpTransport http; @@ -242,15 +249,15 @@ class DockerApiTests { @Test void pullWithPlatformPullsImageAndProducesEvents() throws Exception { ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); - ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); URI createUri = new URI(PLATFORM_IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1"); - URI imageUri = new URI(PLATFORM_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); + URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json?platform=" + + ENCODED_LINUX_ARM64_PLATFORM_JSON); given(http().head(eq(new URI(PING_URL)))) - .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49"))); 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, platform, this.pullListener); + Image image = this.api.pull(reference, LINUX_ARM64_PLATFORM, this.pullListener); assertThat(image.getLayers()).hasSize(46); InOrder ordered = inOrder(this.pullListener); ordered.verify(this.pullListener).onStart(); @@ -400,6 +407,32 @@ class DockerApiTests { URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json"); given(http().get(imageUri)).willReturn(responseOf("type/image.json")); Image image = this.api.inspect(reference); + assertThat(image.getArchitecture()).isEqualTo("amd64"); + assertThat(image.getLayers()).hasSize(46); + } + + @Test + void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception { + ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); + URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + + "/docker.io/paketobuildpacks/builder:base/json?platform=" + ENCODED_LINUX_ARM64_PLATFORM_JSON); + given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders( + new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_INSPECT_API_VERSION))); + given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json")); + Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM); + assertThat(image.getArchitecture()).isEqualTo("arm64"); + assertThat(image.getLayers()).hasSize(2); + } + + @Test + void inspectWithPlatformWhenOldVersionInspectImage() throws Exception { + ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); + URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json"); + given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders( + new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_API_VERSION))); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM); + assertThat(image.getArchitecture()).isEqualTo("amd64"); assertThat(image.getLayers()).hasSize(46); } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ImagePlatformTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ImagePlatformTests.java index 07b24fcb29b..4a6fbd0d497 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ImagePlatformTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ImagePlatformTests.java @@ -64,6 +64,20 @@ class ImagePlatformTests extends AbstractJsonTests { assertThat(platform.toString()).isEqualTo("linux/amd64/v1"); } + @Test + void toJsonString() { + ImagePlatform platform = ImagePlatform.of("linux/amd64/v1"); + assertThat(platform.toJson()).isEqualTo(""" + {"os":"linux","architecture":"amd64","variant":"v1"}"""); + } + + @Test + void toJsonStringWhenOnlyOs() { + ImagePlatform platform = ImagePlatform.of("linux"); + assertThat(platform.toJson()).isEqualTo(""" + {"os":"linux"}"""); + } + private Image getImage() throws IOException { return Image.of(getContent("type/image.json")); } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java index 23ae8d651a6..ef7044a5dff 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java @@ -23,6 +23,7 @@ import java.util.Map; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.springframework.boot.buildpack.platform.docker.type.Image.Descriptor; import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; import static org.assertj.core.api.Assertions.assertThat; @@ -98,6 +99,35 @@ class ImageTests extends AbstractJsonTests { assertThat(image.getCreated()).isEqualTo("2019-10-30T19:34:56.296666503Z"); } + @Test + void getDescriptorReturnsDescriptor() throws Exception { + Image image = getImage(); + Descriptor descriptor = image.getDescriptor(); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getDigest()) + .isEqualTo("sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96"); + } + + @Test + void getPrimaryDigestWhenHasDescriptor() throws Exception { + Image image = getImage(); + assertThat(image.getPrimaryDigest()) + .isEqualTo("sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96"); + } + + @Test + void getPrimaryDigestWhenNoDescriptor() throws Exception { + Image image = Image.of(getContent("image-no-descriptor.json")); + assertThat(image.getPrimaryDigest()) + .isEqualTo("sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba"); + } + + @Test + void getPrimaryDigestWhenNoDigest() throws Exception { + Image image = Image.of(getContent("image-no-digest.json")); + assertThat(image.getPrimaryDigest()).isNull(); + } + private Image getImage() throws IOException { return Image.of(getContent("image.json")); } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-no-descriptor.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-no-descriptor.json new file mode 100644 index 00000000000..0172de685fc --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-no-descriptor.json @@ -0,0 +1,25 @@ +{ + "Id": "sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba", + "RepoTags": [ + "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1" + ], + "RepoDigests": [ + "ghcr.io/spring-io/spring-boot-cnb-test-builder@sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba" + ], + "Parent": "", + "Comment": "", + "DockerVersion": "", + "Author": "", + "Config": null, + "Architecture": "", + "Os": "", + "Size": 166797518, + "GraphDriver": { + "Data": null, + "Name": "overlayfs" + }, + "RootFS": {}, + "Metadata": { + "LastTagTime": "2025-04-10T22:41:27.520294922Z" + } +} \ No newline at end of file diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-no-digest.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-no-digest.json new file mode 100644 index 00000000000..7822a6fde86 --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-no-digest.json @@ -0,0 +1,22 @@ +{ + "Id": "sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba", + "RepoTags": [ + "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1" + ], + "Parent": "", + "Comment": "", + "DockerVersion": "", + "Author": "", + "Config": null, + "Architecture": "", + "Os": "", + "Size": 166797518, + "GraphDriver": { + "Data": null, + "Name": "overlayfs" + }, + "RootFS": {}, + "Metadata": { + "LastTagTime": "2025-04-10T22:41:27.520294922Z" + } +} \ No newline at end of file diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-platform.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-platform.json new file mode 100644 index 00000000000..5be11c39b29 --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-platform.json @@ -0,0 +1,99 @@ +{ + "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "RepoTags": [ + "paketo-buildpacks/cnb:latest" + ], + "RepoDigests": [ + "paketo-buildpacks/cnb@sha256:915802bb193b66e3fc1a5a8f5584c6a1b6db05425e573887673bddcf426f1b90" + ], + "Parent": "", + "Comment": "", + "Created": "2019-10-30T19:34:56.296666503Z", + "Container": "84597380a7968131ab47dd1b8183a96dcfe9e1e4acff1efe5824dcd762184a67", + "ContainerConfig": { + "Hostname": "84597380a796", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "Os": "linux", + "Architecture": "arm64", + "Variant": "v1", + "Size": 1559461360, + "VirtualSize": 1559461360, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/58e30cd9f3a4da4e0d30f20c3b50de7655e261fb3d32f04818f1bd960c1e8b6c/diff:/var/lib/docker/overlay2/ad95d738069aa405ff17a9ebb1fdc32f8490b0dd885c3ba3a28e2c3b25d64641/diff:/var/lib/docker/overlay2/74d2896cfe9efc6945ff18870a7213583b987ecf4306e189ff6b793f77af5dcd/diff:/var/lib/docker/overlay2/1052615e5c240724e10928048f735cc9e7a7676a9af5f173b895df57c6921a40/diff:/var/lib/docker/overlay2/b5a62216c4282e7568e84427073f096551977c8c6f80d3a04ebb04c25730edde/diff:/var/lib/docker/overlay2/016a36bf7d7d7258eca08da62c01e47bf8e531531f914dde7cae33e191ab2218/diff:/var/lib/docker/overlay2/a585012bf1cf9da0472b2bbe86c4919355593e1a02cf399a9b012928eb816bcd/diff:/var/lib/docker/overlay2/b4aa8b70bd59d7b7dc6d6fb2e655c2334dc8360c764232f83d036d1f241e3298/diff:/var/lib/docker/overlay2/5f4cab16092522163e2dba6587b48d53ee3b09c8778b0736999bc120dd3753b1/diff:/var/lib/docker/overlay2/90e60622603d230f238976f4d9f65797fc9f070df62b1d2ccad0cefe4e205b43/diff:/var/lib/docker/overlay2/c43877934a580e47cc477ed46e71246468d7b6d7151abc5f1a97bb1e8c8104cf/diff:/var/lib/docker/overlay2/8734b165cabb3ff234a08d488f622135aeae9b7347cf41273445ff7d07aa4565/diff:/var/lib/docker/overlay2/2743cd9d4b7da84925b1b530732dad97108fe77e75865de580255579ba2cdb92/diff:/var/lib/docker/overlay2/68308d057b24bbcde7a4880f5db0e653743debdcc0ff3e736d1776296c4168a1/diff:/var/lib/docker/overlay2/7a4411dc4ac1ed7a1da9aabf088985b8b131e0db047e513f9890eb9c001c1895/diff:/var/lib/docker/overlay2/7f7c262fea8dea5ec86507188848ea391354a76468b09ec93523920e18a400ea/diff:/var/lib/docker/overlay2/8b3bfa567fb956204ad866e49489dacd2fdf5fbfa4f9b05ed3668e1106a5383b/diff:/var/lib/docker/overlay2/31bbc4f1616a35b7ce157266e44513963502e30d836a8fd7b7ee18436a8c46cf/diff:/var/lib/docker/overlay2/149b8e9f1142cdf6dcdfe17ea286ec17197f1a329cf23d5c82958a2032facf54/diff:/var/lib/docker/overlay2/92fb1e680083eb8314c5310bf10ced63ec2b0a98afbf84cc5175a98b3d44507a/diff:/var/lib/docker/overlay2/175a35b6f7af6eb91ca500dbd3d7e798f6d174cf8549881ffe5eed8e92a70b9f/diff:/var/lib/docker/overlay2/48ca54bbd27f7df19acf2b6cc719d05dd3b63f8133038a55d216a4498d4dc913/diff:/var/lib/docker/overlay2/ffe3cc3b93c9030f9dcb0e64c258d1e554f1f0cf27a0f8d4e98bb7ece5ffe882/diff:/var/lib/docker/overlay2/1fb2d962bb27e95c40a9a2c1aa910ca847d186d04e3d7dcdf93967101cc30dde/diff:/var/lib/docker/overlay2/10b34138f9e9e8d70c684d0a564452b1309363441b9d7e048f75e0e1179411dc/diff:/var/lib/docker/overlay2/1d888c7e9c62c22ccda6478f03f3df4b43d43fa3b32a2c2fdc9345fdc7193cd9/diff:/var/lib/docker/overlay2/649fc275c002d7336b277365636e1c8e5651bb3ed1557806d26dd6dfa1d9119a/diff:/var/lib/docker/overlay2/4484c2c0ee4a20aa17017c8cd54c842c876fea32afb297e88614d759ec5410dc/diff:/var/lib/docker/overlay2/bd5f374e0ea6749c90535d778f2689c076b7290ad9d3f050af0a40c9626fdea4/diff:/var/lib/docker/overlay2/c6ba97531b15be65bccaf7ebc866d8bc0b88ce838b224aceb196a55824b289a5/diff:/var/lib/docker/overlay2/6c65fab249fe652cd20a6391b2e0786379b6d2c7d4fde02914dfb4fac84035bd/diff:/var/lib/docker/overlay2/f391b54493024e0183331b8ec7835107bc1b84b8a6e77d852f5357724eb940ff/diff:/var/lib/docker/overlay2/8044f9e3ceb529c80531fa2fe52ad550286f788e69843f235e7d756b90c213b8/diff:/var/lib/docker/overlay2/7d3b5539c46c9f0e7c4f6f733f435d1bf6428a8ca81ba71f4da1031cef58aa6c/diff:/var/lib/docker/overlay2/b8080b36b0ddec4e4d738571ddf9d89815f6a95a555d282cfebb73519b4835a0/diff:/var/lib/docker/overlay2/8a737007d5862aa43119254122eb7050c8bd110a3b653c8d6afca23e76fc4042/diff:/var/lib/docker/overlay2/3bb8f3670831e2031be2173381caf02874ad72e664716a990a330bcc3454f4a2/diff:/var/lib/docker/overlay2/cbd675efde19ccac72d3566404e5df8b152a9063c1668d8154711c7db398f852/diff:/var/lib/docker/overlay2/84fb9095136cb645f7f15aeeeba1db6fae3999cb48a559daf8dd46bf3befbeba/diff:/var/lib/docker/overlay2/cbc51912822c4a3fb8624e0cf678e5dedeb76dc2fa0e5bc56f3cbfbfefb26d68/diff:/var/lib/docker/overlay2/d08d5bdcf39aaf46bdf1e0f4576bb64931af646213ff350065b4d306e00f7e28/diff:/var/lib/docker/overlay2/cf180c218fe181bdf836065c5e85103816ea9e8dbb8ab54fb311209c33455eb2/diff:/var/lib/docker/overlay2/b0aef801fd38973eaf116001e05e7c3f8e2eb58ccc7ed37a4bd8d4fcc2ad172b/diff:/var/lib/docker/overlay2/f73c585ae34bd962e1fee2c3e2d95d47b9daf68b23cf469fb13bc3282cf77238/diff:/var/lib/docker/overlay2/c071c8471b26e55a90b6573a21c581dec43b6c7683a3fe87cb33a0734c83342a/diff", + "MergedDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/merged", + "UpperDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/diff", + "WorkDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json index 901e3b90f5d..70d4286bd09 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json @@ -1,5 +1,27 @@ { "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "Descriptor": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96", + "size": 424, + "urls": [ + "https://example.com" + ], + "annotations": { + "com.docker.official-images.bashbrew.arch": "amd64", + "org.opencontainers.image.version": "24.04" + }, + "data": null, + "platform": { + "architecture": "arm", + "os": "windows", + "os.version": "10.0.19041.1165", + "os.features": [ + ], + "variant": "v7" + }, + "artifactType": null + }, "RepoTags": [ "paketo-buildpacks/cnb:latest" ],