Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(topology): correct GraphQL schema for Topology actions, implement missing mutations #315

Merged
merged 19 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a34db8d
fix(topology): correct GraphQL schema for Topology actions, implement…
andrewazores Mar 4, 2024
0f7a38d
refactor
andrewazores Mar 5, 2024
2018274
minor cleanups
andrewazores Mar 5, 2024
d9cc8d5
client may not have supplied labels
andrewazores Mar 6, 2024
701e0c3
env vars for controlling discovery mechanisms
andrewazores Mar 6, 2024
768f9ce
refactor to return ArchivedRecording instance rather than only filena…
andrewazores Mar 7, 2024
045672c
add API endpoint for per-JVM ID archived recordings
andrewazores Mar 7, 2024
0c2c027
add proper top-level side-effect-ful mutations to schema
andrewazores Mar 7, 2024
aaa7a5d
add Target activeRecordings and archivedRecordings accessors to bypas…
andrewazores Mar 7, 2024
89772fa
refactor, reuse existing filter type
andrewazores Mar 7, 2024
5b5e40e
add filter by list of ids, remove filter by singular id and singular …
andrewazores Mar 7, 2024
fc69d92
restore filter by singular id and name
andrewazores Mar 7, 2024
f0c4971
refactor, remove duplicate filter type
andrewazores Mar 7, 2024
2fac41f
refactor
andrewazores Mar 7, 2024
14e6e49
revert accidentally added testing changes
andrewazores Mar 7, 2024
91b01fb
allow JSON POSTs with ID fields for particular endpoints
andrewazores Mar 7, 2024
68c641a
fixup! allow JSON POSTs with ID fields for particular endpoints
andrewazores Mar 7, 2024
2411264
fixup! fixup! allow JSON POSTs with ID fields for particular endpoints
andrewazores Mar 7, 2024
975a935
fix metadata tagging and querying for uploaded archived recordings
andrewazores Mar 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading