From 7722394e19c8d5ff4c6df62fd47283cb5d4f94c4 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 29 May 2020 12:37:06 -0500 Subject: [PATCH] Provide better error message if Docker is not running Previously, if the Spring Boot build plugins got a connection error when attempting to communicate with a Docker daemon (for example, when the daemon isn't running), the error message made it appear that the daemon returned an HTTP error code. This commit makes a connection error distinct from an HTTP error response code to make it easier for the user to diagnose the root cause of the problem. Fixes gh-21554 --- .../transport/DockerConnectionException.java | 55 ++++++++++++++++ .../transport/DockerEngineException.java | 2 +- .../docker/transport/HttpClientTransport.java | 2 +- .../DockerConnectionExceptionTests.java | 62 +++++++++++++++++++ .../transport/HttpClientTransportTests.java | 6 +- 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java new file mode 100644 index 0000000000..04ab30477f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java @@ -0,0 +1,55 @@ +/* + * 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.springframework.util.Assert; + +/** + * Exception thrown when connection to the Docker daemon fails. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public class DockerConnectionException extends RuntimeException { + + private static final String JNA_EXCEPTION_CLASS_NAME = "com.sun.jna.LastErrorException"; + + public DockerConnectionException(String host, Exception cause) { + super(buildMessage(host, cause), cause); + } + + private static String buildMessage(String host, Exception cause) { + Assert.notNull(host, "host must not be null"); + Assert.notNull(cause, "cause must not be null"); + StringBuilder message = new StringBuilder("Connection to the Docker daemon at '" + host + "' failed"); + String causeMessage = getCauseMessage(cause); + if (causeMessage != null && !causeMessage.isEmpty()) { + message.append(" with error \"").append(causeMessage).append("\""); + } + message.append("; ensure the Docker daemon is running and accessible"); + return message.toString(); + } + + private static String getCauseMessage(Exception cause) { + if (cause.getCause() != null && cause.getCause().getClass().getName().equals(JNA_EXCEPTION_CLASS_NAME)) { + return cause.getCause().getMessage(); + } + + return cause.getMessage(); + } + +} 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 853e5cea00..d4a78ffe73 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 @@ -21,7 +21,7 @@ import java.net.URI; import org.springframework.util.Assert; /** - * Exception throw when the Docker API fails. + * Exception thrown when a call to the Docker API fails. * * @author Phillip Webb * @author Scott Frederick 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 7e52d90a82..c25cda682d 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 @@ -137,7 +137,7 @@ abstract class HttpClientTransport implements HttpTransport { return new HttpClientResponse(response); } catch (IOException ex) { - throw new DockerEngineException(this.host.toHostString(), request.getURI(), 500, ex.getMessage(), null); + throw new DockerConnectionException(this.host.toHostString(), ex); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java new file mode 100644 index 0000000000..b2bd4fb5b6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java @@ -0,0 +1,62 @@ +/* + * 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 java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerEngineException}. + * + * @author Scott Frederick + */ +class DockerConnectionExceptionTests { + + private static final String HOST = "docker://localhost/"; + + @Test + void createWhenHostIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new DockerConnectionException(null, null)) + .withMessage("host must not be null"); + } + + @Test + void createWhenCauseIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new DockerConnectionException(HOST, null)) + .withMessage("cause must not be null"); + } + + @Test + void createWithIOException() { + DockerConnectionException exception = new DockerConnectionException(HOST, new IOException("error")); + assertThat(exception.getMessage()) + .contains("Connection to the Docker daemon at 'docker://localhost/' failed with error \"error\""); + } + + @Test + void createWithLastErrorException() { + DockerConnectionException exception = new DockerConnectionException(HOST, + new IOException(new com.sun.jna.LastErrorException("root cause"))); + assertThat(exception.getMessage()) + .contains("Connection to the Docker daemon at 'docker://localhost/' failed with error \"root cause\""); + } + +} 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 65270cbf04..b32915aa79 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 @@ -195,10 +195,8 @@ class HttpClientTransportTests { void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException { given(this.client.execute(any(HttpHost.class), any(HttpRequest.class))) .willThrow(new IOException("test IO exception")); - assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) - .satisfies((ex) -> assertThat(ex.getErrors()).isNull()).satisfies(DockerEngineException::getStatusCode) - .withMessageContaining("500") - .satisfies((ex) -> assertThat(ex.getReasonPhrase()).contains("test IO exception")); + assertThatExceptionOfType(DockerConnectionException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> assertThat(ex.getMessage()).contains("test IO exception")); } private String writeToString(HttpEntity entity) throws IOException {