diff --git a/pom.xml b/pom.xml
index 852bd8fa95..564e3de7ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,7 +61,7 @@
2.34.12.26
- 2.3.1
+ 2.4.03.12.01.15
@@ -395,6 +395,8 @@
type=tmpfs,target=/opt/cryostat.d/recordings.d--mounttype=tmpfs,target=/opt/cryostat.d/truststore.d
+ --mount
+ type=tmpfs,target=/opt/cryostat.d/probes.d--envCRYOSTAT_TARGET_CACHE_TTL=60--env
@@ -416,6 +418,8 @@
--envCRYOSTAT_TEMPLATE_PATH=/opt/cryostat.d/templates.d--env
+ CRYOSTAT_PROBE_TEMPLATE_PATH=/opt/cryostat.d/probes.d
+ --envSSL_TRUSTSTORE_DIR=/opt/cryostat.d/truststore.d--envGRAFANA_DATASOURCE_URL=http://${cryostat.itest.webHost}:${cryostat.itest.jfr-datasource.port}
diff --git a/run.sh b/run.sh
index 3c6e9e450d..8153b565a8 100755
--- a/run.sh
+++ b/run.sh
@@ -97,6 +97,7 @@ podman run \
--mount type=bind,source="$(dirname $0)/conf",destination=/opt/cryostat.d/conf.d,relabel=shared \
--mount type=bind,source="$(dirname $0)/templates",destination=/opt/cryostat.d/templates.d,relabel=shared \
--mount type=bind,source="$(dirname $0)/truststore",destination=/truststore,relabel=shared \
+ --mount type=tmpfs,target=/opt/cryostat.d/probes.d \
-e CRYOSTAT_PLATFORM=$CRYOSTAT_PLATFORM \
-e CRYOSTAT_DISABLE_SSL=$CRYOSTAT_DISABLE_SSL \
-e CRYOSTAT_DISABLE_JMX_AUTH=$CRYOSTAT_DISABLE_JMX_AUTH \
@@ -114,6 +115,7 @@ podman run \
-e CRYOSTAT_CONFIG_PATH="/opt/cryostat.d/conf.d" \
-e CRYOSTAT_ARCHIVE_PATH="/opt/cryostat.d/recordings.d" \
-e CRYOSTAT_TEMPLATE_PATH="/opt/cryostat.d/templates.d" \
+ -e CRYOSTAT_PROBE_TEMPLATE_PATH="/opt/cryostat.d/probes.d" \
-e CRYOSTAT_CLIENTLIB_PATH="/clientlib" \
-e CRYOSTAT_REPORT_GENERATION_MAX_HEAP="$CRYOSTAT_REPORT_GENERATION_MAX_HEAP" \
-e GRAFANA_DATASOURCE_URL=$GRAFANA_DATASOURCE_URL \
diff --git a/src/main/java/io/cryostat/net/security/ResourceAction.java b/src/main/java/io/cryostat/net/security/ResourceAction.java
index 427c1fbce2..90d31ccfbd 100644
--- a/src/main/java/io/cryostat/net/security/ResourceAction.java
+++ b/src/main/java/io/cryostat/net/security/ResourceAction.java
@@ -72,6 +72,9 @@ public enum ResourceAction {
UPDATE_TEMPLATE(UPDATE, TEMPLATE),
DELETE_TEMPLATE(DELETE, TEMPLATE),
+ CREATE_PROBE_TEMPLATE(CREATE, TEMPLATE),
+ DELETE_PROBE_TEMPLATE(DELETE, TEMPLATE),
+
CREATE_REPORT(CREATE, REPORT),
READ_REPORT(READ, REPORT),
UPDATE_REPORT(UPDATE, REPORT),
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java b/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java
index a447e73580..4cbe2da830 100644
--- a/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java
@@ -49,6 +49,31 @@ public abstract class HttpApiBetaModule {
@IntoSet
abstract RequestHandler bindDiscoveryGetHandler(DiscoveryGetHandler handler);
+ @Binds
+ @IntoSet
+ abstract RequestHandler bindProbeTemplateUploadHandler(ProbeTemplateUploadHandler handler);
+
+ @Binds
+ @IntoSet
+ abstract RequestHandler bindProbeTemplateUploadBodyHandler(
+ ProbeTemplateUploadBodyHandler handler);
+
+ @Binds
+ @IntoSet
+ abstract RequestHandler bindProbeTemplateDeleteHandler(ProbeTemplateDeleteHandler handler);
+
+ @Binds
+ @IntoSet
+ abstract RequestHandler bindTargetProbePostHandler(TargetProbePostHandler handler);
+
+ @Binds
+ @IntoSet
+ abstract RequestHandler bindTargetProbeDeleteHandler(TargetProbeDeleteHandler handler);
+
+ @Binds
+ @IntoSet
+ abstract RequestHandler bindTargetProbesGetHandler(TargetProbesGetHandler handler);
+
@Binds
@IntoSet
abstract RequestHandler bindAuthTokenPostHandler(AuthTokenPostHandler handler);
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandler.java
new file mode 100644
index 0000000000..d612014b3f
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandler.java
@@ -0,0 +1,144 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.core.agent.LocalProbeTemplateService;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+
+public class ProbeTemplateDeleteHandler extends AbstractV2RequestHandler {
+
+ static final String PATH = "probes/:probetemplateName";
+
+ private final Logger logger;
+ private final NotificationFactory notificationFactory;
+ private final LocalProbeTemplateService probeTemplateService;
+ private final FileSystem fs;
+ private static final String NOTIFICATION_CATEGORY = "ProbeTemplateUploaded";
+
+ @Inject
+ ProbeTemplateDeleteHandler(
+ AuthManager auth,
+ NotificationFactory notificationFactory,
+ LocalProbeTemplateService probeTemplateService,
+ Logger logger,
+ FileSystem fs,
+ Gson gson) {
+ super(auth, gson);
+ this.notificationFactory = notificationFactory;
+ this.logger = logger;
+ this.probeTemplateService = probeTemplateService;
+ this.fs = fs;
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.V2;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.DELETE;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + PATH;
+ }
+
+ @Override
+ public boolean isAsync() {
+ return false;
+ }
+
+ @Override
+ public boolean isOrdered() {
+ return true;
+ }
+
+ @Override
+ public boolean requiresAuthentication() {
+ return true;
+ }
+
+ @Override
+ public IntermediateResponse handle(RequestParameters params) throws Exception {
+ String probeTemplateName = params.getPathParams().get("probetemplateName");
+ try {
+ this.probeTemplateService.deleteTemplate(probeTemplateName);
+ notificationFactory
+ .createBuilder()
+ .metaCategory(NOTIFICATION_CATEGORY)
+ .metaType(HttpMimeType.JSON)
+ .message(Map.of("probeTemplate", probeTemplateName))
+ .build()
+ .send();
+ } catch (Exception e) {
+ throw new HttpStatusException(400, e.getMessage(), e);
+ }
+ return new IntermediateResponse().body(null);
+ }
+
+ @Override
+ public Set resourceActions() {
+ return EnumSet.of(ResourceAction.DELETE_PROBE_TEMPLATE);
+ }
+
+ @Override
+ public HttpMimeType mimeType() {
+ return HttpMimeType.PLAINTEXT;
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadBodyHandler.java
new file mode 100644
index 0000000000..e22b84eb00
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadBodyHandler.java
@@ -0,0 +1,91 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
+import io.cryostat.net.web.http.api.ApiVersion;
+
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.BodyHandler;
+
+public class ProbeTemplateUploadBodyHandler extends AbstractAuthenticatedRequestHandler {
+
+ static final BodyHandler BODY_HANDLER = BodyHandler.create(true);
+
+ @Inject
+ ProbeTemplateUploadBodyHandler(AuthManager auth) {
+ super(auth);
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.V2;
+ }
+
+ @Override
+ public int getPriority() {
+ return DEFAULT_PRIORITY - 1;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + ProbeTemplateUploadHandler.PATH;
+ }
+
+ @Override
+ public Set resourceActions() {
+ return ResourceAction.NONE;
+ }
+
+ @Override
+ public void handleAuthenticated(RoutingContext ctx) {
+ BODY_HANDLER.handle(ctx);
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandler.java
new file mode 100644
index 0000000000..bc4546e122
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandler.java
@@ -0,0 +1,163 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.core.agent.LocalProbeTemplateService;
+import io.cryostat.core.agent.ProbeValidationException;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.FileUpload;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+
+class ProbeTemplateUploadHandler extends AbstractV2RequestHandler {
+
+ static final String PATH = "probes/:probetemplateName";
+
+ private final Logger logger;
+ private final NotificationFactory notificationFactory;
+ private final LocalProbeTemplateService probeTemplateService;
+ private final FileSystem fs;
+ private static final String NOTIFICATION_CATEGORY = "ProbeTemplateUploaded";
+
+ @Inject
+ ProbeTemplateUploadHandler(
+ AuthManager auth,
+ NotificationFactory notificationFactory,
+ LocalProbeTemplateService probeTemplateService,
+ Logger logger,
+ FileSystem fs,
+ Gson gson) {
+ super(auth, gson);
+ this.notificationFactory = notificationFactory;
+ this.logger = logger;
+ this.probeTemplateService = probeTemplateService;
+ this.fs = fs;
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.V2;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + PATH;
+ }
+
+ @Override
+ public boolean isAsync() {
+ return false;
+ }
+
+ @Override
+ public boolean isOrdered() {
+ return true;
+ }
+
+ @Override
+ public Set resourceActions() {
+ return EnumSet.of(ResourceAction.CREATE_PROBE_TEMPLATE);
+ }
+
+ @Override
+ public boolean requiresAuthentication() {
+ return true;
+ }
+
+ @Override
+ public IntermediateResponse handle(RequestParameters requestParams) throws Exception {
+ try {
+ for (FileUpload u : requestParams.getFileUploads()) {
+ String templateName = requestParams.getPathParams().get("probetemplateName");
+ Path path = fs.pathOf(u.uploadedFileName());
+ if (!"probeTemplate".equals(u.name())) {
+ fs.deleteIfExists(path);
+ continue;
+ }
+ try (InputStream is = fs.newInputStream(path)) {
+ notificationFactory
+ .createBuilder()
+ .metaCategory(NOTIFICATION_CATEGORY)
+ .metaType(HttpMimeType.JSON)
+ .message(Map.of("probeTemplate", u.uploadedFileName()))
+ .build()
+ .send();
+ probeTemplateService.addTemplate(is, templateName);
+ } finally {
+ fs.deleteIfExists(path);
+ }
+ }
+ } catch (ProbeValidationException pve) {
+ logger.error(pve.getMessage());
+ throw new HttpStatusException(400, pve.getMessage(), pve);
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ throw new HttpStatusException(500, e.getMessage(), e);
+ }
+ return new IntermediateResponse().body(null);
+ }
+
+ @Override
+ public HttpMimeType mimeType() {
+ return HttpMimeType.PLAINTEXT;
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandler.java
new file mode 100644
index 0000000000..c55e4cf458
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandler.java
@@ -0,0 +1,159 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.core.agent.AgentJMXHelper;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.Environment;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.TargetConnectionManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+import org.apache.commons.lang3.StringUtils;
+
+public class TargetProbeDeleteHandler extends AbstractV2RequestHandler {
+
+ static final String PATH = "targets/:targetId/probes";
+
+ private final Logger logger;
+ private final NotificationFactory notificationFactory;
+ private final FileSystem fs;
+ private final TargetConnectionManager connectionManager;
+ private final Environment env;
+ private static final String NOTIFICATION_CATEGORY = "ProbeTemplateDeleted";
+
+ @Inject
+ TargetProbeDeleteHandler(
+ Logger logger,
+ NotificationFactory notificationFactory,
+ FileSystem fs,
+ AuthManager auth,
+ TargetConnectionManager connectionManager,
+ Environment env,
+ Gson gson) {
+ super(auth, gson);
+ this.logger = logger;
+ this.notificationFactory = notificationFactory;
+ this.connectionManager = connectionManager;
+ this.env = env;
+ this.fs = fs;
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.V2;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + PATH;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.DELETE;
+ }
+
+ @Override
+ public Set resourceActions() {
+ return ResourceAction.NONE;
+ }
+
+ @Override
+ public boolean requiresAuthentication() {
+ return true;
+ }
+
+ @Override
+ public IntermediateResponse handle(RequestParameters requestParams) throws Exception {
+ Map pathParams = requestParams.getPathParams();
+ String targetId = pathParams.get("targetId");
+ StringBuilder sb = new StringBuilder();
+ if (StringUtils.isBlank(targetId)) {
+ sb.append("targetId is required.");
+ throw new HttpStatusException(400, sb.toString().trim());
+ }
+ try {
+ return connectionManager.executeConnectedTask(
+ getConnectionDescriptorFromParams(requestParams),
+ connection -> {
+ connection.connect();
+ AgentJMXHelper helper = new AgentJMXHelper(connection.getHandle());
+ // The convention for removing probes in the agent controller mbean is to
+ // call
+ // defineEventProbes with a null argument.
+ helper.defineEventProbes(null);
+ notificationFactory
+ .createBuilder()
+ .metaCategory(NOTIFICATION_CATEGORY)
+ .metaType(HttpMimeType.JSON)
+ .message(Map.of("targetId", targetId))
+ .build()
+ .send();
+ return new IntermediateResponse().body(null);
+ });
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+
+ @Override
+ public HttpMimeType mimeType() {
+ return HttpMimeType.PLAINTEXT;
+ }
+
+ @Override
+ public boolean isAsync() {
+ return false;
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandler.java
new file mode 100644
index 0000000000..4f6f003bbb
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandler.java
@@ -0,0 +1,190 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.core.agent.AgentJMXHelper;
+import io.cryostat.core.agent.LocalProbeTemplateService;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.Environment;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.TargetConnectionManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * TargetProbePostHandler will facilitate adding probes to a target and will have the following form
+ * and response types:
+ *
+ *
POST /api/v2/targets/:targetId/probes/
+ *
+ *
targetId - The location of the target JVM to connect to, in the form of a service:rmi:jmx://
+ * JMX Service URL. Should use percent-encoding.
+ *
+ *
Parameters
+ *
+ *
probeTemplate - name of the probe template to use
+ *
+ *
Responses
+ *
+ *
200 - No body
+ *
+ *
401 - User authentication failed. The body is an error message. There will be an
+ * X-WWW-Authenticate: $SCHEME header that indicates the authentication scheme that is used.
+ *
+ *
404 - The target could not be found. The body is an error message.
+ *
+ *
427 - JMX authentication failed. The body is an error message. There will be an
+ * X-JMX-Authenticate: $SCHEME header that indicates the authentication scheme that is used.
+ */
+public class TargetProbePostHandler extends AbstractV2RequestHandler {
+
+ static final String PATH = "targets/:targetId/probes/:probeTemplate";
+
+ private final Logger logger;
+ private final NotificationFactory notificationFactory;
+ private final LocalProbeTemplateService probeTemplateService;
+ private final FileSystem fs;
+ private final TargetConnectionManager connectionManager;
+ private final Environment env;
+ private static final String NOTIFICATION_CATEGORY = "ProbeTemplateUploaded";
+
+ @Inject
+ TargetProbePostHandler(
+ Logger logger,
+ NotificationFactory notificationFactory,
+ LocalProbeTemplateService service,
+ FileSystem fs,
+ AuthManager auth,
+ TargetConnectionManager connectionManager,
+ Environment env,
+ Gson gson) {
+ super(auth, gson);
+ this.logger = logger;
+ this.notificationFactory = notificationFactory;
+ this.probeTemplateService = service;
+ this.connectionManager = connectionManager;
+ this.env = env;
+ this.fs = fs;
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.V2;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + PATH;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public Set resourceActions() {
+ return ResourceAction.NONE;
+ }
+
+ @Override
+ public boolean requiresAuthentication() {
+ return true;
+ }
+
+ @Override
+ public IntermediateResponse handle(RequestParameters requestParams) throws Exception {
+ Map pathParams = requestParams.getPathParams();
+ String targetId = pathParams.get("targetId");
+ String probeTemplate = pathParams.get("probeTemplate");
+ if (StringUtils.isAnyBlank(targetId, probeTemplate)) {
+ StringBuilder sb = new StringBuilder();
+ if (StringUtils.isBlank(targetId)) {
+ sb.append("targetId is required.");
+ }
+ if (StringUtils.isBlank(probeTemplate)) {
+ sb.append("\"probeTemplate\" is required.");
+ }
+ throw new HttpStatusException(400, sb.toString().trim());
+ }
+ return connectionManager.executeConnectedTask(
+ getConnectionDescriptorFromParams(requestParams),
+ connection -> {
+ connection.connect();
+ AgentJMXHelper helper = new AgentJMXHelper(connection.getHandle());
+ helper.defineEventProbes(probeTemplateService.getTemplate(probeTemplate));
+ notificationFactory
+ .createBuilder()
+ .metaCategory(NOTIFICATION_CATEGORY)
+ .metaType(HttpMimeType.JSON)
+ .message(
+ Map.of(
+ Map.of("targetId", targetId),
+ Map.of("probeTemplate", probeTemplate)))
+ .build()
+ .send();
+ return new IntermediateResponse().body(null);
+ });
+ }
+
+ @Override
+ public HttpMimeType mimeType() {
+ return HttpMimeType.PLAINTEXT;
+ }
+
+ @Override
+ public boolean isAsync() {
+ return false;
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbesGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbesGetHandler.java
new file mode 100644
index 0000000000..e2d8c5a985
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbesGetHandler.java
@@ -0,0 +1,140 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.core.agent.AgentJMXHelper;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.TargetConnectionManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+import org.apache.commons.lang3.StringUtils;
+
+public class TargetProbesGetHandler extends AbstractV2RequestHandler {
+
+ static final String PATH = "targets/:targetId/probes";
+
+ private final TargetConnectionManager connectionManager;
+ private static final String NOTIFICATION_CATEGORY = "TargetProbesGet";
+ private final NotificationFactory notificationFactory;
+
+ @Inject
+ TargetProbesGetHandler(
+ AuthManager auth,
+ TargetConnectionManager connectionManager,
+ NotificationFactory notificationFactory,
+ Gson gson) {
+ super(auth, gson);
+ this.notificationFactory = notificationFactory;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.V2;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + PATH;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.GET;
+ }
+
+ @Override
+ public Set resourceActions() {
+ return ResourceAction.NONE;
+ }
+
+ @Override
+ public boolean requiresAuthentication() {
+ return true;
+ }
+
+ @Override
+ public IntermediateResponse handle(RequestParameters requestParams) throws Exception {
+ Map pathParams = requestParams.getPathParams();
+ String targetId = pathParams.get("targetId");
+ StringBuilder sb = new StringBuilder();
+ if (StringUtils.isBlank(targetId)) {
+ sb.append("targetId is required.");
+ throw new HttpStatusException(400, sb.toString().trim());
+ }
+ return connectionManager.executeConnectedTask(
+ getConnectionDescriptorFromParams(requestParams),
+ connection -> {
+ connection.connect();
+ AgentJMXHelper helper = new AgentJMXHelper(connection.getHandle());
+ String probes = helper.retrieveEventProbes();
+ notificationFactory
+ .createBuilder()
+ .metaCategory(NOTIFICATION_CATEGORY)
+ .metaType(HttpMimeType.JSON)
+ .message(Map.of("targetId", targetId))
+ .build()
+ .send();
+ return new IntermediateResponse().body(probes);
+ });
+ }
+
+ @Override
+ public HttpMimeType mimeType() {
+ return HttpMimeType.JSON;
+ }
+
+ @Override
+ public boolean isAsync() {
+ return false;
+ }
+}
diff --git a/src/main/java/io/cryostat/templates/TemplatesModule.java b/src/main/java/io/cryostat/templates/TemplatesModule.java
index 69a83748a0..fc9bd9b1b8 100644
--- a/src/main/java/io/cryostat/templates/TemplatesModule.java
+++ b/src/main/java/io/cryostat/templates/TemplatesModule.java
@@ -39,6 +39,7 @@
import javax.inject.Singleton;
+import io.cryostat.core.agent.LocalProbeTemplateService;
import io.cryostat.core.sys.Environment;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.core.templates.LocalStorageTemplateService;
@@ -55,4 +56,17 @@ static LocalStorageTemplateService provideLocalStorageTemplateService(
FileSystem fs, Environment env) {
return new LocalStorageTemplateService(fs, env);
}
+
+ @Provides
+ @Singleton
+ static LocalProbeTemplateService provideLocalProbeTemplateService(
+ FileSystem fs, Environment env) {
+ try {
+ return new LocalProbeTemplateService(fs, env);
+ } catch (Exception e) {
+ // Dagger doesn't like constructors that can throw exceptions, the probeTemplateService
+ // throws an exception if the sanity checks fail so we need to deal with it here
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandlerTest.java
new file mode 100644
index 0000000000..b625862800
--- /dev/null
+++ b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandlerTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import static org.mockito.Mockito.lenient;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import io.cryostat.MainModule;
+import io.cryostat.core.agent.LocalProbeTemplateService;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.Notification;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class ProbeTemplateDeleteHandlerTest {
+
+ ProbeTemplateDeleteHandler handler;
+ @Mock AuthManager auth;
+ @Mock LocalProbeTemplateService templateService;
+ @Mock FileSystem fs;
+ @Mock Logger logger;
+ @Mock NotificationFactory notificationFactory;
+ @Mock Notification notification;
+ @Mock Notification.Builder notificationBuilder;
+ Gson gson = MainModule.provideGson(logger);
+
+ @BeforeEach
+ void setup() {
+ lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaCategory(Mockito.any()))
+ .thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class)))
+ .thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class)))
+ .thenReturn(notificationBuilder);
+ lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder);
+ lenient().when(notificationBuilder.build()).thenReturn(notification);
+ this.handler =
+ new ProbeTemplateDeleteHandler(
+ auth, notificationFactory, templateService, logger, fs, gson);
+ }
+
+ @Nested
+ class BasicHandlerDefinition {
+ @Test
+ void shouldBePOSTHandler() {
+ MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.DELETE));
+ }
+
+ @Test
+ void shouldBeAPIV2() {
+ MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2));
+ }
+
+ @Test
+ void shouldHaveExpectedPath() {
+ MatcherAssert.assertThat(
+ handler.path(), Matchers.equalTo("/api/v2/probes/:probetemplateName"));
+ }
+
+ @Test
+ void shouldHaveExpectedRequiredPermissions() {
+ MatcherAssert.assertThat(
+ handler.resourceActions(),
+ Matchers.equalTo(Set.of(ResourceAction.DELETE_PROBE_TEMPLATE)));
+ }
+
+ @Test
+ void shouldReturnPlaintextMimeType() {
+ MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT));
+ }
+
+ @Test
+ void shouldRequireAuthentication() {
+ MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true));
+ }
+ }
+
+ @Nested
+ class RequestHandling {
+
+ @Mock RequestParameters requestParams;
+
+ @Test
+ void shouldRespond400WhenTemplateNotFound() throws Exception {
+ Mockito.when(requestParams.getPathParams())
+ .thenReturn(Map.of("probetemplateName", "foo.xml"));
+ Mockito.doThrow(new IOException()).when(templateService).deleteTemplate("foo.xml");
+ HttpStatusException ex =
+ Assertions.assertThrows(
+ HttpStatusException.class, () -> handler.handle(requestParams));
+ MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(400));
+ }
+
+ @Test
+ void shouldCallThroughToService() throws Exception {
+ Mockito.when(requestParams.getPathParams())
+ .thenReturn(Map.of("probetemplateName", "foo.xml"));
+ IntermediateResponse response = handler.handle(requestParams);
+
+ Mockito.verify(templateService).deleteTemplate("foo.xml");
+ Mockito.verifyNoMoreInteractions(templateService);
+
+ MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200));
+ MatcherAssert.assertThat(response.getBody(), Matchers.nullValue());
+ }
+ }
+}
diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandlerTest.java
new file mode 100644
index 0000000000..4c3f8f495a
--- /dev/null
+++ b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandlerTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import static org.mockito.Mockito.lenient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Set;
+
+import io.cryostat.MainModule;
+import io.cryostat.core.agent.LocalProbeTemplateService;
+import io.cryostat.core.agent.ProbeValidationException;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.Notification;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.FileUpload;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class ProbeTemplateUploadHandlerTest {
+
+ ProbeTemplateUploadHandler handler;
+ @Mock AuthManager auth;
+ @Mock LocalProbeTemplateService templateService;
+ @Mock FileSystem fs;
+ @Mock Logger logger;
+ @Mock NotificationFactory notificationFactory;
+ @Mock Notification notification;
+ @Mock Notification.Builder notificationBuilder;
+ Gson gson = MainModule.provideGson(logger);
+
+ @BeforeEach
+ void setup() {
+ lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaCategory(Mockito.any()))
+ .thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class)))
+ .thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class)))
+ .thenReturn(notificationBuilder);
+ lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder);
+ lenient().when(notificationBuilder.build()).thenReturn(notification);
+ this.handler =
+ new ProbeTemplateUploadHandler(
+ auth, notificationFactory, templateService, logger, fs, gson);
+ }
+
+ @Nested
+ class BasicHandlerDefinition {
+ @Test
+ void shouldBePOSTHandler() {
+ MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.POST));
+ }
+
+ @Test
+ void shouldBeAPIV2() {
+ MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2));
+ }
+
+ @Test
+ void shouldHaveExpectedPath() {
+ MatcherAssert.assertThat(
+ handler.path(), Matchers.equalTo("/api/v2/probes/:probetemplateName"));
+ }
+
+ @Test
+ void shouldHaveExpectedRequiredPermissions() {
+ MatcherAssert.assertThat(
+ handler.resourceActions(),
+ Matchers.equalTo(Set.of(ResourceAction.CREATE_PROBE_TEMPLATE)));
+ }
+
+ @Test
+ void shouldReturnPlaintextMimeType() {
+ MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT));
+ }
+
+ @Test
+ void shouldRequireAuthentication() {
+ MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true));
+ }
+ }
+
+ @Nested
+ class RequestHandling {
+
+ @Mock RequestParameters requestParams;
+
+ @Test
+ void shouldRespond500WhenUploadFails() throws Exception {
+ FileUpload upload = Mockito.mock(FileUpload.class);
+ Mockito.when(upload.name()).thenReturn("probeTemplate");
+ Mockito.when(requestParams.getFileUploads()).thenReturn(Set.of(upload));
+ Mockito.when(requestParams.getPathParams())
+ .thenReturn(Map.of("probetemplateName", "foo.xml"));
+
+ Mockito.when(upload.uploadedFileName()).thenReturn("/file-uploads/abcd-1234");
+ Path uploadPath = Mockito.mock(Path.class);
+ Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath);
+
+ Mockito.when(fs.newInputStream(Mockito.any())).thenThrow(IOException.class);
+ HttpStatusException ex =
+ Assertions.assertThrows(
+ HttpStatusException.class, () -> handler.handle(requestParams));
+ MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(500));
+ Mockito.verify(fs).deleteIfExists(uploadPath);
+ }
+
+ @Test
+ void shouldRespond400IfXmlInvalid() throws Exception {
+ FileUpload upload = Mockito.mock(FileUpload.class);
+ Mockito.when(upload.name()).thenReturn("probeTemplate");
+ Mockito.when(upload.uploadedFileName()).thenReturn("/file-uploads/abcd-1234");
+
+ Mockito.when(requestParams.getFileUploads()).thenReturn(Set.of(upload));
+ Mockito.when(requestParams.getPathParams())
+ .thenReturn(Map.of("probetemplateName", "foo.xml"));
+
+ Path uploadPath = Mockito.mock(Path.class);
+ Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath);
+
+ InputStream stream = Mockito.mock(InputStream.class);
+ Mockito.when(fs.newInputStream(Mockito.any())).thenReturn(stream);
+
+ Mockito.doThrow(ProbeValidationException.class)
+ .when(templateService)
+ .addTemplate(stream, "foo.xml");
+
+ HttpStatusException ex =
+ Assertions.assertThrows(
+ HttpStatusException.class, () -> handler.handle(requestParams));
+ MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(400));
+ Mockito.verify(fs).deleteIfExists(uploadPath);
+ }
+
+ @Test
+ void shouldProcessGoodRequest() throws Exception {
+ FileUpload upload = Mockito.mock(FileUpload.class);
+ Mockito.when(upload.name()).thenReturn("probeTemplate");
+ Mockito.when(upload.uploadedFileName()).thenReturn("/file-uploads/abcd-1234");
+
+ Mockito.when(requestParams.getFileUploads()).thenReturn(Set.of(upload));
+ Mockito.when(requestParams.getPathParams())
+ .thenReturn(Map.of("probetemplateName", "foo.xml"));
+
+ Path uploadPath = Mockito.mock(Path.class);
+ Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath);
+
+ InputStream stream = Mockito.mock(InputStream.class);
+ Mockito.when(fs.newInputStream(uploadPath)).thenReturn(stream);
+
+ IntermediateResponse response = handler.handle(requestParams);
+
+ Mockito.verify(templateService).addTemplate(stream, "foo.xml");
+ Mockito.verifyNoMoreInteractions(templateService);
+ Mockito.verify(fs).deleteIfExists(uploadPath);
+
+ MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200));
+ MatcherAssert.assertThat(response.getBody(), Matchers.nullValue());
+ }
+ }
+}
diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandlerTest.java
new file mode 100644
index 0000000000..d934b6b6d8
--- /dev/null
+++ b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandlerTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.net.web.http.api.beta;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.lenient;
+
+import java.util.Map;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+
+import org.openjdk.jmc.rjmx.IConnectionHandle;
+
+import io.cryostat.MainModule;
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.JFRConnection;
+import io.cryostat.core.sys.Environment;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.messaging.notifications.Notification;
+import io.cryostat.messaging.notifications.NotificationFactory;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.ConnectionDescriptor;
+import io.cryostat.net.TargetConnectionManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import io.vertx.core.MultiMap;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class TargetProbeDeleteHandlerTest {
+
+ TargetProbeDeleteHandler handler;
+ @Mock AuthManager auth;
+ @Mock FileSystem fs;
+ @Mock Logger logger;
+ @Mock NotificationFactory notificationFactory;
+ @Mock Notification notification;
+ @Mock Notification.Builder notificationBuilder;
+ @Mock TargetConnectionManager targetConnectionManager;
+ @Mock Environment env;
+ Gson gson = MainModule.provideGson(logger);
+
+ @BeforeEach
+ void setup() {
+ lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaCategory(Mockito.any()))
+ .thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class)))
+ .thenReturn(notificationBuilder);
+ lenient()
+ .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class)))
+ .thenReturn(notificationBuilder);
+ lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder);
+ lenient().when(notificationBuilder.build()).thenReturn(notification);
+ this.handler =
+ new TargetProbeDeleteHandler(
+ logger, notificationFactory, fs, auth, targetConnectionManager, env, gson);
+ }
+
+ @Nested
+ class BasicHandlerDefinition {
+ @Test
+ void shouldBeDELETEHandler() {
+ MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.DELETE));
+ }
+
+ @Test
+ void shouldBeBetaAPI() {
+ MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2));
+ }
+
+ @Test
+ void shouldHaveExpectedPath() {
+ MatcherAssert.assertThat(
+ handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/probes"));
+ }
+
+ @Test
+ void shouldHaveExpectedRequiredPermissions() {
+ MatcherAssert.assertThat(
+ handler.resourceActions(), Matchers.equalTo(ResourceAction.NONE));
+ }
+
+ @Test
+ void shouldReturnPlaintextMimeType() {
+ MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT));
+ }
+
+ @Test
+ void shouldRequireAuthentication() {
+ MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true));
+ }
+ }
+
+ @Nested
+ class Requests {
+
+ @Mock RequestParameters requestParams;
+ private static final String AGENT_OBJECT_NAME =
+ "org.openjdk.jmc.jfr.agent:type=AgentController";
+ private static final String DEFINE_EVENT_PROBES = "defineEventProbes";
+
+ @Test
+ public void shouldRespondOK() throws Exception {
+ Mockito.when(requestParams.getPathParams()).thenReturn(Map.of("targetId", "foo"));
+ Mockito.when(requestParams.getHeaders()).thenReturn(MultiMap.caseInsensitiveMultiMap());
+ JFRConnection connection = Mockito.mock(JFRConnection.class);
+ IConnectionHandle handle = Mockito.mock(IConnectionHandle.class);
+ MBeanServerConnection mbsc = Mockito.mock(MBeanServerConnection.class);
+ Mockito.when(
+ targetConnectionManager.executeConnectedTask(
+ Mockito.any(ConnectionDescriptor.class), Mockito.any()))
+ .thenAnswer(
+ arg0 ->
+ ((TargetConnectionManager.ConnectedTask