diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 6e7263af8836..c149b5b137a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -233,7 +233,7 @@ public void load(ImageArchive archive, UpdateListener list Assert.notNull(archive, "Archive must not be null"); Assert.notNull(listener, "Listener must not be null"); URI loadUri = buildUrl("/images/load"); - StreamCaptureUpdateListener streamListener = new StreamCaptureUpdateListener(); + LoadImageUpdateListener streamListener = new LoadImageUpdateListener(archive); listener.onStart(); try { try (Response response = http().post(loadUri, "application/x-tar", archive::writeTo)) { @@ -242,9 +242,7 @@ public void load(ImageArchive archive, UpdateListener list listener.onUpdate(event); }); } - Assert.state(StringUtils.hasText(streamListener.getCapturedStream()), - "Invalid response received when loading image " - + ((archive.getTag() != null) ? "\"" + archive.getTag() + "\"" : "")); + streamListener.assertValidResponseReceived(); } finally { listener.onFinish(); @@ -482,19 +480,33 @@ public void onUpdate(ProgressUpdateEvent event) { } /** - * {@link UpdateListener} used to ensure an image load response stream. + * {@link UpdateListener} for an image load response stream. */ - private static final class StreamCaptureUpdateListener implements UpdateListener { + private static final class LoadImageUpdateListener implements UpdateListener { + + private final ImageArchive archive; private String stream; + private LoadImageUpdateListener(ImageArchive archive) { + this.archive = archive; + } + @Override public void onUpdate(LoadImageUpdateEvent event) { + Assert.state(event.getErrorDetail() == null, + () -> "Error response received when loading image" + image() + ": " + event.getErrorDetail()); this.stream = event.getStream(); } - String getCapturedStream() { - return this.stream; + private String image() { + ImageReference tag = this.archive.getTag(); + return (tag != null) ? " \"" + tag + "\"" : ""; + } + + private void assertValidResponseReceived() { + Assert.state(StringUtils.hasText(this.stream), + () -> "Invalid response received when loading image" + image()); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java index 2fb0f5ad58c9..49d16d0ad057 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 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. @@ -17,6 +17,7 @@ package org.springframework.boot.buildpack.platform.docker; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; /** * A {@link ProgressUpdateEvent} fired as an image is loaded. @@ -28,10 +29,14 @@ public class LoadImageUpdateEvent extends ProgressUpdateEvent { private final String stream; + private final ErrorDetail errorDetail; + @JsonCreator - public LoadImageUpdateEvent(String stream, String status, ProgressDetail progressDetail, String progress) { + public LoadImageUpdateEvent(String stream, String status, ProgressDetail progressDetail, String progress, + ErrorDetail errorDetail) { super(status, progressDetail, progress); this.stream = stream; + this.errorDetail = errorDetail; } /** @@ -42,4 +47,42 @@ public String getStream() { return this.stream; } + /** + * Return the error detail or {@code null} if no error occurred. + * @return the error detail, if any + * @since 3.2.12 + */ + public ErrorDetail getErrorDetail() { + return this.errorDetail; + } + + /** + * Details of an error embedded in a response stream. + * + * @since 3.2.12 + */ + public static class ErrorDetail { + + private final String message; + + @JsonCreator + public ErrorDetail(@JsonProperty("message") String message) { + this.message = message; + } + + /** + * Returns the message field from the error detail. + * @return the message + */ + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return this.message; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 69a32a82f4de..6ab78f68fc27 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -252,6 +252,16 @@ void loadWithEmptyResponseThrowsException() throws Exception { .withMessageContaining("Invalid response received"); } + @Test // gh-31243 + void loadWithErrorResponseThrowsException() throws Exception { + Image image = Image.of(getClass().getResourceAsStream("type/image.json")); + ImageArchive archive = ImageArchive.from(image); + URI loadUri = new URI(IMAGES_URL + "/load"); + given(http().post(eq(loadUri), eq("application/x-tar"), any())).willReturn(responseOf("load-error.json")); + assertThatIllegalStateException().isThrownBy(() -> this.api.load(archive, this.loadListener)) + .withMessageContaining("Error response received"); + } + @Test void loadLoadsImage() throws Exception { Image image = Image.of(getClass().getResourceAsStream("type/image.json")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java index 40fa5a1e4168..a759c7041c76 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 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. @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.buildpack.platform.docker.LoadImageUpdateEvent.ErrorDetail; import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail; import static org.assertj.core.api.Assertions.assertThat; @@ -36,9 +37,16 @@ void getStreamReturnsStream() { assertThat(event.getStream()).isEqualTo("stream"); } + @Test + void getErrorDetailReturnsErrorDetail() { + LoadImageUpdateEvent event = createEvent(); + assertThat(event.getErrorDetail()).extracting(ErrorDetail::getMessage).isEqualTo("max depth exceeded"); + } + @Override protected LoadImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) { - return new LoadImageUpdateEvent("stream", status, progressDetail, progress); + return new LoadImageUpdateEvent("stream", status, progressDetail, progress, + new ErrorDetail("max depth exceeded")); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/load-error.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/load-error.json new file mode 100644 index 000000000000..af93574f7e9a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/load-error.json @@ -0,0 +1 @@ +{"errorDetail":{"message":"max depth exceeded"}}