diff --git a/src/main/java/io/cryostat/ConfigProperties.java b/src/main/java/io/cryostat/ConfigProperties.java index 952461e1f..19b19c648 100644 --- a/src/main/java/io/cryostat/ConfigProperties.java +++ b/src/main/java/io/cryostat/ConfigProperties.java @@ -17,6 +17,7 @@ public class ConfigProperties { public static final String AWS_BUCKET_NAME_ARCHIVES = "storage.buckets.archives.name"; + public static final String AWS_EVENT_TEMPLATE_NAME = "storage.buckets.event-templates.name"; public static final String AWS_OBJECT_EXPIRATION_LABELS = "storage.buckets.archives.expiration-label"; diff --git a/src/main/java/io/cryostat/events/EventTemplates.java b/src/main/java/io/cryostat/events/EventTemplates.java index 61d80cee0..e9cbec391 100644 --- a/src/main/java/io/cryostat/events/EventTemplates.java +++ b/src/main/java/io/cryostat/events/EventTemplates.java @@ -15,23 +15,63 @@ */ package io.cryostat.events; +import java.io.IOException; import java.net.URI; +import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.Optional; +import org.jsoup.nodes.Document; +import org.openjdk.jmc.common.unit.IConstrainedMap; +import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID; +import org.openjdk.jmc.flightrecorder.controlpanel.ui.configuration.model.xml.JFCGrammar; +import org.openjdk.jmc.flightrecorder.controlpanel.ui.configuration.model.xml.XMLAttributeInstance; +import org.openjdk.jmc.flightrecorder.controlpanel.ui.configuration.model.xml.XMLModel; +import org.openjdk.jmc.flightrecorder.controlpanel.ui.configuration.model.xml.XMLTagInstance; +import org.openjdk.jmc.flightrecorder.controlpanel.ui.configuration.model.xml.XMLValidationResult; +import org.openjdk.jmc.flightrecorder.controlpanel.ui.model.EventConfiguration; + +import io.cryostat.core.templates.MutableTemplateService.InvalidEventTemplateException; +import io.cryostat.core.templates.MutableTemplateService.InvalidXmlException; +import io.cryostat.ConfigProperties; +import io.cryostat.core.FlightRecorderException; import io.cryostat.core.templates.Template; +import io.cryostat.core.templates.TemplateService; import io.cryostat.core.templates.TemplateType; import io.cryostat.targets.Target; import io.cryostat.targets.TargetConnectionManager; - +import io.cryostat.util.HttpStatusCodeIdentifier; +import io.quarkus.runtime.StartupEvent; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; +import org.apache.http.entity.ContentType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.RestForm; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; + import org.jboss.resteasy.reactive.RestPath; import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.multipart.FileUpload; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; @Path("") public class EventTemplates { @@ -45,7 +85,37 @@ public class EventTemplates { "Cryostat", TemplateType.TARGET); + @Inject Vertx vertx; @Inject TargetConnectionManager connectionManager; + @Inject S3Client storage; + @Inject Logger logger; + + @ConfigProperty(name = ConfigProperties.AWS_BUCKET_NAME_ARCHIVES) + static String eventTemplatesBucket; + + void onStart(@Observes StartupEvent evt) { + boolean exists = false; + try { + exists = + HttpStatusCodeIdentifier.isSuccessCode( + storage.headBucket( + HeadBucketRequest.builder() + .bucket(eventTemplatesBucket) + .build()) + .sdkHttpResponse() + .statusCode()); + } catch (Exception e) { + logger.info(e); + } + if (!exists) { + try { + storage.createBucket( + CreateBucketRequest.builder().bucket(eventTemplatesBucket).build()); + } catch (Exception e) { + logger.error(e); + } + } + } @GET @Path("/api/v1/targets/{connectUrl}/templates") @@ -58,6 +128,32 @@ public Response listTemplatesV1(@RestPath URI connectUrl) throws Exception { .build(); } + @POST + @Path("/api/v1/templates") + @RolesAllowed("write") + public Uni postTemplatesV1(@RestForm("template") FileUpload body) throws Exception { + CompletableFuture cf = new CompletableFuture<>(); + var path = body.filePath(); + vertx.fileSystem() + .readFile(path.toString()) + .onComplete( + ar -> { + try { + addTemplate(ar.result().toString()); + cf.complete(null); + } catch (Exception e) { + logger.error(e); + cf.completeExceptionally(e); + } + }) + .onFailure( + ar -> { + logger.error(ar.getCause()); + cf.completeExceptionally(ar.getCause()); + }); + return Uni.createFrom().future(cf); + } + @GET @Path("/api/v1/targets/{connectUrl}/templates/{templateName}/type/{templateType}") @RolesAllowed("read") @@ -106,4 +202,94 @@ public String getTargetTemplate( .orElseThrow(NotFoundException::new) .toString()); } + + static class S3TemplateService implements TemplateService { + S3Client s3; + + @Override + public Optional> getEvents(String templateName, TemplateType templateType) + throws FlightRecorderException { + return Optional.empty(); + } + + @Override + public List