diff --git a/.gitignore b/.gitignore index e893de045..5a8f57977 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ nb-configuration.xml .quinoa/ truststore/ +templates/ certs/*.p12 certs/*.pass *.jfr diff --git a/compose/cryostat.yml b/compose/cryostat.yml index 285b14734..ca4d66e3e 100644 --- a/compose/cryostat.yml +++ b/compose/cryostat.yml @@ -15,6 +15,7 @@ services: volumes: - ${XDG_RUNTIME_DIR}/podman/podman.sock:/run/user/1000/podman/podman.sock:Z - jmxtls_cfg:/truststore:U + - templates:/opt/cryostat.d/templates.d:U security_opt: - label:disable hostname: cryostat3 @@ -47,3 +48,5 @@ services: volumes: jmxtls_cfg: external: true + templates: + external: true diff --git a/smoketest.bash b/smoketest.bash index 03c22b812..5548ead1c 100755 --- a/smoketest.bash +++ b/smoketest.bash @@ -184,6 +184,8 @@ cleanup() { fi ${container_engine} rm jmxtls_cfg_helper || true ${container_engine} volume rm jmxtls_cfg || true + ${container_engine} rm templates_helper || true + ${container_engine} volume rm templates || true truncate -s 0 "${HOSTSFILE}" for i in "${PIDS[@]}"; do kill -0 "${i}" && kill "${i}" @@ -225,6 +227,15 @@ createJmxTlsCertVolume() { } createJmxTlsCertVolume +createEventTemplateVolume() { + "${container_engine}" volume create templates + "${container_engine}" container create --name templates_helper -v templates:/templates busybox + if [ -d "${DIR}/templates" ]; then + "${container_engine}" cp "${DIR}/templates" templates_helper:/templates + fi +} +createEventTemplateVolume + setupUserHosts() { # This requires https://github.com/figiel/hosts to work. See README. truncate -s 0 "${HOSTSFILE}" diff --git a/src/main/java/io/cryostat/ConfigProperties.java b/src/main/java/io/cryostat/ConfigProperties.java index 8d8cafbd7..5b5788512 100644 --- a/src/main/java/io/cryostat/ConfigProperties.java +++ b/src/main/java/io/cryostat/ConfigProperties.java @@ -54,5 +54,6 @@ public class ConfigProperties { "storage.transient-archives.enabled"; public static final String STORAGE_TRANSIENT_ARCHIVES_TTL = "storage.transient-archives.ttl"; + public static final String TEMPLATES_DIR = "templates-dir"; public static final String SSL_TRUSTSTORE_DIR = "ssl.truststore.dir"; } diff --git a/src/main/java/io/cryostat/events/S3TemplateService.java b/src/main/java/io/cryostat/events/S3TemplateService.java index 31e4838f5..2592d1aad 100644 --- a/src/main/java/io/cryostat/events/S3TemplateService.java +++ b/src/main/java/io/cryostat/events/S3TemplateService.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.ParseException; import java.util.ArrayList; import java.util.List; @@ -82,6 +84,9 @@ public class S3TemplateService implements MutableTemplateService { @ConfigProperty(name = ConfigProperties.AWS_BUCKET_NAME_EVENT_TEMPLATES) String bucket; + @ConfigProperty(name = ConfigProperties.TEMPLATES_DIR) + Path dir; + @Inject S3Client storage; @Inject StorageBuckets storageBuckets; @@ -95,6 +100,28 @@ public class S3TemplateService implements MutableTemplateService { void onStart(@Observes StartupEvent evt) { storageBuckets.createIfNecessary(bucket); + if (!checkDir()) { + return; + } + try { + Files.walk(dir) + .filter(Files::isRegularFile) + .filter(Files::isReadable) + .forEach( + p -> { + try (var is = Files.newInputStream(p)) { + logger.debugv( + "Uploading template from {0} to S3", p.toString()); + addTemplate(is); + } catch (IOException + | InvalidXmlException + | InvalidEventTemplateException e) { + logger.warn(e); + } + }); + } catch (IOException e) { + logger.warn(e); + } } @Override @@ -192,59 +219,30 @@ private InputStream getModel(String name) { return storage.getObject(req); } - private XMLModel parseXml(InputStream inputStream) throws IOException, ParseException { - try (inputStream) { - var model = EventConfiguration.createModel(inputStream); - model.checkErrors(); - - for (XMLValidationResult result : model.getResults()) { - if (result.isError()) { - throw new IllegalArgumentException( - new InvalidEventTemplateException(result.getText())); - } - } - return model; - } - } - @Override public Template addTemplate(InputStream stream) throws InvalidXmlException, InvalidEventTemplateException, IOException { try (stream) { - XMLModel model = parseXml(stream); - - XMLTagInstance configuration = model.getRoot(); - XMLAttributeInstance labelAttr = null; - for (XMLAttributeInstance attr : configuration.getAttributeInstances()) { - if (attr.getAttribute().getName().equals("label")) { - labelAttr = attr; - break; - } - } - - if (labelAttr == null) { + var model = parseXml(stream); + var template = createTemplate(model); + var existing = getTemplates(); + if (existing.stream().anyMatch(t -> Objects.equals(t.getName(), template.getName()))) { throw new IllegalArgumentException( - new InvalidEventTemplateException( - "Template has no configuration label attribute")); + String.format("Duplicate event template name: %s", template.getName())); } - - String templateName = labelAttr.getExplicitValue().replaceAll("[\\W]+", "_"); - - XMLTagInstance root = model.getRoot(); - root.setValue(JFCGrammar.ATTRIBUTE_LABEL_MANDATORY, templateName); - - String description = getAttributeValue(root, "description"); - String provider = getAttributeValue(root, "provider"); storage.putObject( PutObjectRequest.builder() .bucket(bucket) - .key(templateName) + .key(template.getName()) .contentType(MediaType.APPLICATION_XML) - .tagging(createTemplateTagging(templateName, description, provider)) + .tagging( + createTemplateTagging( + template.getName(), + template.getDescription(), + template.getProvider())) .build(), RequestBody.fromString(model.toString())); - var template = new Template(templateName, description, provider, TemplateType.CUSTOM); bus.publish( MessagingServer.class.getName(), new Notification(EVENT_TEMPLATE_CREATED, Map.of("template", template))); @@ -255,6 +253,8 @@ public Template addTemplate(InputStream stream) throw new IllegalArgumentException("Unable to parse XML stream", ioe); } catch (ParseException | IllegalArgumentException e) { throw new IllegalArgumentException(new InvalidEventTemplateException("Invalid XML", e)); + } catch (FlightRecorderException e) { + throw new IOException(e); } } @@ -303,7 +303,56 @@ private Tagging createTemplateTagging( return Tagging.builder().tagSet(tags).build(); } - protected String getAttributeValue(XMLTagInstance node, String valueKey) { + private boolean checkDir() { + return Files.exists(dir) + && Files.isReadable(dir) + && Files.isExecutable(dir) + && Files.isDirectory(dir); + } + + private XMLModel parseXml(InputStream inputStream) throws IOException, ParseException { + try (inputStream) { + var model = EventConfiguration.createModel(inputStream); + model.checkErrors(); + + for (XMLValidationResult result : model.getResults()) { + if (result.isError()) { + throw new IllegalArgumentException( + new InvalidEventTemplateException(result.getText())); + } + } + return model; + } + } + + private Template createTemplate(XMLModel model) throws IOException, ParseException { + 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 IllegalArgumentException( + new InvalidEventTemplateException( + "Template has no configuration label attribute")); + } + + String templateName = labelAttr.getExplicitValue().replaceAll("[\\W]+", "_"); + + XMLTagInstance root = model.getRoot(); + root.setValue(JFCGrammar.ATTRIBUTE_LABEL_MANDATORY, templateName); + + String description = getAttributeValue(root, "description"); + String provider = getAttributeValue(root, "provider"); + + return new Template(templateName, description, provider, TemplateType.CUSTOM); + } + + private String getAttributeValue(XMLTagInstance node, String valueKey) { return node.getAttributeInstances().stream() .filter(i -> Objects.equals(valueKey, i.getAttribute().getName())) .map(i -> i.getValue()) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 67b8e7f54..1d5d9b4c2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -40,6 +40,7 @@ cryostat.http.proxy.port=${quarkus.http.port} cryostat.http.proxy.path=/ conf-dir=/opt/cryostat.d +templates-dir=${conf-dir}/templates.d ssl.truststore=${conf-dir}/truststore.p12 ssl.truststore.dir=/truststore ssl.truststore.pass-file=${conf-dir}/truststore.pass