Skip to content

Commit

Permalink
fix(topology): correct GraphQL schema for Topology actions, implement…
Browse files Browse the repository at this point in the history
… missing mutations (#315)
  • Loading branch information
andrewazores committed Mar 20, 2024
1 parent 868ae51 commit 1a90348
Show file tree
Hide file tree
Showing 12 changed files with 465 additions and 160 deletions.
6 changes: 3 additions & 3 deletions compose/cryostat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/io/cryostat/JsonRequestFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,12 +32,17 @@
@Provider
public class JsonRequestFilter implements ContainerRequestFilter {

static final Set<String> disallowedFields = Set.of("id");
static final Set<String> allowedPaths =
Set.of("/api/v2.2/graphql", "/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);

Expand All @@ -56,8 +62,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) {
Expand Down
249 changes: 230 additions & 19 deletions src/main/java/io/cryostat/graphql/ActiveRecordings.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
*/
package io.cryostat.graphql;

import java.time.Duration;
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;
Expand All @@ -25,49 +27,226 @@

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;
import io.cryostat.recordings.ActiveRecording;
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;
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<ActiveRecording> createRecording(
@NonNull DiscoveryNodeFilter nodes, @NonNull RecordingSettings recording) {
return DiscoveryNode.<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<ArchivedRecording> archiveRecording(
@NonNull DiscoveryNodeFilter nodes, @Nullable ActiveRecordingsFilter recordings) {
return DiscoveryNode.<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<ActiveRecording> stopRecording(
@NonNull DiscoveryNodeFilter nodes, @Nullable ActiveRecordingsFilter recordings) {
return DiscoveryNode.<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<ActiveRecording> deleteRecording(
@NonNull DiscoveryNodeFilter nodes, @Nullable ActiveRecordingsFilter recordings) {
var activeRecordings =
DiscoveryNode.<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<ActiveRecording> createSnapshot(@NonNull DiscoveryNodeFilter nodes) {
return DiscoveryNode.<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
@Description("Start a new Flight Recording on the specified Target")
public Uni<ActiveRecording> doStartRecording(
@Source Target target, @NonNull RecordingSettings settings)
@Source Target target, @NonNull RecordingSettings recording)
throws QuantityConversionException {
var fTarget = Target.<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(),
Optional.ofNullable(recording.metadata).map(s -> s.labels).orElse(Map.of()));
}

@Blocking
Expand All @@ -78,6 +257,30 @@ public Uni<ActiveRecording> doSnapshot(@Source Target target) {
return recordingHelper.createSnapshot(fTarget);
}

@Blocking
@Transactional
@Description("Stop the specified Flight Recording")
public Uni<ActiveRecording> doStop(@Source ActiveRecording recording) {
var ar = ActiveRecording.<ActiveRecording>findById(recording.id);
return recordingHelper.stopRecording(ar);
}

@Blocking
@Transactional
@Description("Delete the specified Flight Recording")
public Uni<ActiveRecording> doDelete(@Source ActiveRecording recording) {
var ar = ActiveRecording.<ActiveRecording>findById(recording.id);
return recordingHelper.deleteRecording(ar);
}

@Blocking
@Transactional
@Description("Archive the specified Flight Recording")
public Uni<ArchivedRecording> doArchive(@Source ActiveRecording recording) throws Exception {
var ar = ActiveRecording.<ActiveRecording>findById(recording.id);
return Uni.createFrom().item(recordingHelper.archiveRecording(ar, null, null));
}

public TargetNodes.ActiveRecordings active(
@Source Recordings recordings, ActiveRecordingsFilter filter) {
var out = new TargetNodes.ActiveRecordings();
Expand All @@ -98,15 +301,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(
Expand All @@ -119,6 +322,11 @@ public RecordingOptions asOptions() {
}
}

@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public static class RecordingMetadata {
public @Nullable Map<String, String> labels;
}

@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public static class ActiveRecordingsFilter implements Predicate<ActiveRecording> {
public @Nullable String name;
Expand Down Expand Up @@ -161,16 +369,19 @@ public boolean test(ActiveRecording r) {
Predicate<ActiveRecording> 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);
}
}
Expand Down
Loading

0 comments on commit 1a90348

Please sign in to comment.