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 38134d7251d..f85e5e875cc 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 @@ -17,8 +17,12 @@ package org.springframework.boot.buildpack.platform.docker.transport; import java.net.URI; +import java.nio.charset.StandardCharsets; + +import org.apache.hc.core5.http.HttpStatus; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -26,6 +30,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Scott Frederick + * @author Siva Sai Udayagiri * @since 2.3.0 */ public class DockerEngineException extends RuntimeException { @@ -38,9 +43,26 @@ public class DockerEngineException extends RuntimeException { private final Message responseMessage; + /** + * Create a new {@link DockerEngineException}. + * @param host the host + * @param uri the URI being called + * @param statusCode the status code + * @param reasonPhrase the reason phrase + * @param errors the errors or {@code null} + * @param responseMessage the response message + * @deprecated since 3.4.12 for removal in 4.0.0 since the exception should only be + * thrown by the transport. + */ + @Deprecated(since = "3.4.12", forRemoval = true) public DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, Message responseMessage) { - super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage)); + this(host, uri, statusCode, reasonPhrase, errors, responseMessage, null); + } + + DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + Message responseMessage, byte[] content) { + super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage, content)); this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; this.errors = errors; @@ -82,7 +104,7 @@ public class DockerEngineException extends RuntimeException { } private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, - Message responseMessage) { + Message responseMessage, byte[] content) { Assert.notNull(host, "Host must not be null"); Assert.notNull(uri, "URI must not be null"); StringBuilder message = new StringBuilder( @@ -93,6 +115,10 @@ public class DockerEngineException extends RuntimeException { if (responseMessage != null && StringUtils.hasLength(responseMessage.getMessage())) { message.append(" and message \"").append(responseMessage.getMessage()).append("\""); } + else if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED && !ObjectUtils.isEmpty(content)) { + String contentString = new String(content, StandardCharsets.UTF_8); + message.append(" and content \"").append(contentString.trim()).append("\""); + } if (errors != null && !errors.isEmpty()) { message.append(" ").append(errors); } 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 0dfe6dc5160..fc85dc900fb 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 @@ -166,7 +166,7 @@ abstract class HttpClientTransport implements HttpTransport { Errors errors = (statusCode != 500) ? deserializeErrors(content) : null; Message message = deserializeMessage(content); throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode, - response.getReasonPhrase(), errors, message); + response.getReasonPhrase(), errors, message, content); } return new HttpClientResponse(response); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index 0a107706f1e..e9737310887 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -31,7 +31,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; -import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; +import org.springframework.boot.buildpack.platform.docker.transport.TestDockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; @@ -313,12 +313,12 @@ class BuilderTests { any(), isNull())) .willAnswer(withPulledImage(runImage)); given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) - .willThrow( - new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) + .willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, + null, null)) .willReturn(builderImage); given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) - .willThrow( - new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) + .willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, + null, null)) .willReturn(runImage); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT); 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 b9550b86f2e..b5648ebb45b 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 @@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.docker.transport; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.Collections; import org.junit.jupiter.api.Test; @@ -56,20 +57,20 @@ class DockerEngineExceptionTests { @Test void createWhenHostIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE)) + .isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE, null)) .withMessage("Host must not be null"); } @Test void createWhenUriIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE)) + .isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE, null)) .withMessage("URI must not be null"); } @Test void create() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE, null); assertThat(exception.getMessage()).isEqualTo( "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\" [code: message]"); assertThat(exception.getStatusCode()).isEqualTo(404); @@ -80,7 +81,7 @@ class DockerEngineExceptionTests { @Test void createWhenReasonPhraseIsNull() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE, null); assertThat(exception.getMessage()).isEqualTo( "Docker API call to 'docker://localhost/example' failed with status code 404 and message \"response message\" [code: message]"); assertThat(exception.getStatusCode()).isEqualTo(404); @@ -91,7 +92,7 @@ class DockerEngineExceptionTests { @Test void createWhenErrorsIsNull() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE, null); 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(); @@ -99,7 +100,8 @@ class DockerEngineExceptionTests { @Test void createWhenErrorsIsEmpty() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE, + null); 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); @@ -109,7 +111,7 @@ class DockerEngineExceptionTests { @Test void createWhenMessageIsNull() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null, null); assertThat(exception.getMessage()).isEqualTo( "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); assertThat(exception.getResponseMessage()).isNull(); @@ -117,10 +119,20 @@ class DockerEngineExceptionTests { @Test void createWhenMessageIsEmpty() { - DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE, + null); 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); } + @Test + void createWhenProxyAuthFailureWithTextContent() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 407, "Proxy Authentication Required", + null, null, "Badness".getBytes(StandardCharsets.UTF_8)); + assertThat(exception.getMessage()) + .isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 407 " + + "\"Proxy Authentication Required\" and content \"Badness\""); + } + } 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 ee037ea790c..56aa3a4ad64 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 @@ -33,6 +33,7 @@ import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpStatus; import org.assertj.core.api.ThrowingConsumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -322,6 +323,19 @@ class HttpClientTransportTests { }); } + @Test + void shouldReturnErrorsAndConentIfProxyAuthError() throws IOException { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("proxy-error.txt")); + given(this.response.getCode()).willReturn(HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage()).isNull(); + assertThat(ex.getMessage()).contains("Some kind of procy auth problem!"); + }); + } + @Test void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException { given(this.client.executeOpen(any(HttpHost.class), any(HttpUriRequest.class), isNull())) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/TestDockerEngineException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/TestDockerEngineException.java new file mode 100644 index 00000000000..9c91506582a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/TestDockerEngineException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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 java.net.URI; + +/** + * Subclass of {@link DockerEngineException} for testing. + * + * @author Phillip Webb + */ +public class TestDockerEngineException extends DockerEngineException { + + public TestDockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + Message responseMessage, byte[] content) { + super(host, uri, statusCode, reasonPhrase, errors, responseMessage, content); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/proxy-error.txt b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/proxy-error.txt new file mode 100644 index 00000000000..fdad6464b3c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/proxy-error.txt @@ -0,0 +1,2 @@ +Some kind of procy auth problem! +