From 34b28fd12fe85b8c01e32a6a7381e05e046e6582 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 24 Jan 2024 16:50:15 -0500 Subject: [PATCH 1/2] feat(s3): add endpoint for requesting report generation from presigned S3 URL --- .../cryostat/reports/PresignedFormData.java | 34 ++++++++ .../io/cryostat/reports/ReportResource.java | 84 ++++++++++++++++++- src/main/resources/application.properties | 4 + 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/cryostat/reports/PresignedFormData.java diff --git a/src/main/java/io/cryostat/reports/PresignedFormData.java b/src/main/java/io/cryostat/reports/PresignedFormData.java new file mode 100644 index 0000000..4378a1c --- /dev/null +++ b/src/main/java/io/cryostat/reports/PresignedFormData.java @@ -0,0 +1,34 @@ +/* + * 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.reports; + +import jakarta.ws.rs.core.MediaType; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; + +public class PresignedFormData { + @RestForm + @PartType(MediaType.TEXT_PLAIN) + public String path; + + @RestForm + @PartType(MediaType.TEXT_PLAIN) + public String query; + + @RestForm + @PartType(MediaType.TEXT_PLAIN) + public String filter; +} diff --git a/src/main/java/io/cryostat/reports/ReportResource.java b/src/main/java/io/cryostat/reports/ReportResource.java index 46c1e8f..622507b 100644 --- a/src/main/java/io/cryostat/reports/ReportResource.java +++ b/src/main/java/io/cryostat/reports/ReportResource.java @@ -15,10 +15,17 @@ */ package io.cryostat.reports; +import java.io.FileOutputStream; import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.Channels; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.time.Duration; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -47,6 +54,7 @@ import jakarta.ws.rs.ServerErrorException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; import org.apache.commons.lang3.tuple.Pair; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; @@ -66,9 +74,19 @@ public class ReportResource { @ConfigProperty(name = "io.cryostat.reports.timeout", defaultValue = "29000") String timeoutMs; - @Inject Logger logger; + @ConfigProperty(name = "cryostat.storage.base-uri") + Optional storageBase; + + @ConfigProperty(name = "cryostat.storage.auth-method") + Optional storageAuthMethod; + + @ConfigProperty(name = "cryostat.storage.auth") + Optional storageAuth; + @Inject InterruptibleReportGenerator generator; @Inject FileSystem fs; + @Inject ObjectMapper mapper; + @Inject Logger logger; RuleFilterParser rfp = new RuleFilterParser(); @@ -87,12 +105,71 @@ void onStart(@Observes StartupEvent ev) { @Produces(MediaType.TEXT_PLAIN) public void healthCheck() {} + @Blocking + @Path("remote_report") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @POST + public String getReportFromPresigned(RoutingContext ctx, @BeanParam PresignedFormData form) + throws IOException, URISyntaxException { + try { + long timeout = TimeUnit.MILLISECONDS.toNanos(Long.parseLong(timeoutMs)); + long start = System.nanoTime(); + + var file = Files.createTempFile(null, null); + + UriBuilder uriBuilder = + UriBuilder.newInstance() + .uri(new URI(storageBase.get())) + .path(form.path) + .replaceQuery(form.query); + URI downloadUri = uriBuilder.build(); + logger.infov("Attempting to download presigned recording from {0}", downloadUri); + HttpURLConnection httpConn = (HttpURLConnection) downloadUri.toURL().openConnection(); + httpConn.setRequestMethod("GET"); + if (storageAuth.isPresent() && storageAuth.isPresent()) { + httpConn.setRequestProperty( + "Authorization", + String.format("%s %s", storageAuthMethod.get(), storageAuth.get())); + } + try (var stream = httpConn.getInputStream(); + var downloadChannel = Channels.newChannel(stream); + var fos = new FileOutputStream(file.toFile()); + var fileChannel = fos.getChannel(); ) { + fileChannel.transferFrom(downloadChannel, 0, Long.MAX_VALUE); + } finally { + httpConn.disconnect(); + } + long elapsed = System.nanoTime() - start; + logger.infov("Downloaded {0} in {1}", downloadUri, Duration.ofNanos(elapsed)); + + Predicate predicate = rfp.parse(form.filter); + Future> evalMapFuture = null; + + try (var stream = fs.newInputStream(file)) { + evalMapFuture = generator.generateEvalMapInterruptibly(stream, predicate); + ctxHelper(ctx, evalMapFuture); + return mapper.writeValueAsString( + evalMapFuture.get(timeout - elapsed, TimeUnit.NANOSECONDS)); + } catch (ExecutionException | InterruptedException e) { + throw new InternalServerErrorException(e); + } catch (TimeoutException e) { + throw new ServerErrorException(Response.Status.GATEWAY_TIMEOUT, e); + } finally { + cleanupHelper(evalMapFuture, file, file.toFile().getName(), start); + } + } catch (Exception e) { + logger.error(e); + throw e; + } + } + @Blocking @Path("report") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) @POST - public String getEval(RoutingContext ctx, @BeanParam RecordingFormData form) + public String getReport(RoutingContext ctx, @BeanParam RecordingFormData form) throws IOException { FileUpload upload = form.file; @@ -105,11 +182,10 @@ public String getEval(RoutingContext ctx, @BeanParam RecordingFormData form) Predicate predicate = rfp.parse(form.filter); Future> evalMapFuture = null; - ObjectMapper oMapper = new ObjectMapper(); try (var stream = fs.newInputStream(file)) { evalMapFuture = generator.generateEvalMapInterruptibly(stream, predicate); ctxHelper(ctx, evalMapFuture); - return oMapper.writeValueAsString( + return mapper.writeValueAsString( evalMapFuture.get(timeout - elapsed, TimeUnit.NANOSECONDS)); } catch (ExecutionException | InterruptedException e) { throw new InternalServerErrorException(e); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cc1a906..6879f79 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,3 +13,7 @@ quarkus.http.body.delete-uploaded-files-on-end=true quarkus.native.additional-build-args =\ -H:ResourceConfigurationFiles=resource-config.json,\ -H:ReflectionConfigurationFiles=reflect-config.json + +cryostat.storage.base-uri= +cryostat.storage.auth-method= +cryostat.storage.auth= From f98e67b0024c21b9b7cb3e0ca22b47d48eabf6e4 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 24 Jan 2024 17:18:31 -0500 Subject: [PATCH 2/2] remove temp file --- .../io/cryostat/reports/ReportResource.java | 75 +++++++------------ 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/src/main/java/io/cryostat/reports/ReportResource.java b/src/main/java/io/cryostat/reports/ReportResource.java index 622507b..ef98c08 100644 --- a/src/main/java/io/cryostat/reports/ReportResource.java +++ b/src/main/java/io/cryostat/reports/ReportResource.java @@ -15,15 +15,12 @@ */ package io.cryostat.reports; -import java.io.FileOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; -import java.nio.channels.Channels; import java.nio.file.Files; import java.nio.file.StandardCopyOption; -import java.time.Duration; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -112,55 +109,39 @@ public void healthCheck() {} @POST public String getReportFromPresigned(RoutingContext ctx, @BeanParam PresignedFormData form) throws IOException, URISyntaxException { - try { - long timeout = TimeUnit.MILLISECONDS.toNanos(Long.parseLong(timeoutMs)); - long start = System.nanoTime(); - - var file = Files.createTempFile(null, null); + long timeout = TimeUnit.MILLISECONDS.toNanos(Long.parseLong(timeoutMs)); + long start = System.nanoTime(); - UriBuilder uriBuilder = - UriBuilder.newInstance() - .uri(new URI(storageBase.get())) - .path(form.path) - .replaceQuery(form.query); - URI downloadUri = uriBuilder.build(); - logger.infov("Attempting to download presigned recording from {0}", downloadUri); - HttpURLConnection httpConn = (HttpURLConnection) downloadUri.toURL().openConnection(); - httpConn.setRequestMethod("GET"); - if (storageAuth.isPresent() && storageAuth.isPresent()) { - httpConn.setRequestProperty( - "Authorization", - String.format("%s %s", storageAuthMethod.get(), storageAuth.get())); - } - try (var stream = httpConn.getInputStream(); - var downloadChannel = Channels.newChannel(stream); - var fos = new FileOutputStream(file.toFile()); - var fileChannel = fos.getChannel(); ) { - fileChannel.transferFrom(downloadChannel, 0, Long.MAX_VALUE); - } finally { - httpConn.disconnect(); - } - long elapsed = System.nanoTime() - start; - logger.infov("Downloaded {0} in {1}", downloadUri, Duration.ofNanos(elapsed)); + UriBuilder uriBuilder = + UriBuilder.newInstance() + .uri(new URI(storageBase.get())) + .path(form.path) + .replaceQuery(form.query); + URI downloadUri = uriBuilder.build(); + logger.infov("Attempting to download presigned recording from {0}", downloadUri); + HttpURLConnection httpConn = (HttpURLConnection) downloadUri.toURL().openConnection(); + httpConn.setRequestMethod("GET"); + if (storageAuth.isPresent() && storageAuth.isPresent()) { + httpConn.setRequestProperty( + "Authorization", + String.format("%s %s", storageAuthMethod.get(), storageAuth.get())); + } + try (var stream = httpConn.getInputStream()) { Predicate predicate = rfp.parse(form.filter); Future> evalMapFuture = null; - try (var stream = fs.newInputStream(file)) { - evalMapFuture = generator.generateEvalMapInterruptibly(stream, predicate); - ctxHelper(ctx, evalMapFuture); - return mapper.writeValueAsString( - evalMapFuture.get(timeout - elapsed, TimeUnit.NANOSECONDS)); - } catch (ExecutionException | InterruptedException e) { - throw new InternalServerErrorException(e); - } catch (TimeoutException e) { - throw new ServerErrorException(Response.Status.GATEWAY_TIMEOUT, e); - } finally { - cleanupHelper(evalMapFuture, file, file.toFile().getName(), start); - } - } catch (Exception e) { - logger.error(e); - throw e; + evalMapFuture = generator.generateEvalMapInterruptibly(stream, predicate); + long elapsed = System.nanoTime() - start; + ctxHelper(ctx, evalMapFuture); + return mapper.writeValueAsString( + evalMapFuture.get(timeout - elapsed, TimeUnit.NANOSECONDS)); + } catch (ExecutionException | InterruptedException e) { + throw new InternalServerErrorException(e); + } catch (TimeoutException e) { + throw new ServerErrorException(Response.Status.GATEWAY_TIMEOUT, e); + } finally { + httpConn.disconnect(); } }