From a34db8dc5009fcf6c6e4441cf16f938839892d89 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 17:35:23 -0500 Subject: [PATCH 01/19] fix(topology): correct GraphQL schema for Topology actions, implement missing mutations --- .../io/cryostat/graphql/ActiveRecordings.java | 59 ++++++++++++++++--- .../io/cryostat/graphql/EnvironmentNodes.java | 8 +-- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index ff011c236..8c73f840b 100644 --- a/src/main/java/io/cryostat/graphql/ActiveRecordings.java +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; @@ -32,7 +33,7 @@ import io.cryostat.recordings.RecordingHelper; import io.cryostat.recordings.RecordingHelper.RecordingOptions; import io.cryostat.recordings.RecordingHelper.RecordingReplace; -import io.cryostat.recordings.Recordings.Metadata; +import io.cryostat.recordings.Recordings.ArchivedRecording; import io.cryostat.targets.Target; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -56,18 +57,20 @@ public class ActiveRecordings { @Transactional @Description("Start a new Flight Recording on the specified Target") public Uni doStartRecording( - @Source Target target, @NonNull RecordingSettings settings) + @Source Target target, @NonNull RecordingSettings recording) throws QuantityConversionException { var fTarget = Target.findById(target.id); Template template = recordingHelper.getPreferredTemplate( - fTarget, settings.template, settings.templateType); + fTarget, recording.template, TemplateType.valueOf(recording.templateType)); return recordingHelper.startRecording( fTarget, - RecordingReplace.STOPPED, + Optional.ofNullable(recording.replace) + .map(RecordingReplace::valueOf) + .orElse(RecordingReplace.STOPPED), template, - settings.asOptions(), - settings.metadata.labels()); + recording.asOptions(), + recording.metadata.labels); } @Blocking @@ -78,6 +81,39 @@ public Uni doSnapshot(@Source Target target) { return recordingHelper.createSnapshot(fTarget); } + @Blocking + @Transactional + @Description("Stop the specified Flight Recording") + public Uni doStop(@Source ActiveRecording recording) { + var ar = ActiveRecording.findById(recording.id); + ar.state = RecordingState.STOPPED; + ar.persist(); + return Uni.createFrom().item(ar); + } + + @Blocking + @Transactional + @Description("Delete the specified Flight Recording") + public Uni doDelete(@Source ActiveRecording recording) { + var ar = ActiveRecording.findById(recording.id); + ar.delete(); + return Uni.createFrom().item(ar); + } + + @Blocking + @Transactional + @Description("Archive the specified Flight Recording") + public Uni doArchive(@Source ActiveRecording recording) throws Exception { + var ar = ActiveRecording.findById(recording.id); + var filename = recordingHelper.saveRecording(ar); + var archive = + recordingHelper.listArchivedRecordings(ar.target).stream() + .filter(r -> r.name().equals(filename)) + .findFirst() + .orElseThrow(); + return Uni.createFrom().item(archive); + } + public TargetNodes.ActiveRecordings active( @Source Recordings recordings, ActiveRecordingsFilter filter) { var out = new TargetNodes.ActiveRecordings(); @@ -98,15 +134,15 @@ public TargetNodes.ActiveRecordings active( public static class RecordingSettings { public @NonNull String name; public @NonNull String template; - public @NonNull TemplateType templateType; - public @Nullable RecordingReplace replace; + public @NonNull String templateType; + public @Nullable String replace; public @Nullable Boolean continuous; public @Nullable Boolean archiveOnStop; public @Nullable Boolean toDisk; public @Nullable Long duration; public @Nullable Long maxSize; public @Nullable Long maxAge; - public @Nullable Metadata metadata; + public @Nullable RecordingMetadata metadata; public RecordingOptions asOptions() { return new RecordingOptions( @@ -119,6 +155,11 @@ public RecordingOptions asOptions() { } } + @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") + public static class RecordingMetadata { + public @Nullable Map labels; + } + @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public static class ActiveRecordingsFilter implements Predicate { public @Nullable String name; diff --git a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java index bb0d0422e..d22810fac 100644 --- a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java +++ b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java @@ -30,7 +30,7 @@ @GraphQLApi public class EnvironmentNodes { - public static class EnvironmentNodeFilterInput { + public static class EnvironmentNodesFilter { @Nullable public Long id; @Nullable public String name; @Nullable public List names; @@ -40,13 +40,13 @@ public static class EnvironmentNodeFilterInput { @Query("environmentNodes") @Description("Get all environment nodes in the discovery tree with optional filtering") - public List environmentNodes(EnvironmentNodeFilterInput filter) { + public List environmentNodes(EnvironmentNodesFilter filter) { DiscoveryNode rootNode = DiscoveryNode.getUniverse(); return filterAndTraverse(rootNode, filter); } private List filterAndTraverse( - DiscoveryNode node, EnvironmentNodeFilterInput filter) { + DiscoveryNode node, EnvironmentNodesFilter filter) { List filteredNodes = new ArrayList<>(); if (matchesFilter(node, filter)) { filteredNodes.add(node); @@ -59,7 +59,7 @@ private List filterAndTraverse( return filteredNodes; } - private static boolean matchesFilter(DiscoveryNode node, EnvironmentNodeFilterInput filter) { + private static boolean matchesFilter(DiscoveryNode node, EnvironmentNodesFilter filter) { if (node.target != null) return false; if (filter == null) return true; From 0f7a38d3f6f4c75145f7236d28dab991ef2433ce Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 5 Mar 2024 09:09:46 -0500 Subject: [PATCH 02/19] refactor --- .../io/cryostat/graphql/ActiveRecordings.java | 7 ++----- .../cryostat/recordings/RecordingHelper.java | 18 +++++++++++++++++- .../io/cryostat/recordings/Recordings.java | 5 ++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index 8c73f840b..57dce86cd 100644 --- a/src/main/java/io/cryostat/graphql/ActiveRecordings.java +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -86,9 +86,7 @@ public Uni doSnapshot(@Source Target target) { @Description("Stop the specified Flight Recording") public Uni doStop(@Source ActiveRecording recording) { var ar = ActiveRecording.findById(recording.id); - ar.state = RecordingState.STOPPED; - ar.persist(); - return Uni.createFrom().item(ar); + return recordingHelper.stopRecording(ar); } @Blocking @@ -96,8 +94,7 @@ public Uni doStop(@Source ActiveRecording recording) { @Description("Delete the specified Flight Recording") public Uni doDelete(@Source ActiveRecording recording) { var ar = ActiveRecording.findById(recording.id); - ar.delete(); - return Uni.createFrom().item(ar); + return recordingHelper.deleteRecording(ar); } @Blocking diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index b12a8fc81..93f8a078f 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -87,6 +87,7 @@ import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Named; +import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ServerErrorException; import jdk.jfr.RecordingState; @@ -195,6 +196,7 @@ void onStart(@Observes StartupEvent evt) { } } + @Transactional public Uni startRecording( Target target, RecordingReplace replace, @@ -265,7 +267,7 @@ public Uni startRecording( }); } - @Blocking + @Transactional public Uni createSnapshot(Target target) { return connectionManager.executeConnectedTaskUni( target, @@ -331,6 +333,20 @@ public Uni createSnapshot(Target target) { }); } + @Transactional + public Uni stopRecording(ActiveRecording recording) { + recording.state = RecordingState.STOPPED; + recording.persist(); + return Uni.createFrom().item(recording); + } + + @Transactional + public Uni deleteRecording(ActiveRecording recording) { + recording.delete(); + recording.persist(); + return Uni.createFrom().item(recording); + } + @Blocking private boolean snapshotIsReadable(Target target, InputStream snapshot) throws IOException { if (!connectionManager.markConnectionInUse(target)) { diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 23c529370..29f0d9958 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -439,8 +439,7 @@ public String patch(@RestPath long targetId, @RestPath long remoteId, String bod ActiveRecording activeRecording = recording.get(); switch (body.toLowerCase()) { case "stop": - activeRecording.state = RecordingState.STOPPED; - activeRecording.persist(); + recordingHelper.stopRecording(activeRecording).await().indefinitely(); return null; case "save": try { @@ -681,7 +680,7 @@ public void deleteRecording(@RestPath long targetId, @RestPath long remoteId) th .filter(r -> r.remoteId == remoteId) .findFirst() .ifPresentOrElse( - ActiveRecording::delete, + recordingHelper::deleteRecording, () -> { throw new NotFoundException(); }); From 2018274e919960b94b427ee69ba0ba21509d2c6f Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 5 Mar 2024 16:51:19 -0500 Subject: [PATCH 03/19] minor cleanups --- .../io/cryostat/graphql/ActiveRecordings.java | 23 +++++++++++-------- .../cryostat/graphql/ArchivedRecordings.java | 21 +++++++++-------- .../java/io/cryostat/graphql/RootNode.java | 8 +++---- .../java/io/cryostat/graphql/TargetNodes.java | 11 ++++----- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index 57dce86cd..66addd0f5 100644 --- a/src/main/java/io/cryostat/graphql/ActiveRecordings.java +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -199,16 +199,19 @@ public boolean test(ActiveRecording r) { Predicate matchesStartTimeBefore = n -> startTimeMsBeforeEqual == null || startTimeMsBeforeEqual <= n.startTime; - return matchesName - .and(matchesNames) - .and(matchesLabels) - .and(matchesState) - .and(matchesContinuous) - .and(matchesToDisk) - .and(matchesDurationGte) - .and(matchesDurationLte) - .and(matchesStartTimeBefore) - .and(matchesStartTimeAfter) + return List.of( + matchesName, + matchesNames, + matchesLabels, + matchesState, + matchesContinuous, + matchesToDisk, + matchesDurationGte, + matchesDurationLte, + matchesStartTimeBefore, + matchesStartTimeAfter) + .stream() + .reduce(x -> true, Predicate::and) .test(r); } } diff --git a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java index eec113d24..880591ae8 100644 --- a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java +++ b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java @@ -45,8 +45,6 @@ public TargetNodes.ArchivedRecordings listArchivedRecordings(ArchivedRecordingsF var r = new TargetNodes.ArchivedRecordings(); r.data = recordingHelper.listArchivedRecordings(); r.aggregate = AggregateInfo.fromArchived(r.data); - r.aggregate.size = r.data.stream().mapToLong(ArchivedRecording::size).sum(); - r.aggregate.count = r.data.size(); return r; } @@ -109,14 +107,17 @@ public boolean test(ArchivedRecording r) { archivedTimeBeforeEqual == null || archivedTimeBeforeEqual <= n.archivedTime(); - return matchesName - .and(matchesNames) - .and(matchesSourceTarget) - .and(matchesLabels) - .and(matchesSizeGte) - .and(matchesSizeLte) - .and(matchesArchivedTimeGte) - .and(matchesArchivedTimeLte) + return List.of( + matchesName, + matchesNames, + matchesSourceTarget, + matchesLabels, + matchesSizeGte, + matchesSizeLte, + matchesArchivedTimeGte, + matchesArchivedTimeLte) + .stream() + .reduce(x -> true, Predicate::and) .test(r); } } diff --git a/src/main/java/io/cryostat/graphql/RootNode.java b/src/main/java/io/cryostat/graphql/RootNode.java index d74f77c27..27c754431 100644 --- a/src/main/java/io/cryostat/graphql/RootNode.java +++ b/src/main/java/io/cryostat/graphql/RootNode.java @@ -98,11 +98,9 @@ public boolean test(DiscoveryNode t) { n.target.annotations .merged())); - return matchesId - .and(matchesName) - .and(matchesNames) - .and(matchesLabels) - .and(matchesAnnotations) + return List.of(matchesId, matchesName, matchesNames, matchesLabels, matchesAnnotations) + .stream() + .reduce(x -> true, Predicate::and) .test(t); } } diff --git a/src/main/java/io/cryostat/graphql/TargetNodes.java b/src/main/java/io/cryostat/graphql/TargetNodes.java index 5648c3b22..4f11be898 100644 --- a/src/main/java/io/cryostat/graphql/TargetNodes.java +++ b/src/main/java/io/cryostat/graphql/TargetNodes.java @@ -101,6 +101,7 @@ public List getTargetNodes(DiscoveryNodeFilter filter) { @Blocking @Description("Get the active and archived recordings belonging to this target") public Recordings recordings(@Source Target target, Context context) { + var fTarget = Target.findById(target.id); var dfe = context.unwrap(DataFetchingEnvironment.class); var requestedFields = dfe.getSelectionSet().getFields().stream().map(field -> field.getName()).toList(); @@ -109,17 +110,14 @@ public Recordings recordings(@Source Target target, Context context) { if (requestedFields.contains("active")) { recordings.active = new ActiveRecordings(); - recordings.active.data = target.activeRecordings; + recordings.active.data = fTarget.activeRecordings; recordings.active.aggregate = AggregateInfo.fromActive(recordings.active.data); } if (requestedFields.contains("archived")) { recordings.archived = new ArchivedRecordings(); - recordings.archived.data = recordingHelper.listArchivedRecordings(target); + recordings.archived.data = recordingHelper.listArchivedRecordings(fTarget); recordings.archived.aggregate = AggregateInfo.fromArchived(recordings.archived.data); - recordings.archived.aggregate.count = recordings.archived.data.size(); - recordings.archived.aggregate.size = - recordings.archived.data.stream().mapToLong(ArchivedRecording::size).sum(); } return recordings; @@ -128,7 +126,8 @@ public Recordings recordings(@Source Target target, Context context) { @Blocking @Description("Get live MBean metrics snapshot from the specified Target") public Uni mbeanMetrics(@Source Target target) { - return connectionManager.executeConnectedTaskUni(target, JFRConnection::getMBeanMetrics); + var fTarget = Target.findById(target.id); + return connectionManager.executeConnectedTaskUni(fTarget, JFRConnection::getMBeanMetrics); } @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") From d9cc8d54509c29ebe8b7f32863e39ae650ac9b22 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 6 Mar 2024 11:15:12 -0500 Subject: [PATCH 04/19] client may not have supplied labels --- src/main/java/io/cryostat/graphql/ActiveRecordings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index 66addd0f5..e743a5b67 100644 --- a/src/main/java/io/cryostat/graphql/ActiveRecordings.java +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -70,7 +70,7 @@ public Uni doStartRecording( .orElse(RecordingReplace.STOPPED), template, recording.asOptions(), - recording.metadata.labels); + Optional.ofNullable(recording.metadata).map(s -> s.labels).orElse(Map.of())); } @Blocking From 701e0c32c84ae4ec637126cc9ed5ed9b6d02aac7 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 6 Mar 2024 11:15:49 -0500 Subject: [PATCH 05/19] env vars for controlling discovery mechanisms --- compose/cryostat.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose/cryostat.yml b/compose/cryostat.yml index c42fb185d..19aeaa863 100644 --- a/compose/cryostat.yml +++ b/compose/cryostat.yml @@ -28,9 +28,9 @@ services: QUARKUS_HTTP_HOST: "cryostat" QUARKUS_HTTP_PORT: ${CRYOSTAT_HTTP_PORT} QUARKUS_HIBERNATE_ORM_LOG_SQL: "true" - CRYOSTAT_DISCOVERY_JDP_ENABLED: "true" - CRYOSTAT_DISCOVERY_PODMAN_ENABLED: "true" - CRYOSTAT_DISCOVERY_DOCKER_ENABLED: "true" + CRYOSTAT_DISCOVERY_JDP_ENABLED: ${CRYOSTAT_DISCOVERY_JDP_ENABLED:-true} + CRYOSTAT_DISCOVERY_PODMAN_ENABLED: ${CRYOSTAT_DISCOVERY_PODMAN_ENABLED:-true} + CRYOSTAT_DISCOVERY_DOCKER_ENABLED: ${CRYOSTAT_DISCOVERY_DOCKER_ENABLED:-true} JAVA_OPTS_APPEND: "-XX:+FlightRecorder -XX:StartFlightRecording=name=onstart,settings=default,disk=true,maxage=5m -XX:StartFlightRecording=name=startup,settings=profile,disk=true,duration=30s -Dcom.sun.management.jmxremote.autodiscovery=true -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false" restart: unless-stopped healthcheck: From 768f9ceb0e87459155487175f381d8beae7fe48c Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:30:53 -0500 Subject: [PATCH 06/19] refactor to return ArchivedRecording instance rather than only filename String --- .../io/cryostat/graphql/ActiveRecordings.java | 8 +--- .../cryostat/recordings/RecordingHelper.java | 39 +++++++++---------- .../io/cryostat/recordings/Recordings.java | 10 +++-- .../cryostat/rules/ScheduledArchiveJob.java | 2 +- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index e743a5b67..0d7b8c4e3 100644 --- a/src/main/java/io/cryostat/graphql/ActiveRecordings.java +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -102,13 +102,7 @@ public Uni doDelete(@Source ActiveRecording recording) { @Description("Archive the specified Flight Recording") public Uni doArchive(@Source ActiveRecording recording) throws Exception { var ar = ActiveRecording.findById(recording.id); - var filename = recordingHelper.saveRecording(ar); - var archive = - recordingHelper.listArchivedRecordings(ar.target).stream() - .filter(r -> r.name().equals(filename)) - .findFirst() - .orElseThrow(); - return Uni.createFrom().item(archive); + return Uni.createFrom().item(recordingHelper.archiveRecording(ar, null, null)); } public TargetNodes.ActiveRecordings active( diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index 93f8a078f..d37ec658d 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -606,34 +606,26 @@ public List listArchivedRecordings(Target target) { .toList(); } - public String saveRecording(ActiveRecording recording) throws Exception { - return saveRecording(recording, null); - } - - public String saveRecording(ActiveRecording recording, Instant expiry) throws Exception { - return saveRecording(recording, null, expiry); - } - - @Blocking - public String saveRecording(ActiveRecording recording, String savename, Instant expiry) - throws Exception { + public ArchivedRecording archiveRecording( + ActiveRecording activeRecording, String savename, Instant expiry) throws Exception { // AWS object key name guidelines advise characters to avoid (% so we should not pass url // encoded characters) String transformedAlias = - URLDecoder.decode(recording.target.alias, StandardCharsets.UTF_8) + URLDecoder.decode(activeRecording.target.alias, StandardCharsets.UTF_8) .replaceAll("[\\._/]+", "-"); - String timestamp = - clock.now().truncatedTo(ChronoUnit.SECONDS).toString().replaceAll("[-:]+", ""); + Instant now = clock.now(); + String timestamp = now.truncatedTo(ChronoUnit.SECONDS).toString().replaceAll("[-:]+", ""); String filename = - String.format("%s_%s_%s.jfr", transformedAlias, recording.name, timestamp); + String.format("%s_%s_%s.jfr", transformedAlias, activeRecording.name, timestamp); if (StringUtils.isBlank(savename)) { savename = filename; } int mib = 1024 * 1024; - String key = archivedRecordingKey(recording.target.jvmId, filename); + String key = archivedRecordingKey(activeRecording.target.jvmId, filename); String multipartId = null; List> parts = new ArrayList<>(); - try (var stream = remoteRecordingStreamFactory.open(recording); + long accum = 0; + try (var stream = remoteRecordingStreamFactory.open(activeRecording); var ch = Channels.newChannel(stream)) { ByteBuffer buf = ByteBuffer.allocate(20 * mib); CreateMultipartUploadRequest.Builder builder = @@ -643,14 +635,13 @@ public String saveRecording(ActiveRecording recording, String savename, Instant .contentType(JFR_MIME) .contentDisposition( String.format("attachment; filename=\"%s\"", savename)) - .tagging(createActiveRecordingTagging(recording, expiry)); + .tagging(createActiveRecordingTagging(activeRecording, expiry)); if (expiry != null && expiry.isAfter(Instant.now())) { builder = builder.expires(expiry); } CreateMultipartUploadRequest request = builder.build(); multipartId = storage.createMultipartUpload(request).uploadId(); int read = 0; - long accum = 0; for (int i = 1; i <= 10_000; i++) { read = ch.read(buf); @@ -731,13 +722,19 @@ public String saveRecording(ActiveRecording recording, String savename, Instant var event = new ActiveRecordingEvent( Recordings.RecordingEventCategory.ACTIVE_SAVED, - ActiveRecordingEvent.Payload.of(this, recording)); + ActiveRecordingEvent.Payload.of(this, activeRecording)); bus.publish(event.category().category(), event.payload().recording()); bus.publish( MessagingServer.class.getName(), new Notification(event.category().category(), event.payload())); } - return filename; + return new ArchivedRecording( + filename, + downloadUrl(activeRecording.target.jvmId, filename), + reportUrl(activeRecording.target.jvmId, filename), + activeRecording.metadata, + accum, + now.getEpochSecond()); } public Optional getArchivedRecordingMetadata(String jvmId, String filename) { diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 29f0d9958..cc1825ff2 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -449,7 +449,7 @@ public String patch(@RestPath long targetId, @RestPath long remoteId, String bod // completes before sending a response - it should be async. Here we should just // return an Accepted response, and if a failure occurs that should be indicated // as a websocket notification. - return recordingHelper.saveRecording(activeRecording); + return recordingHelper.archiveRecording(activeRecording, null, null).name(); } catch (IOException ioe) { logger.warn(ioe); return null; @@ -621,7 +621,7 @@ void stopRecording(long id, boolean archive) { recording.state = RecordingState.STOPPED; recording.persist(); if (archive) { - recordingHelper.saveRecording(recording); + recordingHelper.archiveRecording(recording, null, null); } } catch (Exception e) { logger.error("couldn't update recording", e); @@ -944,8 +944,10 @@ public Response handleActiveDownload(@RestPath long id) throws Exception { String savename = recording.name; String filename = - recordingHelper.saveRecording( - recording, savename, Instant.now().plus(transientArchivesTtl)); + recordingHelper + .archiveRecording( + recording, savename, Instant.now().plus(transientArchivesTtl)) + .name(); String encodedKey = recordingHelper.encodedKey(recording.target.jvmId, filename); if (!savename.endsWith(".jfr")) { savename += ".jfr"; diff --git a/src/main/java/io/cryostat/rules/ScheduledArchiveJob.java b/src/main/java/io/cryostat/rules/ScheduledArchiveJob.java index 742dc099e..136eee5e2 100644 --- a/src/main/java/io/cryostat/rules/ScheduledArchiveJob.java +++ b/src/main/java/io/cryostat/rules/ScheduledArchiveJob.java @@ -91,7 +91,7 @@ void initPreviousRecordings(Target target, Rule rule, Queue previousReco @Transactional void performArchival(ActiveRecording recording, Queue previousRecordings) throws Exception { - String filename = recordingHelper.saveRecording(recording); + String filename = recordingHelper.archiveRecording(recording, null, null).name(); previousRecordings.add(filename); } From 045672c4367c7aa5784a8799e6d6bacab452f59b Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:31:44 -0500 Subject: [PATCH 07/19] add API endpoint for per-JVM ID archived recordings --- .../io/cryostat/recordings/Recordings.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index cc1825ff2..9124e8c57 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -403,6 +403,43 @@ public Collection listFsArchives() { return map.values(); } + @GET + @Blocking + @Path("/api/beta/fs/recordings/{jvmId}") + @RolesAllowed("read") + public Collection listFsArchives(@RestPath String jvmId) { + var map = new HashMap(); + recordingHelper + .listArchivedRecordingObjects(jvmId) + .forEach( + item -> { + String filename = item.key().strip().replace(jvmId + "/", ""); + + Metadata metadata = + recordingHelper + .getArchivedRecordingMetadata(jvmId, filename) + .orElseGet(Metadata::empty); + + String connectUrl = + metadata.labels.computeIfAbsent("connectUrl", k -> jvmId); + var dir = + map.computeIfAbsent( + jvmId, + id -> + new ArchivedRecordingDirectory( + connectUrl, id, new ArrayList<>())); + dir.recordings.add( + new ArchivedRecording( + filename, + recordingHelper.downloadUrl(jvmId, filename), + recordingHelper.reportUrl(jvmId, filename), + metadata, + item.size(), + item.lastModified().getEpochSecond())); + }); + return map.values(); + } + @GET @Path("/api/v3/targets/{id}/recordings") @RolesAllowed("read") From 0c2c02743caf0d868932eab4a6d2b744e4441fa0 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:32:32 -0500 Subject: [PATCH 08/19] add proper top-level side-effect-ful mutations to schema --- .../io/cryostat/graphql/ActiveRecordings.java | 176 ++++++++++++++++++ .../io/cryostat/graphql/EnvironmentNodes.java | 4 +- .../java/io/cryostat/graphql/RootNode.java | 2 +- 3 files changed, 179 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index 0d7b8c4e3..efd1bc997 100644 --- a/src/main/java/io/cryostat/graphql/ActiveRecordings.java +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -15,6 +15,7 @@ */ package io.cryostat.graphql; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -26,6 +27,8 @@ import io.cryostat.core.templates.Template; import io.cryostat.core.templates.TemplateType; +import io.cryostat.discovery.DiscoveryNode; +import io.cryostat.graphql.RootNode.DiscoveryNodeFilter; import io.cryostat.graphql.TargetNodes.AggregateInfo; import io.cryostat.graphql.TargetNodes.Recordings; import io.cryostat.graphql.matchers.LabelSelectorMatcher; @@ -39,19 +42,192 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.smallrye.common.annotation.Blocking; import io.smallrye.graphql.api.Nullable; +import io.smallrye.graphql.execution.ExecutionException; import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jdk.jfr.RecordingState; import org.eclipse.microprofile.graphql.Description; import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; import org.eclipse.microprofile.graphql.NonNull; import org.eclipse.microprofile.graphql.Source; +import org.jboss.logging.Logger; @GraphQLApi public class ActiveRecordings { @Inject RecordingHelper recordingHelper; + @Inject Logger logger; + + @Blocking + @Transactional + @Mutation + @Description( + "Start a new Flight Recording on all Targets under the subtrees of the discovery nodes" + + " matching the given filter") + public List createRecording( + @NonNull DiscoveryNodeFilter nodes, @NonNull RecordingSettings recording) { + return DiscoveryNode.listAll().stream() + .filter(nodes) + .flatMap( + node -> + RootNode.recurseChildren(node, n -> n.target != null).stream() + .map(n -> n.target)) + .map( + target -> { + var template = + recordingHelper.getPreferredTemplate( + target, + recording.template, + TemplateType.valueOf(recording.templateType)); + try { + return recordingHelper + .startRecording( + target, + Optional.ofNullable(recording.replace) + .map(RecordingReplace::valueOf) + .orElse(RecordingReplace.STOPPED), + template, + recording.asOptions(), + Optional.ofNullable(recording.metadata) + .map(s -> s.labels) + .orElse(Map.of())) + .await() + .atMost(Duration.ofSeconds(10)); + } catch (QuantityConversionException qce) { + throw new ExecutionException(qce); + } + }) + .toList(); + } + + @Blocking + @Transactional + @Mutation + @Description( + "Archive an existing Flight Recording matching the given filter, on all Targets under" + + " the subtrees of the discovery nodes matching the given filter") + public List archiveRecording( + @NonNull DiscoveryNodeFilter nodes, @Nullable ActiveRecordingsFilter recordings) { + return DiscoveryNode.listAll().stream() + .filter(nodes) + .flatMap( + node -> + RootNode.recurseChildren(node, n -> n.target != null).stream() + .map(n -> n.target)) + .flatMap( + t -> + t.activeRecordings.stream() + .filter(r -> recordings == null || recordings.test(r))) + .map( + recording -> { + try { + return recordingHelper.archiveRecording(recording, null, null); + } catch (Exception e) { + throw new ExecutionException(e); + } + }) + .toList(); + } + + @Blocking + @Transactional + @Mutation + @Description( + "Stop an existing Flight Recording matching the given filter, on all Targets under" + + " the subtrees of the discovery nodes matching the given filter") + public List stopRecording( + @NonNull DiscoveryNodeFilter nodes, @Nullable ActiveRecordingsFilter recordings) { + return DiscoveryNode.listAll().stream() + .filter(nodes) + .flatMap( + node -> + RootNode.recurseChildren(node, n -> n.target != null).stream() + .map(n -> n.target)) + .flatMap( + t -> + t.activeRecordings.stream() + .filter(r -> recordings == null || recordings.test(r))) + .map( + recording -> { + try { + return recordingHelper + .stopRecording(recording) + .await() + .atMost(Duration.ofSeconds(10)); + } catch (Exception e) { + throw new ExecutionException(e); + } + }) + .toList(); + } + + @Blocking + @Transactional + @Mutation + @Description( + "Delete an existing Flight Recording matching the given filter, on all Targets under" + + " the subtrees of the discovery nodes matching the given filter") + public List deleteRecording( + @NonNull DiscoveryNodeFilter nodes, @Nullable ActiveRecordingsFilter recordings) { + var activeRecordings = + DiscoveryNode.listAll().stream() + .filter(nodes) + .flatMap( + node -> + RootNode.recurseChildren(node, n -> n.target != null) + .stream() + .map(n -> n.target)) + .flatMap( + t -> + t.activeRecordings.stream() + .filter( + r -> + recordings == null + || recordings.test(r))) + .toList(); + return activeRecordings.stream() + .map( + recording -> { + try { + return recordingHelper + .deleteRecording(recording) + .await() + .atMost(Duration.ofSeconds(10)); + } catch (Exception e) { + throw new ExecutionException(e); + } + }) + .toList(); + } + + @Blocking + @Transactional + @Mutation + @Description( + "Create a Flight Recorder Snapshot on all Targets under" + + " the subtrees of the discovery nodes matching the given filter") + public List createSnapshot(@NonNull DiscoveryNodeFilter nodes) { + return DiscoveryNode.listAll().stream() + .filter(nodes) + .flatMap( + node -> + RootNode.recurseChildren(node, n -> n.target != null).stream() + .map(n -> n.target)) + .map( + target -> { + try { + return recordingHelper + .createSnapshot(target) + .await() + .atMost(Duration.ofSeconds(10)); + } catch (Exception e) { + throw new ExecutionException(e); + } + }) + .toList(); + } @Blocking @Transactional diff --git a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java index d22810fac..56c83cc4c 100644 --- a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java +++ b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java @@ -40,12 +40,12 @@ public static class EnvironmentNodesFilter { @Query("environmentNodes") @Description("Get all environment nodes in the discovery tree with optional filtering") - public List environmentNodes(EnvironmentNodesFilter filter) { + public List environmentNodes(@Nullable EnvironmentNodesFilter filter) { DiscoveryNode rootNode = DiscoveryNode.getUniverse(); return filterAndTraverse(rootNode, filter); } - private List filterAndTraverse( + private static List filterAndTraverse( DiscoveryNode node, EnvironmentNodesFilter filter) { List filteredNodes = new ArrayList<>(); if (matchesFilter(node, filter)) { diff --git a/src/main/java/io/cryostat/graphql/RootNode.java b/src/main/java/io/cryostat/graphql/RootNode.java index 27c754431..52fdb2b09 100644 --- a/src/main/java/io/cryostat/graphql/RootNode.java +++ b/src/main/java/io/cryostat/graphql/RootNode.java @@ -53,7 +53,7 @@ public List descendantTargets( .toList(); } - private Set recurseChildren( + static Set recurseChildren( DiscoveryNode node, Predicate predicate) { Set result = new HashSet<>(); if (predicate.test(node)) { From aaa7a5d8086ab4205febde4f622390d01ef92a7f Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:32:58 -0500 Subject: [PATCH 09/19] add Target activeRecordings and archivedRecordings accessors to bypass 'recordings' struct field --- .../java/io/cryostat/graphql/TargetNodes.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/io/cryostat/graphql/TargetNodes.java b/src/main/java/io/cryostat/graphql/TargetNodes.java index 4f11be898..fe124b29b 100644 --- a/src/main/java/io/cryostat/graphql/TargetNodes.java +++ b/src/main/java/io/cryostat/graphql/TargetNodes.java @@ -21,6 +21,8 @@ import io.cryostat.core.net.JFRConnection; import io.cryostat.core.net.MBeanMetrics; import io.cryostat.discovery.DiscoveryNode; +import io.cryostat.graphql.ActiveRecordings.ActiveRecordingsFilter; +import io.cryostat.graphql.ArchivedRecordings.ArchivedRecordingsFilter; import io.cryostat.graphql.RootNode.DiscoveryNodeFilter; import io.cryostat.recordings.ActiveRecording; import io.cryostat.recordings.RecordingHelper; @@ -35,6 +37,7 @@ import graphql.schema.GraphQLSchema; import io.smallrye.common.annotation.Blocking; import io.smallrye.graphql.api.Context; +import io.smallrye.graphql.api.Nullable; import io.smallrye.mutiny.Uni; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; @@ -98,6 +101,32 @@ public List getTargetNodes(DiscoveryNodeFilter filter) { // return t -> observed.add(fn.apply(t)); // } + @Blocking + public ActiveRecordings activeRecordings( + @Source Target target, @Nullable ActiveRecordingsFilter filter) { + var fTarget = Target.findById(target.id); + var recordings = new ActiveRecordings(); + recordings.data = + fTarget.activeRecordings.stream() + .filter(r -> filter == null || filter.test(r)) + .toList(); + recordings.aggregate = AggregateInfo.fromActive(recordings.data); + return recordings; + } + + @Blocking + public ArchivedRecordings archivedRecordings( + @Source Target target, @Nullable ArchivedRecordingsFilter filter) { + var fTarget = Target.findById(target.id); + var recordings = new ArchivedRecordings(); + recordings.data = + recordingHelper.listArchivedRecordings(fTarget).stream() + .filter(r -> filter == null || filter.test(r)) + .toList(); + recordings.aggregate = AggregateInfo.fromArchived(recordings.data); + return recordings; + } + @Blocking @Description("Get the active and archived recordings belonging to this target") public Recordings recordings(@Source Target target, Context context) { From 89772fa88d5404a5541429ebb5999483648e4dc9 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:43:48 -0500 Subject: [PATCH 10/19] refactor, reuse existing filter type --- .../io/cryostat/graphql/EnvironmentNodes.java | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java index 56c83cc4c..e5f97f490 100644 --- a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java +++ b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java @@ -17,10 +17,9 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import io.cryostat.discovery.DiscoveryNode; -import io.cryostat.graphql.matchers.LabelSelectorMatcher; +import io.cryostat.graphql.RootNode.DiscoveryNodeFilter; import io.smallrye.graphql.api.Nullable; import org.eclipse.microprofile.graphql.Description; @@ -30,12 +29,12 @@ @GraphQLApi public class EnvironmentNodes { - public static class EnvironmentNodesFilter { - @Nullable public Long id; - @Nullable public String name; - @Nullable public List names; - @Nullable public String nodeType; - @Nullable public List labels; + public static class EnvironmentNodesFilter extends DiscoveryNodeFilter { + @Override + public boolean test(DiscoveryNode node) { + boolean hasTarget = node.target == null; + return !hasTarget && super.test(node); + } } @Query("environmentNodes") @@ -48,7 +47,7 @@ public List environmentNodes(@Nullable EnvironmentNodesFilter fil private static List filterAndTraverse( DiscoveryNode node, EnvironmentNodesFilter filter) { List filteredNodes = new ArrayList<>(); - if (matchesFilter(node, filter)) { + if (filter.test(node)) { filteredNodes.add(node); } if (node.children != null) { @@ -58,23 +57,4 @@ private static List filterAndTraverse( } return filteredNodes; } - - private static boolean matchesFilter(DiscoveryNode node, EnvironmentNodesFilter filter) { - if (node.target != null) return false; - if (filter == null) return true; - - boolean matchesId = filter.id == null || filter.id.equals(node.id); - boolean matchesName = filter.name == null || Objects.equals(filter.name, node.name); - boolean matchesNames = filter.names == null || filter.names.contains(node.name); - boolean matchesLabels = - filter.labels == null - || filter.labels.stream() - .allMatch( - label -> - LabelSelectorMatcher.parse(label) - .test(node.labels)); - boolean matchesNodeType = filter.nodeType == null || filter.nodeType.equals(node.nodeType); - - return matchesId && matchesName && matchesNames && matchesLabels && matchesNodeType; - } } From 5b5e40e347541612cc0ea4e348794a6d56c2a426 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:45:46 -0500 Subject: [PATCH 11/19] add filter by list of ids, remove filter by singular id and singular name --- .../java/io/cryostat/graphql/RootNode.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/RootNode.java b/src/main/java/io/cryostat/graphql/RootNode.java index 52fdb2b09..9a11577c3 100644 --- a/src/main/java/io/cryostat/graphql/RootNode.java +++ b/src/main/java/io/cryostat/graphql/RootNode.java @@ -17,7 +17,6 @@ import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.function.Predicate; @@ -67,18 +66,18 @@ static Set recurseChildren( @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public static class DiscoveryNodeFilter implements Predicate { - public @Nullable Long id; - public @Nullable String name; + public @Nullable List ids; public @Nullable List names; + public @Nullable List nodeTypes; public @Nullable List labels; public @Nullable List annotations; @Override public boolean test(DiscoveryNode t) { - Predicate matchesId = n -> id == null || id.equals(n.id); - Predicate matchesName = - n -> name == null || Objects.equals(name, n.name); + Predicate matchesIds = n -> ids == null || ids.contains(n.id); Predicate matchesNames = n -> names == null || names.contains(n.name); + Predicate matchesNodeTypes = + n -> nodeTypes == null || nodeTypes.contains(n.nodeType); Predicate matchesLabels = n -> labels == null @@ -98,7 +97,12 @@ public boolean test(DiscoveryNode t) { n.target.annotations .merged())); - return List.of(matchesId, matchesName, matchesNames, matchesLabels, matchesAnnotations) + return List.of( + matchesIds, + matchesNames, + matchesNodeTypes, + matchesLabels, + matchesAnnotations) .stream() .reduce(x -> true, Predicate::and) .test(t); From fc69d923560b77ba995761a76b192be63d0f3e40 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 11:06:46 -0500 Subject: [PATCH 12/19] restore filter by singular id and name --- compose/reports.yml | 4 +- compose/sample-apps.yml | 8 +- graphql-archive-test.bash | 140 ++++++++++++++++++ graphql-mutations-test.bash | 98 ++++++++++++ .../java/io/cryostat/graphql/RootNode.java | 6 + 5 files changed, 250 insertions(+), 6 deletions(-) create mode 100755 graphql-archive-test.bash create mode 100755 graphql-mutations-test.bash diff --git a/compose/reports.yml b/compose/reports.yml index ea26ac4ee..e24580bdd 100644 --- a/compose/reports.yml +++ b/compose/reports.yml @@ -10,8 +10,8 @@ services: deploy: resources: limits: - cpus: '0.5' - memory: 512m + cpus: '8' + memory: 4G expose: - "10001" labels: diff --git a/compose/sample-apps.yml b/compose/sample-apps.yml index 6166e2489..8b7053bdd 100644 --- a/compose/sample-apps.yml +++ b/compose/sample-apps.yml @@ -15,7 +15,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: "sample-app-1" CRYOSTAT_AGENT_WEBSERVER_PORT: "8910" CRYOSTAT_AGENT_CALLBACK: "http://sample-app-1:8910/" - CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" + # CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" CRYOSTAT_AGENT_TRUST_ALL: "true" CRYOSTAT_AGENT_AUTHORIZATION: Basic dXNlcjpwYXNz ports: @@ -47,7 +47,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: "sample-app-2" CRYOSTAT_AGENT_WEBSERVER_PORT: "8911" CRYOSTAT_AGENT_CALLBACK: "http://sample-app-2:8911/" - CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" + # CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" CRYOSTAT_AGENT_TRUST_ALL: "true" CRYOSTAT_AGENT_AUTHORIZATION: "Basic dXNlcjpwYXNz" ports: @@ -76,7 +76,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: "sample-app-3" CRYOSTAT_AGENT_WEBSERVER_PORT: "8910" CRYOSTAT_AGENT_CALLBACK: "http://sample-app-3:8912/" - CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" + # CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" CRYOSTAT_AGENT_TRUST_ALL: "true" CRYOSTAT_AGENT_AUTHORIZATION: "Basic dXNlcjpwYXNz" ports: @@ -104,7 +104,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: quarkus-test-agent CRYOSTAT_AGENT_WEBSERVER_PORT: 9977 CRYOSTAT_AGENT_CALLBACK: http://quarkus-test-agent:9977/ - CRYOSTAT_AGENT_BASEURI: http://cryostat:${CRYOSTAT_HTTP_PORT}/ + # CRYOSTAT_AGENT_BASEURI: http://cryostat:${CRYOSTAT_HTTP_PORT}/ CRYOSTAT_AGENT_BASEURI_RANGE: public CRYOSTAT_AGENT_SSL_TRUST_ALL: "true" CRYOSTAT_AGENT_SSL_VERIFY_HOSTNAME: "false" diff --git a/graphql-archive-test.bash b/graphql-archive-test.bash new file mode 100755 index 000000000..585479c6c --- /dev/null +++ b/graphql-archive-test.bash @@ -0,0 +1,140 @@ +#!/usr/bin/env bash + +set -e + +doQuery() { + http --follow -v --auth=user:pass :8080/api/v3/graphql query="$1" +} + +queryArchives() { + doQuery ' + query queryArchives { + targetNodes { + name + target { + connectUrl + recordings { + archived { + aggregate { + count + size + } + data { + name + } + } + } + } + } + } + ' +} + +createRecording() { + doQuery ' + query createRecording { + environmentNodes(filter: { name: "Podman" }) { + name + descendantTargets { + name + target { + doStartRecording(recording: { + name: "test", + template: "Profiling", + templateType: "TARGET" + }) { + name + } + } + } + } + } + ' +} + +listRecordings() { + doQuery ' + query listRecordings { + environmentNodes(filter: { name: "Podman" }) { + name + descendantTargets { + name + target { + recordings { + active { + aggregate { + count + } + data { + name + } + } + } + } + } + } + } + ' +} + +doArchive() { + doQuery ' + query doArchive { + environmentNodes(filter: { name: "Podman" }) { + name + descendantTargets { + name + target { + recordings { + active(filter: { name: "test" }) { + data { + name + doArchive { + name + } + } + } + } + } + } + } + } + ' +} + +deleteRecordings() { + doQuery ' + query deleteRecordings { + environmentNodes(filter: { name: "Podman" }) { + name + descendantTargets { + name + target { + recordings { + active(filter: { name: "test" }) { + data { + name + doDelete { + name + } + } + } + } + } + } + } + } + ' +} + +queryArchives +sleep 3 +createRecording +sleep 2 +listRecordings +sleep 10 +doArchive +sleep 2 +queryArchives +sleep 5 +deleteRecordings diff --git a/graphql-mutations-test.bash b/graphql-mutations-test.bash new file mode 100755 index 000000000..97cb66b18 --- /dev/null +++ b/graphql-mutations-test.bash @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +set -e + +doQuery() { + http --follow -v --auth=user:pass :8080/api/v3/graphql query="$1" +} + +queryArchives() { + doQuery ' + query queryArchives { + targetNodes { + name + target { + connectUrl + archivedRecordings { + aggregate { + count + size + } + data { + name + } + } + } + } + } + ' +} + +createRecording() { + doQuery ' + mutation createRecording { + createRecording(nodes: { name: "Podman" }, recording: { + name: "test", + template: "Profiling", + templateType: "TARGET" + }) { + name + } + } + ' +} + +listRecordings() { + doQuery ' + query listRecordings { + environmentNodes(filter: { name: "Podman" }) { + name + descendantTargets { + name + target { + activeRecordings { + aggregate { + count + } + data { + name + } + } + } + } + } + } + ' +} + +doArchive() { + doQuery ' + mutation archiveRecording { + archiveRecording(nodes: { name: "Podman" }, recordings: { name: "test" }) { + name + } + } + ' +} + +deleteRecordings() { + doQuery ' + mutation deleteRecording { + deleteRecording(nodes: { name: "Podman" }, recordings: { name: "test" }) { + name + } + } + ' +} + +queryArchives +sleep 3 +createRecording +sleep 2 +listRecordings +sleep 10 +doArchive +sleep 2 +queryArchives +sleep 5 +deleteRecordings diff --git a/src/main/java/io/cryostat/graphql/RootNode.java b/src/main/java/io/cryostat/graphql/RootNode.java index 9a11577c3..ba5f5e74e 100644 --- a/src/main/java/io/cryostat/graphql/RootNode.java +++ b/src/main/java/io/cryostat/graphql/RootNode.java @@ -66,7 +66,9 @@ static Set recurseChildren( @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public static class DiscoveryNodeFilter implements Predicate { + public @Nullable Long id; public @Nullable List ids; + public @Nullable String name; public @Nullable List names; public @Nullable List nodeTypes; public @Nullable List labels; @@ -74,7 +76,9 @@ public static class DiscoveryNodeFilter implements Predicate { @Override public boolean test(DiscoveryNode t) { + Predicate matchesId = n -> id == null || id.equals(n.id); Predicate matchesIds = n -> ids == null || ids.contains(n.id); + Predicate matchesName = n -> name == null || name.equals(n.name); Predicate matchesNames = n -> names == null || names.contains(n.name); Predicate matchesNodeTypes = n -> nodeTypes == null || nodeTypes.contains(n.nodeType); @@ -98,7 +102,9 @@ public boolean test(DiscoveryNode t) { .merged())); return List.of( + matchesId, matchesIds, + matchesName, matchesNames, matchesNodeTypes, matchesLabels, From f0c49717a2e346f1a218fffd2351809675653db1 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 11:27:23 -0500 Subject: [PATCH 13/19] refactor, remove duplicate filter type --- .../io/cryostat/graphql/EnvironmentNodes.java | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java index e5f97f490..d70859989 100644 --- a/src/main/java/io/cryostat/graphql/EnvironmentNodes.java +++ b/src/main/java/io/cryostat/graphql/EnvironmentNodes.java @@ -15,7 +15,6 @@ */ package io.cryostat.graphql; -import java.util.ArrayList; import java.util.List; import io.cryostat.discovery.DiscoveryNode; @@ -29,32 +28,12 @@ @GraphQLApi public class EnvironmentNodes { - public static class EnvironmentNodesFilter extends DiscoveryNodeFilter { - @Override - public boolean test(DiscoveryNode node) { - boolean hasTarget = node.target == null; - return !hasTarget && super.test(node); - } - } - @Query("environmentNodes") @Description("Get all environment nodes in the discovery tree with optional filtering") - public List environmentNodes(@Nullable EnvironmentNodesFilter filter) { - DiscoveryNode rootNode = DiscoveryNode.getUniverse(); - return filterAndTraverse(rootNode, filter); - } - - private static List filterAndTraverse( - DiscoveryNode node, EnvironmentNodesFilter filter) { - List filteredNodes = new ArrayList<>(); - if (filter.test(node)) { - filteredNodes.add(node); - } - if (node.children != null) { - for (DiscoveryNode child : node.children) { - filteredNodes.addAll(filterAndTraverse(child, filter)); - } - } - return filteredNodes; + public List environmentNodes(@Nullable DiscoveryNodeFilter filter) { + return RootNode.recurseChildren(DiscoveryNode.getUniverse(), node -> node.target == null) + .stream() + .filter(filter) + .toList(); } } From 2fac41f351d9205d6028619be0ee22530bf67d63 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 11:31:29 -0500 Subject: [PATCH 14/19] refactor --- .../io/cryostat/graphql/SchemaExtension.java | 54 +++++++++++++++++++ .../java/io/cryostat/graphql/TargetNodes.java | 31 ----------- 2 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 src/main/java/io/cryostat/graphql/SchemaExtension.java diff --git a/src/main/java/io/cryostat/graphql/SchemaExtension.java b/src/main/java/io/cryostat/graphql/SchemaExtension.java new file mode 100644 index 000000000..c330a057e --- /dev/null +++ b/src/main/java/io/cryostat/graphql/SchemaExtension.java @@ -0,0 +1,54 @@ +/* + * 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.graphql; + +import java.util.Arrays; + +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLSchema; +import jakarta.enterprise.event.Observes; +import jdk.jfr.RecordingState; +import org.eclipse.microprofile.graphql.GraphQLApi; + +@GraphQLApi +public class SchemaExtension { + + public GraphQLSchema.Builder registerRecordingStateEnum( + @Observes GraphQLSchema.Builder builder) { + return createEnumType( + builder, RecordingState.class, "Running state of an active Flight Recording"); + } + + private static GraphQLSchema.Builder createEnumType( + GraphQLSchema.Builder builder, Class> klazz, String description) { + return builder.additionalType( + GraphQLEnumType.newEnum() + .name(klazz.getSimpleName()) + .description(description) + .values( + Arrays.asList(klazz.getEnumConstants()).stream() + .map( + s -> + new GraphQLEnumValueDefinition.Builder() + .name(s.name()) + .value(s) + .description(s.name()) + .build()) + .toList()) + .build()); + } +} diff --git a/src/main/java/io/cryostat/graphql/TargetNodes.java b/src/main/java/io/cryostat/graphql/TargetNodes.java index fe124b29b..321653955 100644 --- a/src/main/java/io/cryostat/graphql/TargetNodes.java +++ b/src/main/java/io/cryostat/graphql/TargetNodes.java @@ -15,7 +15,6 @@ */ package io.cryostat.graphql; -import java.util.Arrays; import java.util.List; import io.cryostat.core.net.JFRConnection; @@ -32,16 +31,11 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLEnumValueDefinition; -import graphql.schema.GraphQLSchema; import io.smallrye.common.annotation.Blocking; import io.smallrye.graphql.api.Context; import io.smallrye.graphql.api.Nullable; import io.smallrye.mutiny.Uni; -import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; -import jdk.jfr.RecordingState; import org.eclipse.microprofile.graphql.Description; import org.eclipse.microprofile.graphql.GraphQLApi; import org.eclipse.microprofile.graphql.NonNull; @@ -54,31 +48,6 @@ public class TargetNodes { @Inject RecordingHelper recordingHelper; @Inject TargetConnectionManager connectionManager; - public GraphQLSchema.Builder registerRecordingStateEnum( - @Observes GraphQLSchema.Builder builder) { - return createEnumType( - builder, RecordingState.class, "Running state of an active Flight Recording"); - } - - private static GraphQLSchema.Builder createEnumType( - GraphQLSchema.Builder builder, Class> klazz, String description) { - return builder.additionalType( - GraphQLEnumType.newEnum() - .name(klazz.getSimpleName()) - .description(description) - .values( - Arrays.asList(klazz.getEnumConstants()).stream() - .map( - s -> - new GraphQLEnumValueDefinition.Builder() - .name(s.name()) - .value(s) - .description(s.name()) - .build()) - .toList()) - .build()); - } - @Blocking @Query("targetNodes") @Description("Get the Target discovery nodes, i.e. the leaf nodes of the discovery tree") From 14e6e495e2d59ab2ee7064d6a8201638c6328d46 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 11:32:52 -0500 Subject: [PATCH 15/19] revert accidentally added testing changes --- compose/reports.yml | 4 +- compose/sample-apps.yml | 8 +-- graphql-archive-test.bash | 140 ------------------------------------ graphql-mutations-test.bash | 98 ------------------------- 4 files changed, 6 insertions(+), 244 deletions(-) delete mode 100755 graphql-archive-test.bash delete mode 100755 graphql-mutations-test.bash diff --git a/compose/reports.yml b/compose/reports.yml index e24580bdd..ea26ac4ee 100644 --- a/compose/reports.yml +++ b/compose/reports.yml @@ -10,8 +10,8 @@ services: deploy: resources: limits: - cpus: '8' - memory: 4G + cpus: '0.5' + memory: 512m expose: - "10001" labels: diff --git a/compose/sample-apps.yml b/compose/sample-apps.yml index 8b7053bdd..6166e2489 100644 --- a/compose/sample-apps.yml +++ b/compose/sample-apps.yml @@ -15,7 +15,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: "sample-app-1" CRYOSTAT_AGENT_WEBSERVER_PORT: "8910" CRYOSTAT_AGENT_CALLBACK: "http://sample-app-1:8910/" - # CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" + CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" CRYOSTAT_AGENT_TRUST_ALL: "true" CRYOSTAT_AGENT_AUTHORIZATION: Basic dXNlcjpwYXNz ports: @@ -47,7 +47,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: "sample-app-2" CRYOSTAT_AGENT_WEBSERVER_PORT: "8911" CRYOSTAT_AGENT_CALLBACK: "http://sample-app-2:8911/" - # CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" + CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" CRYOSTAT_AGENT_TRUST_ALL: "true" CRYOSTAT_AGENT_AUTHORIZATION: "Basic dXNlcjpwYXNz" ports: @@ -76,7 +76,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: "sample-app-3" CRYOSTAT_AGENT_WEBSERVER_PORT: "8910" CRYOSTAT_AGENT_CALLBACK: "http://sample-app-3:8912/" - # CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" + CRYOSTAT_AGENT_BASEURI: "http://cryostat:${CRYOSTAT_HTTP_PORT}/" CRYOSTAT_AGENT_TRUST_ALL: "true" CRYOSTAT_AGENT_AUTHORIZATION: "Basic dXNlcjpwYXNz" ports: @@ -104,7 +104,7 @@ services: CRYOSTAT_AGENT_WEBSERVER_HOST: quarkus-test-agent CRYOSTAT_AGENT_WEBSERVER_PORT: 9977 CRYOSTAT_AGENT_CALLBACK: http://quarkus-test-agent:9977/ - # CRYOSTAT_AGENT_BASEURI: http://cryostat:${CRYOSTAT_HTTP_PORT}/ + CRYOSTAT_AGENT_BASEURI: http://cryostat:${CRYOSTAT_HTTP_PORT}/ CRYOSTAT_AGENT_BASEURI_RANGE: public CRYOSTAT_AGENT_SSL_TRUST_ALL: "true" CRYOSTAT_AGENT_SSL_VERIFY_HOSTNAME: "false" diff --git a/graphql-archive-test.bash b/graphql-archive-test.bash deleted file mode 100755 index 585479c6c..000000000 --- a/graphql-archive-test.bash +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash - -set -e - -doQuery() { - http --follow -v --auth=user:pass :8080/api/v3/graphql query="$1" -} - -queryArchives() { - doQuery ' - query queryArchives { - targetNodes { - name - target { - connectUrl - recordings { - archived { - aggregate { - count - size - } - data { - name - } - } - } - } - } - } - ' -} - -createRecording() { - doQuery ' - query createRecording { - environmentNodes(filter: { name: "Podman" }) { - name - descendantTargets { - name - target { - doStartRecording(recording: { - name: "test", - template: "Profiling", - templateType: "TARGET" - }) { - name - } - } - } - } - } - ' -} - -listRecordings() { - doQuery ' - query listRecordings { - environmentNodes(filter: { name: "Podman" }) { - name - descendantTargets { - name - target { - recordings { - active { - aggregate { - count - } - data { - name - } - } - } - } - } - } - } - ' -} - -doArchive() { - doQuery ' - query doArchive { - environmentNodes(filter: { name: "Podman" }) { - name - descendantTargets { - name - target { - recordings { - active(filter: { name: "test" }) { - data { - name - doArchive { - name - } - } - } - } - } - } - } - } - ' -} - -deleteRecordings() { - doQuery ' - query deleteRecordings { - environmentNodes(filter: { name: "Podman" }) { - name - descendantTargets { - name - target { - recordings { - active(filter: { name: "test" }) { - data { - name - doDelete { - name - } - } - } - } - } - } - } - } - ' -} - -queryArchives -sleep 3 -createRecording -sleep 2 -listRecordings -sleep 10 -doArchive -sleep 2 -queryArchives -sleep 5 -deleteRecordings diff --git a/graphql-mutations-test.bash b/graphql-mutations-test.bash deleted file mode 100755 index 97cb66b18..000000000 --- a/graphql-mutations-test.bash +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash - -set -e - -doQuery() { - http --follow -v --auth=user:pass :8080/api/v3/graphql query="$1" -} - -queryArchives() { - doQuery ' - query queryArchives { - targetNodes { - name - target { - connectUrl - archivedRecordings { - aggregate { - count - size - } - data { - name - } - } - } - } - } - ' -} - -createRecording() { - doQuery ' - mutation createRecording { - createRecording(nodes: { name: "Podman" }, recording: { - name: "test", - template: "Profiling", - templateType: "TARGET" - }) { - name - } - } - ' -} - -listRecordings() { - doQuery ' - query listRecordings { - environmentNodes(filter: { name: "Podman" }) { - name - descendantTargets { - name - target { - activeRecordings { - aggregate { - count - } - data { - name - } - } - } - } - } - } - ' -} - -doArchive() { - doQuery ' - mutation archiveRecording { - archiveRecording(nodes: { name: "Podman" }, recordings: { name: "test" }) { - name - } - } - ' -} - -deleteRecordings() { - doQuery ' - mutation deleteRecording { - deleteRecording(nodes: { name: "Podman" }, recordings: { name: "test" }) { - name - } - } - ' -} - -queryArchives -sleep 3 -createRecording -sleep 2 -listRecordings -sleep 10 -doArchive -sleep 2 -queryArchives -sleep 5 -deleteRecordings From 91b01fbf41c3adbd924ae6d6b1bc913585a73d45 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 14:18:36 -0500 Subject: [PATCH 16/19] allow JSON POSTs with ID fields for particular endpoints graphql and discovery (re-)registration --- src/main/java/io/cryostat/JsonRequestFilter.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/cryostat/JsonRequestFilter.java b/src/main/java/io/cryostat/JsonRequestFilter.java index c8334fab2..94b79d5fc 100644 --- a/src/main/java/io/cryostat/JsonRequestFilter.java +++ b/src/main/java/io/cryostat/JsonRequestFilter.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -31,12 +32,16 @@ @Provider public class JsonRequestFilter implements ContainerRequestFilter { + static final Set disallowedFields = Set.of("id"); + static final Set allowedPaths = Set.of("/api/v3/graphql", "/api/v2.2/discovery"); + private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void filter(ContainerRequestContext requestContext) throws IOException { if (requestContext.getMediaType() != null - && requestContext.getMediaType().isCompatible(MediaType.APPLICATION_JSON_TYPE)) { + && requestContext.getMediaType().isCompatible(MediaType.APPLICATION_JSON_TYPE) + && !allowedPaths.contains(requestContext.getUriInfo().getPath())) { try (InputStream stream = requestContext.getEntityStream()) { JsonNode rootNode = objectMapper.readTree(stream); @@ -56,8 +61,10 @@ public void filter(ContainerRequestContext requestContext) throws IOException { } private boolean containsIdField(JsonNode node) { - if (node.has("id")) { - return true; + for (String field : disallowedFields) { + if (node.has(field)) { + return true; + } } if (node.isContainerNode()) { for (JsonNode child : node) { From 68c641a73659f71956e49e12d16c519b76edea07 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 14:43:20 -0500 Subject: [PATCH 17/19] fixup! allow JSON POSTs with ID fields for particular endpoints --- src/main/java/io/cryostat/JsonRequestFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/cryostat/JsonRequestFilter.java b/src/main/java/io/cryostat/JsonRequestFilter.java index 94b79d5fc..836d95aee 100644 --- a/src/main/java/io/cryostat/JsonRequestFilter.java +++ b/src/main/java/io/cryostat/JsonRequestFilter.java @@ -33,7 +33,8 @@ public class JsonRequestFilter implements ContainerRequestFilter { static final Set disallowedFields = Set.of("id"); - static final Set allowedPaths = Set.of("/api/v3/graphql", "/api/v2.2/discovery"); + static final Set allowedPaths = + Set.of("/api/v2.2/graphql", "/api/v3/graphql", "/api/v2.2/discovery"); private final ObjectMapper objectMapper = new ObjectMapper(); From 2411264fb470043514a46f55d793ec75b7b37478 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 15:42:27 -0500 Subject: [PATCH 18/19] fixup! fixup! allow JSON POSTs with ID fields for particular endpoints --- src/test/java/io/cryostat/JsonRequestFilterTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/io/cryostat/JsonRequestFilterTest.java b/src/test/java/io/cryostat/JsonRequestFilterTest.java index d2668cde0..0540104a0 100644 --- a/src/test/java/io/cryostat/JsonRequestFilterTest.java +++ b/src/test/java/io/cryostat/JsonRequestFilterTest.java @@ -25,9 +25,11 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; public class JsonRequestFilterTest { private JsonRequestFilter filter; @@ -76,6 +78,9 @@ private void simulateRequest(String jsonPayload) throws Exception { new ByteArrayInputStream(jsonPayload.getBytes(StandardCharsets.UTF_8)); when(requestContext.getEntityStream()).thenReturn(payloadStream); when(requestContext.getMediaType()).thenReturn(MediaType.APPLICATION_JSON_TYPE); + UriInfo uriInfo = Mockito.mock(UriInfo.class); + when(uriInfo.getPath()).thenReturn("/api/v3/rules"); + when(requestContext.getUriInfo()).thenReturn(uriInfo); filter.filter(requestContext); } } From 975a935a8fd15096ff3394c196155477996e2c51 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 8 Mar 2024 10:09:51 -0500 Subject: [PATCH 19/19] fix metadata tagging and querying for uploaded archived recordings --- .../java/io/cryostat/graphql/ArchivedRecordings.java | 7 ++++++- .../java/io/cryostat/recordings/RecordingHelper.java | 10 +++++++--- src/main/java/io/cryostat/recordings/Recordings.java | 2 ++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java index 880591ae8..afcf36f77 100644 --- a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java +++ b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java @@ -43,7 +43,12 @@ public class ArchivedRecordings { @Query("archivedRecordings") public TargetNodes.ArchivedRecordings listArchivedRecordings(ArchivedRecordingsFilter filter) { var r = new TargetNodes.ArchivedRecordings(); - r.data = recordingHelper.listArchivedRecordings(); + r.data = + recordingHelper + .listArchivedRecordings(filter == null ? null : filter.sourceTarget) + .stream() + .filter(filter) + .toList(); r.aggregate = AggregateInfo.fromArchived(r.data); return r; } diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index d37ec658d..1e60de209 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -584,13 +584,12 @@ public List listArchivedRecordingObjects(String jvmId) { } @Blocking - public List listArchivedRecordings(Target target) { - return listArchivedRecordingObjects(target.jvmId).stream() + public List listArchivedRecordings(String jvmId) { + return listArchivedRecordingObjects(jvmId).stream() .map( item -> { String path = item.key().strip(); String[] parts = path.split("/"); - String jvmId = parts[0]; String filename = parts[1]; Metadata metadata = getArchivedRecordingMetadata(jvmId, filename) @@ -606,6 +605,11 @@ public List listArchivedRecordings(Target target) { .toList(); } + @Blocking + public List listArchivedRecordings(Target target) { + return listArchivedRecordings(target.jvmId); + } + public ArchivedRecording archiveRecording( ActiveRecording activeRecording, String savename, Instant expiry) throws Exception { // AWS object key name guidelines advise characters to avoid (% so we should not pass url diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 9124e8c57..0ab21388f 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -171,6 +171,8 @@ public Map upload( if (rawLabels != null) { rawLabels.getMap().forEach((k, v) -> labels.put(k, v.toString())); } + labels.put("jvmId", "uploads"); + labels.put("connectUrl", "uploads"); Metadata metadata = new Metadata(labels); return doUpload(recording, metadata, "uploads"); }