Skip to content

Commit

Permalink
feat(eventtemplates): custom event templates in S3
Browse files Browse the repository at this point in the history
  • Loading branch information
mwangggg committed Oct 24, 2023
1 parent 6a752e3 commit 7e59c79
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 3 deletions.
177 changes: 176 additions & 1 deletion src/main/java/io/cryostat/events/EventTemplates.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,53 @@
*/
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 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.core.templates.Template;
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 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 {
Expand All @@ -45,7 +75,37 @@ public class EventTemplates {
"Cryostat",
TemplateType.TARGET);

@Inject Vertx vertx;
@Inject TargetConnectionManager connectionManager;
@Inject S3Client storage;
@Inject Logger logger;

@ConfigProperty(name = "storage.buckets.event-templates.name")
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")
Expand All @@ -58,6 +118,32 @@ public Response listTemplatesV1(@RestPath URI connectUrl) throws Exception {
.build();
}

@POST
@Path("/api/v1/templates")
@RolesAllowed("write")
public Uni<Void> postTemplatesV1(@RestForm("template") FileUpload body) throws Exception {
CompletableFuture<Void> 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")
Expand Down Expand Up @@ -106,4 +192,93 @@ public String getTargetTemplate(
.orElseThrow(NotFoundException::new)
.toString());
}

// static class S3TemplateService implements TemplateService {
// S3Client s3;

// @Override
// public Optional<IConstrainedMap<EventOptionID>> getEvents(String arg0, TemplateType arg1)
// throws FlightRecorderException {
// return Optional.empty();
// }

// @Override
// public List<Template> getTemplates() throws FlightRecorderException {
// var objects = s3.listObjectsV2();
// var templates = convertObjects(objects);
// return templates;
// }

// @Override
// public Optional<Document> getXml(String arg0, TemplateType arg1)
// throws FlightRecorderException {
// return Optional.empty();
// }
// }

@Blocking
public Template addTemplate(String templateText)
throws InvalidXmlException, InvalidEventTemplateException, IOException {
try {
XMLModel model = EventConfiguration.createModel(templateText);
model.checkErrors();

for (XMLValidationResult result : model.getResults()) {
if (result.isError()) {
// throw new InvalidEventTemplateException(result.getText());
throw new IllegalArgumentException(result.getText());
}
}

XMLTagInstance configuration = model.getRoot();
XMLAttributeInstance labelAttr = null;
for (XMLAttributeInstance attr : configuration.getAttributeInstances()) {
if (attr.getAttribute().getName().equals("label")) {
labelAttr = attr;
break;
}
}

if (labelAttr == null) {
// throw new InvalidEventTemplateException(
// "Template has no configuration label attribute");
throw new IllegalArgumentException("Template has no configuration label attribute");
}

String templateName = labelAttr.getExplicitValue();
templateName = templateName.replaceAll("[\\W]+", "_");

XMLTagInstance root = model.getRoot();
root.setValue(JFCGrammar.ATTRIBUTE_LABEL_MANDATORY, templateName);

String key = templateName;
storage.putObject(
PutObjectRequest.builder()
.bucket(eventTemplatesBucket)
.key(key)
.contentType(ContentType.APPLICATION_XML.getMimeType())
.build(),
RequestBody.fromString(model.toString()));

return new Template(
templateName,
getAttributeValue(root, "description"),
getAttributeValue(root, "provider"),
TemplateType.CUSTOM);
} catch (IOException ioe) {
// throw new InvalidXmlException("Unable to parse XML stream", ioe);
throw new IllegalArgumentException("Unable to parse XML stream", ioe);
} catch (ParseException | IllegalArgumentException e) {
// throw new InvalidEventTemplateException("Invalid XML", e);
throw new IllegalArgumentException("Invalid XML", e);
}
}

protected String getAttributeValue(XMLTagInstance node, String valueKey) {
return node.getAttributeInstances().stream()
.filter(i -> Objects.equals(valueKey, i.getAttribute().getName()))
.map(i -> i.getValue())
.findFirst()
.get();
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ quarkus.datasource.devservices.command=postgres -c encrypt.key=REPLACEME
# !!!

storage.buckets.archives.name=archivedrecordings
storage.buckets.event-templates.name=eventtemplates
storage.buckets.archives.expiration-label=expiration
quarkus.s3.devservices.enabled=true
quarkus.s3.devservices.buckets=archivedrecordings
Expand Down
8 changes: 6 additions & 2 deletions src/test/java/itest/TemplatePostDeleteIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@QuarkusIntegrationTest
@Disabled("TODO")
// @Disabled("TODO")
public class TemplatePostDeleteIT extends StandardSelfTest {
static final String INVALID_TEMPLATE_FILE_NAME = "invalidTemplate.xml";
static final String SANITIZE_TEMPLATE_FILE_NAME = "TemplateToSanitize.jfc";
Expand All @@ -57,6 +56,7 @@ public void shouldThrowIfTemplateUploadNameInvalid() throws Exception {

webClient
.post(REQ_URL)
.basicAuthentication("user", "pass")
.sendMultipartForm(
form,
ar -> {
Expand Down Expand Up @@ -86,6 +86,7 @@ public void shouldThrowWhenPostingInvalidTemplate() throws Exception {

webClient
.post(REQ_URL)
.basicAuthentication("user", "pass")
.sendMultipartForm(
form,
ar -> {
Expand All @@ -105,6 +106,7 @@ public void testDeleteRecordingThrowsOnNonExistentTemplate() throws Exception {

webClient
.delete(String.format("%s/%s", REQ_URL, INVALID_TEMPLATE_FILE_NAME))
.basicAuthentication("user", "pass")
.send(
ar -> {
assertRequestStatus(ar, response);
Expand Down Expand Up @@ -142,6 +144,7 @@ public void testPostedTemplateIsSanitizedAndCanBeDeleted() throws Exception {
CompletableFuture<JsonArray> getResponse = new CompletableFuture<>();
webClient
.get("/api/v1/targets/localhost:0/templates")
.basicAuthentication("user", "pass")
.send(
ar -> {
assertRequestStatus(ar, getResponse);
Expand All @@ -160,6 +163,7 @@ public void testPostedTemplateIsSanitizedAndCanBeDeleted() throws Exception {
CompletableFuture<Integer> deleteResponse = new CompletableFuture<>();
webClient
.delete(REQ_URL + "/Template_To_Sanitize")
.basicAuthentication("user", "pass")
.send(
ar -> {
assertRequestStatus(ar, deleteResponse);
Expand Down

0 comments on commit 7e59c79

Please sign in to comment.