Improve error message from image building

Translate IOException to DockerException for a more meaningful error
message when the Docker daemon is not available.

Fixes gh-20151
This commit is contained in:
Mike Smithson 2020-02-21 18:29:18 -05:00 committed by Scott Frederick
parent e73ee7b3fe
commit 6f095d6fec
2 changed files with 44 additions and 33 deletions

View File

@ -19,6 +19,8 @@ package org.springframework.boot.buildpack.platform.docker;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI; import java.net.URI;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
@ -36,7 +38,6 @@ import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.buildpack.platform.io.Content; import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.io.IOConsumer;
@ -46,6 +47,7 @@ import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
* {@link Http} implementation backed by a {@link HttpClient}. * {@link Http} implementation backed by a {@link HttpClient}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Mike Smithson
*/ */
class HttpClientHttp implements Http { class HttpClientHttp implements Http {
@ -66,10 +68,9 @@ class HttpClientHttp implements Http {
* Perform a HTTP GET operation. * Perform a HTTP GET operation.
* @param uri the destination URI * @param uri the destination URI
* @return the operation response * @return the operation response
* @throws IOException on IO error
*/ */
@Override @Override
public Response get(URI uri) throws IOException { public Response get(URI uri) {
return execute(new HttpGet(uri)); return execute(new HttpGet(uri));
} }
@ -77,10 +78,9 @@ class HttpClientHttp implements Http {
* Perform a HTTP POST operation. * Perform a HTTP POST operation.
* @param uri the destination URI * @param uri the destination URI
* @return the operation response * @return the operation response
* @throws IOException on IO error
*/ */
@Override @Override
public Response post(URI uri) throws IOException { public Response post(URI uri) {
return execute(new HttpPost(uri)); return execute(new HttpPost(uri));
} }
@ -90,11 +90,10 @@ class HttpClientHttp implements Http {
* @param contentType the content type to write * @param contentType the content type to write
* @param writer a content writer * @param writer a content writer
* @return the operation response * @return the operation response
* @throws IOException on IO error
*/ */
@Override @Override
public Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) throws IOException { public Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) {
return execute(new HttpPost(uri), contentType, writer); return execute(new HttpPost(uri), contentType, writer);
} }
@ -104,11 +103,10 @@ class HttpClientHttp implements Http {
* @param contentType the content type to write * @param contentType the content type to write
* @param writer a content writer * @param writer a content writer
* @return the operation response * @return the operation response
* @throws IOException on IO error
*/ */
@Override @Override
public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) throws IOException { public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) {
return execute(new HttpPut(uri), contentType, writer); return execute(new HttpPut(uri), contentType, writer);
} }
@ -116,39 +114,44 @@ class HttpClientHttp implements Http {
* Perform a HTTP DELETE operation. * Perform a HTTP DELETE operation.
* @param uri the destination URI * @param uri the destination URI
* @return the operation response * @return the operation response
* @throws IOException on IO error
*/ */
@Override @Override
public Response delete(URI uri) throws IOException { public Response delete(URI uri) {
return execute(new HttpDelete(uri)); return execute(new HttpDelete(uri));
} }
private Response execute(HttpEntityEnclosingRequestBase request, String contentType, private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
IOConsumer<OutputStream> writer) throws IOException { IOConsumer<OutputStream> writer) {
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType); request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
request.setEntity(new WritableHttpEntity(writer)); request.setEntity(new WritableHttpEntity(writer));
return execute(request); return execute(request);
} }
private Response execute(HttpUriRequest request) throws IOException { private Response execute(HttpUriRequest request) {
CloseableHttpResponse response = this.client.execute(request); CloseableHttpResponse response;
StatusLine statusLine = response.getStatusLine(); try {
int statusCode = statusLine.getStatusCode(); response = this.client.execute(request);
HttpEntity entity = response.getEntity(); StatusLine statusLine = response.getStatusLine();
if (statusCode >= 200 && statusCode < 300) { int statusCode = statusLine.getStatusCode();
return new HttpClientResponse(response); HttpEntity entity = response.getEntity();
}
Errors errors = null; if (statusCode >= 400 && statusCode < 500) {
if (statusCode >= 400 && statusCode < 500) { Errors errors = SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
try { throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), errors);
errors = SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
} }
catch (Exception ex) { if (statusCode == 500) {
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), null);
} }
} }
EntityUtils.consume(entity); catch (IOException ioe) {
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), errors); StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
ioe.printStackTrace(printWriter);
throw new DockerException(request.getURI(), 500, stringWriter.toString(), null);
}
return new HttpClientResponse(response);
} }
/** /**
@ -175,7 +178,7 @@ class HttpClientHttp implements Http {
} }
@Override @Override
public InputStream getContent() throws IOException, UnsupportedOperationException { public InputStream getContent() throws UnsupportedOperationException {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -26,7 +26,6 @@ import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.StatusLine; import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
@ -54,6 +53,7 @@ import static org.mockito.Mockito.verify;
* Tests for {@link HttpClientHttp}. * Tests for {@link HttpClientHttp}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Mike Smithson
*/ */
class HttpClientHttpTests { class HttpClientHttpTests {
@ -132,7 +132,7 @@ class HttpClientHttpTests {
assertThat(entity.isRepeatable()).isFalse(); assertThat(entity.isRepeatable()).isFalse();
assertThat(entity.getContentLength()).isEqualTo(-1); assertThat(entity.getContentLength()).isEqualTo(-1);
assertThat(entity.isStreaming()).isTrue(); assertThat(entity.isStreaming()).isTrue();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> entity.getContent()); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
assertThat(writeToString(entity)).isEqualTo("test"); assertThat(writeToString(entity)).isEqualTo("test");
assertThat(response.getContent()).isSameAs(this.content); assertThat(response.getContent()).isSameAs(this.content);
} }
@ -152,7 +152,7 @@ class HttpClientHttpTests {
assertThat(entity.isRepeatable()).isFalse(); assertThat(entity.isRepeatable()).isFalse();
assertThat(entity.getContentLength()).isEqualTo(-1); assertThat(entity.getContentLength()).isEqualTo(-1);
assertThat(entity.isStreaming()).isTrue(); assertThat(entity.isStreaming()).isTrue();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> entity.getContent()); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
assertThat(writeToString(entity)).isEqualTo("test"); assertThat(writeToString(entity)).isEqualTo("test");
assertThat(response.getContent()).isSameAs(this.content); assertThat(response.getContent()).isSameAs(this.content);
} }
@ -171,7 +171,7 @@ class HttpClientHttpTests {
} }
@Test @Test
void executeWhenResposeIsIn400RangeShouldThrowDockerException() throws ClientProtocolException, IOException { void executeWhenResposeIsIn400RangeShouldThrowDockerException() throws IOException {
given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json")); given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json"));
given(this.statusLine.getStatusCode()).willReturn(404); given(this.statusLine.getStatusCode()).willReturn(404);
assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri))
@ -179,12 +179,20 @@ class HttpClientHttpTests {
} }
@Test @Test
void executeWhenResposeIsIn500RangeShouldThrowDockerException() throws ClientProtocolException, IOException { void executeWhenResposeIsIn500RangeShouldThrowDockerException() {
given(this.statusLine.getStatusCode()).willReturn(500); given(this.statusLine.getStatusCode()).willReturn(500);
assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri))
.satisfies((ex) -> assertThat(ex.getErrors()).isNull()); .satisfies((ex) -> assertThat(ex.getErrors()).isNull());
} }
@Test
void executeWhenClientExecutesRequestThrowsIOExceptionRethrowsAsDockerException() throws IOException {
given(this.client.execute(any())).willThrow(IOException.class);
assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri))
.satisfies((ex) -> assertThat(ex.getErrors()).isNull()).satisfies(DockerException::getStatusCode)
.withMessageContaining("500").satisfies((ex) -> assertThat(ex.getReasonPhrase())).isNotNull();
}
private String writeToString(HttpEntity entity) throws IOException { private String writeToString(HttpEntity entity) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out); entity.writeTo(out);