From 0fa655793ef89824292d473dc8b039c78a2810aa Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 24 Jan 2024 16:44:46 -0500 Subject: [PATCH 1/2] use single HTTP client instance --- .../cryostat/reports/ReportsServiceImpl.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/cryostat/reports/ReportsServiceImpl.java b/src/main/java/io/cryostat/reports/ReportsServiceImpl.java index b12b28749..a0589aefb 100644 --- a/src/main/java/io/cryostat/reports/ReportsServiceImpl.java +++ b/src/main/java/io/cryostat/reports/ReportsServiceImpl.java @@ -16,6 +16,7 @@ package io.cryostat.reports; import java.io.BufferedInputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.Map; @@ -35,13 +36,17 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.handler.HttpException; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; @@ -57,6 +62,16 @@ class ReportsServiceImpl implements ReportsService { @Inject InterruptibleReportGenerator reportGenerator; @Inject Logger logger; + CloseableHttpClient http; + + void onStart(@Observes StartupEvent evt) { + this.http = HttpClients.createSystem(); + } + + void onStop(@Observes ShutdownEvent evt) throws IOException { + this.http.close(); + } + @Blocking @Override public Uni> reportFor( @@ -126,8 +141,7 @@ private Future> process( private Future> fireRequest(URI uri, InputStream stream) { var cf = new CompletableFuture>(); - try (var http = HttpClients.createDefault(); - stream) { + try { var post = new HttpPost(uri.resolve("report")); var form = MultipartEntityBuilder.create().addBinaryBody("file", stream).build(); post.setEntity(form); From 0236d3b2b225a85f43cd98ecfd4c34520ee977ff Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 24 Jan 2024 16:47:04 -0500 Subject: [PATCH 2/2] feat(reports): use S3 presigned URL for report recording transfer --- smoketest/compose/reports.yml | 3 + .../cryostat/reports/ReportsServiceImpl.java | 61 ++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/smoketest/compose/reports.yml b/smoketest/compose/reports.yml index ea26ac4ee..85eff2850 100644 --- a/smoketest/compose/reports.yml +++ b/smoketest/compose/reports.yml @@ -22,6 +22,9 @@ services: environment: JAVA_OPTS_APPEND: "-Dcom.sun.management.jmxremote.autodiscovery=true -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11224 -Dcom.sun.management.jmxremote.rmi.port=11224 -Djava.rmi.server.hostname=reports -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false" QUARKUS_HTTP_PORT: 10001 + CRYOSTAT_STORAGE_BASE_URI: http://auth:8080/storage + CRYOSTAT_STORAGE_AUTH_METHOD: Basic + CRYOSTAT_STORAGE_AUTH: dXNlcjpwYXNz # user:pass healthcheck: test: curl --fail http://localhost:10001/ || exit 1 retries: 3 diff --git a/src/main/java/io/cryostat/reports/ReportsServiceImpl.java b/src/main/java/io/cryostat/reports/ReportsServiceImpl.java index a0589aefb..1fb22aad8 100644 --- a/src/main/java/io/cryostat/reports/ReportsServiceImpl.java +++ b/src/main/java/io/cryostat/reports/ReportsServiceImpl.java @@ -19,6 +19,9 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -44,12 +47,16 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; +import org.apache.commons.lang3.tuple.Pair; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; @ApplicationScoped class ReportsServiceImpl implements ReportsService { @@ -57,9 +64,16 @@ class ReportsServiceImpl implements ReportsService { @ConfigProperty(name = ConfigProperties.REPORTS_SIDECAR_URL) Optional sidecarUri; + @ConfigProperty(name = ConfigProperties.AWS_BUCKET_NAME_ARCHIVES) + String archiveBucket; + + @ConfigProperty(name = ConfigProperties.STORAGE_EXT_URL) + Optional externalStorageUrl; + @Inject ObjectMapper mapper; @Inject RecordingHelper helper; @Inject InterruptibleReportGenerator reportGenerator; + @Inject S3Presigner presigner; @Inject Logger logger; CloseableHttpClient http; @@ -84,8 +98,7 @@ public Uni> reportFor( "sidecar reportFor active recording {0} {1}", recording.target.jvmId, recording.remoteId); try { - return fireRequest( - uri, helper.getActiveInputStream(recording)); + return fireRequest(uri, getPresignedUri(recording)); } catch (Exception e) { throw new ReportGenerationException(e); } @@ -116,9 +129,11 @@ public Uni> reportFor( logger.tracev( "sidecar reportFor archived recording {0} {1}", jvmId, filename); - return fireRequest( - uri, - helper.getArchivedRecordingStream(jvmId, filename)); + try { + return fireRequest(uri, getPresignedPath(jvmId, filename)); + } catch (Exception e) { + throw new ReportGenerationException(e); + } }) .orElseGet( () -> { @@ -139,11 +154,39 @@ private Future> process( new BufferedInputStream(stream), predicate); } - private Future> fireRequest(URI uri, InputStream stream) { + private URI getPresignedUri(ActiveRecording recording) throws Exception { + // TODO refactor, this is copied out of Recordings.java + String savename = recording.name; + String filename = helper.saveRecording(recording, savename, Instant.now().plusSeconds(60)); + return getPresignedPath(recording.target.jvmId, filename); + } + + private URI getPresignedPath(String jvmId, String filename) throws URISyntaxException { + // TODO refactor, this is copied out of Recordings.java + logger.infov("Handling presigned download request for {0}/{1}", jvmId, filename); + GetObjectRequest getRequest = + GetObjectRequest.builder() + .bucket(archiveBucket) + .key(helper.archivedRecordingKey(Pair.of(jvmId, filename))) + .build(); + GetObjectPresignRequest presignRequest = + GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(1)) + .getObjectRequest(getRequest) + .build(); + return URI.create(presigner.presignGetObject(presignRequest).url().toString()).normalize(); + } + + private Future> fireRequest( + URI sidecarUri, URI presignedRecordingUri) { var cf = new CompletableFuture>(); - try { - var post = new HttpPost(uri.resolve("report")); - var form = MultipartEntityBuilder.create().addBinaryBody("file", stream).build(); + try { + var post = new HttpPost(sidecarUri.resolve("remote_report")); + var form = + MultipartEntityBuilder.create() + .addTextBody("path", presignedRecordingUri.getPath()) + .addTextBody("query", presignedRecordingUri.getQuery()) + .build(); post.setEntity(form); http.execute( post,