From e52ee3577fe9e0d6e8264cf083b530fad28c9d4f Mon Sep 17 00:00:00 2001 From: manusa Date: Fri, 13 Nov 2020 12:05:59 +0100 Subject: [PATCH] fix: Support for Podman REST API (when configured) - fix: Valid content type for REST docker-compatible API requests with body - fix: Push works with Podman REST API --- .../access/hc/ApacheHttpClientDelegate.java | 21 ++- .../access/hc/DockerAccessWithHcClient.java | 20 --- .../hc/HcChunkedResponseHandlerWrapper.java | 34 ++++ .../hc/ApacheHttpClientDelegateTest.java | 154 ++++++++++++++++++ .../HcChunkedResponseHandlerWrapperTest.java | 70 ++++++++ 5 files changed, 271 insertions(+), 28 deletions(-) create mode 100644 src/main/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapper.java create mode 100644 src/test/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegateTest.java create mode 100644 src/test/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapperTest.java diff --git a/src/main/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegate.java b/src/main/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegate.java index 435ddc40b..d9a8b7c91 100644 --- a/src/main/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegate.java +++ b/src/main/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegate.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.net.URLConnection; import java.nio.charset.Charset; import java.util.Map; import java.util.Map.Entry; @@ -56,8 +57,7 @@ public int delete(String url, int... statusCodes) throws IOException { public static class StatusCodeResponseHandler implements ResponseHandler { @Override - public Integer handleResponse(HttpResponse response) - throws IOException { + public Integer handleResponse(HttpResponse response) { return response.getStatusLine().getStatusCode(); } @@ -122,31 +122,36 @@ public int put(String url, Object body, int... statusCodes) throws IOException { // ========================================================================================= - private HttpUriRequest addDefaultHeaders(HttpUriRequest req) { + private HttpUriRequest addDefaultHeaders(HttpUriRequest req, Object body) { req.addHeader(HttpHeaders.ACCEPT, "*/*"); - req.addHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + if (body instanceof File) { + req.addHeader(HttpHeaders.CONTENT_TYPE, URLConnection.guessContentTypeFromName(((File)body).getName())); + } + if (body != null && !req.containsHeader(HttpHeaders.CONTENT_TYPE)) { + req.addHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + } return req; } private HttpUriRequest newDelete(String url) { - return addDefaultHeaders(new HttpDelete(url)); + return addDefaultHeaders(new HttpDelete(url), null); } private HttpUriRequest newGet(String url) { - return addDefaultHeaders(new HttpGet(url)); + return addDefaultHeaders(new HttpGet(url), null); } private HttpUriRequest newPut(String url, Object body) { HttpPut put = new HttpPut(url); setEntityIfGiven(put, body); - return addDefaultHeaders(put); + return addDefaultHeaders(put, body); } private HttpUriRequest newPost(String url, Object body) { HttpPost post = new HttpPost(url); setEntityIfGiven(post, body); - return addDefaultHeaders(post); + return addDefaultHeaders(post, body); } diff --git a/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java b/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java index b5b9fc31e..64203e9e0 100644 --- a/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java +++ b/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java @@ -38,7 +38,6 @@ import io.fabric8.maven.docker.access.UrlBuilder; import io.fabric8.maven.docker.access.VolumeCreateConfig; import io.fabric8.maven.docker.access.chunked.BuildJsonResponseHandler; -import io.fabric8.maven.docker.access.chunked.EntityStreamReaderUtil; import io.fabric8.maven.docker.access.chunked.PullOrPushResponseJsonHandler; import io.fabric8.maven.docker.access.hc.ApacheHttpClientDelegate.BodyAndStatusResponseHandler; import io.fabric8.maven.docker.access.hc.ApacheHttpClientDelegate.HttpBodyAndStatus; @@ -763,25 +762,6 @@ private static boolean isSSL(String url) { return url != null && url.toLowerCase().startsWith("https"); } - // Preparation for performing requests - private static class HcChunkedResponseHandlerWrapper implements ResponseHandler { - - private EntityStreamReaderUtil.JsonEntityResponseHandler handler; - - HcChunkedResponseHandlerWrapper(EntityStreamReaderUtil.JsonEntityResponseHandler handler) { - this.handler = handler; - } - - @Override - public Object handleResponse(HttpResponse response) throws IOException { - try (InputStream stream = response.getEntity().getContent()) { - // Parse text as json - EntityStreamReaderUtil.processJsonStream(handler, stream); - } - return null; - } - } - public String fetchApiVersionFromServer(String baseUrl, ApacheHttpClientDelegate delegate) throws IOException { HttpGet get = new HttpGet(baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "version"); get.addHeader(HttpHeaders.ACCEPT, "*/*"); diff --git a/src/main/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapper.java b/src/main/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapper.java new file mode 100644 index 000000000..41c27c305 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapper.java @@ -0,0 +1,34 @@ +package io.fabric8.maven.docker.access.hc; + +import io.fabric8.maven.docker.access.chunked.EntityStreamReaderUtil; +import org.apache.http.HttpResponse; +import org.apache.http.client.ResponseHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.util.stream.Stream; + +public class HcChunkedResponseHandlerWrapper implements ResponseHandler { + private final EntityStreamReaderUtil.JsonEntityResponseHandler handler; + + HcChunkedResponseHandlerWrapper(EntityStreamReaderUtil.JsonEntityResponseHandler handler) { + this.handler = handler; + } + + @Override + public Object handleResponse(HttpResponse response) throws IOException { + try (InputStream stream = response.getEntity().getContent()) { + // Parse text as json + if (isJson(response)) { + EntityStreamReaderUtil.processJsonStream(handler, stream); + } + } + return null; + } + + private static boolean isJson(HttpResponse response) { + return Stream.of(response.getAllHeaders()) + .filter(h -> h.getName().equalsIgnoreCase("Content-Type")) + .anyMatch(h -> h.getValue().toLowerCase().startsWith("application/json")); + } +} diff --git a/src/test/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegateTest.java b/src/test/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegateTest.java new file mode 100644 index 000000000..42b2aeb3c --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/access/hc/ApacheHttpClientDelegateTest.java @@ -0,0 +1,154 @@ +package io.fabric8.maven.docker.access.hc; + +import io.fabric8.maven.docker.access.hc.util.ClientBuilder; +import mockit.Expectations; +import mockit.Mocked; +import mockit.Verifications; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.assertj.core.groups.Tuple; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.function.BiConsumer; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings({"rawtypes", "unused"}) +public class ApacheHttpClientDelegateTest { + + @Mocked + private ClientBuilder clientBuilder; + @Mocked + private CloseableHttpClient httpClient; + + private ApacheHttpClientDelegate apacheHttpClientDelegate; + + @Before + public void setUp() throws Exception { + // @formatter:off + new Expectations() {{ + clientBuilder.buildBasicClient(); + result = httpClient; + }}; + // @formatter:on + apacheHttpClientDelegate = new ApacheHttpClientDelegate(clientBuilder, false); + } + + @Test + public void createBasicClient() { + final CloseableHttpClient result = apacheHttpClientDelegate.createBasicClient(); + assertThat(result).isNotNull(); + } + + @Test + public void delete() throws IOException { + // Given + // @formatter:off + new Expectations() {{ + httpClient.execute((HttpUriRequest) any, (ResponseHandler) any); + result = 1337; + }}; + // @formatter:on + // When + final int result = apacheHttpClientDelegate.delete("http://example.com"); + // Then + assertThat(result).isEqualTo(1337); + verifyHttpClientExecute((request, responseHandler) -> + assertThat(request.getAllHeaders()) + .hasSize(1) + .extracting("name", "value") + .containsOnly(new Tuple("Accept", "*/*")) + ); + } + + @Test + public void get() throws IOException { + // Given + // @formatter:off + new Expectations() {{ + httpClient.execute((HttpUriRequest) any, (ResponseHandler) any); + result = "Response"; + }}; + // @formatter:on + // When + final String response = apacheHttpClientDelegate.get("http://example.com"); + // Then + assertThat(response).isEqualTo("Response"); + verifyHttpClientExecute((request, responseHandler) -> { + assertThat(request.getAllHeaders()) + .hasSize(1) + .extracting("name", "value") + .containsOnly(new Tuple("Accept", "*/*")); + assertThat(responseHandler) + .extracting("delegate") + .hasSize(1) + .hasOnlyElementsOfType(ApacheHttpClientDelegate.BodyResponseHandler.class); + }); + } + + @Test + public void postWithStringBody() throws IOException { + // Given + // @formatter:off + new Expectations() {{ + httpClient.execute((HttpUriRequest) any, (ResponseHandler) any); + result = "Response"; + }}; + // @formatter:on + // When + final String response = apacheHttpClientDelegate.post( + "http://example.com", "{body}", Collections.singletonMap("EXTRA", "HEADER"), null); + // Then + assertThat(response).isEqualTo("Response"); + verifyHttpClientExecute((request, responseHandler) -> + assertThat(request.getAllHeaders()) + .hasSize(3) + .extracting("name", "value") + .containsOnly( + new Tuple("Accept", "*/*"), + new Tuple("Content-Type", "application/json"), + new Tuple("EXTRA", "HEADER")) + ); + } + + @Test + public void postWithFileBody() throws IOException { + // Given + // @formatter:off + new Expectations() {{ + httpClient.execute((HttpUriRequest) any, (ResponseHandler) any); + result = "Response"; + }}; + // @formatter:on + // When + final String response = apacheHttpClientDelegate.post( + "http://example.com", new File("fake-file.tar"), null); + // Then + assertThat(response).isEqualTo("Response"); + verifyHttpClientExecute((request, responseHandler) -> + assertThat(request.getAllHeaders()) + .hasSize(2) + .extracting("name", "value") + .containsOnly( + new Tuple("Accept", "*/*"), + new Tuple("Content-Type", "application/x-tar")) + ); + } + + private void verifyHttpClientExecute(BiConsumer consumer) throws IOException { + // @formatter:off + new Verifications() {{ + HttpUriRequest request; + H responseHandler; + httpClient.execute(request = withCapture(), responseHandler = withCapture()); + consumer.accept(request, responseHandler); + }}; + // @formatter:on + } + +} diff --git a/src/test/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapperTest.java b/src/test/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapperTest.java new file mode 100644 index 000000000..8a6654c72 --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/access/hc/HcChunkedResponseHandlerWrapperTest.java @@ -0,0 +1,70 @@ +package io.fabric8.maven.docker.access.hc; + +import io.fabric8.maven.docker.access.chunked.EntityStreamReaderUtil; +import mockit.Expectations; +import mockit.Mocked; +import mockit.Verifications; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.message.BasicHeader; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +@SuppressWarnings("unused") +public class HcChunkedResponseHandlerWrapperTest { + + @Mocked + private EntityStreamReaderUtil.JsonEntityResponseHandler handler; + @Mocked + private HttpResponse response; + @Mocked + private EntityStreamReaderUtil entityStreamReaderUtil; + + private Header[] headers; + private HcChunkedResponseHandlerWrapper hcChunkedResponseHandlerWrapper; + + @Before + public void setUp() { + hcChunkedResponseHandlerWrapper = new HcChunkedResponseHandlerWrapper(handler); + } + + @Test + public void handleResponseWithJsonResponse() throws IOException { + givenResponseHeaders(new BasicHeader("ConTenT-Type", "application/json; charset=UTF-8")); + hcChunkedResponseHandlerWrapper.handleResponse(response); + verifyProcessJsonStream(1); + } + + @Test + public void handleResponseWithTextPlainResponse() throws IOException { + givenResponseHeaders(new BasicHeader("Content-Type", "text/plain")); + hcChunkedResponseHandlerWrapper.handleResponse(response); + verifyProcessJsonStream(0); + } + + @Test + public void handleResponseWithNoContentType() throws IOException { + givenResponseHeaders(); + hcChunkedResponseHandlerWrapper.handleResponse(response); + verifyProcessJsonStream(0); + } + + private void givenResponseHeaders(Header... headers) { + // @formatter:off + new Expectations() {{ + response.getAllHeaders(); result = headers; + }}; + // @formatter:on + } + + @SuppressWarnings("AccessStaticViaInstance") + private void verifyProcessJsonStream(int timesCalled) throws IOException { + // @formatter:off + new Verifications() {{ + entityStreamReaderUtil.processJsonStream(handler, response.getEntity().getContent()); times = timesCalled; + }}; + // @formatter:on + } +}