Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(docker-build): failing container build with Podman should fail properly #1882

Merged
merged 2 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Usage:
./scripts/extract-changelog-for-version.sh 1.3.37 5
```
### 1.10-SNAPSHOT
* Fix #1684: Podman builds with errors are correctly processed and reported

### 1.9.1 (2022-09-14)
* Fix #1747: Apply service doesn't attempt to create OpenShift Projects in Kubernetes clusters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static void processJsonStream(JsonEntityResponseHandler handler, InputStr
try(JsonReader json = new JsonReader(new InputStreamReader(stream))) {
json.setLenient(true);
while (json.peek() != JsonToken.END_DOCUMENT) {
JsonObject jsonObject = JsonParser.parseReader(json).getAsJsonObject();
JsonObject jsonObject = JsonParser.parseReader(json).getAsJsonObject();
handler.process(jsonObject);
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.function.Function;
import java.util.stream.Stream;

class HcChunkedResponseHandlerWrapper implements ResponseHandler<Object> {
Expand All @@ -31,18 +32,35 @@ class HcChunkedResponseHandlerWrapper implements ResponseHandler<Object> {

@Override
public Object handleResponse(HttpResponse response) throws IOException {
if (!hasJsonContentType(response) && !hasTextPlainContentType(response) && !hasNoContentTypeAsForPodman(response)) {
throw new IllegalStateException(
"Docker daemon returned an unexpected content type while trying to build the Dockerfile.\n" +
"Status: " + response.getStatusLine().getStatusCode() + " - " + response.getStatusLine().getReasonPhrase());
}

try (InputStream stream = response.getEntity().getContent()) {
// Parse text as json
if (isJson(response)) {
EntityStreamReaderUtil.processJsonStream(handler, stream);
}
EntityStreamReaderUtil.processJsonStream(handler, stream);
}
return null;
}

private static boolean isJson(HttpResponse response) {
private static Function<HttpResponse, Boolean> isContentType(String contentType) {
return response -> Stream.of(response.getAllHeaders())
.filter(h -> h.getName().equalsIgnoreCase("Content-Type"))
.anyMatch(h -> h.getValue().toLowerCase().startsWith(contentType));
}

private static boolean hasJsonContentType(HttpResponse response) {
return isContentType("application/json").apply(response);
}

private static boolean hasTextPlainContentType(HttpResponse response) {
return isContentType("text/plain").apply(response);
}

private static boolean hasNoContentTypeAsForPodman(HttpResponse response) {
return Stream.of(response.getAllHeaders())
.filter(h -> h.getName().equalsIgnoreCase("Content-Type"))
.anyMatch(h -> h.getValue().toLowerCase().startsWith("application/json"));
.noneMatch(h -> h.getName().equalsIgnoreCase("Content-Type"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,72 +13,105 @@
*/
package org.eclipse.jkube.kit.build.service.docker.access.hc;

import java.io.IOException;

import org.eclipse.jkube.kit.build.service.docker.access.chunked.EntityStreamReaderUtil;

import mockit.Expectations;
import mockit.Mocked;
import mockit.Verifications;
import com.google.gson.JsonObject;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.eclipse.jkube.kit.build.service.docker.access.chunked.EntityStreamReaderUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

@SuppressWarnings("unused")
class HcChunkedResponseHandlerWrapperTest {

@Mocked
private EntityStreamReaderUtil.JsonEntityResponseHandler handler;
@Mocked
private HttpResponse response;
@Mocked
private EntityStreamReaderUtil entityStreamReaderUtil;

private Header[] headers;
private TestJsonEntityResponseHandler handler;
private HcChunkedResponseHandlerWrapper hcChunkedResponseHandlerWrapper;

@BeforeEach
void setUp() {
handler = new TestJsonEntityResponseHandler();
hcChunkedResponseHandlerWrapper = new HcChunkedResponseHandlerWrapper(handler);
}

@Test
void handleResponseWithJsonResponse() throws IOException {
givenResponseHeaders(new BasicHeader("ConTenT-Type", "application/json; charset=UTF-8"));
void handleResponseWithInvalidStatusAndJsonBody() throws IOException {
final HttpResponse response = response(400, "WRONG!!",
new StringEntity("{}"));
hcChunkedResponseHandlerWrapper.handleResponse(response);
verifyProcessJsonStream(1);
// TODO: Maybe we should propagate the status in some way
assertThat(handler.processedObject).isNotNull();
}

@Test
void handleResponseWithTextPlainResponse() throws IOException {
givenResponseHeaders(new BasicHeader("Content-Type", "text/plain"));
void handleResponseWithJsonContentTypeAndJsonBody() throws IOException {
final HttpResponse response = response(200, "OK",
new StringEntity("{\"field\":\"value\"}"),
new BasicHeader("ConTenT-Type", "application/json; charset=UTF-8"));
hcChunkedResponseHandlerWrapper.handleResponse(response);
verifyProcessJsonStream(0);
assertThat(handler.processedObject)
.returns("value", jo -> jo.get("field").getAsString());
}
@Test
void handleResponseWithJsonContentTypeAndInvalidBody() throws IOException {
final HttpResponse response = response(200, "OK",
new StringEntity("This is not a JSON string"),
new BasicHeader("ConTenT-Type", "application/json"));
assertThatIllegalStateException()
.isThrownBy(() -> hcChunkedResponseHandlerWrapper.handleResponse(response))
.withMessageStartingWith("Not a JSON Object:");
}

@Test
void handleResponseWithNoContentType() throws IOException {
givenResponseHeaders();
void handleResponseWithTextPlainAndJsonBody() throws IOException {
final HttpResponse response = response(200, "OK",
new StringEntity("{\"field\":\"value\"}"),
new BasicHeader("ConTenT-Type", "text/plain"));
hcChunkedResponseHandlerWrapper.handleResponse(response);
verifyProcessJsonStream(0);
assertThat(handler.processedObject)
.returns("value", jo -> jo.get("field").getAsString());
}

private void givenResponseHeaders(Header... headers) {
// @formatter:off
new Expectations() {{
response.getAllHeaders(); result = headers;
}};
// @formatter:on
@Test
void handleResponseWithNoContentTypeAndJsonBody() throws IOException {
final HttpResponse response = response(200, "OK",
new StringEntity("{\"field\":\"value\"}"));
hcChunkedResponseHandlerWrapper.handleResponse(response);
assertThat(handler.processedObject)
.returns("value", jo -> jo.get("field").getAsString());
}

@SuppressWarnings("AccessStaticViaInstance")
private void verifyProcessJsonStream(int timesCalled) throws IOException {
// @formatter:off
new Verifications() {{
entityStreamReaderUtil.processJsonStream(handler, response.getEntity().getContent()); times = timesCalled;
}};
// @formatter:on
private static HttpResponse response(int code, String reason, HttpEntity entity, Header... headers) {
final BasicHttpResponse response = new BasicHttpResponse(
new ProtocolVersion("HTTP", 1, 1), code, reason);
response.setEntity(entity);
response.setHeaders(headers);
return response;
}

private static final class TestJsonEntityResponseHandler implements EntityStreamReaderUtil.JsonEntityResponseHandler {

private JsonObject processedObject;
@Override
public void process(JsonObject toProcess) {
processedObject = toProcess;
}

@Override
public void start() {
}

@Override
public void stop() {
}
}
}
}