mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Merge branch '3.5.x'
Closes gh-48099
This commit is contained in:
+70
-34
@@ -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
|
||||
|
||||
+26
-5
@@ -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");
|
||||
|
||||
+21
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+56
@@ -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<LayerId> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-6
@@ -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<ImageArchive> 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();
|
||||
|
||||
+41
-8
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -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"));
|
||||
}
|
||||
|
||||
+30
@@ -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"));
|
||||
}
|
||||
|
||||
+25
@@ -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"
|
||||
}
|
||||
}
|
||||
+22
@@ -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"
|
||||
}
|
||||
}
|
||||
+99
File diff suppressed because one or more lines are too long
+22
@@ -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"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user