diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java index d4a78ffe739..075b5314bfe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java @@ -19,6 +19,7 @@ package org.springframework.boot.buildpack.platform.docker.transport; import java.net.URI; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Exception thrown when a call to the Docker API fails. @@ -35,11 +36,15 @@ public class DockerEngineException extends RuntimeException { private final Errors errors; - DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) { - super(buildMessage(host, uri, statusCode, reasonPhrase, errors)); + private final Message responseMessage; + + DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + Message responseMessage) { + super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage)); this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; this.errors = errors; + this.responseMessage = responseMessage; } /** @@ -51,7 +56,7 @@ public class DockerEngineException extends RuntimeException { } /** - * Return the reason phrase returned by the Docker API error. + * Return the reason phrase returned by the Docker API. * @return the reasonPhrase */ public String getReasonPhrase() { @@ -59,24 +64,37 @@ public class DockerEngineException extends RuntimeException { } /** - * Return the Errors from the body of the Docker API error, or {@code null} if the - * error JSON could not be read. + * Return the errors from the body of the Docker API response, or {@code null} if the + * errors JSON could not be read. * @return the errors or {@code null} */ public Errors getErrors() { return this.errors; } - private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) { - Assert.notNull(host, "host must not be null"); + /** + * Return the message from the body of the Docker API response, or {@code null} if the + * message JSON could not be read. + * @return the message or {@code null} + */ + public Message getResponseMessage() { + return this.responseMessage; + } + + private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + Message responseMessage) { + Assert.notNull(host, "Host must not be null"); Assert.notNull(uri, "URI must not be null"); StringBuilder message = new StringBuilder( "Docker API call to '" + host + uri + "' failed with status code " + statusCode); - if (reasonPhrase != null && !reasonPhrase.isEmpty()) { - message.append(" \"" + reasonPhrase + "\""); + if (!StringUtils.isEmpty(reasonPhrase)) { + message.append(" \"").append(reasonPhrase).append("\""); + } + if (responseMessage != null && !StringUtils.isEmpty(responseMessage.getMessage())) { + message.append(" and message \"").append(responseMessage.getMessage()).append("\""); } if (errors != null && !errors.isEmpty()) { - message.append(" " + errors); + message.append(" ").append(errors); } return message.toString(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index c25cda682d9..20395f41b66 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -131,8 +131,9 @@ abstract class HttpClientTransport implements HttpTransport { HttpEntity entity = response.getEntity(); if (statusCode >= 400 && statusCode <= 500) { Errors errors = (statusCode != 500) ? getErrorsFromResponse(entity) : null; + Message message = getMessageFromResponse(entity); throw new DockerEngineException(this.host.toHostString(), request.getURI(), statusCode, - statusLine.getReasonPhrase(), errors); + statusLine.getReasonPhrase(), errors, message); } return new HttpClientResponse(response); } @@ -150,6 +151,16 @@ abstract class HttpClientTransport implements HttpTransport { } } + private Message getMessageFromResponse(HttpEntity entity) { + try { + return (entity.getContent() != null) + ? SharedObjectMapper.get().readValue(entity.getContent(), Message.class) : null; + } + catch (IOException ex) { + return null; + } + } + HttpHost getHost() { return this.host; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Message.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Message.java new file mode 100644 index 00000000000..e1f51a16288 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Message.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A message returned from the Docker API. + * + * @author Scott Frederick + * @since 2.3.1 + */ +public class Message { + + private final String message; + + @JsonCreator + Message(@JsonProperty("message") String message) { + this.message = message; + } + + /** + * Return the message contained in the response. + * @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/transport/DockerEngineExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java index 3151bc8dfb1..d3a76a38ce8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java @@ -49,55 +49,78 @@ class DockerEngineExceptionTests { private static final Errors ERRORS = new Errors(Collections.singletonList(new Errors.Error("code", "message"))); + private static final Message NO_MESSAGE = new Message(null); + + private static final Message MESSAGE = new Message("response message"); + @Test void createWhenHostIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS)) - .withMessage("host must not be null"); + .isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE)) + .withMessage("Host must not be null"); } @Test void createWhenUriIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS)) + .isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE)) .withMessage("URI must not be null"); } @Test void create() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE); assertThat(exception.getMessage()).isEqualTo( - "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\" [code: message]"); assertThat(exception.getStatusCode()).isEqualTo(404); assertThat(exception.getReasonPhrase()).isEqualTo("missing"); assertThat(exception.getErrors()).isSameAs(ERRORS); + assertThat(exception.getResponseMessage()).isSameAs(MESSAGE); } @Test void createWhenReasonPhraseIsNull() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE); assertThat(exception.getMessage()).isEqualTo( - "Docker API call to 'docker://localhost/example' failed with status code 404 [code: message]"); + "Docker API call to 'docker://localhost/example' failed with status code 404 and message \"response message\" [code: message]"); assertThat(exception.getStatusCode()).isEqualTo(404); assertThat(exception.getReasonPhrase()).isNull(); assertThat(exception.getErrors()).isSameAs(ERRORS); + assertThat(exception.getResponseMessage()).isSameAs(MESSAGE); } @Test void createWhenErrorsIsNull() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\""); assertThat(exception.getErrors()).isNull(); } @Test void createWhenErrorsIsEmpty() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS); - assertThat(exception.getMessage()) - .isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\""); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\""); assertThat(exception.getStatusCode()).isEqualTo(404); assertThat(exception.getReasonPhrase()).isEqualTo("missing"); assertThat(exception.getErrors()).isSameAs(NO_ERRORS); + } + @Test + void createWhenMessageIsNull() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); + assertThat(exception.getResponseMessage()).isNull(); + } + + @Test + void createWhenMessageIsEmpty() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); + assertThat(exception.getResponseMessage()).isSameAs(NO_MESSAGE); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java index b32915aa799..1e6e256c420 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java @@ -181,14 +181,42 @@ class HttpClientTransportTests { given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json")); given(this.statusLine.getStatusCode()).willReturn(404); assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) - .satisfies((ex) -> assertThat(ex.getErrors()).hasSize(2)); + .satisfies((ex) -> { + assertThat(ex.getErrors()).hasSize(2); + assertThat(ex.getResponseMessage()).isNull(); + }); } @Test - void executeWhenResponseIsIn500RangeShouldThrowDockerException() { + void executeWhenResponseIsIn500RangeWithNoContentShouldThrowDockerException() { given(this.statusLine.getStatusCode()).willReturn(500); assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) - .satisfies((ex) -> assertThat(ex.getErrors()).isNull()); + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage()).isNull(); + }); + } + + @Test + void executeWhenResponseIsIn500RangeWithMessageShouldThrowDockerException() throws IOException { + given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("message.json")); + given(this.statusLine.getStatusCode()).willReturn(500); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage().getMessage()).contains("test message"); + }); + } + + @Test + void executeWhenResponseIsIn500RangeWithOtherContentShouldThrowDockerException() throws IOException { + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(500); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage()).isNull(); + }); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/MessageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/MessageTests.java new file mode 100644 index 00000000000..c504ac426b2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/MessageTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Message}. + * + * @author Scott Frederick + */ +class MessageTests extends AbstractJsonTests { + + @Test + void readValueDeserializesJson() throws Exception { + Message message = getObjectMapper().readValue(getContent("message.json"), Message.class); + assertThat(message.getMessage()).isEqualTo("test message"); + } + + @Test + void toStringHasErrorDetails() throws Exception { + Message errors = getObjectMapper().readValue(getContent("message.json"), Message.class); + assertThat(errors.toString()).isEqualTo("test message"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/message.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/message.json new file mode 100644 index 00000000000..59580d06123 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/message.json @@ -0,0 +1,3 @@ +{ + "message": "test message" +}