Skip to content

docs(api): document API and classes #908

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
304 changes: 302 additions & 2 deletions schema/openapi.yaml

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ type Annotations {

type ArchivedRecording {
archivedTime: BigInteger!
"Delete an archived recording"
doDelete: ArchivedRecording!
"Update the metadata associated with an archived recording"
doPutMetadata(metadataInput: MetadataLabelsInput): ArchivedRecording!
downloadUrl: String
jvmId: String
Expand Down Expand Up @@ -160,6 +162,7 @@ type OperatingSystemMetrics {

"Query root"
type Query {
"List archived recordings"
archivedRecordings(filter: ArchivedRecordingsFilterInput): ArchivedRecordings
"Get all environment nodes in the discovery tree with optional filtering"
environmentNodes(filter: DiscoveryNodeFilterInput): [DiscoveryNode]
Expand All @@ -177,7 +180,9 @@ type RecordingAggregateInfo {
}

type Recordings {
"List and optionally filter active recordings belonging to a Target"
active(filter: ActiveRecordingsFilterInput): ActiveRecordings
"List and optionally filter archived recordings belonging to a Target"
archived(filter: ArchivedRecordingsFilterInput): ArchivedRecordings
}

Expand Down Expand Up @@ -226,10 +231,12 @@ type Suggestion {
}

type Target {
"Retrieve a list of active recordings currently available on the target"
activeRecordings(filter: ActiveRecordingsFilterInput): ActiveRecordings
agent: Boolean!
alias: String!
annotations: Annotations!
"Retrieve a list of archived recordings belonging to the target"
archivedRecordings(filter: ArchivedRecordingsFilterInput): ArchivedRecordings
connectUrl: String!
"Create a new Flight Recorder Snapshot on the specified Target"
Expand All @@ -246,6 +253,12 @@ type Target {
mbeanMetrics: MBeanMetrics
"Get the active and archived recordings belonging to this target"
recordings: Recordings
"""
Retrieve an automated analysis report from the selected target(s). If there is no report currently
available then this request will not cause a report to be generated, and instead it will return an empty
result. Report generation may be an expensive operation, especially if many reports are to be generated at
once, and should not be triggered by broad GraphQL selections.
"""
report: Report
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/cryostat/BuildInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import jakarta.inject.Inject;
import org.jboss.logging.Logger;

/** Describes the current build of the application. Contains information about the VCS commit. */
@ApplicationScoped
public class BuildInfo {

Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/cryostat/ConfigProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.cryostat;

/** Java constants corresponding to configuration keys set in application.properties. */
public class ConfigProperties {
public static final String AWS_BUCKET_NAME_ARCHIVES = "storage.buckets.archives.name";
public static final String AWS_BUCKET_NAME_EVENT_TEMPLATES =
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/Cryostat.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;

/**
* Main application entrypoint. Perform any required early initialization tasks, then kick off the
* webserver.
*/
@QuarkusMain
public class Cryostat {

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/io/cryostat/ExceptionMappers.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.S3Exception;

/**
* Map uncaught exceptions at the endpoint level to REST responses. For example, without a
* corresponding exception mapper, an uncaught jakarta.persistence.NoResultException would result in
* the web server responding with an HTTP 500 Internal Server Error (the default behaviour for all
* uncaught exceptions). Defining a mapper for NoResultException allows the global behaviour to be
* overridden to an HTTP 404 Not Found response. Individual endpoints may still use standard
* try-catch handling to deal with NoResultExceptions in other ways as needed.
*/
public class ExceptionMappers {

@Inject Logger logger;
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/io/cryostat/Health.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.jboss.logging.Logger;

/** Status and configuration verification for the application. */
@Path("")
class Health {

Expand Down Expand Up @@ -70,6 +72,15 @@ class Health {
@Blocking
@Path("/health")
@PermitAll
@Operation(
summary = "Check the overall status of the application",
description =
"""
Returns a map indicating whether various external components (ex.
jfr-datasource, grafana-dashboard) are configured and whether those
components can be reached by the Cryostat application. Also includes
application semantic version and build information.
""")
public ApplicationHealth health() {
CompletableFuture<Boolean> datasourceAvailable = new CompletableFuture<>();
CompletableFuture<Boolean> dashboardAvailable = new CompletableFuture<>();
Expand Down Expand Up @@ -113,12 +124,31 @@ public ApplicationHealth health() {
@Blocking
@Path("/health/liveness")
@PermitAll
@Operation(
summary = "Check if the application is able to accept and respond to requests.",
description =
"""
Performs a no-op on a worker thread. This is a simply check to determine if
the application has available threads to service requests. HTTP 204 No Content
is the only expected response. If the application is not live and no worker
threads are available, then the client will never receive a response.
""")
public void liveness() {}

@GET
@Path("/api/v4/grafana_dashboard_url")
@PermitAll
@Produces({MediaType.APPLICATION_JSON})
@Operation(
summary =
"Return the URL which users can visit to access the associated Grafana"
+ " dashboard instance.",
description =
"""
Returns the URL for the associated Grafana dashboard instance. If there is an internally-accessible
(for Cryostat) URL and an externally-accessible URL (for users) URL, the externally-accessible URL
is preferred. If neither are configured then the response is an HTTP 400 Bad Request.
""")
public DashboardUrl grafanaDashboardUrl() {
String url =
dashboardExternalURL.orElseGet(
Expand All @@ -130,6 +160,14 @@ public DashboardUrl grafanaDashboardUrl() {
@Path("/api/v4/grafana_datasource_url")
@PermitAll
@Produces({MediaType.APPLICATION_JSON})
@Operation(
summary = "Return the URL to the associated jfr-datasource instance.",
description =
"""
Returns the URL for the jfr-datasource instance which Cryostat is configured to use. This datasource
accepts JFR file uploads from Cryostat and allows the Grafana dashboard to perform queries on the
data within the recording file.
""")
public DatasourceUrl grafanaDatasourceUrl() {
String url = datasourceURL.orElseThrow(() -> new BadRequestException());
return new DatasourceUrl(url);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/cryostat/JsonRequestFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import jakarta.ws.rs.ext.Provider;

@Provider
/** Filter incoming request bodies or outgoing response bodies to scrub particular content. */
public class JsonRequestFilter implements ContainerRequestFilter {

static final Set<String> disallowedFields = Set.of("id");
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/ProgressInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

import org.apache.commons.io.input.ProxyInputStream;

/**
* An InputStream which informs a provided {@link java.util.function.Consumer} about the number of
* bytes read each time a chunk is read from this stream.
*/
public class ProgressInputStream extends ProxyInputStream {

private final Consumer<Integer> onUpdate;
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/StorageBuckets.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;

/**
* Utility for interacting with S3 object storage buckets. Use to ensure that the S3 object storage
* meets the application requirements, ex. that the expected buckets exist.
*/
@ApplicationScoped
public class StorageBuckets {

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/io/cryostat/credentials/Credential.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
import org.hibernate.annotations.ColumnTransformer;
import org.projectnessie.cel.tools.ScriptException;

/**
* Stored Credentials are used for communicating with secured remote targets. Target JMX servers may
* (should) be configured to require authentication from clients like Cryostat. Cryostat needs to be
* able to establish connections to these targets and pass their authentication checks, and needs to
* be able to do so without prompting a user for credentials every time, therefore the credentials
* for remote targets are stored in this encrypted keyring. Each Credential instance has a single
* username and password, as well as a {@link io.cryostat.expressions.MatchExpression} which should
* evaluate to match one or more target applications which Cryostat has discovered or will discover.
* The entire database table containing these credentials is encrypted using the Postgres 'pgcrypto'
* module and pgp symmetric encryption/decryption. The encryption key is set by configuration on the
* database deployment. Whenever Cryostat attempts to open a network connection to a target (see
* {@link io.cryostat.targets.TargetConnectionManager}) it will first check for any Credentials that
* match the target, then use the first matching Credential (see
* https://github.com/cryostatio/cryostat/issues/376)
*/
@Entity
@EntityListeners(Credential.Listener.class)
public class Credential extends PanacheEntity {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/io/cryostat/credentials/Credentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.UriInfo;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;
Expand Down Expand Up @@ -117,6 +118,10 @@ private void createFromFile(java.nio.file.Path path) {
@Blocking
@RolesAllowed("read")
@Path("/test/{targetId}")
@Operation(
summary =
"Test if the supplied username/password are valid credentials for the specified"
+ " target.")
public Uni<CredentialTestResult> checkCredentialForTarget(
@RestPath long targetId, @RestForm String username, @RestForm String password)
throws URISyntaxException {
Expand Down Expand Up @@ -155,6 +160,14 @@ public Uni<CredentialTestResult> checkCredentialForTarget(
@Blocking
@GET
@RolesAllowed("read")
@Operation(
summary = "List information about all of the available Stored Credentials.",
description =
"""
Returns a list of match results. A match result includes the Stored Credential's ID,
its Match Expression, and a list of currently discovered Targets which match that expression
and are therefore candidates for Cryostat to select this Credential.
""")
public List<CredentialMatchResult> list() {
return Credential.<Credential>listAll().stream()
.map(
Expand All @@ -174,6 +187,14 @@ public List<CredentialMatchResult> list() {
@GET
@RolesAllowed("read")
@Path("/{id}")
@Operation(
summary = "Get information about a Stored Credential",
description =
"""
Get match result information about a specific Stored Credential. A match result includes the Stored
Credential's ID, its Match Expression, and a list of currently discovered Targets which match that
expression and are therefore candidates for Cryostat to select this Credential.
""")
public CredentialMatchResult get(@RestPath long id) {
try {
Credential credential = Credential.find("id", id).singleResult();
Expand All @@ -187,6 +208,15 @@ public CredentialMatchResult get(@RestPath long id) {
@Transactional
@POST
@RolesAllowed("write")
@Operation(
summary = "Define a new Stored Credential",
description =
"""
Define a new Stored Credential. Requires a match expression which defines which targets require
this credential, and the username and password to use to pass authentication checks on those
targets. Stored Credentials are stored in an encrypted keyring using symmetric encryption and an
encryption key configured on the Cryostat database.
""")
public RestResponse<Credential> create(
@Context UriInfo uriInfo,
@RestForm String matchExpression,
Expand All @@ -209,6 +239,7 @@ public RestResponse<Credential> create(
@DELETE
@RolesAllowed("write")
@Path("/{id}")
@Operation(summary = "Delete a Stored Credential")
public void delete(@RestPath long id) {
Credential.find("id", id).singleResult().delete();
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/credentials/CredentialsFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
import org.jboss.logging.Logger;
import org.projectnessie.cel.tools.ScriptException;

/**
* Utility for mapping and caching {@link io.cryostat.targets.Target} to associated {@link
* io.cryostat.credentials.Credential}.
*/
@ApplicationScoped
public class CredentialsFinder {

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/io/cryostat/diagnostic/Diagnostics.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.jboss.resteasy.reactive.RestPath;

@Path("/api/beta/diagnostics/targets/{targetId}")
Expand All @@ -34,6 +35,13 @@ public class Diagnostics {
@RolesAllowed("write")
@Blocking
@POST
@Operation(
summary = "Initiate a garbage collection on the specified target",
description =
"""
Request the remote target to perform a garbage collection. The target JVM is free to ignore this
request. This is generally equivalent to a System.gc() call made within the target JVM.
""")
public void gc(@RestPath long targetId) {
targetConnectionManager.executeConnectedTask(
Target.getTargetById(targetId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

/** Discovery mechanism for Docker and Podman container engines. */
@ApplicationScoped
class PodmanDiscovery extends ContainerDiscovery {
@ConfigProperty(name = "cryostat.discovery.podman.enabled")
Expand Down
Loading
Loading