From c473f3c9afb333b8ceb57cfc09aa11386d22535e Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 24 Dec 2024 15:58:57 -0500 Subject: [PATCH] fixup! tmp --- .../resources/AgentApplicationResource.java | 76 ++++++-- .../resources/AuthProxyContainer.java | 78 ++++++++ src/test/java/itest/AgentDiscoveryIT.java | 58 +++++- src/test/java/itest/bases/HttpClientTest.java | 183 ++++++++++++++++++ .../java/itest/bases/StandardSelfTest.java | 156 +-------------- 5 files changed, 376 insertions(+), 175 deletions(-) create mode 100644 src/test/java/io/cryostat/resources/AuthProxyContainer.java create mode 100644 src/test/java/itest/bases/HttpClientTest.java diff --git a/src/test/java/io/cryostat/resources/AgentApplicationResource.java b/src/test/java/io/cryostat/resources/AgentApplicationResource.java index c2b22d17c..c8cdbc62b 100644 --- a/src/test/java/io/cryostat/resources/AgentApplicationResource.java +++ b/src/test/java/io/cryostat/resources/AgentApplicationResource.java @@ -23,8 +23,11 @@ import io.quarkus.test.common.DevServicesContext; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import org.jboss.logging.Logger; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.utility.DockerImageName; public class AgentApplicationResource @@ -32,49 +35,86 @@ public class AgentApplicationResource private static final String IMAGE_NAME = "quay.io/redhat-java-monitoring/quarkus-cryostat-agent:latest"; - private static final int AGENT_PORT = 9977; + public static final int PORT = 9977; + public static final String ALIAS = "quarkus-cryostat-agent"; private static final Map envMap = new HashMap<>( Map.of( "JAVA_OPTS_APPEND", - "-javaagent:/deployments/app/cryostat-agent.jar", + """ + -javaagent:/deployments/app/cryostat-agent.jar + -Djava.util.logging.manager=org.jboss.logmanager.LogManager + -Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace + """, "QUARKUS_HTTP_PORT", - "101010", + "9898", "CRYOSTAT_AGENT_APP_NAME", "quarkus-cryostat-agent", + "CRYOSTAT_AGENT_WEBCLIENT_TLS_REQUIRED", + "false", "CRYOSTAT_AGENT_WEBSERVER_HOST", "0.0.0.0", "CRYOSTAT_AGENT_WEBSERVER_PORT", - Integer.toString(AGENT_PORT), - "CRYOSTAT_AGENT_BASEURI", - "http://cryostat:8081/", + Integer.toString(PORT), "CRYOSTAT_AGENT_BASEURI_RANGE", "public", "CRYOSTAT_AGENT_API_WRITES_ENABLED", "true")); private static final Logger logger = Logger.getLogger(AgentApplicationResource.class); private Optional containerNetworkId; + private AuthProxyContainer authProxy; private GenericContainer container; private AtomicInteger cryostatPort = new AtomicInteger(8081); @Override public Map start() { + Optional network = + containerNetworkId.map( + id -> + new Network() { + @Override + public String getId() { + return id; + } + + @Override + public void close() {} + + @Override + public Statement apply( + Statement base, Description description) { + throw new UnsupportedOperationException( + "Unimplemented method 'apply'"); + } + }); + authProxy = new AuthProxyContainer(network, cryostatPort.get()); + container = new GenericContainer<>(DockerImageName.parse(IMAGE_NAME)) - .withExposedPorts(AGENT_PORT) + .dependsOn(authProxy) + .withExposedPorts(PORT) .withEnv(envMap) - .waitingFor(Wait.forListeningPort()); - containerNetworkId.ifPresent(container::withNetworkMode); + .withNetworkAliases(ALIAS) + .waitingFor(new HostPortWaitStrategy().forPorts(PORT)); + network.ifPresent(container::withNetwork); container.addEnv( - "CRYOSTAT_AGENT_BASEURI", String.format("http://cryostat:%d/", cryostatPort.get())); - container.addEnv( - "CRYOSTAT_AGENT_CALLBACK", - String.format("http://%s:%d/", container.getContainerName(), AGENT_PORT)); + "CRYOSTAT_AGENT_BASEURI", + String.format("http://%s:%d/", AuthProxyContainer.ALIAS, AuthProxyContainer.PORT)); + container.addEnv("CRYOSTAT_AGENT_CALLBACK", String.format("http://%s:%d/", ALIAS, PORT)); container.start(); - // return Map.of("quarkus.test.arg-line", "--name=cryostat"); - return Map.of(); + return Map.of( + "quarkus.test.arg-line", "--network-alias=cryostat", + "cryostat.agent.tls.required", "false", + "cryostat.http.proxy.host", ALIAS, + "cryostat.http.proxy.port", Integer.toString(cryostatPort.get()), + "quarkus.http.proxy.proxy-address-forwarding", "true", + "quarkus.http.proxy.allow-x-forwarded", "true", + "quarkus.http.proxy.enable-forwarded-host", "true", + "quarkus.http.proxy.enable-forwarded-prefix", "true", + "quarkus.http.access-log.pattern", "long", + "quarkus.http.access-log.enabled", "true"); } @Override @@ -83,6 +123,10 @@ public void stop() { container.stop(); container.close(); } + if (authProxy != null) { + authProxy.stop(); + authProxy.close(); + } } @Override diff --git a/src/test/java/io/cryostat/resources/AuthProxyContainer.java b/src/test/java/io/cryostat/resources/AuthProxyContainer.java new file mode 100644 index 000000000..4f705f54c --- /dev/null +++ b/src/test/java/io/cryostat/resources/AuthProxyContainer.java @@ -0,0 +1,78 @@ +/* + * Copyright The Cryostat 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 + * + * http://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 io.cryostat.resources; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; + +public class AuthProxyContainer extends GenericContainer { + + private static final String IMAGE_NAME = "quay.io/oauth2-proxy/oauth2-proxy:latest"; + private static final String CFG_FILE_PATH = "/tmp/auth_proxy_alpha_config.yml"; + static final int PORT = 8080; + static final String ALIAS = "authproxy"; + private static final Map envMap = + new HashMap<>( + Map.of( + "OAUTH2_PROXY_REDIRECT_URL", + "http://localhost:8080/oauth2/callback", + "OAUTH2_PROXY_COOKIE_SECRET", + "__24_BYTE_COOKIE_SECRET_", + "OAUTH2_PROXY_SKIP_AUTH_ROUTES", + ".*", + "OAUTH2_PROXY_EMAIL_DOMAINS", + "*")); + private static final String ALPHA_CONFIG = + """ +server: + BindAddress: http://AUTHPROXY_HOST:AUTHPROXY_PORT +upstreamConfig: + proxyRawPath: true + upstreams: + - id: cryostat + path: / + uri: http://cryostat:CRYOSTAT_PORT +providers: + - id: dummy + name: Unused - Sign In Below + clientId: CLIENT_ID + clientSecret: CLIENT_SECRET + provider: google +"""; + + public AuthProxyContainer(Optional network, int cryostatPort) { + super(IMAGE_NAME); + network.ifPresent(this::withNetwork); + withCommand(String.format("--alpha-config=%s", CFG_FILE_PATH)); + withExposedPorts(PORT); + withNetworkAliases(ALIAS); + withEnv(envMap); + withCopyToContainer( + Transferable.of( + ALPHA_CONFIG + .replaceAll("AUTHPROXY_HOST", "0.0.0.0") + .replaceAll("AUTHPROXY_PORT", Integer.toString(PORT)) + .replaceAll("CRYOSTAT_PORT", Integer.toString(cryostatPort))), + CFG_FILE_PATH); + waitingFor(Wait.forLogMessage(".*OAuthProxy configured.*", 1)); + } +} diff --git a/src/test/java/itest/AgentDiscoveryIT.java b/src/test/java/itest/AgentDiscoveryIT.java index 28c9e7e7d..30cd7a4e4 100644 --- a/src/test/java/itest/AgentDiscoveryIT.java +++ b/src/test/java/itest/AgentDiscoveryIT.java @@ -15,19 +15,69 @@ */ package itest; +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + import io.cryostat.resources.AgentApplicationResource; +import io.cryostat.util.HttpStatusCodeIdentifier; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; -import itest.bases.StandardSelfTest; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.HttpResponse; +import itest.bases.HttpClientTest; +import junit.framework.AssertionFailedError; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.jboss.logging.Logger; import org.junit.jupiter.api.Test; @QuarkusIntegrationTest @QuarkusTestResource(value = AgentApplicationResource.class, restrictToAnnotatedClass = true) -public class AgentDiscoveryIT extends StandardSelfTest { +public class AgentDiscoveryIT extends HttpClientTest { + + static final Logger logger = Logger.getLogger(AgentDiscoveryIT.class); + static final Duration TIMEOUT = Duration.ofSeconds(60); @Test - void shouldDiscoverTarget() { - waitForDiscovery(1); + void shouldDiscoverTarget() throws InterruptedException, TimeoutException, ExecutionException { + long last = System.nanoTime(); + long elapsed = 0; + while (true) { + HttpResponse req = + webClient.extensions().get("/api/v4/targets", REQUEST_TIMEOUT_SECONDS); + if (HttpStatusCodeIdentifier.isSuccessCode(req.statusCode())) { + JsonArray result = req.bodyAsJsonArray(); + if (result.size() == 1) { + JsonObject obj = result.getJsonObject(0); + MatcherAssert.assertThat( + obj.getString("alias"), + Matchers.equalTo(AgentApplicationResource.ALIAS)); + MatcherAssert.assertThat( + obj.getString("connectUrl"), + Matchers.equalTo( + String.format( + "http://%s:%d/", + AgentApplicationResource.ALIAS, + AgentApplicationResource.PORT))); + + MatcherAssert.assertThat(obj.getBoolean("agent"), Matchers.is(true)); + break; + } else if (result.size() > 1) { + throw new IllegalStateException("Discovered too many targets"); + } + } + + long now = System.nanoTime(); + elapsed += (now - last); + last = now; + if (Duration.ofNanos(elapsed).compareTo(TIMEOUT) > 0) { + throw new AssertionFailedError("Timed out"); + } + Thread.sleep(5_000); + } } } diff --git a/src/test/java/itest/bases/HttpClientTest.java b/src/test/java/itest/bases/HttpClientTest.java new file mode 100644 index 000000000..981350b00 --- /dev/null +++ b/src/test/java/itest/bases/HttpClientTest.java @@ -0,0 +1,183 @@ +/* + * Copyright The Cryostat 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 + * + * http://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 itest.bases; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.cryostat.util.HttpStatusCodeIdentifier; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.file.FileSystem; +import io.vertx.core.http.WebSocket; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.handler.HttpException; +import itest.util.Utils; +import itest.util.Utils.TestWebClient; +import org.jboss.logging.Logger; + +public abstract class HttpClientTest { + + protected static final ExecutorService WORKER = Executors.newCachedThreadPool(); + public static final Logger logger = Logger.getLogger(HttpClientTest.class); + public static final ObjectMapper mapper; + public static final int REQUEST_TIMEOUT_SECONDS = 30; + public static final TestWebClient webClient = Utils.getWebClient(); + + static { + mapper = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setVisibility(PropertyAccessor.ALL, Visibility.ANY); + } + + public static CompletableFuture expectNotification( + String category, long timeout, TimeUnit unit) + throws TimeoutException, ExecutionException, InterruptedException { + logger.debugv( + "Waiting for a \"{0}\" message within the next {1} {2} ...", + category, timeout, unit.name()); + CompletableFuture future = new CompletableFuture<>(); + + var a = new WebSocket[1]; + Utils.HTTP_CLIENT.webSocket( + "ws://localhost/api/notifications", + ar -> { + if (ar.failed()) { + future.completeExceptionally(ar.cause()); + return; + } + a[0] = ar.result(); + var ws = a[0]; + + ws.handler( + m -> { + JsonObject resp = m.toJsonObject(); + JsonObject meta = resp.getJsonObject("meta"); + String c = meta.getString("category"); + if (Objects.equals(c, category)) { + logger.tracev( + "Received expected \"{0}\" message", category); + ws.end(unused -> future.complete(resp)); + ws.close(); + } + }) + // FIXME in the cryostat itests we DO use auth. The message below is + // copy-pasted from the old codebase, however cryostat does not yet + // perform authentication when websocket clients connect. + + // just to initialize the connection - Cryostat expects + // clients to send a message after the connection opens + // to authenticate themselves, but in itests we don't + // use auth + .writeTextMessage(""); + }); + + return future.orTimeout(timeout, unit).whenComplete((o, t) -> a[0].close()); + } + + public static boolean assertRequestStatus( + AsyncResult> result, CompletableFuture future) { + if (result.failed()) { + result.cause().printStackTrace(); + future.completeExceptionally(result.cause()); + + return false; + } + HttpResponse response = result.result(); + if (!HttpStatusCodeIdentifier.isSuccessCode(response.statusCode()) + && !HttpStatusCodeIdentifier.isRedirectCode(response.statusCode())) { + System.err.println("HTTP " + response.statusCode() + ": " + response.statusMessage()); + future.completeExceptionally( + new HttpException(response.statusCode(), response.statusMessage())); + return false; + } + return true; + } + + public static CompletableFuture downloadFile(String url, String name, String suffix) { + return fireDownloadRequest( + webClient.get(url), name, suffix, MultiMap.caseInsensitiveMultiMap()); + } + + public static CompletableFuture downloadFile( + String url, String name, String suffix, MultiMap headers) { + return fireDownloadRequest(webClient.get(url), name, suffix, headers); + } + + public static CompletableFuture downloadFileAbs(String url, String name, String suffix) { + return fireDownloadRequest( + webClient.getAbs(url), name, suffix, MultiMap.caseInsensitiveMultiMap()); + } + + public static CompletableFuture downloadFileAbs( + String url, String name, String suffix, MultiMap headers) { + return fireDownloadRequest(webClient.getAbs(url), name, suffix, headers); + } + + private static CompletableFuture fireDownloadRequest( + HttpRequest request, String filename, String fileSuffix, MultiMap headers) { + CompletableFuture future = new CompletableFuture<>(); + WORKER.submit( + () -> { + request.putHeaders(headers) + .followRedirects(true) + .send( + ar -> { + if (ar.failed()) { + future.completeExceptionally(ar.cause()); + return; + } + HttpResponse resp = ar.result(); + logger.tracev( + "GET {0} -> HTTP {1} {2}: [{3}]", + request.uri(), + resp.statusCode(), + resp.statusMessage(), + resp.headers()); + if (!(HttpStatusCodeIdentifier.isSuccessCode( + resp.statusCode()))) { + future.completeExceptionally( + new Exception( + String.format( + "HTTP %d", resp.statusCode()))); + return; + } + FileSystem fs = Utils.getFileSystem(); + String file = + fs.createTempFileBlocking(filename, fileSuffix); + fs.writeFileBlocking(file, ar.result().body()); + future.complete(Paths.get(file)); + }); + }); + return future; + } +} diff --git a/src/test/java/itest/bases/StandardSelfTest.java b/src/test/java/itest/bases/StandardSelfTest.java index 9b5a3292b..cf53b38fa 100644 --- a/src/test/java/itest/bases/StandardSelfTest.java +++ b/src/test/java/itest/bases/StandardSelfTest.java @@ -16,66 +16,37 @@ package itest.bases; import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import io.cryostat.util.HttpStatusCodeIdentifier; -import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.vertx.core.AsyncResult; -import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; -import io.vertx.core.file.FileSystem; -import io.vertx.core.http.WebSocket; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.codec.BodyCodec; -import io.vertx.ext.web.handler.HttpException; import itest.util.ITestCleanupFailedException; -import itest.util.Utils; -import itest.util.Utils.TestWebClient; import jakarta.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import org.apache.http.client.utils.URLEncodedUtils; import org.jboss.logging.Logger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -public abstract class StandardSelfTest { +public abstract class StandardSelfTest extends HttpClientTest { public static final String SELF_JMX_URL = "service:jmx:rmi:///jndi/rmi://localhost:0/jmxrmi"; public static String SELF_JMX_URL_ENCODED = URLEncodedUtils.formatSegments(SELF_JMX_URL).substring(1); public static final String SELFTEST_ALIAS = "selftest"; - private static final ExecutorService WORKER = Executors.newCachedThreadPool(); public static final Logger logger = Logger.getLogger(StandardSelfTest.class); - public static final ObjectMapper mapper; - public static final int REQUEST_TIMEOUT_SECONDS = 30; public static final int DISCOVERY_DEADLINE_SECONDS = 60; - public static final TestWebClient webClient = Utils.getWebClient(); public static volatile String selfCustomTargetLocation; - static { - mapper = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .setVisibility(PropertyAccessor.ALL, Visibility.ANY); - } - @BeforeAll public static void waitForDiscovery() { waitForDiscovery(0); @@ -262,129 +233,4 @@ public static String getSelfReferenceConnectUrl() { public static String getSelfReferenceConnectUrlEncoded() { return URLEncodedUtils.formatSegments(getSelfReferenceConnectUrl()).substring(1); } - - public static final Pair VERTX_FIB_CREDENTIALS = - Pair.of("admin", "adminpass123"); - - public static CompletableFuture expectNotification( - String category, long timeout, TimeUnit unit) - throws TimeoutException, ExecutionException, InterruptedException { - logger.debugv( - "Waiting for a \"{0}\" message within the next {1} {2} ...", - category, timeout, unit.name()); - CompletableFuture future = new CompletableFuture<>(); - - var a = new WebSocket[1]; - Utils.HTTP_CLIENT.webSocket( - "ws://localhost/api/notifications", - ar -> { - if (ar.failed()) { - future.completeExceptionally(ar.cause()); - return; - } - a[0] = ar.result(); - var ws = a[0]; - - ws.handler( - m -> { - JsonObject resp = m.toJsonObject(); - JsonObject meta = resp.getJsonObject("meta"); - String c = meta.getString("category"); - if (Objects.equals(c, category)) { - logger.tracev( - "Received expected \"{0}\" message", category); - ws.end(unused -> future.complete(resp)); - ws.close(); - } - }) - // FIXME in the cryostat itests we DO use auth. The message below is - // copy-pasted from the old codebase, however cryostat does not yet - // perform authentication when websocket clients connect. - - // just to initialize the connection - Cryostat expects - // clients to send a message after the connection opens - // to authenticate themselves, but in itests we don't - // use auth - .writeTextMessage(""); - }); - - return future.orTimeout(timeout, unit).whenComplete((o, t) -> a[0].close()); - } - - public static boolean assertRequestStatus( - AsyncResult> result, CompletableFuture future) { - if (result.failed()) { - result.cause().printStackTrace(); - future.completeExceptionally(result.cause()); - - return false; - } - HttpResponse response = result.result(); - if (!HttpStatusCodeIdentifier.isSuccessCode(response.statusCode()) - && !HttpStatusCodeIdentifier.isRedirectCode(response.statusCode())) { - System.err.println("HTTP " + response.statusCode() + ": " + response.statusMessage()); - future.completeExceptionally( - new HttpException(response.statusCode(), response.statusMessage())); - return false; - } - return true; - } - - public static CompletableFuture downloadFile(String url, String name, String suffix) { - return fireDownloadRequest( - webClient.get(url), name, suffix, MultiMap.caseInsensitiveMultiMap()); - } - - public static CompletableFuture downloadFile( - String url, String name, String suffix, MultiMap headers) { - return fireDownloadRequest(webClient.get(url), name, suffix, headers); - } - - public static CompletableFuture downloadFileAbs(String url, String name, String suffix) { - return fireDownloadRequest( - webClient.getAbs(url), name, suffix, MultiMap.caseInsensitiveMultiMap()); - } - - public static CompletableFuture downloadFileAbs( - String url, String name, String suffix, MultiMap headers) { - return fireDownloadRequest(webClient.getAbs(url), name, suffix, headers); - } - - private static CompletableFuture fireDownloadRequest( - HttpRequest request, String filename, String fileSuffix, MultiMap headers) { - CompletableFuture future = new CompletableFuture<>(); - WORKER.submit( - () -> { - request.putHeaders(headers) - .followRedirects(true) - .send( - ar -> { - if (ar.failed()) { - future.completeExceptionally(ar.cause()); - return; - } - HttpResponse resp = ar.result(); - logger.tracev( - "GET {0} -> HTTP {1} {2}: [{3}]", - request.uri(), - resp.statusCode(), - resp.statusMessage(), - resp.headers()); - if (!(HttpStatusCodeIdentifier.isSuccessCode( - resp.statusCode()))) { - future.completeExceptionally( - new Exception( - String.format( - "HTTP %d", resp.statusCode()))); - return; - } - FileSystem fs = Utils.getFileSystem(); - String file = - fs.createTempFileBlocking(filename, fileSuffix); - fs.writeFileBlocking(file, ar.result().body()); - future.complete(Paths.get(file)); - }); - }); - return future; - } }