From 4c507213515aeb042c107fd7f242c5b9d61e3980 Mon Sep 17 00:00:00 2001 From: Atif Ali <56743004+aali309@users.noreply.github.com> Date: Fri, 12 Apr 2024 23:34:57 -0400 Subject: [PATCH] feat(graphQL): add missing labels/archives subqueries/mutations (#319) --- .../io/cryostat/graphql/ActiveRecordings.java | 34 +++++++ .../cryostat/graphql/ArchivedRecordings.java | 28 ++++++ .../cryostat/recordings/ActiveRecording.java | 11 ++- .../cryostat/recordings/RecordingHelper.java | 88 +++++++++++++++++++ .../io/cryostat/recordings/Recordings.java | 10 +++ src/main/java/io/cryostat/targets/Target.java | 12 ++- 6 files changed, 178 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java index efd1bc997..a78c3c490 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.time.Duration; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -322,6 +323,39 @@ public RecordingOptions asOptions() { } } + @Blocking + @Transactional + @Description("Updates the metadata labels for an existing Flight Recording.") + public Uni doPutMetadata( + @Source ActiveRecording recording, MetadataLabels metadataInput) { + return Uni.createFrom() + .item( + () -> { + return recordingHelper.updateRecordingMetadata( + recording.id, metadataInput.getLabels()); + }); + } + + @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") + public static class MetadataLabels { + + private Map labels; + + public MetadataLabels() {} + + public MetadataLabels(Map labels) { + this.labels = new HashMap<>(labels); + } + + public Map getLabels() { + return new HashMap<>(labels); + } + + public void setLabels(Map labels) { + this.labels = new HashMap<>(labels); + } + } + @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public static class RecordingMetadata { public @Nullable Map labels; diff --git a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java index afcf36f77..e62a40a5e 100644 --- a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java +++ b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java @@ -20,17 +20,20 @@ import java.util.Objects; import java.util.function.Predicate; +import io.cryostat.graphql.ActiveRecordings.MetadataLabels; import io.cryostat.graphql.TargetNodes.AggregateInfo; import io.cryostat.graphql.TargetNodes.Recordings; import io.cryostat.graphql.matchers.LabelSelectorMatcher; import io.cryostat.recordings.RecordingHelper; import io.cryostat.recordings.Recordings.ArchivedRecording; +import io.cryostat.recordings.Recordings.Metadata; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.smallrye.common.annotation.Blocking; import io.smallrye.graphql.api.Nullable; import jakarta.inject.Inject; import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.NonNull; import org.eclipse.microprofile.graphql.Query; import org.eclipse.microprofile.graphql.Source; @@ -69,6 +72,31 @@ public TargetNodes.ArchivedRecordings archived( return out; } + @NonNull + public ArchivedRecording doDelete(@Source ArchivedRecording recording) { + recordingHelper.deleteArchivedRecording(recording.jvmId(), recording.name()); + return recording; + } + + @NonNull + public ArchivedRecording doPutMetadata( + @Source ArchivedRecording recording, MetadataLabels metadataInput) { + recordingHelper.updateArchivedRecordingMetadata( + recording.jvmId(), recording.name(), metadataInput.getLabels()); + + String downloadUrl = recordingHelper.downloadUrl(recording.jvmId(), recording.name()); + String reportUrl = recordingHelper.reportUrl(recording.jvmId(), recording.name()); + + return new ArchivedRecording( + recording.jvmId(), + recording.name(), + downloadUrl, + reportUrl, + new Metadata(metadataInput.getLabels()), + recording.size(), + recording.archivedTime()); + } + @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public static class ArchivedRecordingsFilter implements Predicate { public @Nullable String name; diff --git a/src/main/java/io/cryostat/recordings/ActiveRecording.java b/src/main/java/io/cryostat/recordings/ActiveRecording.java index d52aaef36..9ff46c66e 100644 --- a/src/main/java/io/cryostat/recordings/ActiveRecording.java +++ b/src/main/java/io/cryostat/recordings/ActiveRecording.java @@ -141,6 +141,10 @@ public static ActiveRecording getByName(String name) { return find("name", name).singleResult(); } + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + @Transactional public static boolean deleteFromTarget(Target target, String recordingName) { Optional recording = @@ -282,16 +286,19 @@ public record ActiveRecordingEvent( Objects.requireNonNull(payload); } - public record Payload(String target, LinkedRecordingDescriptor recording) { + public record Payload( + String target, LinkedRecordingDescriptor recording, String jvmId) { public Payload { Objects.requireNonNull(target); Objects.requireNonNull(recording); + Objects.requireNonNull(jvmId); } public static Payload of(RecordingHelper helper, ActiveRecording recording) { return new Payload( recording.target.connectUrl.toString(), - helper.toExternalForm(recording)); + helper.toExternalForm(recording), + recording.target.jvmId); } } } diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index a7f04568e..2cb63f8d1 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -88,6 +88,7 @@ import jakarta.inject.Named; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.ServerErrorException; import jdk.jfr.RecordingState; import org.apache.commons.codec.binary.Base64; @@ -108,8 +109,10 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.PutObjectTaggingRequest; import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.services.s3.model.Tag; import software.amazon.awssdk.services.s3.model.Tagging; @@ -135,6 +138,7 @@ public class RecordingHelper { @Inject EventOptionsBuilder.Factory eventOptionsBuilderFactory; @Inject TargetTemplateService.Factory targetTemplateServiceFactory; @Inject S3TemplateService customTemplateService; + @Inject RecordingHelper recordingHelper; @Inject @Named(Producers.BASE64_URL) @@ -548,6 +552,7 @@ public List listArchivedRecordings() { getArchivedRecordingMetadata(jvmId, filename) .orElseGet(Metadata::empty); return new ArchivedRecording( + jvmId, filename, downloadUrl(jvmId, filename), reportUrl(jvmId, filename), @@ -584,6 +589,7 @@ public List listArchivedRecordings(String jvmId) { getArchivedRecordingMetadata(jvmId, filename) .orElseGet(Metadata::empty); return new ArchivedRecording( + jvmId, filename, downloadUrl(jvmId, filename), reportUrl(jvmId, filename), @@ -721,6 +727,7 @@ public ArchivedRecording archiveRecording( new Notification(event.category().category(), event.payload())); } return new ArchivedRecording( + activeRecording.target.jvmId, filename, downloadUrl(activeRecording.target.jvmId, filename), reportUrl(activeRecording.target.jvmId, filename), @@ -853,6 +860,7 @@ public void deleteArchivedRecording(String jvmId, String filename) { ArchivedRecordingEvent.Payload.of( target.map(t -> t.connectUrl).orElse(null), new ArchivedRecording( + jvmId, filename, downloadUrl(jvmId, filename), reportUrl(jvmId, filename), @@ -929,6 +937,86 @@ private Metadata taggingToMetadata(List tagSet) { return new Metadata(labels, expiry); } + public ActiveRecording updateRecordingMetadata( + long recordingId, Map newLabels) { + ActiveRecording recording = ActiveRecording.findById(recordingId); + + if (recording == null) { + throw new NotFoundException("Recording not found for ID: " + recordingId); + } + + if (!recording.metadata.labels().equals(newLabels)) { + Metadata updatedMetadata = new Metadata(newLabels); + recording.setMetadata(updatedMetadata); + recording.persist(); + + notify( + new ActiveRecordingEvent( + Recordings.RecordingEventCategory.METADATA_UPDATED, + ActiveRecordingEvent.Payload.of(recordingHelper, recording))); + } + return recording; + } + + private void notify(ActiveRecordingEvent event) { + bus.publish( + MessagingServer.class.getName(), + new Notification(event.category().category(), event.payload())); + } + + public ArchivedRecording updateArchivedRecordingMetadata( + String jvmId, String filename, Map updatedLabels) { + String key = archivedRecordingKey(jvmId, filename); + Optional existingMetadataOpt = getArchivedRecordingMetadata(key); + + if (existingMetadataOpt.isEmpty()) { + throw new NotFoundException( + "Could not find metadata for archived recording with key: " + key); + } + + Metadata updatedMetadata = new Metadata(updatedLabels); + + Tagging tagging = createMetadataTagging(updatedMetadata); + storage.putObjectTagging( + PutObjectTaggingRequest.builder() + .bucket(archiveBucket) + .key(key) + .tagging(tagging) + .build()); + + var response = + storage.headObject( + HeadObjectRequest.builder().bucket(archiveBucket).key(key).build()); + long size = response.contentLength(); + Instant lastModified = response.lastModified(); + + ArchivedRecording updatedRecording = + new ArchivedRecording( + jvmId, + filename, + downloadUrl(jvmId, filename), + reportUrl(jvmId, filename), + updatedMetadata, + size, + lastModified.getEpochSecond()); + + notifyArchiveMetadataUpdate(updatedRecording); + return updatedRecording; + } + + private void notifyArchiveMetadataUpdate(ArchivedRecording updatedRecording) { + + var event = + new ArchivedRecordingEvent( + Recordings.RecordingEventCategory.METADATA_UPDATED, + new ArchivedRecordingEvent.Payload( + updatedRecording.downloadUrl(), updatedRecording)); + bus.publish(event.category().category(), event.payload().recording()); + bus.publish( + MessagingServer.class.getName(), + new Notification(event.category().category(), event.payload())); + } + public Uni uploadToJFRDatasource(long targetEntityId, long remoteId) throws Exception { Target target = Target.getTargetById(targetEntityId); Objects.requireNonNull(target, "Target from targetId not found"); diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 0ab21388f..1059f7beb 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -222,6 +222,7 @@ public void agentPush( ArchivedRecordingEvent.Payload.of( target.map(t -> t.connectUrl).orElse(null), new ArchivedRecording( + jvmId, recording.fileName(), recordingHelper.downloadUrl(jvmId, recording.fileName()), recordingHelper.reportUrl(jvmId, recording.fileName()), @@ -275,6 +276,7 @@ public List agentGet(@RestPath String jvmId) { .orElseGet(Metadata::empty); result.add( new ArchivedRecording( + jvmId, filename, recordingHelper.downloadUrl(jvmId, filename), recordingHelper.reportUrl(jvmId, filename), @@ -338,6 +340,7 @@ Map doUpload(FileUpload recording, Metadata metadata, String jvm ArchivedRecordingEvent.Payload.of( target.map(t -> t.connectUrl).orElse(null), new ArchivedRecording( + jvmId, filename, recordingHelper.downloadUrl(jvmId, filename), recordingHelper.reportUrl(jvmId, filename), @@ -395,6 +398,7 @@ public Collection listFsArchives() { connectUrl, id, new ArrayList<>())); dir.recordings.add( new ArchivedRecording( + jvmId, filename, recordingHelper.downloadUrl(jvmId, filename), recordingHelper.reportUrl(jvmId, filename), @@ -432,6 +436,7 @@ public Collection listFsArchives(@RestPath String jv connectUrl, id, new ArrayList<>())); dir.recordings.add( new ArchivedRecording( + jvmId, filename, recordingHelper.downloadUrl(jvmId, filename), recordingHelper.reportUrl(jvmId, filename), @@ -768,6 +773,7 @@ public void deleteArchivedRecording(@RestPath String jvmId, @RestPath String fil ArchivedRecordingEvent.Payload.of( URI.create(connectUrl), new ArchivedRecording( + jvmId, filename, recordingHelper.downloadUrl(jvmId, filename), recordingHelper.reportUrl(jvmId, filename), @@ -1127,6 +1133,7 @@ public record LinkedRecordingDescriptor( // TODO include jvmId and filename public record ArchivedRecording( + String jvmId, String name, String downloadUrl, String reportUrl, @@ -1134,6 +1141,7 @@ public record ArchivedRecording( long size, long archivedTime) { public ArchivedRecording { + Objects.requireNonNull(jvmId); Objects.requireNonNull(name); Objects.requireNonNull(downloadUrl); Objects.requireNonNull(reportUrl); @@ -1183,6 +1191,7 @@ public static Metadata empty() { public static final String ACTIVE_RECORDING_DELETED = "ActiveRecordingDeleted"; public static final String ACTIVE_RECORDING_SAVED = "ActiveRecordingSaved"; public static final String SNAPSHOT_RECORDING_CREATED = "SnapshotCreated"; + public static final String RECORDING_METADATA_UPDATED = "RecordingMetadataUpdated"; public enum RecordingEventCategory { ACTIVE_CREATED(ACTIVE_RECORDING_CREATED), @@ -1192,6 +1201,7 @@ public enum RecordingEventCategory { ARCHIVED_CREATED(ARCHIVED_RECORDING_CREATED), ARCHIVED_DELETED(ARCHIVED_RECORDING_DELETED), SNAPSHOT_CREATED(SNAPSHOT_RECORDING_CREATED), + METADATA_UPDATED(RECORDING_METADATA_UPDATED), ; private final String category; diff --git a/src/main/java/io/cryostat/targets/Target.java b/src/main/java/io/cryostat/targets/Target.java index 036f83981..67b33bd0b 100644 --- a/src/main/java/io/cryostat/targets/Target.java +++ b/src/main/java/io/cryostat/targets/Target.java @@ -188,10 +188,11 @@ public enum EventKind { } @SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) - public record TargetDiscovery(EventKind kind, Target serviceRef) { + public record TargetDiscovery(EventKind kind, Target serviceRef, String jvmId) { public TargetDiscovery { Objects.requireNonNull(kind); Objects.requireNonNull(serviceRef); + Objects.requireNonNull(jvmId); } } @@ -275,14 +276,19 @@ private void notify(EventKind eventKind, Target target) { MessagingServer.class.getName(), new Notification( TARGET_JVM_DISCOVERY, - new TargetDiscoveryEvent(new TargetDiscovery(eventKind, target)))); - bus.publish(TARGET_JVM_DISCOVERY, new TargetDiscovery(eventKind, target)); + new TargetDiscoveryEvent( + new TargetDiscovery(eventKind, target, target.jvmId)))); + bus.publish(TARGET_JVM_DISCOVERY, new TargetDiscovery(eventKind, target, target.jvmId)); } public record TargetDiscoveryEvent(TargetDiscovery event) { public TargetDiscoveryEvent { Objects.requireNonNull(event); } + + public String jvmId() { + return event.serviceRef().jvmId; + } } } }