From 7b7ce94775456f78de67d2454b1cae2892d20622 Mon Sep 17 00:00:00 2001 From: Max Cao Date: Tue, 1 Aug 2023 12:42:24 -0700 Subject: [PATCH] feat(automated-rules): implement automated rules (#34) Signed-off-by: Max Cao Co-authored-by: Andrew Azores --- .gitignore | 1 + pom.xml | 34 ++ smoketest/compose/sample-apps.yml | 4 +- .../k8s/quarkus-test-agent-deployment.yaml | 2 +- smoketest/k8s/sample-app-deployment.yaml | 2 +- smoketest/k8s/smoketest.sh | 2 +- .../java/io/cryostat/ConfigProperties.java | 42 ++ .../java/io/cryostat/ExceptionMappers.java | 5 + src/main/java/io/cryostat/Producers.java | 9 + src/main/java/io/cryostat/V2Response.java | 4 +- .../io/cryostat/credentials/Credentials.java | 6 +- src/main/java/io/cryostat/events/Events.java | 4 +- .../cryostat/recordings/ActiveRecording.java | 26 +- .../cryostat/recordings/RecordingHelper.java | 510 ++++++++++++++++++ .../io/cryostat/recordings/Recordings.java | 300 +---------- .../RemoteRecordingInputStreamFactory.java | 3 +- .../rules/MatchExpressionEvaluator.java | 215 ++++++++ src/main/java/io/cryostat/rules/Rule.java | 67 ++- .../java/io/cryostat/rules/RuleService.java | 310 +++++++++++ src/main/java/io/cryostat/rules/Rules.java | 63 ++- .../cryostat/rules/ScheduledArchiveJob.java | 124 +++++ .../targets/TargetConnectionManager.java | 6 +- src/main/resources/application-dev.properties | 5 +- src/main/resources/application.properties | 2 + src/test/java/io/cryostat/TestUtils.java | 48 ++ .../java/io/cryostat/rules/RulesTest.java | 279 ++++++++++ src/test/java/itest/RulesPostFormIT.java | 30 +- src/test/java/itest/RulesPostJsonIT.java | 74 ++- src/test/java/itest/TargetEventsGetIT.java | 2 +- 29 files changed, 1830 insertions(+), 349 deletions(-) create mode 100644 src/main/java/io/cryostat/ConfigProperties.java create mode 100644 src/main/java/io/cryostat/recordings/RecordingHelper.java create mode 100644 src/main/java/io/cryostat/rules/MatchExpressionEvaluator.java create mode 100644 src/main/java/io/cryostat/rules/RuleService.java create mode 100644 src/main/java/io/cryostat/rules/ScheduledArchiveJob.java create mode 100644 src/test/java/io/cryostat/TestUtils.java create mode 100644 src/test/java/io/cryostat/rules/RulesTest.java diff --git a/.gitignore b/.gitignore index 02bf54be7..e893de045 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ truststore/ certs/*.p12 certs/*.pass *.jfr +.quarkus/ diff --git a/pom.xml b/pom.xml index 8d5aaf611..a21bfd775 100644 --- a/pom.xml +++ b/pom.xml @@ -31,11 +31,13 @@ UTF-8 2.21.0 + 1.15 2.11.0 4.5.14 3.12.0 1.7 + 0.3.21 quarkus-bom io.quarkus.platform 3.1.1.Final @@ -63,6 +65,13 @@ pom import + + org.projectnessie.cel + cel-bom + ${org.projectnessie.cel.bom.version} + pom + import + @@ -112,6 +121,10 @@ io.quarkus quarkus-hibernate-orm-panache + + io.quarkus + quarkus-hibernate-validator + io.quarkus quarkus-jdbc-postgresql @@ -144,6 +157,14 @@ httpclient ${org.apache.httpcomponents.version} + + org.projectnessie.cel + cel-tools + + + org.projectnessie.cel + cel-jackson + commons-validator commons-validator @@ -153,6 +174,14 @@ io.quarkus quarkus-rest-client-reactive-jackson + + io.quarkus + quarkus-cache + + + io.quarkus + quarkus-quartz + io.netty netty-transport-native-epoll @@ -170,6 +199,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-junit5-mockito + test + com.google.code.gson gson diff --git a/smoketest/compose/sample-apps.yml b/smoketest/compose/sample-apps.yml index e17c41d09..b05af3b60 100644 --- a/smoketest/compose/sample-apps.yml +++ b/smoketest/compose/sample-apps.yml @@ -4,7 +4,7 @@ services: depends_on: cryostat: condition: service_healthy - image: quay.io/andrewazores/vertx-fib-demo:0.12.3 + image: quay.io/andrewazores/vertx-fib-demo:0.13.0 hostname: vertx-fib-demo-1 environment: HTTP_PORT: 8081 @@ -32,7 +32,7 @@ services: start_period: 10s timeout: 5s quarkus-test-agent: - image: quay.io/andrewazores/quarkus-test:0.0.11 + image: quay.io/andrewazores/quarkus-test:latest # do not add a depends_on:cryostat here, so that we can test that the agent is tolerant of that state hostname: quarkus-test-agent ports: diff --git a/smoketest/k8s/quarkus-test-agent-deployment.yaml b/smoketest/k8s/quarkus-test-agent-deployment.yaml index 69570c0ec..8ba09c589 100644 --- a/smoketest/k8s/quarkus-test-agent-deployment.yaml +++ b/smoketest/k8s/quarkus-test-agent-deployment.yaml @@ -57,7 +57,7 @@ spec: value: "false" - name: QUARKUS_HTTP_PORT value: "10010" - image: quay.io/andrewazores/quarkus-test:0.0.11 + image: quay.io/andrewazores/quarkus-test:latest livenessProbe: exec: command: diff --git a/smoketest/k8s/sample-app-deployment.yaml b/smoketest/k8s/sample-app-deployment.yaml index b77300666..8c12a83c3 100644 --- a/smoketest/k8s/sample-app-deployment.yaml +++ b/smoketest/k8s/sample-app-deployment.yaml @@ -55,7 +55,7 @@ spec: value: "8081" - name: JMX_PORT value: "9093" - image: quay.io/andrewazores/vertx-fib-demo:0.12.3 + image: quay.io/andrewazores/vertx-fib-demo:0.13.0 livenessProbe: exec: command: diff --git a/smoketest/k8s/smoketest.sh b/smoketest/k8s/smoketest.sh index 4c15677d1..8e052f31a 100755 --- a/smoketest/k8s/smoketest.sh +++ b/smoketest/k8s/smoketest.sh @@ -54,7 +54,7 @@ while [ "$#" -ne 0 ]; do fi ;; *) - echo "Usage: $0 [clean|generate]" + echo "Usage: $0 [clean|generate|apply|kind|unkind|forward]" exit 1 ;; esac diff --git a/src/main/java/io/cryostat/ConfigProperties.java b/src/main/java/io/cryostat/ConfigProperties.java new file mode 100644 index 000000000..6a0c7b124 --- /dev/null +++ b/src/main/java/io/cryostat/ConfigProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat; + +public class ConfigProperties { + public static final String AWS_BUCKET_NAME_ARCHIVES = "storage.buckets.archives.name"; +} diff --git a/src/main/java/io/cryostat/ExceptionMappers.java b/src/main/java/io/cryostat/ExceptionMappers.java index b06bd4a43..889f4277a 100644 --- a/src/main/java/io/cryostat/ExceptionMappers.java +++ b/src/main/java/io/cryostat/ExceptionMappers.java @@ -53,4 +53,9 @@ public RestResponse mapNoResultException(NoResultException ex) { public RestResponse mapNoResultException(ConstraintViolationException ex) { return RestResponse.status(HttpResponseStatus.BAD_REQUEST.code()); } + + @ServerExceptionMapper + public RestResponse mapValidationException(jakarta.validation.ValidationException ex) { + return RestResponse.status(HttpResponseStatus.BAD_REQUEST.code()); + } } diff --git a/src/main/java/io/cryostat/Producers.java b/src/main/java/io/cryostat/Producers.java index e0e4e22a3..94dfd239a 100644 --- a/src/main/java/io/cryostat/Producers.java +++ b/src/main/java/io/cryostat/Producers.java @@ -49,6 +49,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.projectnessie.cel.tools.ScriptHost; +import org.projectnessie.cel.types.jackson.JacksonRegistry; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.presigner.S3Presigner; @@ -98,4 +100,11 @@ public static S3Presigner produceS3Presigner( return builder.build(); } + + @Produces + @ApplicationScoped + @DefaultBean + public static ScriptHost provideScriptHost() { + return ScriptHost.newBuilder().registry(JacksonRegistry.newRegistry()).build(); + } } diff --git a/src/main/java/io/cryostat/V2Response.java b/src/main/java/io/cryostat/V2Response.java index f78e66435..d5e70a764 100644 --- a/src/main/java/io/cryostat/V2Response.java +++ b/src/main/java/io/cryostat/V2Response.java @@ -38,8 +38,8 @@ package io.cryostat; public record V2Response(Meta meta, Data data) { - public static V2Response json(Object payload) { - return new V2Response(new Meta("application/json", "OK"), new Data(payload)); + public static V2Response json(Object payload, String status) { + return new V2Response(new Meta("application/json", status), new Data(payload)); } public record Meta(String type, String status) {} diff --git a/src/main/java/io/cryostat/credentials/Credentials.java b/src/main/java/io/cryostat/credentials/Credentials.java index eb2ee1a42..50d185864 100644 --- a/src/main/java/io/cryostat/credentials/Credentials.java +++ b/src/main/java/io/cryostat/credentials/Credentials.java @@ -54,6 +54,7 @@ import org.jboss.resteasy.reactive.RestPath; import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder; +import org.jboss.resteasy.reactive.RestResponse.Status; @Path("/api/v2.2/credentials") public class Credentials { @@ -62,7 +63,8 @@ public class Credentials { @RolesAllowed("read") public V2Response list() { List credentials = Credential.listAll(); - return V2Response.json(credentials.stream().map(Credentials::safeResult).toList()); + return V2Response.json( + credentials.stream().map(Credentials::safeResult).toList(), Status.OK.toString()); } @GET @@ -70,7 +72,7 @@ public V2Response list() { @Path("/{id}") public V2Response get(@RestPath long id) { Credential credential = Credential.find("id", id).singleResult(); - return V2Response.json(safeMatchedResult(credential)); + return V2Response.json(safeMatchedResult(credential), Status.OK.toString()); } @Transactional diff --git a/src/main/java/io/cryostat/events/Events.java b/src/main/java/io/cryostat/events/Events.java index 983f3dc48..77442e9ca 100644 --- a/src/main/java/io/cryostat/events/Events.java +++ b/src/main/java/io/cryostat/events/Events.java @@ -59,6 +59,7 @@ import org.jboss.resteasy.reactive.RestPath; import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.RestResponse.Status; @Path("") public class Events { @@ -85,7 +86,8 @@ public Response listEventsV1(@RestPath URI connectUrl, @RestQuery String q) thro @Path("/api/v2/targets/{connectUrl}/events") @RolesAllowed("read") public V2Response listEventsV2(@RestPath URI connectUrl, @RestQuery String q) throws Exception { - return V2Response.json(searchEvents(Target.getTargetByConnectUrl(connectUrl), q)); + return V2Response.json( + searchEvents(Target.getTargetByConnectUrl(connectUrl), q), Status.OK.toString()); } @GET diff --git a/src/main/java/io/cryostat/recordings/ActiveRecording.java b/src/main/java/io/cryostat/recordings/ActiveRecording.java index 1ed87ec1f..0b6d6280b 100644 --- a/src/main/java/io/cryostat/recordings/ActiveRecording.java +++ b/src/main/java/io/cryostat/recordings/ActiveRecording.java @@ -68,6 +68,8 @@ import jakarta.persistence.PostUpdate; import jakarta.persistence.PreRemove; import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import jdk.jfr.RecordingState; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -75,14 +77,21 @@ @Entity @EntityListeners(ActiveRecording.Listener.class) +@Table( + uniqueConstraints = { + @UniqueConstraint(columnNames = {"target_id", "name"}), + @UniqueConstraint(columnNames = {"target_id", "remoteId"}) + }) public class ActiveRecording extends PanacheEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "target_id") private Target target; - public long remoteId; + @Column(nullable = false) public String name; + + public long remoteId; public RecordingState state; public long duration; public long startTime; @@ -151,10 +160,21 @@ public static ActiveRecording getByName(String name) { return find("name", name).singleResult(); } + // Default implementation of delete is bulk so does not trigger lifecycle events public static boolean deleteByName(String name) { return delete("name", name) > 0; } + public static boolean deleteFromTarget(Target target, String name) { + ActiveRecording recordingToDelete = + find("target = ?1 and name = ?2", target, name).firstResult(); + if (recordingToDelete != null) { + recordingToDelete.delete(); + return true; + } + return false; // Recording not found or already deleted. + } + public LinkedRecordingDescriptor toExternalForm() { return new LinkedRecordingDescriptor( this.remoteId, @@ -189,7 +209,7 @@ public void preUpdate(ActiveRecording activeRecording) throws Exception { connectionManager.executeConnectedTask( activeRecording.target, conn -> { - Recordings.getDescriptorById(conn, activeRecording.remoteId) + RecordingHelper.getDescriptorById(conn, activeRecording.remoteId) .ifPresent( d -> { try { @@ -219,7 +239,7 @@ public void preRemove(ActiveRecording activeRecording) throws Exception { connectionManager.executeConnectedTask( activeRecording.target, conn -> { - Recordings.getDescriptor(conn, activeRecording) + RecordingHelper.getDescriptor(conn, activeRecording) .ifPresent(rec -> Recordings.safeCloseRecording(conn, rec, logger)); return null; }); diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java new file mode 100644 index 000000000..d1576bf15 --- /dev/null +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -0,0 +1,510 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.cryostat.recordings; + +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.StandardCharsets; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.openjdk.jmc.common.unit.IConstrainedMap; +import org.openjdk.jmc.common.unit.UnitLookup; +import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID; +import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; +import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo; +import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; + +import io.cryostat.ConfigProperties; +import io.cryostat.core.net.JFRConnection; +import io.cryostat.core.sys.Clock; +import io.cryostat.core.templates.Template; +import io.cryostat.core.templates.TemplateType; +import io.cryostat.recordings.ActiveRecording.Listener.RecordingEvent; +import io.cryostat.recordings.Recordings.LinkedRecordingDescriptor; +import io.cryostat.recordings.Recordings.Metadata; +import io.cryostat.targets.Target; +import io.cryostat.targets.TargetConnectionManager; +import io.cryostat.ws.MessagingServer; +import io.cryostat.ws.Notification; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.smallrye.common.annotation.Blocking; +import io.vertx.mutiny.core.eventbus.EventBus; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ServerErrorException; +import jdk.jfr.RecordingState; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.services.s3.model.Tag; +import software.amazon.awssdk.services.s3.model.Tagging; +import software.amazon.awssdk.services.s3.model.UploadPartRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +@ApplicationScoped +public class RecordingHelper { + + private static final String JFR_MIME = "application/jfr"; + private static final Pattern TEMPLATE_PATTERN = + Pattern.compile("^template=([\\w]+)(?:,type=([\\w]+))?$"); + private final Base64 base64Url = new Base64(0, null, true); + + @Inject Logger logger; + @Inject TargetConnectionManager connectionManager; + @Inject RecordingOptionsBuilderFactory recordingOptionsBuilderFactory; + @Inject EventOptionsBuilder.Factory eventOptionsBuilderFactory; + @Inject ScheduledExecutorService scheduler; + @Inject EventBus bus; + + @Inject Clock clock; + @Inject S3Presigner presigner; + @Inject RemoteRecordingInputStreamFactory remoteRecordingStreamFactory; + @Inject ObjectMapper mapper; + @Inject S3Client storage; + + @ConfigProperty(name = ConfigProperties.AWS_BUCKET_NAME_ARCHIVES) + String archiveBucket; + + boolean shouldRestartRecording( + RecordingReplace replace, RecordingState state, String recordingName) + throws BadRequestException { + switch (replace) { + case ALWAYS: + return true; + case NEVER: + return false; + case STOPPED: + if (state == RecordingState.RUNNING) { + throw new BadRequestException( + String.format( + "replace=='STOPPED' but recording with name \"%s\" is already" + + " running", + recordingName)); + } + return state == RecordingState.STOPPED; + default: + return true; + } + } + + @Blocking + @Transactional(Transactional.TxType.REQUIRES_NEW) + public LinkedRecordingDescriptor startRecording( + Target target, + IConstrainedMap recordingOptions, + String templateName, + TemplateType templateType, + Metadata metadata, + boolean archiveOnStop, + RecordingReplace replace, + JFRConnection connection) + throws Exception { + String recordingName = (String) recordingOptions.get(RecordingOptionsBuilder.KEY_NAME); + TemplateType preferredTemplateType = + getPreferredTemplateType(connection, templateName, templateType); + Optional previous = getDescriptorByName(connection, recordingName); + if (previous.isPresent()) { + RecordingState previousState = mapState(previous.get()); + boolean restart = shouldRestartRecording(replace, previousState, recordingName); + if (!restart) { + throw new BadRequestException( + String.format("Recording with name \"%s\" already exists", recordingName)); + } + if (!ActiveRecording.deleteFromTarget(target, recordingName)) { + logger.warnf( + "Could not delete recording %s from target %s", + recordingName, target.alias); + } + } + + IRecordingDescriptor desc = + connection + .getService() + .start( + recordingOptions, + enableEvents(connection, templateName, preferredTemplateType)); + + Map labels = metadata.labels(); + + labels.put("template.name", templateName); + labels.put("template.type", preferredTemplateType.name()); + + Metadata meta = new Metadata(labels); + return new LinkedRecordingDescriptor( + desc.getId(), + mapState(desc), + desc.getDuration().in(UnitLookup.MILLISECOND).longValue(), + desc.getStartTime().in(UnitLookup.EPOCH_MS).longValue(), + desc.isContinuous(), + desc.getToDisk(), + desc.getMaxSize().in(UnitLookup.BYTE).longValue(), + desc.getMaxAge().in(UnitLookup.MILLISECOND).longValue(), + desc.getName(), + "TODO", + "TODO", + meta); + } + + public Pair parseEventSpecifierToTemplate(String eventSpecifier) { + if (TEMPLATE_PATTERN.matcher(eventSpecifier).matches()) { + Matcher m = TEMPLATE_PATTERN.matcher(eventSpecifier); + m.find(); + String templateName = m.group(1); + String typeName = m.group(2); + TemplateType templateType = null; + if (StringUtils.isNotBlank(typeName)) { + templateType = TemplateType.valueOf(typeName.toUpperCase()); + } + return Pair.of(templateName, templateType); + } + throw new BadRequestException(eventSpecifier); + } + + private IConstrainedMap enableAllEvents(JFRConnection connection) + throws Exception { + EventOptionsBuilder builder = eventOptionsBuilderFactory.create(connection); + + for (IEventTypeInfo eventTypeInfo : connection.getService().getAvailableEventTypes()) { + builder.addEvent(eventTypeInfo.getEventTypeID().getFullKey(), "enabled", "true"); + } + + return builder.build(); + } + + public IConstrainedMap enableEvents( + JFRConnection connection, String templateName, TemplateType templateType) + throws Exception { + if (templateName.equals("ALL")) { + return enableAllEvents(connection); + } + // if template type not specified, try to find a Custom template by that name. If none, + // fall back on finding a Target built-in template by the name. If not, throw an + // exception and bail out. + TemplateType type = getPreferredTemplateType(connection, templateName, templateType); + return connection.getTemplateService().getEvents(templateName, type).get(); + } + + public TemplateType getPreferredTemplateType( + JFRConnection connection, String templateName, TemplateType templateType) + throws Exception { + if (templateType != null) { + return templateType; + } + if (templateName.equals("ALL")) { + // special case for the ALL meta-template + return TemplateType.TARGET; + } + List